diff --git a/code/fms/fms-api/config/migrations/036_add_module_icon.sql b/code/fms/fms-api/config/migrations/036_add_module_icon.sql new file mode 100644 index 00000000..b96cc31a --- /dev/null +++ b/code/fms/fms-api/config/migrations/036_add_module_icon.sql @@ -0,0 +1,16 @@ +/* + Add b_icon column to s_module so directory/page modules can assign a + Lucide icon for sidebar menu display. +*/ +SET NOCOUNT ON; +SET XACT_ABORT ON; + +IF DB_NAME() <> N'FMS' + THROW 53600, N'This migration can only run against the FMS database.', 1; + +IF COL_LENGTH(N'dbo.s_module', N'b_icon') IS NOT NULL + THROW 53601, N'Column b_icon already exists on s_module.', 1; + +ALTER TABLE dbo.s_module ADD b_icon VARCHAR(50) NULL; + +PRINT N'Migration 036 applied: added s_module.b_icon.'; diff --git a/code/fms/fms-vue/src/App.vue b/code/fms/fms-vue/src/App.vue index 731a7104..56374d02 100644 --- a/code/fms/fms-vue/src/App.vue +++ b/code/fms/fms-vue/src/App.vue @@ -2,29 +2,6 @@ import zhCN from "antdv-next/locale/zh_CN"; const themeConfig = { - token: { - controlHeight: 32, - borderRadius: 6, - colorPrimary: '#0a0a0a', - colorBgContainer: '#ffffff', - colorBgLayout: '#fafafa', - colorBorder: '#e4e4e7', - colorText: '#0a0a0a', - colorTextSecondary: '#71717a', - fontSize: 14, - }, - components: { - Splitter: { - splitBarSize: 1, - }, - Button: { - contentFontSize: 14, - }, - Tree: { - nodeSelectedBg: '#0a0a0a', - nodeSelectedColor: '#fafafa', - }, - }, }; diff --git a/code/fms/fms-vue/src/components/FmsTree.vue b/code/fms/fms-vue/src/components/FmsTree.vue index 3d691487..9b3abe18 100644 --- a/code/fms/fms-vue/src/components/FmsTree.vue +++ b/code/fms/fms-vue/src/components/FmsTree.vue @@ -19,6 +19,7 @@ const handledEventAttrs = new Set([ "onDragEnd", "onDragend", "onRightClick", + "onSelect", "onUpdate:expandedKeys", ]); @@ -35,6 +36,7 @@ const props = defineProps({ expandedKeys: { type: Array, default: undefined }, blockNode: { type: Boolean, default: true }, virtual: { type: Boolean, default: true }, + showLine: { type: Boolean, default: false }, draggable: { type: [Boolean, Function, Object], default: false }, allowDrop: { type: Function, default: null }, rootKey: { type: [String, Number], default: "__fms_tree_root__" }, @@ -86,6 +88,7 @@ const emit = defineEmits([ "dragOver", "dragEnd", "rightClick", + "select", "update:expandedKeys", ]); @@ -121,7 +124,21 @@ function onMenuClick({ key }) { if (node) emit("menuClick", { key, node }); } +const selectSuppress = ref(false); + +function onSelect(keys, info) { + // 点击有子节点的父节点时,阻止 a-tree 内部自动触发展开 + if (info?.node?.children?.length) { + selectSuppress.value = true; + } + emit("select", keys, info); +} + function onExpandedKeysChange(keys) { + if (selectSuppress.value) { + selectSuppress.value = false; + return; + } emit("update:expandedKeys", keys); } @@ -141,10 +158,11 @@ function onExpandedKeysChange(keys) { :default-expanded-keys="[rootKey]" :block-node="blockNode" :virtual="effectiveVirtual" - show-line + :show-line="showLine" :draggable="draggable" :allow-drop="allowDrop" @right-click="onTreeRightClick" + @select="onSelect" @drop="$emit('drop', $event)" @dragstart="$emit('dragStart', $event)" @dragenter="$emit('dragEnter', $event)" @@ -167,9 +185,10 @@ function onExpandedKeysChange(keys) { :default-expanded-keys="[rootKey]" :block-node="blockNode" :virtual="effectiveVirtual" - show-line + :show-line="showLine" :draggable="draggable" :allow-drop="allowDrop" + @select="onSelect" @drop="$emit('drop', $event)" @dragstart="$emit('dragStart', $event)" @dragenter="$emit('dragEnter', $event)" diff --git a/code/fms/fms-vue/src/layouts/DefaultLayout.vue b/code/fms/fms-vue/src/layouts/DefaultLayout.vue index 180d1eef..114ef238 100644 --- a/code/fms/fms-vue/src/layouts/DefaultLayout.vue +++ b/code/fms/fms-vue/src/layouts/DefaultLayout.vue @@ -77,7 +77,7 @@ const routeInstanceKey = computed( border-radius: 8px; box-shadow: 0 0 0 1px rgb(0 0 0 / 4%), 0 1px 3px rgb(0 0 0 / 4%), 0 4px 12px rgb(0 0 0 / 3%); background: #ffffff; - padding: 8px; + padding: 0 16px; } .app-content { @@ -86,7 +86,7 @@ const routeInstanceKey = computed( min-height: 0; overflow-x: hidden; overflow-y: auto; - padding: 6px; + padding: 6px 0; background: #ffffff; } diff --git a/code/fms/fms-vue/src/layouts/components/AppSidebar.vue b/code/fms/fms-vue/src/layouts/components/AppSidebar.vue index d416c073..2e643243 100644 --- a/code/fms/fms-vue/src/layouts/components/AppSidebar.vue +++ b/code/fms/fms-vue/src/layouts/components/AppSidebar.vue @@ -1,30 +1,23 @@ - - - @@ -329,13 +293,14 @@ onMounted(() => permissionStore.load().catch(() => {})); .sidebar-header { display: flex; flex: none; - height: 48px; + height: 44px; align-items: center; justify-content: center; - padding: 8px 12px 0; + margin-top: 8px; + padding: 0 12px; &.collapsed { - padding: 8px 0 0; + padding: 0; } } @@ -412,12 +377,18 @@ onMounted(() => permissionStore.load().catch(() => {})); color: #3f3f46; } - &.active, &:active { + background: var(--accent, #f4f4f5); + color: #3f3f46; + } + + &.active { background: #f4f4f5; color: #3f3f46; font-weight: 500; - box-shadow: 0 1px 3px rgb(0 0 0 / 8%), 0 2px 4px rgb(0 0 0 / 6%); + box-shadow: + 0 1px 3px rgb(0 0 0 / 8%), + 0 2px 4px rgb(0 0 0 / 6%); } &.collapsed { @@ -473,41 +444,4 @@ onMounted(() => permissionStore.load().catch(() => {})); background: var(--border, #e4e4e7); } } - -/* ============================================================ - * Footer / 底部操作区 - * ============================================================ */ -.sidebar-footer { - display: flex; - flex: none; - padding: 0 6px 6px; -} - -.sidebar-collapse-btn { - display: flex; - width: 100%; - height: 32px; - align-items: center; - justify-content: center; - padding: 0; - border: 0; - border-radius: 6px; - background: transparent; - color: var(--muted-foreground, #71717a); - cursor: pointer; - transition: - background-color 0.15s ease, - color 0.15s ease; - - &:hover { - background: var(--accent, #f4f4f5); - color: var(--foreground, #0a0a0a); - } - - .lucide { - width: 16px; - height: 16px; - stroke-width: 1.8; - } -} diff --git a/code/fms/fms-vue/src/layouts/components/AppTabs.vue b/code/fms/fms-vue/src/layouts/components/AppTabs.vue index 03e5d32b..6448988c 100644 --- a/code/fms/fms-vue/src/layouts/components/AppTabs.vue +++ b/code/fms/fms-vue/src/layouts/components/AppTabs.vue @@ -96,11 +96,10 @@ function updateScrollState() { const track = trackRef.value; if (!header || !track) return; - const shouldOverflow = track.scrollWidth > header.clientWidth - 16 + 1; + const shouldOverflow = track.scrollWidth > header.clientWidth + 1; if (overflowed.value !== shouldOverflow) { overflowed.value = shouldOverflow; if (!shouldOverflow) track.scrollLeft = 0; - nextTick(updateScrollState); return; } @@ -219,7 +218,7 @@ function handleTabKeydown(event, index) { height: 34px; align-items: center; gap: 4px; - padding: 3px 8px; + padding: 3px 0; background: #ffffff; } @@ -231,7 +230,7 @@ function handleTabKeydown(event, index) { height: 28px; padding: 0; place-items: center; - border: 0; + border: 1px solid #e4e4e7; border-radius: 6px; color: #71717a; background: transparent; diff --git a/code/fms/fms-vue/src/layouts/components/AppToolbar.vue b/code/fms/fms-vue/src/layouts/components/AppToolbar.vue index 69903e16..2d8c3255 100644 --- a/code/fms/fms-vue/src/layouts/components/AppToolbar.vue +++ b/code/fms/fms-vue/src/layouts/components/AppToolbar.vue @@ -2,7 +2,7 @@ import { computed, onMounted, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { message } from "antdv-next"; -import { Bell, ChevronDown, Languages, LogOut, UserRound } from "@lucide/vue"; +import { Bell, Languages, LogOut, PanelLeftClose, PanelLeftOpen, UserRound } from "@lucide/vue"; import { useAppStore } from "@/stores/app"; import { useAuthStore } from "@/stores/auth"; import { usePermissionStore } from "@/stores/permissions"; @@ -18,9 +18,6 @@ const logoutOpen = ref(false); const breadcrumbItems = computed(() => { const items = []; - if (route.path === "/dashboard") { - return [{ label: appStore.t("common.dashboard", "工作台") }]; - } if (route.meta.section) { const directory = permissionStore.modules.find( (item) => item.b_module_type === "directory" && item.b_name === route.meta.section, @@ -34,6 +31,11 @@ const breadcrumbItems = computed(() => { items.push({ label: appStore.t(module?.b_i18n, route.meta.title) }); } + // 兜底确保始终有内容 + if (items.length === 0) { + items.push({ label: appStore.t("common.dashboard", route.meta.title || "工作台") }); + } + return items; }); @@ -106,6 +108,23 @@ function confirmLogout() {