| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127 |
- /**
- * 右侧信息栏的共享类型与纯函数(2026-11-06)
- *
- * ## 为什么抽出来
- *
- * 右栏原本整个写死在日常对话页里。群聊页要做成一致的观感就必然需要同一套:
- * 四个标签、拖宽、收起、本地记忆。复制一份会产生两份会各自漂移的实现,
- * 所以抽成共享层,日常页与群聊页都从这里取。
- *
- * 本文件**只有纯函数与类型**,不依赖 Angular,便于直接单测。
- */
- /** 右栏标签(顺序即显示顺序) */
- export type RailTab = 'tasks' | 'files' | 'results' | 'status';
- /** 标签定义(模板遍历用,避免在模板里重复写四份) */
- export interface RailTabDef {
- key: RailTab;
- label: string;
- icon: string;
- hint: string;
- }
- /**
- * 四个标签的默认定义。
- *
- * 群聊页把 `hint` 换成多成员语义(如「群成员的任务」),
- * 标签名与图标保持不变 —— 用户在两处看到的按钮是同一组。
- */
- export const RAIL_TABS: RailTabDef[] = [
- { key: 'tasks', label: '任务', icon: 'fa-list-check', hint: '名下的任务' },
- { key: 'files', label: '文件', icon: 'fa-paperclip', hint: '本次对话产出的文件' },
- { key: 'results', label: '成果', icon: 'fa-box', hint: '平台上报的交付成果' },
- { key: 'status', label: '状态', icon: 'fa-gauge-high', hint: '运行指标与最近动态' },
- ];
- const RAIL_TAB_KEY = 'fmode_rail_tab';
- /**
- * 读上次选中的标签。
- *
- * 默认「文件」—— 用户最关心这次对话产出了什么。
- */
- export function readRailTabPref(): RailTab {
- try {
- const v = localStorage.getItem(RAIL_TAB_KEY);
- if (v === 'tasks' || v === 'files' || v === 'results' || v === 'status') return v;
- } catch {
- /* 隐私模式 */
- }
- return 'files';
- }
- /** 记住用户的选择(跨刷新保持) */
- export function saveRailTabPref(tab: RailTab): void {
- try {
- localStorage.setItem(RAIL_TAB_KEY, tab);
- } catch {
- /* 隐私模式:只保留内存态 */
- }
- }
- /** 右栏宽度的本地存储键(日常页与群聊页共用同一偏好) */
- export const RAIL_W_KEY = 'fmode_rail_w';
- export const RAIL_W_MIN = 280;
- export const RAIL_W_MAX = 560;
- export const RAIL_W_DEFAULT = 320;
- /** 预览形态下的宽度上限:看文档/表格时 560 太窄 */
- export const RAIL_W_PREVIEW_MAX = 720;
- /**
- * 右栏宽度夹取 —— **纯函数**,便于单测。
- *
- * 下限 280:再窄右栏里「文件名 + 大小」会折行成两三行;
- * 上限 560:再宽就把对话区挤到不可读(主区最小可读宽约 480)。
- */
- export function clampRailWidth(w: number): number {
- if (!Number.isFinite(w)) return RAIL_W_DEFAULT;
- return Math.min(RAIL_W_MAX, Math.max(RAIL_W_MIN, Math.round(w)));
- }
- /** 预览形态下的宽度夹取(上限放宽到 720) */
- export function clampPreviewRailWidth(w: number): number {
- return Math.min(
- RAIL_W_PREVIEW_MAX,
- Math.max(RAIL_W_MIN, Math.round(Number.isFinite(w) ? w : RAIL_W_DEFAULT)),
- );
- }
- /** 按 mimeType 选文件图标(原生附件没有 fileType 字段) */
- export function fileIconByMime(mime: string): string {
- if (mime.startsWith('image/')) return 'fa-file-image';
- if (mime.includes('html')) return 'fa-file-code';
- if (mime.includes('pdf')) return 'fa-file-pdf';
- if (mime.includes('word') || mime.includes('officedocument.word')) return 'fa-file-word';
- if (mime.includes('sheet') || mime.includes('excel')) return 'fa-file-excel';
- if (mime.includes('zip')) return 'fa-file-zipper';
- return 'fa-file-lines';
- }
- /** 按成果文件后缀选图标 */
- export function fileIconByType(type?: string): string {
- const t = (type || '').toLowerCase();
- if (t.includes('pdf')) return 'fa-file-pdf';
- if (t.includes('doc')) return 'fa-file-word';
- if (t.includes('xls') || t.includes('sheet') || t.includes('csv')) return 'fa-file-excel';
- if (t.includes('ppt')) return 'fa-file-powerpoint';
- if (t.includes('zip') || t.includes('rar')) return 'fa-file-zipper';
- if (t.includes('png') || t.includes('jpg') || t.includes('jpeg') || t.includes('image'))
- return 'fa-file-image';
- if (t.includes('md') || t.includes('txt')) return 'fa-file-lines';
- return 'fa-file';
- }
- /** 字节数 → 人类可读 */
- export function fmtSize(bytes: number): string {
- if (!bytes) return '';
- /*
- * 负数 = 「大小未知」(2026-11-09)。云盘链接不带大小,
- * 显示「-1 B」会让用户以为文件坏了,这里当作「不显示」。
- */
- if (bytes < 0) return '';
- if (bytes < 1024) return `${bytes} B`;
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
- return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
- }
|