This commit is contained in:
itcsx committed 2026-09-23 13:24:49 +08:00
1 parent eb7a9e9b88
commit fd51982804
7 files changed
+242 -11

No files matched your search

+211
View File
@@ -0,0 +1,211 @@
<template>
<section class="hero">
<div class="hero-inner">
<div class="hero-top">
<div class="brand" :class="{ 'is-clickable': clickable }" @click="onBrandClick">
<a-avatar
shape="square"
:size="36"
src="/youjia.svg"
class="brand-mark"
/>
<span>{{ title }}</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>
<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";
/**
* 欢迎页顶部那一条「品牌 + 头像」的渐变头图。
*
* 欢迎页(DashboardHero)与后台布局(layout/index.vue、layout/master.vue)共用它,
* 两处顶栏才会长得一模一样。
*/
const props = withDefaults(
defineProps<{
/** 品牌名 */
title?: string;
/** 品牌名是否可点(后台布局点了回工作台) */
clickable?: boolean;
}>(),
{ title: "有家相册", clickable: false },
);
const emit = defineEmits<{ (e: "brand-click"): void }>();
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";
});
const onBrandClick = () => {
if (!props.clickable) return;
emit("brand-click");
};
/* ==================== 头像菜单 ==================== */
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 完全一致 */
.hero {
/* 通栏:不管父容器是 block 还是 flex,渐变条都要铺满整宽 */
width: 100%;
box-sizing: border-box;
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-inner {
margin: 0 auto;
}
.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;
&.is-clickable {
cursor: pointer;
}
}
.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;
}
@media (min-width: 576px) {
.hero {
padding: 22px 20px 40px;
}
}
@media (min-width: 1200px) {
.hero-inner {
max-width: 1080px;
margin-right: auto;
margin-left: auto;
}
.hero {
padding: 24px 24px 44px;
}
}
</style>