From efd55a3a75a47365875533542fb87d9e648ca2c6 Mon Sep 17 00:00:00 2001 From: chenxing Date: Tue, 6 Oct 2026 20:16:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(avatar):=20=E9=A1=B6=E6=A0=8F=E4=B8=89?= =?UTF-8?q?=E6=80=81=E5=A4=B4=E5=83=8F=E6=8E=A5=E5=85=A5=EF=BC=88YschTheme?= =?UTF-8?q?=20=E9=BB=98=E8=AE=A4=E5=A4=B4=E5=83=8F=E8=BD=AE=20sync=20?= =?UTF-8?q?=E4=BB=B6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/utils/avatar.ts + src/components/UserAvatar.vue:YschTheme sync 下发公共源(取值链 URL > yh-avatar > yh-avatar-sch-default > text,值域 text|silhouette) - layout.vue:顶栏 img 头像换 UserAvatar 组件(placement=brand,size=28);头像/姓名点击统一跳 UserHome 设置页「个人信息」pane(原 /v/userinfo 已下线语义) --- src/components/UserAvatar.vue | 93 ++++++++++++++++++++++++++++++++++ src/components/layout.vue | 6 ++- src/utils/avatar.ts | 94 +++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 src/components/UserAvatar.vue create mode 100644 src/utils/avatar.ts diff --git a/src/components/UserAvatar.vue b/src/components/UserAvatar.vue new file mode 100644 index 0000000..aefb0b1 --- /dev/null +++ b/src/components/UserAvatar.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/src/components/layout.vue b/src/components/layout.vue index 93f0dc8..dd706e5 100644 --- a/src/components/layout.vue +++ b/src/components/layout.vue @@ -5,6 +5,7 @@ import { useRoute, useRouter } from "vue-router"; import { Message } from "view-ui-plus"; import useCurrentInstance from "@/utils/hook/useCurrentInstance"; import SvgIcon from "@/components/SvgIcon.vue"; +import UserAvatar from "@/components/UserAvatar.vue"; let props = defineProps<{ layoutData: LayoutData }>(); const reload: Function = inject("reload") || new Function("console.error('empty')"); @@ -138,7 +139,8 @@ const linkMenu = (page: string) => { }; const linkUserInfo = () => { - window.location.href = baseHost + "/v/userinfo"; + // 头像/姓名点击统一跳 UserHome 设置页「个人信息」pane(默认头像轮 2026-09-30,原 /v/userinfo 已下线语义) + window.location.href = baseHost + "/userhome/front/index.html#/settings?pane=profile"; }; const logout = () => { let url = window.location.href; @@ -174,7 +176,7 @@ function linkHomePage() {

{{ appName }}

- + {{ userInfo.name }} diff --git a/src/utils/avatar.ts b/src/utils/avatar.ts new file mode 100644 index 0000000..40d5bda --- /dev/null +++ b/src/utils/avatar.ts @@ -0,0 +1,94 @@ +/** + * 默认头像样式(首字 / 剪影)公共源(YschTheme,scripts/sync-apps.mjs 下发三端 src/utils/avatar.ts)。 + * 取值链与主题同构(theme-core.js resolveEffectiveThemeState 的合成链写法): + * URL ?yh-avatar=text|silhouette(仅预览/排障覆盖:只影响读、绝不写回) + * > localStorage["yh-avatar"](用户自选,纯字符串) + * > localStorage["yh-avatar-sch-default"](学校默认,UserHome 登录后写入) + * > "text"(系统默认) + * 值域恒为 "text" | "silhouette",键缺失/值非法均静默回落下一级,不报错不阻塞渲染。 + * 渲染三态与取字规则的唯一契约见 UserHome《设置页修复与默认头像落地方案》§10.1/§10.2。 + */ + +export type AvatarStyle = "text" | "silhouette"; + +interface StorageLike { + getItem(k: string): string | null; + setItem(k: string, v: string): void; + removeItem(k: string): void; +} + +export const AVATAR_STORAGE_KEY = "yh-avatar"; + +/** 学校默认头像存储键(UserHome 登录时写入;值格式与用户键相同,仅无"用户自选"语义) */ +export const SCHOOL_DEFAULT_AVATAR_STORAGE_KEY = "yh-avatar-sch-default"; + +export const AVATAR_STYLES: readonly AvatarStyle[] = ["text", "silhouette"]; + +export const DEFAULT_AVATAR_STYLE: AvatarStyle = "text"; + +/** 值合法才采用(用户键/学校键同一判据;脏数据不生效) */ +function readAvatarStyleOrNull(raw: string | null): AvatarStyle | null { + return raw === "text" || raw === "silhouette" ? raw : null; +} + +/** URL 覆盖参数(?yh-avatar=text|silhouette):非法值忽略,且只影响"读哪一档"、绝不写回 localStorage */ +function readAvatarOverride(): AvatarStyle | null { + try { + const search = (globalThis.location && globalThis.location.search) || ""; + const m = new RegExp("[?&]yh-avatar=([^&]*)").exec(search); + return m ? readAvatarStyleOrNull(decodeURIComponent(m[1])) : null; + } catch { + /* 无 location 环境(测试/SSR):无覆盖 */ + return null; + } +} + +/** + * 生效样式合成(与 resolveEffectiveThemeState 同构): + * URL 覆盖 > localStorage["yh-avatar"](用户自选)> localStorage["yh-avatar-sch-default"](学校默认) + * > DEFAULT_AVATAR_STYLE。 + */ +export function resolveAvatarStyle(storage: StorageLike | undefined = globalThis.localStorage): AvatarStyle { + let style: AvatarStyle | null = null; + if (storage) { + style = readAvatarStyleOrNull(storage.getItem(AVATAR_STORAGE_KEY)); + if (!style) style = readAvatarStyleOrNull(storage.getItem(SCHOOL_DEFAULT_AVATAR_STORAGE_KEY)); + } + if (!style) style = DEFAULT_AVATAR_STYLE; + return readAvatarOverride() || style; +} + +/** 保存用户自选(直写 localStorage,无响应式;调用方操作后自行刷新 UI) */ +export function saveAvatarStyle(style: AvatarStyle, storage: StorageLike | undefined = globalThis.localStorage): void { + try { + storage?.setItem(AVATAR_STORAGE_KEY, style); + } catch { + /* 隐私模式/配额失败静默:仅丢持久化,不丢本次会话生效 */ + } +} + +/** 跟随学校默认 = 删用户自选键(之后回落学校默认/系统默认) */ +export function clearAvatarStyle(storage: StorageLike | undefined = globalThis.localStorage): void { + try { + storage?.removeItem(AVATAR_STORAGE_KEY); + } catch { + /* 隐私模式静默 */ + } +} + +/** + * 取字(顺序判定,先命中先返回):含 "@" 先截 "@" 前段(邮箱取首字母); + * 首字符必须用展开 [...s][0](禁用 charAt(0),避免代理对截半出乱码)。 + * ASCII 字母转大写;纯数字回落 ""(剪影);符号/emoji/空白回落 ""(剪影);其余(CJK 等)原样。 + */ +export function pickAvatarText(name: string | null | undefined): string { + let s = String(name ?? ""); + const at = s.indexOf("@"); + if (at > -1) s = s.slice(0, at); + const ch = [...s.trim()][0]; + if (ch === undefined) return ""; + if (/^[A-Za-z]$/.test(ch)) return ch.toUpperCase(); + if (/^[0-9]$/.test(ch)) return ""; + if (/^[\p{P}\p{S}\s]$/u.test(ch)) return ""; + return ch; +}