Files
workspace/code/fms/fms-vue/src/layouts/components/AppSidebar.vue
T
2026-07-29 17:33:21 +08:00

196 lines
5.1 KiB
Vue

<script setup>
import { computed, h, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Database, House, PanelLeftClose, PanelLeftOpen, Settings } from "@lucide/vue";
import { useAppStore } from "@/stores/app";
import { usePermissionStore } from "@/stores/permissions";
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const permissionStore = usePermissionStore();
const menuIcon = (icon) => () => h(icon, { size: 16, strokeWidth: 1.8 });
const openKeys = ref(["base", "system"]);
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 canAccessMenu(item) {
if (item.moduleId) return permissionStore.canAccess(item.moduleId);
return true;
}
const menuItems = computed(() => {
const basePages = [
{
key: "/base/othercompany",
label: moduleLabel("base_othercompany", "/base/othercompany", "合作伙伴"),
moduleId: "base_othercompany",
},
].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",
},
].filter(canAccessMenu);
const items = [
{
key: "/dashboard",
icon: menuIcon(House),
label: appStore.t("common.dashboard", "工作台"),
},
];
if (basePages.length)
items.push({
key: "base",
icon: menuIcon(Database),
label: directoryLabel("基础数据"),
children: basePages,
});
if (systemPages.length)
items.push({
key: "system",
icon: menuIcon(Settings),
label: directoryLabel("系统管理"),
children: systemPages,
});
return items;
});
const selectedKeys = computed(() => [route.meta.menuKey || route.path]);
watch(
() => route.path,
(path) => {
if (path.startsWith("/base") && !openKeys.value.includes("base"))
openKeys.value = [...openKeys.value, "base"];
if (path.startsWith("/system") && !openKeys.value.includes("system"))
openKeys.value = [...openKeys.value, "system"];
},
{ immediate: true },
);
function navigate({ key }) {
if (typeof key === "string" && key.startsWith("/")) router.push(key);
}
onMounted(() => permissionStore.load().catch(() => {}));
</script>
<template>
<a-layout-sider
class="app-sider"
:collapsed="effectiveCollapsed"
:trigger="null"
:collapsed-width="56"
collapsible
theme="light"
width="208"
>
<div class="brand" :class="{ 'brand--collapsed': effectiveCollapsed }">
<div class="brand__mark">F</div>
<strong>{{ appStore.t("common.appName", "FMS 管理系统") }}</strong>
</div>
<div class="sider-menu">
<a-menu
mode="inline"
:items="menuItems"
:selected-keys="selectedKeys"
:open-keys="openKeys"
@click="navigate"
@open-change="openKeys = $event"
/>
</div>
<div class="sider-footer">
<a-tooltip
:title="
effectiveCollapsed
? appStore.t('common.expandSidebar', '展开侧栏')
: appStore.t('common.collapseSidebar', '收起侧栏')
"
placement="right"
><a-button type="text" class="sider-collapse" @click="appStore.toggleSidebar"
><PanelLeftOpen v-if="effectiveCollapsed" /><PanelLeftClose v-else /></a-button
></a-tooltip>
</div>
</a-layout-sider>
</template>
<style scoped lang="scss">
.app-sider {
height: 100%;
overflow: hidden;
border-right: 1px solid var(--fms-border);
:deep(.ant-layout-sider-children) {
display: flex;
height: 100%;
flex-direction: column;
}
:deep(.ant-menu) {
border-inline-end: 0;
}
:deep(.ant-menu-item),
:deep(.ant-menu-submenu-title) {
width: 100%;
height: 36px;
margin: 0;
border-radius: 0;
line-height: 36px;
}
}
.brand {
display: flex;
min-height: 44px;
align-items: center;
gap: 8px;
padding: 6px 12px 6px 15px;
overflow: hidden;
white-space: nowrap;
}
.brand__mark {
display: grid;
flex: none;
width: 26px;
height: 26px;
place-items: center;
border-radius: 6px;
background: #1677ff;
color: #fff;
font-weight: 700;
}
.brand strong {
font-size: 14px;
}
.brand--collapsed strong {
display: none;
}
.sider-menu {
flex: 1;
min-height: 0;
overflow: auto;
}
.sider-footer {
padding: 4px;
border-top: 1px solid var(--fms-border);
}
.sider-collapse {
width: 100%;
}
</style>