diff --git a/package.json b/package.json index a3e0ee8..6d18c0c 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "pinia": "^2.1.4", "sass": "^1.64.1", "v-wave": "^3.0.2", - "view-ui-plus": "^1.3.19", + "view-ui-plus": "^1.3.24", "vite-plugin-importer": "^0.2.4", "vue": "^3.4.19", "vue-codemirror": "^6.1.1", 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/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; +} diff --git a/src/utils/theme.ts b/src/utils/theme.ts index fbc7ceb..8e83c37 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -1,5 +1,5 @@ /** - * 本文件由 YschTheme scripts/gen-theme.mjs 从 assets-src/pc/theme-core.js 生成(版本 20260930-1248-4e12e6d),勿手改。 + * 本文件由 YschTheme scripts/gen-theme.mjs 从 assets-src/pc/theme-core.js 生成(版本 20261006-2013-75c1d10),勿手改。 * 分发方式:YschTheme `npm run sync` 拷进各端 src/utils/theme.ts(与 ThemeSwitcher.vue 配套,MD5 对齐报告)。 * 运行时档位类名由 /static/js/yh-theme-init.js 在 内先行挂好,本模块只负责读写与切换。 */