272 lines
10 KiB
Vue
272 lines
10 KiB
Vue
<template>
|
||
<a-spin :spinning="loading" :delay="200">
|
||
<div class="page-container dash-page">
|
||
<!-- 顶部欢迎区:通栏,滚动时随内容一起上移 -->
|
||
<DashboardHero />
|
||
|
||
<!-- 功能分组:卡片外壳 / 标题字号沿用 ft-page 体系 -->
|
||
<div class="page-main ft-page">
|
||
<DashboardGroupCard v-for="grp in groups" :id="'grp-' + grp.key" :key="grp.key" :name="grp.name"
|
||
:color="grp.color" :soft="grp.soft" :icon="grp.icon" :layout="grp.layout" :cover="grp.cover"
|
||
:badge="grp.badge" :items="grp.items" @navigate="onpush" @help="onHelp" />
|
||
|
||
<a-empty v-if="!groups.length && !loading" description="没有匹配的功能" />
|
||
</div>
|
||
|
||
<!-- 新手帮助:统一挂在页面上,由行尾图标触发,按 FunctionName 分发不同文档 -->
|
||
<HelpTimelineModal v-model:open="helpOpen" :function-name="helpTarget?.FunctionName"
|
||
:title="helpTarget?.FunctionLocalName || helpTarget?.FunctionName" :color="helpColor" :soft="helpSoft"
|
||
:icon="helpIcon" />
|
||
|
||
<!-- 底部快捷栏:与其它页面一致,固定在底部 -->
|
||
<div class="page-footer dash-footer">
|
||
<DashboardBottomNav :items="navItems" @jump="onNavClick" />
|
||
</div>
|
||
</div>
|
||
</a-spin>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, computed, onMounted } from "vue";
|
||
import { useRouter } from "vue-router";
|
||
import securityUserService from "@/service/security/securityUserService";
|
||
import type { SecurityFunctionDisplayData } from "@/data/auto/securityFunctionDisplayData";
|
||
import DashboardHero from "@/components/dashboard/DashboardHero.vue";
|
||
import DashboardGroupCard, { type GroupCardItem } from "@/components/dashboard/DashboardGroupCard.vue";
|
||
import DashboardBottomNav from "@/components/dashboard/DashboardBottomNav.vue";
|
||
import HelpTimelineModal from "@/components/basic/HelpTimelineModal.vue";
|
||
|
||
interface NavItem {
|
||
key: string;
|
||
label: string;
|
||
icon: string;
|
||
}
|
||
|
||
const router = useRouter();
|
||
|
||
const loading = ref(false);
|
||
const allItems = ref<SecurityFunctionDisplayData[]>([]);
|
||
|
||
/* ==================== 分组定义 ==================== */
|
||
type GroupKey = "disk" | "share" | "photo" | "audio" | "video" | "family" | "other";
|
||
|
||
const groupDefs: {
|
||
key: GroupKey;
|
||
name: string;
|
||
color: string;
|
||
soft: string;
|
||
icon: string;
|
||
layout: 2 | 3;
|
||
/** 封面图片地址(可选),缺省时由卡片用主题色渐变占位 */
|
||
cover?: string;
|
||
/** 封面左上角标签(可选),缺省时显示分组名称 */
|
||
badge?: string;
|
||
}[] = [
|
||
{ key: "disk", name: "网盘", color: "#1677ff", soft: "#e6f4ff", icon: "dbank", layout: 2},
|
||
{ key: "share", name: "共享中心", color: "#fa8c16", soft: "#fff7e6", icon: "dgroup", layout: 2},
|
||
// { key: "photo", name: "相册", color: "#722ed1", soft: "#f9f0ff", icon: "picture", layout: 3, cover: "/images/main/p4.png" },
|
||
{ key: "photo", name: "相册", color: "#722ed1", soft: "#f9f0ff", icon: "picture", layout: 3 },
|
||
{ key: "audio", name: "音乐", color: "#eb2f96", soft: "#fff0f6", icon: "foldermp3", layout: 3},
|
||
{ key: "video", name: "视频", color: "#13c2c2", soft: "#e6fffb", icon: "foldervideo", layout: 3 },
|
||
{ key: "family", name: "族谱管理", color: "#52c41a", soft: "#f6ffed", icon: "family", layout: 2},
|
||
{ key: "other", name: "更多", color: "#8a919f", soft: "#f5f6f8", icon: "gailan", layout: 3 },
|
||
];
|
||
|
||
const GROUP_OF: Record<string, GroupKey> = {
|
||
DmsFileEnquiry: "disk",
|
||
DmsFileDeleteEnquiry: "disk",
|
||
FamilyGroupShare: "share",
|
||
FamilyShareOperation: "share",
|
||
FamilyGroupOperation: "share",
|
||
FamilyGroupEnquiry: "share",
|
||
Preview: "photo",
|
||
PublishPhotoOperation: "photo",
|
||
PublishPhotoEnquiry: "photo",
|
||
PreviewAudio: "audio",
|
||
PublishAudioOperation: "audio",
|
||
PublishAudioEnquiry: "audio",
|
||
PreviewVideo: "video",
|
||
PublishVideoOperation: "video",
|
||
PublishVideoEnquiry: "video",
|
||
FamilyPersonEnquiry: "family",
|
||
FamilyPersonOperation: "family",
|
||
FamilyTreeEnquiry: "family",
|
||
FamilyTreeOperation: "family",
|
||
};
|
||
|
||
const resolveGroup = (it: SecurityFunctionDisplayData): GroupKey =>
|
||
GROUP_OF[it.FunctionName] ?? "other";
|
||
|
||
/* ==================== 自定义副标题 ==================== */
|
||
/** 留空时卡片会自动回退到 FunctionEngName */
|
||
const SUBTITLE_OF: Record<string, string> = {
|
||
DmsFileEnquiry: "",
|
||
DmsFileDeleteEnquiry: "",
|
||
FamilyGroupShare: "",
|
||
FamilyShareOperation: "",
|
||
FamilyGroupOperation: "",
|
||
FamilyGroupEnquiry: "",
|
||
Preview: "",
|
||
PublishPhotoOperation: "",
|
||
PublishPhotoEnquiry: "",
|
||
PreviewAudio: "",
|
||
PublishAudioOperation: "",
|
||
PublishAudioEnquiry: "",
|
||
PreviewVideo: "",
|
||
PublishVideoOperation: "",
|
||
PublishVideoEnquiry: "",
|
||
FamilyPersonEnquiry: "",
|
||
FamilyPersonOperation: "",
|
||
FamilyTreeEnquiry: "",
|
||
FamilyTreeOperation: "",
|
||
};
|
||
|
||
const resolveSubtitle = (it: SecurityFunctionDisplayData): string =>
|
||
SUBTITLE_OF[it.FunctionName] ?? "";
|
||
|
||
/* ==================== 分组结果 ==================== */
|
||
const groups = computed(() => {
|
||
const taken = new Set<string>();
|
||
const result: { key: GroupKey; name: string; color: string; soft: string; icon: string; layout: 2 | 3; cover?: string; badge?: string; items: GroupCardItem[] }[] = [];
|
||
|
||
for (const g of groupDefs) {
|
||
const items: GroupCardItem[] = [];
|
||
for (const it of allItems.value) {
|
||
if (resolveGroup(it) !== g.key) continue;
|
||
const label = it.FunctionLocalName || it.FunctionEngName;
|
||
if (taken.has(label)) continue;
|
||
taken.add(label);
|
||
items.push({ ...it, subtitle: resolveSubtitle(it) });
|
||
}
|
||
if (items.length) result.push({ ...g, items });
|
||
}
|
||
return result;
|
||
});
|
||
|
||
/** 底部快捷栏只保留五大高频域 */
|
||
const navItems = computed<NavItem[]>(() =>
|
||
groupDefs
|
||
.filter(g => ["disk", "photo", "audio", "video", "family"].includes(g.key))
|
||
.map(g => {
|
||
let label = g.name;
|
||
if (label === "共享中心") label = "共享";
|
||
if (label === "族谱管理") label = "族谱";
|
||
return { key: g.key, label, icon: g.icon };
|
||
})
|
||
);
|
||
|
||
/**
|
||
* 按左侧菜单的顺序排列。
|
||
*
|
||
* Orderby 只在「同一个父节点下」才是连续有意义的序号,跨父组直接比大小会错:
|
||
* 某个父组(比如从「有家网盘」拆出来的组)内部可能从 0 重新计数,
|
||
* 它的子项就会被平铺排序甩到全局最前面。
|
||
*
|
||
* 所以这里先按父节点自身的序号把「组」排好,再在组内按 Orderby 排,
|
||
* 平铺出来的先后就和菜单的树形展开顺序一致。
|
||
*/
|
||
const sortByMenuOrder = (list: SecurityFunctionDisplayData[]): SecurityFunctionDisplayData[] => {
|
||
// 每个节点自己的序号,用作它作为「父」时的排序依据
|
||
const parentOrder = new Map<string, number>();
|
||
list.forEach((it) => {
|
||
if (it.FunctionName) parentOrder.set(it.FunctionName, it.Orderby ?? 0);
|
||
});
|
||
|
||
/** 有父节点就跟着父走,没有(顶层)就用自己的序号 */
|
||
const groupOrder = (it: SecurityFunctionDisplayData): number => {
|
||
if (it.FunctionNameParent && parentOrder.has(it.FunctionNameParent)) {
|
||
return parentOrder.get(it.FunctionNameParent)!;
|
||
}
|
||
return it.Orderby ?? 0;
|
||
};
|
||
|
||
return [...list].sort(
|
||
(a, b) => groupOrder(a) - groupOrder(b) || (a.Orderby ?? 0) - (b.Orderby ?? 0)
|
||
);
|
||
};
|
||
|
||
/* ==================== 交互 ==================== */
|
||
const onpush = (apiRoute: string) => {
|
||
if (apiRoute) router.push(apiRoute);
|
||
};
|
||
const onNavClick = (key: string) => {
|
||
document.getElementById("grp-" + key)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||
};
|
||
|
||
/* ==================== 新手帮助弹窗 ==================== */
|
||
const helpOpen = ref(false);
|
||
/** 当前请求帮助的功能项,决定 HelpTimelineModal 加载哪份文档 */
|
||
const helpTarget = ref<GroupCardItem | null>(null);
|
||
|
||
/** 帮助弹窗沿用所属分组的主题色,与卡片保持视觉一致 */
|
||
const helpTheme = computed(() => {
|
||
const key = helpTarget.value ? resolveGroup(helpTarget.value) : undefined;
|
||
return groupDefs.find(g => g.key === key) ?? groupDefs[groupDefs.length - 1];
|
||
});
|
||
const helpColor = computed(() => helpTheme.value.color);
|
||
const helpSoft = computed(() => helpTheme.value.soft);
|
||
const helpIcon = computed(() => helpTheme.value.icon);
|
||
|
||
const onHelp = (it: GroupCardItem) => {
|
||
helpTarget.value = it;
|
||
helpOpen.value = true;
|
||
};
|
||
|
||
onMounted(async () => {
|
||
loading.value = true;
|
||
try {
|
||
const fd = await securityUserService.FindMenuByUsername();
|
||
const raw = fd?.SecurityFunctionDisplayDatas ?? [];
|
||
|
||
// 有些功能会挂在「不存在的父节点」下(例如测试残留的 abc),
|
||
// 它和正式记录同名却属于另一条,按 Orderby 排序时会窜到最前面,
|
||
// 表现为首页位置与左侧菜单对不上、改了后台顺序也不生效。
|
||
// 这里剔掉这类孤立记录,只保留父节点真实存在的。
|
||
const knownFunctions = new Set(raw.map(it => it.FunctionName));
|
||
allItems.value = sortByMenuOrder(
|
||
raw.filter(
|
||
it =>
|
||
it.FunctionIndc !== "N" &&
|
||
(it.PageUrl?.indexOf("master") ?? -1) > 0 &&
|
||
(!it.FunctionNameParent || knownFunctions.has(it.FunctionNameParent))
|
||
)
|
||
);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
/* a-spin 会插入两层容器,把高度透传下去,height: 100% 才会生效 */
|
||
.ant-spin-nested-loading {
|
||
height: 100%;
|
||
}
|
||
|
||
:deep(.ant-spin-container) {
|
||
height: 100%;
|
||
}
|
||
|
||
/* 独立外壳(layout/home.vue)下没有侧边菜单与面包屑,页面自己撑满整屏 */
|
||
.dash-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
/* 卡片区独占剩余高度并内部滚动,底栏因此始终贴底 */
|
||
.dash-page .page-main {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
/* 底栏已是列内固定项,不再需要为 fixed 预留的 96px */
|
||
padding-bottom: 12px;
|
||
}
|
||
|
||
/* 底部快捷栏:沿用 .page-footer,仅取消 fixed,改由 flex 列天然贴底 */
|
||
.dash-footer {
|
||
position: relative;
|
||
bottom: auto;
|
||
flex: none;
|
||
padding: 0;
|
||
}
|
||
</style> |