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>
|
||||
Reference in new issue
Block a user