20260826173130

This commit is contained in:
oneao committed 2026-08-26 17:31:31 +08:00
1 parent 9957757b5d
commit 0bbbbd3ecc
15 files changed
+2472 -874

No files matched your search

+112 -29
View File
@@ -1,5 +1,5 @@
<script setup>
import { computed, defineComponent, h, ref, watch } from 'vue'
import { computed, defineComponent, h, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useAppStore } from '@/stores/app'
import Splitter from '@/components/ui/splitter/splitter.vue'
@@ -57,7 +57,6 @@ watch(
)
/* ---------- 侧栏宽度(展开态可拖拽调宽,由 Splitter 接管) ---------- */
const SIDEBAR_MIN = 180
const SIDEBAR_MAX = 400
const SIDEBAR_DEFAULT = 220
// 折叠态:48px(对齐 shadcn sidebar 默认 3rem,仅容图标)
@@ -76,6 +75,68 @@ function onSplitterResize(sizes) {
const width = Math.round(Number(sizes[0]))
if (Number.isFinite(width)) sidebarWidth.value = width
}
/* 展开/折叠逻辑:
* - 折叠:侧栏固定 48px(不可拖拽)
* - 展开:恢复为拖拽宽度;但若上次被拖得过窄(< 默认 220),自动恢复默认宽度,
* 避免「拖窄 → 折叠 → 再展开」后侧栏变成一条细缝。
* 用 watch 监听折叠态,无论从顶栏按钮还是菜单点击触发展开都生效 */
watch(
() => appStore.sidebarCollapsed,
(collapsed) => {
if (!collapsed && sidebarWidth.value < SIDEBAR_DEFAULT) {
sidebarWidth.value = SIDEBAR_DEFAULT
}
},
)
/* 顶栏折叠/展开按钮的点击逻辑(经 provide 提供给 AppTopbar):
* - 折叠态:展开(宽度由上方 watch 自动恢复)
* - 展开态且宽度已被拖到很窄(< 默认 220):不折叠,直接展开到默认宽度
* ——用户把侧栏拖没了以后点按钮,期望的是"恢复宽度"而不是"再折叠"
* - 其余情况:正常折叠(48px) */
function toggleSidebar() {
if (appStore.sidebarCollapsed) {
appStore.setSidebar(false)
return
}
if (sidebarWidth.value < SIDEBAR_DEFAULT) {
sidebarWidth.value = SIDEBAR_DEFAULT
return
}
appStore.setSidebar(true)
}
provide('toggleSidebar', toggleSidebar)
/* ---------- 内容可视高度(页面撑满用) ----------
* 实测 .fms-content 可视高度(clientHeight - 纵向 padding),写入 CSS 变量
* --fms-content-height(随 DOM 继承给页面所有后代)。
* 需要「固定高度 + 内部滚动」的页面(如工具栏固定、表格内部滚动)直接写:
* height: var(--fms-content-height)
* 替代 calc(100vh - 116px) 这类魔数——顶栏/页签/padding 等布局内部变化时自动跟随,
* 页面无需改动。 */
const contentRef = ref(null)
let contentResizeObserver = null
function syncContentHeight() {
const el = contentRef.value
if (!el) return
const style = getComputedStyle(el)
const height =
el.clientHeight - parseFloat(style.paddingTop || '0') - parseFloat(style.paddingBottom || '0')
el.style.setProperty('--fms-content-height', `${height}px`)
}
onMounted(() => {
// ResizeObserver 首次 observe 会立即回调一次,初始值无需额外同步
contentResizeObserver = new ResizeObserver(syncContentHeight)
if (contentRef.value) contentResizeObserver.observe(contentRef.value)
})
onBeforeUnmount(() => {
contentResizeObserver?.disconnect()
contentResizeObserver = null
})
</script>
<template>
@@ -89,10 +150,11 @@ function onSplitterResize(sizes) {
@resize-start="dragging = true"
@resize-end="dragging = false"
>
<!-- 侧栏面板:折叠态固定 48px 且不可调整 -->
<!-- 侧栏面板:紧贴左/上/下边缘(无 padding),侧栏卡片右侧阴影与主区分隔。
拖拽无最小宽度限制(可拖到任意窄),仅限制最大宽度 -->
<SplitterPanel
class="fms-sidebar-panel"
:size="sidebarSize"
:min="appStore.sidebarCollapsed ? undefined : SIDEBAR_MIN"
:max="appStore.sidebarCollapsed ? undefined : SIDEBAR_MAX"
:resizable="!appStore.sidebarCollapsed"
>
@@ -105,7 +167,7 @@ function onSplitterResize(sizes) {
<AppTopbar />
<div class="fms-main">
<div class="fms-content">
<div ref="contentRef" class="fms-content">
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
<div :key="route.fullPath" class="fms-page-wrap">
@@ -167,19 +229,28 @@ function onSplitterResize(sizes) {
display: none;
}
/* 非折叠态:分隔条命中区改为悬浮(width: 0 + 负 margin),不占 flex 宽度。
* 否则侧栏内容右 padding(8px) + 分隔条占位(6px) 叠加成 14px 空隙,
* 而右侧仅 8px,两侧不对称(折叠态因 display:none 掩盖了此问题)。
* 注意同样只能用 > 限定为布局自身的直接子级分隔条:
* 用后代选择器时 scoped :deep 会把主面板内嵌套 Splitter 的分隔条
* 一并压成 0 宽,导致内层(如 FmsFile)分隔条不可见、无法拖拽 */
/* 非折叠态:分隔条悬浮命中区(width: 0 + margin: 0,不参与 flex 布局)。
* 注意:原实现用负 margin(0 -3px)做悬浮命中区,但 flex 中负 margin 会把相邻面板
* 拉近 6px,抵消主区面板 padding-left 8px → 侧栏白卡与右侧内容(顶栏/灰画布)只剩
* 2px 灰缝,视觉上"紧贴"。命中区改由绝对定位的 ::after 扩展,布局不再受影响。 */
.fms-layout .fms-layout-splitter :deep(> .fms-splitter-bar) {
width: 0;
margin: 0 calc(var(--fms-splitter-trigger-size) / -2);
margin: 0;
position: relative;
z-index: 1;
}
/* 命中区:绝对定位在分隔条两侧各扩展 3px(共 6px),不占 flex 空间,
* 且能正常触发 bar 的 pointerdown 拖拽 */
.fms-layout .fms-layout-splitter :deep(> .fms-splitter-bar)::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: calc(var(--fms-splitter-trigger-size) / -2);
width: var(--fms-splitter-trigger-size);
}
/* 侧栏面板宽度过渡:折叠/展开切换平滑;拖拽时禁用保证跟手 */
.fms-layout-splitter .fms-splitter-panel {
transition: flex-basis 180ms ease;
@@ -189,22 +260,25 @@ function onSplitterResize(sizes) {
transition: none;
}
/* 主区面板:四周留 8px 间距(含左侧),与侧栏拉开标准间隙。
* 折叠态(窄侧栏 48px)左侧归 0,避免过窄时两侧都留间隙 */
/* 主区面板:只留左右 8px padding(顶栏/内容区紧贴上边缘)。
* 折叠态与展开态一致,保持 8px 左灰缝,避免侧栏折叠时与右侧紧贴 */
.fms-main-panel {
padding: 8px;
}
.fms-layout.is-collapsed .fms-main-panel {
padding: 8px 8px 8px 0;
padding: 0 8px;
}
/* 覆盖 Splitter 面板默认 overflow: auto(scoped 同名规则特异性更高,需带 .fms-layout 提权),
* 让主卡片左侧阴影完整绘制到侧栏背景上(对齐 old2 的 z-index 效果) */
* 让顶栏/卡片阴影与圆角完整绘制 */
.fms-layout .fms-splitter-panel.fms-main-panel {
overflow: visible;
}
/* 侧栏面板同样需要 overflow: visible:
* 否则默认 overflow: auto 会把侧栏卡片的右侧投影(box-shadow 向右投射 4px)
* 裁掉,侧栏与右侧内容之间失去可见分隔,看起来"紧贴" */
.fms-layout .fms-splitter-panel.fms-sidebar-panel {
overflow: visible;
}
/* 主区纵向列:顶栏在灰底上独立成条,下方白卡与其留 8px 间距 */
.fms-main-col {
display: flex;
@@ -215,8 +289,9 @@ function onSplitterResize(sizes) {
gap: 8px;
}
/* 主区内容容器:白底承载内容,与透明侧栏(灰底)形成明显区分;
* 圆角 + 阴影让白卡在灰底上浮起,层次清晰 */
/* 主区内容容器:灰底画布(--fms-background),不再是白卡。
* 页面在此画布上自行放置白卡(background: var(--fms-card) + 圆角 + 阴影),
* 靠「灰底 + 白卡」形成层次,避免用大量边框线分隔 */
.fms-main {
display: flex;
flex-direction: column;
@@ -224,20 +299,28 @@ function onSplitterResize(sizes) {
min-width: 0;
min-height: 0;
overflow: hidden;
background: var(--fms-card);
border-radius: var(--fms-control-radius);
box-shadow: var(--fms-shadow);
background: var(--fms-background);
}
/* 内容区:白卡内滚动区,顶部由 .fms-tabs 的底边线分隔。
* flex column:让页面根可用 flex:1 撑满可视高度(百分比高度链在滚动容器内不可靠) */
/* 内容区:灰底画布内的滚动区(顶部由 .fms-tabs 的底边线分隔)。
* flex column:让页面根可用 flex:1 撑满可视高度(百分比高度链在滚动容器内不可靠)。
* overflow: auto hidden —— 只允许纵向滚动,禁止横向滚动条:
* 侧栏折叠/展开过渡(flex-basis 180ms)瞬间内容宽度会短暂超出容器,
* 若允许 x 滚动会出现滚动条闪现;页面内容的横向滚动应由内部容器(表格/splitter)处理。
* 页面需要「固定高度 + 内部滚动」时用 height: var(--fms-content-height)
* (由布局实测写入,随顶栏/页签/padding 变化自动跟随),不要手写 calc(100vh - xx) 魔数。
* 页面内部的分层白卡:background: var(--fms-card) + border-radius + box-shadow,
* 卡间用 margin/gap 留出灰缝,不要用 border 画分隔线 */
.fms-content {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: auto;
padding: 12px 16px 16px;
overflow: auto hidden;
/* 只保留上下 padding(8px),左右为 0:
* 页面内容紧贴灰画布左右边缘(与主区面板的左右 8px 间距配合),
* 让页面白卡尽可能占满横向空间 */
padding: 8px 0;
}
/* 页面过渡外层包裹:稳定的 DOM 根,供 transition 的 out-in 模式直接管理,
@@ -158,6 +158,14 @@ function toNavItem(node) {
flex-direction: column;
height: 100%;
min-width: 0;
/* 白色侧栏,紧贴左/上/下边缘(父面板无 padding、无圆角)。
* 右侧投影参考 soybean-admin 的 sider 设计:
* box-shadow: 2px 0 8px 0 rgb(29, 35, 41, 0.05)
* 2px 右偏移、8px 模糊、5% 透明度 —— 极淡克制的细腻投影,紧贴右缘自然分隔,
* 不用浓阴影(会显脏)。深色模式下 --fms-card 自动变为深灰卡,无需额外覆盖 */
background: var(--fms-card);
box-shadow: 2px 0 4px 0 rgb(29, 35, 41, 0.05);
overflow: hidden;
}
/* 团队切换(对齐 SidebarHeader p-2 + TeamSwitcher h-12) */
@@ -210,6 +218,7 @@ function toNavItem(node) {
width: 32px;
height: 32px;
border-radius: 8px;
/* 主色底 + 白图标(与白侧栏形成反差,保持醒目) */
background: var(--fms-primary);
color: var(--fms-primary-text);
@@ -1,5 +1,5 @@
<script setup>
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
import { computed, inject, ref, onMounted, onBeforeUnmount } from 'vue'
import {
BadgeCheck,
Bell,
@@ -12,6 +12,7 @@ import {
Moon,
Palette,
PanelLeft,
PanelRight,
Sun,
} from '@lucide/vue'
import { useRouter } from 'vue-router'
@@ -27,6 +28,11 @@ const router = useRouter()
const appStore = useAppStore()
const authStore = useAuthStore()
/* 侧栏折叠/展开按钮逻辑由布局(DefaultLayout)提供:
* 折叠态 → 展开并恢复宽度;展开态但被拖得很窄 → 展开到默认宽度(不折叠);
* 其余 → 正常折叠。兜底为 store 的简单切换 */
const toggleSidebar = inject('toggleSidebar', () => appStore.toggleSidebar())
/* 深浅色切换:原 ThemeToggle 组件逻辑内联,按钮样式复用 .fms-icon-btn */
const isDark = computed(() => appStore.darkMode)
function toggleDark() {
@@ -95,15 +101,15 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
<template>
<header class="fms-topbar">
<!-- 左侧:折叠按钮 -->
<!-- 左侧:折叠/展开按钮(图标随状态切换:折叠态显示"展开"图标) -->
<div class="fms-topbar-left">
<Button
type="ghost"
class="fms-icon-btn"
:title="appStore.sidebarCollapsed ? '展开侧栏' : '折叠侧栏'"
@click="appStore.toggleSidebar()"
@click="toggleSidebar()"
>
<PanelLeft />
<component :is="appStore.sidebarCollapsed ? PanelRight : PanelLeft" />
</Button>
<span class="fms-separator" aria-hidden="true" />
@@ -212,10 +218,12 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
gap: 8px;
height: 40px;
padding: 0 12px;
/* 顶栏独立成一块:与 .fms-main 同款白底 + 圆角 + 同款投影,与下方白卡并列分层 */
/* 顶栏紧贴上边缘(主区面板上下无 padding);左下+右下圆角。
* 向下投影参考 soybean-admin 的 header 设计(0 1px 2px rgb(0,21,41,0.08)),
* 配合底圆角让白卡从灰画布上微微浮起,与侧栏的"右侧投影"角色互补 */
background: var(--fms-card);
border-radius: var(--fms-control-radius);
box-shadow: var(--fms-shadow);
border-radius: 0 0 var(--fms-control-radius) var(--fms-control-radius);
box-shadow: 0 2px 4px rgb(0, 21, 41, 0.08);
}
.fms-topbar-left,
@@ -218,8 +218,7 @@ function onToggle() {
min-height: 28px;
}
/* 激活高亮:主色淡底 + 主色文字(与树选中态一致)。
侧栏为浅灰底时白底+阴影会形成突兀的白块,改用主色淡底融入浅灰侧栏 */
/* 激活高亮:主色淡底 + 主色文字(与树选中态一致) */
.fms-nav-link.is-active {
background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
color: var(--fms-primary);