20260825173027

This commit is contained in:
oneao committed 2026-08-25 17:30:28 +08:00
1 parent 7421019ef2
commit a509f93f34
11 files changed
+615 -934

No files matched your search

+16 -13
View File
@@ -5,7 +5,6 @@ import { useAppStore } from '@/stores/app'
import Splitter from '@/components/ui/splitter/splitter.vue'
import SplitterPanel from '@/components/ui/splitter/panel.vue'
import AppSidebar from './components/AppSidebar.vue'
import AppTabs from './components/AppTabs.vue'
import AppTopbar from './components/AppTopbar.vue'
import DetailPage from '@/views/detail/DetailPage.vue'
import { cacheKeyOf } from '@/utils/cacheKey'
@@ -96,18 +95,18 @@ function onSplitterResize(sizes) {
:min="appStore.sidebarCollapsed ? undefined : SIDEBAR_MIN"
:max="appStore.sidebarCollapsed ? undefined : SIDEBAR_MAX"
:resizable="!appStore.sidebarCollapsed"
class="fms-sidebar-panel"
>
<AppSidebar />
</SplitterPanel>
<!-- 主区面板:顶栏独立在灰底上,下方一张白卡放 tabs + 内容 -->
<!-- 主区面板:顶栏(已含多页签)独立在灰底上,下方一张白卡放内容 -->
<SplitterPanel class="fms-main-panel">
<div class="fms-main-col">
<!-- 顶栏已合并多页签,主区只留内容白卡 -->
<AppTopbar />
<div class="fms-main">
<AppTabs />
<div class="fms-content">
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
@@ -193,9 +192,7 @@ function onSplitterResize(sizes) {
}
/* 主区面板:四周留 8px 间距(含左侧),与侧栏拉开标准间隙。
* 折叠态(窄侧栏 48px)左侧归 0,白卡贴侧栏右缘,避免过窄时两侧都留间隙。
* 卡片阴影会外扩到面板边界外,须将主区面板 overflow 改为 visible 才不被裁。
* 卡片自身 overflow: hidden + .fms-content 内部滚动,内容不会真正外溢,改为 visible 安全 */
* 折叠态(窄侧栏 48px)左侧归 0,白卡贴侧栏右缘 */
.fms-main-panel {
padding: 8px;
}
@@ -204,6 +201,16 @@ function onSplitterResize(sizes) {
padding: 8px 8px 8px 0;
}
/* 侧栏面板:四周留 8px,白色侧栏卡浮在灰底上(与顶栏同一套间距) */
.fms-sidebar-panel {
padding: 8px;
box-sizing: border-box;
}
.fms-layout.is-collapsed .fms-sidebar-panel {
padding: 8px 8px 8px 0;
}
/* 覆盖 Splitter 面板默认 overflow: auto(scoped 同名规则特异性更高,需带 .fms-layout 提权),
* 让主卡片左侧阴影完整绘制到侧栏背景上(对齐 old2 的 z-index 效果) */
.fms-layout .fms-splitter-panel.fms-main-panel {
@@ -220,7 +227,7 @@ function onSplitterResize(sizes) {
gap: 8px;
}
/* 主区白卡:tabs + 内容一体,圆角 + 阴影,撑满剩余高度 */
/* 主区:透明容器(不再包白卡),内容区直接坐灰底,页面白卡浮在灰上 */
.fms-main {
display: flex;
flex-direction: column;
@@ -228,13 +235,9 @@ 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);
}
/* 内容区:白卡内滚动区,顶部由 .fms-tabs 的底边线分隔。
* flex column:让页面根可用 flex:1 撑满可视高度(百分比高度链在滚动容器内不可靠) */
/* 内容区:透明,灰底由 .fms-layout 提供(页面白卡直接浮在灰上) */
.fms-content {
flex: 1;
min-height: 0;
@@ -152,12 +152,16 @@ function toNavItem(node) {
</template>
<style lang="scss" scoped>
/* ---- 侧栏容器 ---- */
/* ---- 侧栏容器:白色圆角卡(带框架阴影,浮在灰底上),内部分区滚动 ---- */
.fms-sidebar-body {
display: flex;
flex-direction: column;
height: 100%;
min-width: 0;
overflow: hidden;
background: var(--fms-card);
border-radius: var(--fms-control-radius);
box-shadow: var(--fms-frame-shadow);
}
/* 团队切换(对齐 SidebarHeader p-2 + TeamSwitcher h-12) */
@@ -65,6 +65,10 @@ onMounted(() => {
onBeforeUnmount(() => {
resizeObserver?.disconnect()
resizeObserver = null
if (scrollStateRaf != null) {
cancelAnimationFrame(scrollStateRaf)
scrollStateRaf = null
}
trackRef.value?.removeEventListener('wheel', handleWheel)
})
@@ -131,19 +135,33 @@ function handleTabAction(action, key) {
}
}
// 溢出/滚动状态:仅在实际变化时更新,并用 rAF 延迟到下一帧提交 ——
// 箭头 v-if 的显示切换会改变轨道宽度,若在 ResizeObserver 回调内同步改布局,
// 会在同一帧内触发新一轮观察,造成「ResizeObserver loop」错误。
let scrollStateRaf = null
function updateScrollState() {
const track = trackRef.value
if (!track) return
const shouldOverflow = track.scrollWidth > track.clientWidth + 1
overflowed.value = shouldOverflow
if (!shouldOverflow) {
track.scrollLeft = 0
canScrollLeft.value = false
canScrollRight.value = false
const left = shouldOverflow ? track.scrollLeft > 1 : false
const right = shouldOverflow
? track.scrollLeft + track.clientWidth < track.scrollWidth - 1
: false
if (
shouldOverflow === overflowed.value &&
left === canScrollLeft.value &&
right === canScrollRight.value
) {
return
}
canScrollLeft.value = track.scrollLeft > 1
canScrollRight.value = track.scrollLeft + track.clientWidth < track.scrollWidth - 1
if (scrollStateRaf != null) return
scrollStateRaf = requestAnimationFrame(() => {
scrollStateRaf = null
if (!shouldOverflow) track.scrollLeft = 0
overflowed.value = shouldOverflow
canScrollLeft.value = left
canScrollRight.value = right
})
}
function scrollTabs(direction) {
@@ -12,7 +12,6 @@ import {
Moon,
Palette,
PanelLeft,
Search,
Sun,
} from '@lucide/vue'
import { useRouter } from 'vue-router'
@@ -22,7 +21,7 @@ import { Message } from '@/components/ui/message/message-manager'
import { PRESETS } from '@/theme/presets'
import Dropdown from '@/components/ui/dropdown/dropdown.vue'
import Button from '@/components/ui/button/button.vue'
import Input from '@/components/ui/input/input.vue'
import AppTabs from './AppTabs.vue'
const router = useRouter()
const appStore = useAppStore()
@@ -96,7 +95,7 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
<template>
<header class="fms-topbar">
<!-- 左侧:折叠按钮 + 搜索框 -->
<!-- 左侧:折叠按钮 -->
<div class="fms-topbar-left">
<Button
type="ghost"
@@ -106,20 +105,12 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
>
<PanelLeft />
</Button>
<span class="fms-separator" aria-hidden="true" />
</div>
<Input
class="fms-search"
disabled
model-value=""
placeholder="搜索功能、菜单…"
aria-label="全局搜索"
>
<template #prefix>
<Search />
</template>
</Input>
<!-- 中间:多页签(与顶栏合成一条,不再单独一条) -->
<div class="fms-topbar-tabs">
<AppTabs />
</div>
<!-- 右侧:语言 / 主题色板 / 深浅色 / 通知 / 用户 -->
@@ -212,7 +203,7 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
</template>
<style lang="scss" scoped>
/* ---- 顶栏 ---- */
/* ---- 顶栏(已合并页签,白色浮条,带框架阴影) ---- */
.fms-topbar {
display: flex;
align-items: center;
@@ -220,10 +211,9 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
gap: 8px;
height: 40px;
padding: 0 12px;
/* 顶栏独立成一块:与 .fms-main 同款白底 + 圆角 + 同款投影,与下方白卡并列分层 */
background: var(--fms-card);
border-radius: var(--fms-control-radius);
box-shadow: var(--fms-shadow);
box-shadow: var(--fms-frame-shadow);
}
.fms-topbar-left,
@@ -241,6 +231,22 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
margin-left: auto;
}
/* 中间页签区:填入剩余宽度,高度撑满顶栏 */
.fms-topbar-tabs {
flex: 1;
min-width: 0;
height: 100%;
overflow: hidden;
}
/* AppTabs 嵌入顶栏时:填满容器高度,去掉独立底边线和外侧 padding */
.fms-topbar-tabs :deep(.fms-tabs) {
height: 100%;
padding: 0 4px;
border-bottom: none;
box-shadow: none;
}
/* 顶栏图标按钮:复用组件库 Button type="ghost"(透明底/无边框/hover 高亮)。
用 .fms-topbar 前缀提高特异性,覆盖 ghost 的横向 padding / 次要文字色 / 图标尺寸,
使其保持 32×32 正方形、主文字色图标。hover 沿用 ghost 的 --fms-secondary。 */
@@ -275,52 +281,6 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
}
}
/* 全局搜索框:基于组件库 Input,disabled 仅用于「不可聚焦/不可编辑」,
视觉上用 :deep() 还原为浅灰装饰框(覆盖库默认的禁用灰底 + not-allowed 光标) */
.fms-topbar-left .fms-search {
flex-shrink: 0;
width: 220px;
height: 28px;
min-height: 28px;
padding: 0 8px;
background: color-mix(in srgb, var(--fms-secondary-hover) 50%, var(--fms-card));
cursor: pointer;
transition: border-color 150ms ease;
/* 覆盖组件库禁用态:还原正常外观 + 手型光标 */
&.input-disabled {
background: color-mix(in srgb, var(--fms-secondary-hover) 50%, var(--fms-card));
border-color: var(--fms-border);
color: var(--fms-text-secondary);
cursor: pointer;
}
/* 前缀搜索图标尺寸 */
:deep(.input-prefix) {
margin-right: 6px;
}
:deep(.input-prefix svg) {
width: 16px;
height: 16px;
}
/* 内部输入框 */
:deep(.input) {
font-size: 12px;
cursor: pointer;
color: var(--fms-text);
&::placeholder {
color: var(--fms-text-secondary);
}
&:disabled {
cursor: pointer;
}
}
}
.fms-separator {
flex-shrink: 0;
width: 1px;
@@ -218,12 +218,12 @@ function onToggle() {
min-height: 28px;
}
/* 激活高亮:白底 + shadow-sm + 深色文字(对齐 data-[active=true]) */
/* 激活高亮:主题色浅底 + 主题色文字(白色侧栏上可见) */
.fms-nav-link.is-active {
background: var(--fms-card);
color: var(--fms-text);
background: color-mix(in srgb, var(--fms-primary) 10%, var(--fms-card));
color: var(--fms-primary);
font-weight: 500;
box-shadow: var(--fms-shadow);
box-shadow: none;
}
/* ---- 折叠态:仅图标,居中展示(对齐 size-8 icon 模式) ---- */
+8
View File
@@ -43,6 +43,11 @@
--fms-control-radius: 6px;
--fms-control-size: 16px;
/* 框架阴影(顶栏/侧栏):轻量双层、紧贴、扩散小 */
--fms-frame-shadow:
0 1px 2px rgba(30, 45, 64, 0.06),
0 3px 8px rgba(30, 45, 64, 0.08);
/* 开关(switch)尺寸(不随主题变化) */
--fms-switch-width: 32px;
--fms-switch-height: 18px;
@@ -148,4 +153,7 @@
/* 阴影(深色下加重投影) */
--fms-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
--fms-popover-shadow: 0 3px 8px rgba(0, 0, 0, 0.5);
--fms-frame-shadow:
0 1px 2px rgba(0, 0, 0, 0.4),
0 3px 8px rgba(0, 0, 0, 0.3);
}
File diff suppressed because it is too large. Load diff
@@ -1451,7 +1451,6 @@ loadMenuTree()
</template>
<template #rightExtra>
<div class="mm-header__actions">
<span class="mm-header__hint">Ctrl + S 保存</span>
<Button type="outline" :disabled="busy || !selectedModule" @click="reloadModule">
<RefreshCw :size="14" /> 重载
</Button>
@@ -1686,7 +1685,6 @@ loadMenuTree()
</template>
<template #rightExtra>
<div class="mm-header__actions">
<span class="mm-header__hint">Ctrl + S 保存</span>
<Button type="outline" :disabled="menuSaving" @click="reloadMenu">
<RefreshCw :size="14" /> 重载
</Button>
@@ -1945,13 +1943,6 @@ loadMenuTree()
flex: none;
}
.mm-header__hint {
flex: none;
color: var(--fms-text-secondary);
font-size: 12px;
white-space: nowrap;
}
/* ---- 右侧:配置工作区 ---- */
.mm-body__inner {
display: flex;