| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- 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` 到 `<html>`,消息组件里所有正文字号引用它 ——
- * 这样调字号不需要重建任何组件,浏览器直接重排。
- */
- @Injectable({ providedIn: 'root' })
- export class ChatFontService {
- private readonly session = inject(SessionService);
- private readonly _size = signal<ChatFontSize>(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;
- }
|