import { computed, effect, inject, Injectable, signal } from '@angular/core'; import { SessionService } from './session.service'; /** * 全局字号档位(px) * * 2026-10-30 用户要求:从「只管会话正文」升级为**全局字号**—— * 菜单、列表、设置项一起缩放,否则正文调大后左栏还是小字,比例失衡。 */ export const CHAT_FONT_SIZES = [13, 14, 15, 16, 17, 18, 19, 20] as const; export type ChatFontSize = (typeof CHAT_FONT_SIZES)[number]; /** 默认 **16px** —— 大字号优先(原 15px 在高分屏上偏小,用户要求默认调大) */ export const DEFAULT_CHAT_FONT: ChatFontSize = 16; /** * 会话正文的字号偏好 * * ## 为什么做成独立的可调项 * * 会话是长时间阅读区,字号是**个人偏好**而不是设计统一值: * 有人 15px 正好,有人需要 17px。做成设置项比所有人共用一个值更合理。 * * ## 实现方式 * * 写一个 CSS 变量 `--chat-read-size` 到 ``,消息组件里所有正文字号引用它 —— * 这样调字号不需要重建任何组件,浏览器直接重排。 */ @Injectable({ providedIn: 'root' }) export class ChatFontService { private readonly session = inject(SessionService); private readonly _size = signal(DEFAULT_CHAT_FONT); readonly size = this._size.asReadonly(); /** 当前档位在列表中的位置(供滑块/分段控件用) */ readonly index = computed(() => CHAT_FONT_SIZES.indexOf(this._size())); constructor() { /* * 尺寸变化即写 DOM 变量(无需重建组件)。 * * 写三个变量而不是一个: * --chat-read-size 会话正文(原来只改这个) * --ui-scale 全局缩放基准(本次新增) * 左栏/设置等用 calc(var(--ui-scale) * 原值) 跟随缩放 */ effect(() => { const px = this._size(); if (typeof document === 'undefined') return; const root = document.documentElement; root.style.setProperty('--chat-read-size', `${px}px`); /* * 以 16px 为 1.0 的基准缩放:13px→0.81,20px→1.25。 * 用无单位数字,方便各处 calc() 相乘。 */ root.style.setProperty('--ui-scale', String(Math.round((px / DEFAULT_CHAT_FONT) * 1000) / 1000)); }); } private loadedFor: string | null = null; load(): void { const uid = this.session.me()?.id ?? null; if (this.loadedFor === uid) return; this.loadedFor = uid; const key = this.key(); if (!key) { this.set(DEFAULT_CHAT_FONT, false); return; } try { const raw = localStorage.getItem(key); /* * 🔴 必须先判 null 再转数字。 * `Number(null)` === 0,会被 pick 吸附到最近的 14px —— * 表现为"没设置过字号,却莫名其妙变成 14"(单测实测暴露)。 */ if (raw === null) { this.set(DEFAULT_CHAT_FONT, false); return; } this.set(pick(Number(raw)), false); } catch { this.set(DEFAULT_CHAT_FONT, false); } } set(px: number, persist = true): void { const next = pick(px); this._size.set(next); if (!persist) return; const key = this.key(); if (!key) return; try { localStorage.setItem(key, String(next)); } catch { /* 隐私模式:只保留内存态 */ } } private key(): string | null { const uid = this.session.me()?.id; return uid ? `fmode_chat_font_${uid}` : null; } } /** 把任意数字吸附到最近的合法档位 */ function pick(v: number): ChatFontSize { if (!Number.isFinite(v)) return DEFAULT_CHAT_FONT; return CHAT_FONT_SIZES.reduce((best, cur) => Math.abs(cur - v) < Math.abs(best - v) ? cur : best, ) as ChatFontSize; }