20260825173027
This commit is contained in:
1 parent
7421019ef2
commit
a509f93f34
11 files changed
+615
-934
No files matched your search
@@ -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 模式) ---- */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
+510
-486
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;
|
||||
|
||||
Reference in new issue
Block a user