20260806224941
This commit is contained in:
1 parent
6baa339efa
commit
6938deef39
12 files changed
+367
-300
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,23 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ChevronDown, Database, House, PanelLeftClose, PanelLeftOpen, Settings } from "@lucide/vue";
|
||||
import { ChevronDown, House } from "@lucide/vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
import { getIcon, getIconNames } from "@/utils/lucideIcons";
|
||||
import { buildTree, sortTree } from "@/utils/tree";
|
||||
import { cloneData } from "@/utils/dataChanges";
|
||||
import SidebarMenuItem from "./SidebarMenuItem.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const openKeys = ref(["base", "system", "settings-dept"]);
|
||||
const openKeys = ref([]);
|
||||
const effectiveCollapsed = computed(() => appStore.sidebarCollapsed);
|
||||
|
||||
function moduleLabel(moduleId, path, fallback) {
|
||||
const module = permissionStore.modules.find(
|
||||
(item) => item.b_code === moduleId || item.b_route === path,
|
||||
);
|
||||
return appStore.t(module?.b_i18n, fallback);
|
||||
}
|
||||
|
||||
function directoryLabel(fallback) {
|
||||
const module = permissionStore.modules.find(
|
||||
(item) => item.b_module_type === "directory" && item.b_name === fallback,
|
||||
);
|
||||
return appStore.t(module?.b_i18n, fallback);
|
||||
function resolveIcon(iconName, fallback) {
|
||||
return getIcon(iconName) || fallback;
|
||||
}
|
||||
|
||||
function canAccessMenu(item) {
|
||||
@@ -33,45 +26,6 @@ function canAccessMenu(item) {
|
||||
}
|
||||
|
||||
const menuItems = computed(() => {
|
||||
const basePages = [
|
||||
{
|
||||
key: "/base/othercompany",
|
||||
label: moduleLabel("base_othercompany", "/base/othercompany", "合作伙伴"),
|
||||
moduleId: "base_othercompany",
|
||||
},
|
||||
{
|
||||
key: "/base/contact",
|
||||
label: moduleLabel("v_b_contact", "/base/contact", "联系人"),
|
||||
moduleId: "v_b_contact",
|
||||
},
|
||||
{
|
||||
key: "/base/otherdata",
|
||||
label: moduleLabel("base_otherdata", "/base/otherdata", "其他数据"),
|
||||
moduleId: "base_otherdata",
|
||||
},
|
||||
].filter(canAccessMenu);
|
||||
const systemPages = [
|
||||
{
|
||||
key: "/system/module-management",
|
||||
label: moduleLabel("m_metadata", "/system/module-management", "模块管理"),
|
||||
moduleId: "m_metadata",
|
||||
},
|
||||
{
|
||||
key: "/system/i18n-type",
|
||||
label: moduleLabel("b_i18n_type", "/system/i18n-type", "多语言类型"),
|
||||
moduleId: "b_i18n_type",
|
||||
},
|
||||
{
|
||||
key: "/system/login-log",
|
||||
label: moduleLabel("d_login_log", "/system/login-log", "登录日志"),
|
||||
moduleId: "d_login_log",
|
||||
},
|
||||
{
|
||||
key: "/system/user",
|
||||
label: moduleLabel("b_user", "/system/user", "用户管理"),
|
||||
moduleId: "b_user",
|
||||
},
|
||||
].filter(canAccessMenu);
|
||||
const items = [
|
||||
{
|
||||
key: "/dashboard",
|
||||
@@ -79,46 +33,63 @@ const menuItems = computed(() => {
|
||||
label: appStore.t("common.dashboard", "工作台"),
|
||||
},
|
||||
];
|
||||
if (basePages.length)
|
||||
items.push({
|
||||
key: "base",
|
||||
icon: Database,
|
||||
label: directoryLabel("基础数据"),
|
||||
children: basePages,
|
||||
});
|
||||
if (systemPages.length)
|
||||
items.push({
|
||||
key: "system",
|
||||
icon: Settings,
|
||||
label: directoryLabel("系统管理"),
|
||||
children: systemPages,
|
||||
});
|
||||
|
||||
// --- 模拟三级菜单(爷爷 → 爸爸 → 儿子)---
|
||||
items.push({
|
||||
key: "settings",
|
||||
icon: Settings,
|
||||
label: "系统设置",
|
||||
children: [
|
||||
{
|
||||
key: "settings-dept",
|
||||
label: "部门管理",
|
||||
children: [
|
||||
{ key: "/settings/dept/list", label: "部门列表" },
|
||||
{ key: "/settings/dept/position", label: "职位管理" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "settings-role",
|
||||
label: "角色权限",
|
||||
children: [
|
||||
{ key: "/settings/role/list", label: "角色列表" },
|
||||
{ key: "/settings/role/assign", label: "权限分配" },
|
||||
],
|
||||
},
|
||||
{ key: "/settings/audit", label: "操作日志" },
|
||||
],
|
||||
});
|
||||
// 筛选可显示在菜单中的模块(仅 directory 和 page,且已启用、可菜单)
|
||||
const menuModules = permissionStore.modules.filter(
|
||||
(m) =>
|
||||
m.b_canmenu === 1 &&
|
||||
m.b_canuse === 1 &&
|
||||
["directory", "page"].includes(m.b_module_type),
|
||||
);
|
||||
|
||||
if (!menuModules.length) return items;
|
||||
|
||||
// 父节点未启用菜单 → 子节点也不显示
|
||||
const allowedIds = new Set(menuModules.map((m) => String(m.b_id)));
|
||||
const validModules = menuModules.filter(
|
||||
(m) => m.b_parent_id == null || allowedIds.has(String(m.b_parent_id)),
|
||||
);
|
||||
|
||||
if (!validModules.length) return items;
|
||||
|
||||
// 深克隆避免 buildTree/sortTree 修改响应式对象触发死循环
|
||||
const cloned = cloneData(validModules);
|
||||
const tree = buildTree(cloned);
|
||||
sortTree(tree);
|
||||
|
||||
// 树节点 → 菜单项
|
||||
function toMenuItem(node) {
|
||||
const isDir = node.b_module_type === "directory";
|
||||
const item = {
|
||||
key: node.b_route || node.b_code,
|
||||
label: appStore.t(node.b_i18n, node.b_name || node.b_code),
|
||||
};
|
||||
|
||||
if (isDir) {
|
||||
item.icon = resolveIcon(node.b_icon, null);
|
||||
}
|
||||
|
||||
if (node.children?.length) {
|
||||
const children = node.children
|
||||
.map(toMenuItem)
|
||||
.filter(Boolean)
|
||||
.filter(canAccessMenu);
|
||||
if (children.length) {
|
||||
item.children = children;
|
||||
} else {
|
||||
return null; // 无可见子节点的目录,隐藏
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDir) {
|
||||
item.moduleId = node.b_code;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
const roots = tree.map(toMenuItem).filter(Boolean).filter(canAccessMenu);
|
||||
items.push(...roots);
|
||||
|
||||
return items;
|
||||
});
|
||||
@@ -153,14 +124,22 @@ function navigate(key) {
|
||||
if (typeof key === "string" && key.startsWith("/")) router.push(key);
|
||||
}
|
||||
|
||||
function handleItemClick(item) {
|
||||
if (item.children) {
|
||||
if (!effectiveCollapsed.value) {
|
||||
toggleGroup(item.key);
|
||||
function getDropdownItems(children) {
|
||||
return children.map((item) => {
|
||||
if (item.children) {
|
||||
return {
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
popupClassName: "sidebar-submenu-popup",
|
||||
children: getDropdownItems(item.children),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
navigate(item.key);
|
||||
}
|
||||
return { key: item.key, label: item.label };
|
||||
});
|
||||
}
|
||||
|
||||
function dropdownGetContainer() {
|
||||
return document.body;
|
||||
}
|
||||
|
||||
// 展开某节点及其所有祖先
|
||||
@@ -177,17 +156,21 @@ function expandAncestors(items, targetKey) {
|
||||
}
|
||||
}
|
||||
|
||||
// 路由变化时自动展开父级链
|
||||
// 路由变化或菜单数据加载后,自动展开当前路由的父级链
|
||||
watch(
|
||||
() => route.path,
|
||||
(path) => {
|
||||
const current = route.meta.menuKey || path;
|
||||
[() => route.path, menuItems],
|
||||
() => {
|
||||
const current = route.meta.menuKey || route.path;
|
||||
expandAncestors(menuItems.value, current);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => permissionStore.load().catch(() => {}));
|
||||
onMounted(() => {
|
||||
permissionStore.load().catch(() => {});
|
||||
// 预加载图标,确保菜单目录图标的 computed 能正确解析
|
||||
getIconNames();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -216,11 +199,7 @@ onMounted(() => permissionStore.load().catch(() => {}));
|
||||
<template v-if="group.children">
|
||||
<!-- 展开态 -->
|
||||
<template v-if="!effectiveCollapsed">
|
||||
<button
|
||||
type="button"
|
||||
class="sidebar-menu-button"
|
||||
@click="toggleGroup(group.key)"
|
||||
>
|
||||
<button type="button" class="sidebar-menu-button" @click="toggleGroup(group.key)">
|
||||
<component :is="group.icon" v-if="group.icon" class="sidebar-menu-icon" />
|
||||
<span class="sidebar-menu-label">{{ group.label }}</span>
|
||||
<ChevronDown
|
||||
@@ -242,17 +221,19 @@ onMounted(() => permissionStore.load().catch(() => {}));
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 折叠态 -->
|
||||
<!-- 折叠态:弹出浮动菜单 -->
|
||||
<template v-else>
|
||||
<a-tooltip :title="group.label" placement="right">
|
||||
<button
|
||||
type="button"
|
||||
class="sidebar-menu-button collapsed"
|
||||
@click="handleItemClick(group)"
|
||||
>
|
||||
<a-dropdown
|
||||
:trigger="['click']"
|
||||
placement="rightTop"
|
||||
:getPopupContainer="dropdownGetContainer"
|
||||
:menu="{ items: getDropdownItems(group.children), inlineCollapsed: false }"
|
||||
@menuClick="({ key }) => navigate(key)"
|
||||
>
|
||||
<button type="button" class="sidebar-menu-button collapsed">
|
||||
<component :is="group.icon" v-if="group.icon" class="sidebar-menu-icon" />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -281,23 +262,6 @@ onMounted(() => permissionStore.load().catch(() => {}));
|
||||
</template>
|
||||
</template>
|
||||
</nav>
|
||||
|
||||
<!-- 底部折叠按钮 -->
|
||||
<div class="sidebar-footer">
|
||||
<a-tooltip
|
||||
:title="
|
||||
effectiveCollapsed
|
||||
? appStore.t('common.expandSidebar', '展开侧栏')
|
||||
: appStore.t('common.collapseSidebar', '收起侧栏')
|
||||
"
|
||||
placement="right"
|
||||
>
|
||||
<button type="button" class="sidebar-collapse-btn" @click="appStore.toggleSidebar">
|
||||
<PanelLeftOpen v-if="effectiveCollapsed" class="lucide" />
|
||||
<PanelLeftClose v-else class="lucide" />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
</template>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
<template>
|
||||
<header class="top-toolbar" :class="{ 'top-toolbar--hidden': appStore.contentMaximized }">
|
||||
<!-- 折叠按钮 -->
|
||||
<a-tooltip
|
||||
:title="
|
||||
appStore.sidebarCollapsed
|
||||
? appStore.t('common.expandSidebar', '展开侧栏')
|
||||
: appStore.t('common.collapseSidebar', '收起侧栏')
|
||||
"
|
||||
placement="bottom"
|
||||
>
|
||||
<button type="button" class="toolbar-btn toolbar-btn--bordered" @click="appStore.toggleSidebar">
|
||||
<PanelLeftOpen v-if="appStore.sidebarCollapsed" />
|
||||
<PanelLeftClose v-else />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
|
||||
<div class="toolbar-divider" />
|
||||
|
||||
<!-- 面包屑 -->
|
||||
<nav class="toolbar-breadcrumb" aria-label="面包屑导航">
|
||||
<template v-for="(item, index) in breadcrumbItems" :key="item.label">
|
||||
@@ -118,6 +137,10 @@ function confirmLogout() {
|
||||
|
||||
<!-- 右侧操作 -->
|
||||
<div class="toolbar-actions">
|
||||
<span class="toolbar-org">{{ appStore.t("common.company", "全球货运代理有限公司") }}</span>
|
||||
|
||||
<div class="toolbar-divider" />
|
||||
|
||||
<a-tooltip :title="appStore.t('common.switchLanguage', '切换语言')" placement="bottom">
|
||||
<a-dropdown
|
||||
:menu="languageMenu"
|
||||
@@ -151,7 +174,6 @@ function confirmLogout() {
|
||||
<button type="button" class="toolbar-user" aria-label="用户菜单">
|
||||
<a-avatar :size="26"><UserRound /></a-avatar>
|
||||
<span>{{ displayName }}</span>
|
||||
<ChevronDown class="toolbar-user__arrow" />
|
||||
</button>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
@@ -183,9 +205,9 @@ function confirmLogout() {
|
||||
flex: none;
|
||||
height: 44px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
gap: 12px;
|
||||
padding: 0 8px 0 14px;
|
||||
padding: 0;
|
||||
background: #ffffff;
|
||||
|
||||
&--hidden {
|
||||
@@ -225,6 +247,22 @@ function confirmLogout() {
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.toolbar-org {
|
||||
font-size: 12px;
|
||||
color: #71717a;
|
||||
padding: 0 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.toolbar-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
flex: none;
|
||||
margin: 0 6px;
|
||||
background: #e4e4e7;
|
||||
}
|
||||
|
||||
/* ---- 通用操作按钮 ---- */
|
||||
@@ -249,6 +287,10 @@ function confirmLogout() {
|
||||
background: #f4f4f5;
|
||||
}
|
||||
|
||||
&--bordered {
|
||||
border: 1px solid #e4e4e7;
|
||||
}
|
||||
|
||||
.lucide {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@@ -278,12 +320,5 @@ function confirmLogout() {
|
||||
&:hover {
|
||||
background: #f4f4f5;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: #71717a;
|
||||
stroke-width: 2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user