/** * 默认头像样式(首字 / 剪影)公共源(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; }