ow
This commit is contained in:
1 parent
61869f1af0
commit
9344b56334
5 files changed
+626
-10
No files matched your search
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<nav class="dash-nav">
|
||||||
|
<a v-for="n in items" :key="n.key" class="nav-item" @click="onClick(n.key)">
|
||||||
|
<cust-icon-font :type="n.icon" :size="22" />
|
||||||
|
<span>{{ n.label }}</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
type NavItem = { key: string; label: string; icon: string };
|
||||||
|
|
||||||
|
defineProps<{ items: NavItem[] }>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "jump", key: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const onClick = (key: string) => emit("jump", key);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.dash-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
height: 52px;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 宽屏时与卡片区(1080px)对齐,避免图标被拉得太散 */
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.dash-nav {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 字号对齐 ft 体系:辅助文字 12px(同 .ft-item-label / .ft-chip) */
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
color: #8a919f;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.16s;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active {
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<section class="grp-card">
|
||||||
|
<!-- 封面:有 cover 用图片,否则用分组主题色渐变 + 大号水印图标 -->
|
||||||
|
<div class="grp-cover" :class="{ 'has-image': !!cover, 'grp-test': !cover }" :style="coverStyle">
|
||||||
|
<cust-icon-font v-if="!cover" class="grp-cover-mark" :type="icon" :size="132" />
|
||||||
|
<span class="grp-badge">{{ badge || name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 功能列表:逐行排列,左侧软色图标 + 标题副标题 + 右侧圆形分组图标 -->
|
||||||
|
<div class="grp-list">
|
||||||
|
<a v-for="it in items" :key="it.FunctionName" class="grp-row" :title="it.FunctionLocalName"
|
||||||
|
@click="onClick(it.ApiRoute)">
|
||||||
|
<span class="row-icon" :style="{ background: soft, color }">
|
||||||
|
<cust-icon-font :type="it.Icon || icon" :size="20" />
|
||||||
|
</span>
|
||||||
|
<span class="row-text">
|
||||||
|
<span class="row-title">{{ it.FunctionLocalName }}</span>
|
||||||
|
<span v-if="subtitleOf(it)" class="row-sub">{{ subtitleOf(it) }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="row-glyph" :style="{ background: soft, color }">
|
||||||
|
<cust-icon-font :type="icon" :size="18" />
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type { SecurityFunctionDisplayData } from "@/data/auto/securityFunctionDisplayData";
|
||||||
|
|
||||||
|
/** 单项可额外携带 subtitle,用于封面卡里的两行文案 */
|
||||||
|
export type GroupCardItem = SecurityFunctionDisplayData & { subtitle?: string };
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
name: string;
|
||||||
|
/** 分组主题色:图标着色、无封面时的渐变封面 */
|
||||||
|
color: string;
|
||||||
|
/** 主题浅色:图标底色 */
|
||||||
|
soft: string;
|
||||||
|
icon: string;
|
||||||
|
/** 兼容旧参数:仅作为布局提示,当前统一为纵向列表 */
|
||||||
|
layout?: 2 | 3;
|
||||||
|
/** 封面图片地址;缺省时用主题色渐变占位 */
|
||||||
|
cover?: string;
|
||||||
|
/** 封面左上角标签;缺省时显示分组名称 */
|
||||||
|
badge?: string;
|
||||||
|
items: GroupCardItem[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "navigate", route: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const coverStyle = computed(() =>
|
||||||
|
props.cover
|
||||||
|
? { backgroundImage: `url(${props.cover})` }
|
||||||
|
: { background: `linear-gradient(135deg, ${props.color} 0%, ${props.soft} 100%)` }
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 副标题优先取自定义 subtitle,其次回落英文名;都为空则不渲染该行 */
|
||||||
|
const subtitleOf = (it: GroupCardItem) => (it.subtitle || it.FunctionEngName || "").trim();
|
||||||
|
|
||||||
|
const onClick = (route?: string) => {
|
||||||
|
if (route) emit("navigate", route);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.grp-card {
|
||||||
|
overflow: visible;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 封面 ==================== */
|
||||||
|
.grp-cover {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 16 / 10;
|
||||||
|
max-height: 360px;
|
||||||
|
border-radius: 18px 18px 0 0;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.grp-test {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 40 / 10;
|
||||||
|
max-height: 360px;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grp-cover-mark {
|
||||||
|
position: absolute;
|
||||||
|
right: -8px;
|
||||||
|
bottom: -20px;
|
||||||
|
z-index: 3;
|
||||||
|
color: rgba(255, 255, 255, 0.32);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grp-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 12px;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.28);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 36px;
|
||||||
|
line-height: 58px;
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 列表 ==================== */
|
||||||
|
.grp-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 0 0 18px 18px;
|
||||||
|
background: #b9cbee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grp-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #fff;
|
||||||
|
color: inherit;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
transition:
|
||||||
|
transform 0.16s ease,
|
||||||
|
box-shadow 0.16s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-icon {
|
||||||
|
flex: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-text {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2329;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a919f;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-glyph {
|
||||||
|
flex: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.grp-card {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
<template>
|
||||||
|
<div class="hero-shell">
|
||||||
|
<section class="hero">
|
||||||
|
<div class="hero-inner">
|
||||||
|
<div class="hero-top">
|
||||||
|
<div class="brand">
|
||||||
|
<a-avatar
|
||||||
|
shape="square"
|
||||||
|
:size="36"
|
||||||
|
src="/youjia.svg"
|
||||||
|
class="brand-mark"
|
||||||
|
/>
|
||||||
|
<span>有家相册</span>
|
||||||
|
</div>
|
||||||
|
<a-dropdown placement="bottomRight" :trigger="['click']">
|
||||||
|
<div class="avatar" :title="userName">{{ avatarText }}</div>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu @click="onUserMenuClick">
|
||||||
|
<a-menu-item key="amentUserLocalName">
|
||||||
|
<UserOutlined />
|
||||||
|
{{ t("layout.header.amentUserLocalName") }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="amentPassword">
|
||||||
|
<LockOutlined />
|
||||||
|
{{ t("layout.header.amentPassword") }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="biometricButton">
|
||||||
|
<LockOutlined />
|
||||||
|
{{ biometricButtonLabel }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-divider />
|
||||||
|
<a-menu-item key="logout">
|
||||||
|
<LogoutOutlined />
|
||||||
|
{{ t("layout.header.logout") }}
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="hero-card">
|
||||||
|
<div class="hello">你好,{{ userName }} <span class="wave">👋</span></div>
|
||||||
|
<div class="sub">欢迎回家,记录新美好。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<changePassword v-model:showModel="showChangePassword" />
|
||||||
|
<changeLocalName v-model:showModel="showChangeUserLocalName" />
|
||||||
|
<changeBiometricEnrollCard v-model:showModel="showBiometricEnroll" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { notification } from "ant-design-vue";
|
||||||
|
import type { MenuProps } from "ant-design-vue";
|
||||||
|
import { LockOutlined, LogoutOutlined, UserOutlined } from "@ant-design/icons-vue";
|
||||||
|
import authService from "@/service/security/authService";
|
||||||
|
import { useLoginProfileStore } from "@/store/modules/loginProfile";
|
||||||
|
import i18n from "@/locales";
|
||||||
|
import changePassword from "@/views/login/changePassword.vue";
|
||||||
|
import changeLocalName from "@/views/login/changeLocalName.vue";
|
||||||
|
import changeBiometricEnrollCard from "@/views/login/changeBiometricEnrollCard.vue";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const profile = useLoginProfileStore();
|
||||||
|
const { t } = i18n.global;
|
||||||
|
|
||||||
|
const userName = computed(
|
||||||
|
() => profile.getUserLocalName() || profile.getUsername() || "新朋友"
|
||||||
|
);
|
||||||
|
const avatarText = computed(() => {
|
||||||
|
const u = (userName.value || "U").trim();
|
||||||
|
return u ? u.slice(-1).toUpperCase() : "U";
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ==================== 头像菜单 ==================== */
|
||||||
|
/* 欢迎页使用独立外壳(layout/home.vue),没有原顶栏,账号操作挂到右上角头像上 */
|
||||||
|
const showChangePassword = ref<boolean>(false);
|
||||||
|
const showChangeUserLocalName = ref<boolean>(false);
|
||||||
|
const showBiometricEnroll = ref<boolean>(false);
|
||||||
|
|
||||||
|
// Coarse platform detection so the button label is friendly on each device.
|
||||||
|
const biometricButtonLabel = computed(() => {
|
||||||
|
const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
|
||||||
|
if (/iPhone|iPad/.test(ua)) return "Sign in with Face ID / Touch ID";
|
||||||
|
if (/Android/.test(ua)) return "Sign in with fingerprint";
|
||||||
|
if (/Windows/.test(ua)) return "Sign in with Windows Hello";
|
||||||
|
if (/Mac OS X/.test(ua)) return "Sign in with Touch ID";
|
||||||
|
return "Sign in with fingerprint / face";
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUserMenuClick: MenuProps["onClick"] = (menuInfo) => {
|
||||||
|
if (menuInfo.key === "amentUserLocalName") {
|
||||||
|
showChangeUserLocalName.value = true;
|
||||||
|
} else if (menuInfo.key === "amentPassword") {
|
||||||
|
showChangePassword.value = true;
|
||||||
|
} else if (menuInfo.key === "biometricButton") {
|
||||||
|
showBiometricEnroll.value = true;
|
||||||
|
} else if (menuInfo.key === "logout") {
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
await authService.Logout();
|
||||||
|
profile.clearStorage();
|
||||||
|
notification.info({
|
||||||
|
message: t("layout.notification.message"),
|
||||||
|
description: t("layout.notification.description"),
|
||||||
|
placement: "topRight",
|
||||||
|
});
|
||||||
|
router.push("/");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
/* 渐变头图:只承载顶栏(品牌+头像),不再包含卡片,避免被卡片内容撑高 */
|
||||||
|
.hero {
|
||||||
|
padding: 18px 16px 36px;
|
||||||
|
border-radius: 0 0 22px 22px;
|
||||||
|
background: linear-gradient(135deg, #1677ff 0%, #36cfc9 100%);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 6px 18px rgba(22, 119, 255, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hero 的外壳,把 hero 与 hero-card 编为一组,作为页面里的非伸缩块 */
|
||||||
|
.hero-shell {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-inner {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上层半透明卡片:放在 hero 外面,用负 margin-top 叠到 hero 底部。
|
||||||
|
顶部与 hero 重叠处保持半透明让渐变透出,下方不透明避免露出页面灰底 */
|
||||||
|
.hero-card {
|
||||||
|
margin: -24px 16px 18px;
|
||||||
|
padding: 20px 18px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(255, 255, 255, 0.55) 0%,
|
||||||
|
rgba(255, 255, 255, 0.6) 30%,
|
||||||
|
rgba(255, 255, 255, 0.95) 45%,
|
||||||
|
rgba(255, 255, 255, 0.96) 100%
|
||||||
|
);
|
||||||
|
backdrop-filter: blur(18px) saturate(160%);
|
||||||
|
-webkit-backdrop-filter: blur(18px) saturate(160%);
|
||||||
|
box-shadow:
|
||||||
|
0 14px 34px rgba(15, 23, 42, 0.14),
|
||||||
|
0 4px 12px rgba(15, 23, 42, 0.08);
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-mark {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: rgba(255, 255, 255, 0.22);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
color: #1677ff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-left: 60px;
|
||||||
|
font-size: 20px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.hero {
|
||||||
|
padding: 22px 20px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
margin: -28px 20px 22px;
|
||||||
|
padding: 22px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
/* 宽屏下把 hero + card 整体居中到 1080,避免渐变条在大屏拉得过宽 */
|
||||||
|
.hero-shell {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-inner {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
padding: 24px 24px 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
margin: -28px 24px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home-layout">
|
||||||
|
<!-- 内容区:整屏除底部信息条外全部留给页面 -->
|
||||||
|
<div class="home-viewport">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { WebSitePolicy } from "@/policy/custom/webSitePolicy";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 欢迎页(/dashboard)专用外壳。
|
||||||
|
* 不套用后台的侧边菜单 / 顶栏 / 面包屑,整屏交给页面自己排版:
|
||||||
|
* home-layout(列:内容 + 信息条,100dvh 不出现窗口滚动条)
|
||||||
|
* └ home-viewport(自适应剩余高度,内容由页面内部滚动)
|
||||||
|
*/
|
||||||
|
const copy: string = WebSitePolicy.Copy;
|
||||||
|
const beian: string = WebSitePolicy.Beian;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.home-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f6f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-viewport {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-footer {
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 10px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-top: 1px solid #eef0f4;
|
||||||
|
background: #fff;
|
||||||
|
color: #8a919f;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #8a919f;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.16s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
<!-- 功能分组:卡片外壳 / 标题字号沿用 ft-page 体系 -->
|
<!-- 功能分组:卡片外壳 / 标题字号沿用 ft-page 体系 -->
|
||||||
<div class="page-main ft-page">
|
<div class="page-main ft-page">
|
||||||
<DashboardGroupCard v-for="grp in groups" :id="'grp-' + grp.key" :key="grp.key" :name="grp.name"
|
<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" :items="grp.items"
|
:color="grp.color" :soft="grp.soft" :icon="grp.icon" :layout="grp.layout" :cover="grp.cover"
|
||||||
@navigate="onpush" />
|
:badge="grp.badge" :items="grp.items" @navigate="onpush" />
|
||||||
|
|
||||||
<a-empty v-if="!groups.length && !loading" description="没有匹配的功能" />
|
<a-empty v-if="!groups.length && !loading" description="没有匹配的功能" />
|
||||||
</div>
|
</div>
|
||||||
@@ -27,7 +27,7 @@ import { useRouter } from "vue-router";
|
|||||||
import securityUserService from "@/service/security/securityUserService";
|
import securityUserService from "@/service/security/securityUserService";
|
||||||
import type { SecurityFunctionDisplayData } from "@/data/auto/securityFunctionDisplayData";
|
import type { SecurityFunctionDisplayData } from "@/data/auto/securityFunctionDisplayData";
|
||||||
import DashboardHero from "@/components/dashboard/DashboardHero.vue";
|
import DashboardHero from "@/components/dashboard/DashboardHero.vue";
|
||||||
import DashboardGroupCard from "@/components/dashboard/DashboardGroupCard.vue";
|
import DashboardGroupCard, { type GroupCardItem } from "@/components/dashboard/DashboardGroupCard.vue";
|
||||||
import DashboardBottomNav from "@/components/dashboard/DashboardBottomNav.vue";
|
import DashboardBottomNav from "@/components/dashboard/DashboardBottomNav.vue";
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
@@ -51,13 +51,18 @@ const groupDefs: {
|
|||||||
soft: string;
|
soft: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
layout: 2 | 3;
|
layout: 2 | 3;
|
||||||
|
/** 封面图片地址(可选),缺省时由卡片用主题色渐变占位 */
|
||||||
|
cover?: string;
|
||||||
|
/** 封面左上角标签(可选),缺省时显示分组名称 */
|
||||||
|
badge?: string;
|
||||||
}[] = [
|
}[] = [
|
||||||
{ key: "disk", name: "网盘", color: "#1677ff", soft: "#e6f4ff", icon: "dbank", layout: 2 },
|
{ key: "disk", name: "网盘", color: "#1677ff", soft: "#e6f4ff", icon: "dbank", layout: 2},
|
||||||
{ key: "share", name: "共享中心", color: "#fa8c16", soft: "#fff7e6", icon: "dgroup", 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: "photo", name: "相册", color: "#722ed1", soft: "#f9f0ff", icon: "picture", layout: 3 },
|
||||||
{ key: "audio", name: "音乐", color: "#eb2f96", soft: "#fff0f6", icon: "foldermp3", 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: "video", name: "视频", color: "#13c2c2", soft: "#e6fffb", icon: "foldervideo", layout: 3 },
|
||||||
{ key: "family", name: "族谱管理", color: "#52c41a", soft: "#f6ffed", icon: "family", layout: 2 },
|
{ key: "family", name: "族谱管理", color: "#52c41a", soft: "#f6ffed", icon: "family", layout: 2},
|
||||||
{ key: "other", name: "更多", color: "#8a919f", soft: "#f5f6f8", icon: "gailan", layout: 3 },
|
{ key: "other", name: "更多", color: "#8a919f", soft: "#f5f6f8", icon: "gailan", layout: 3 },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -86,19 +91,46 @@ const GROUP_OF: Record<string, GroupKey> = {
|
|||||||
const resolveGroup = (it: SecurityFunctionDisplayData): GroupKey =>
|
const resolveGroup = (it: SecurityFunctionDisplayData): GroupKey =>
|
||||||
GROUP_OF[it.FunctionName] ?? "other";
|
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 groups = computed(() => {
|
||||||
const taken = new Set<string>();
|
const taken = new Set<string>();
|
||||||
const result: { key: GroupKey; name: string; color: string; soft: string; icon: string; layout: 2 | 3; items: SecurityFunctionDisplayData[] }[] = [];
|
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) {
|
for (const g of groupDefs) {
|
||||||
const items: SecurityFunctionDisplayData[] = [];
|
const items: GroupCardItem[] = [];
|
||||||
for (const it of allItems.value) {
|
for (const it of allItems.value) {
|
||||||
if (resolveGroup(it) !== g.key) continue;
|
if (resolveGroup(it) !== g.key) continue;
|
||||||
const label = it.FunctionLocalName || it.FunctionName;
|
const label = it.FunctionLocalName || it.FunctionName;
|
||||||
if (taken.has(label)) continue;
|
if (taken.has(label)) continue;
|
||||||
taken.add(label);
|
taken.add(label);
|
||||||
items.push(it);
|
items.push({ ...it, subtitle: resolveSubtitle(it) });
|
||||||
}
|
}
|
||||||
if (items.length) result.push({ ...g, items });
|
if (items.length) result.push({ ...g, items });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user