20260826173130
This commit is contained in:
1 parent
9957757b5d
commit
0bbbbd3ecc
15 files changed
+2472
-874
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user