rail-shared.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. /**
  2. * 右侧信息栏的共享类型与纯函数(2026-11-06)
  3. *
  4. * ## 为什么抽出来
  5. *
  6. * 右栏原本整个写死在日常对话页里。群聊页要做成一致的观感就必然需要同一套:
  7. * 四个标签、拖宽、收起、本地记忆。复制一份会产生两份会各自漂移的实现,
  8. * 所以抽成共享层,日常页与群聊页都从这里取。
  9. *
  10. * 本文件**只有纯函数与类型**,不依赖 Angular,便于直接单测。
  11. */
  12. /** 右栏标签(顺序即显示顺序) */
  13. export type RailTab = 'tasks' | 'files' | 'results' | 'status';
  14. /** 标签定义(模板遍历用,避免在模板里重复写四份) */
  15. export interface RailTabDef {
  16. key: RailTab;
  17. label: string;
  18. icon: string;
  19. hint: string;
  20. }
  21. /**
  22. * 四个标签的默认定义。
  23. *
  24. * 群聊页把 `hint` 换成多成员语义(如「群成员的任务」),
  25. * 标签名与图标保持不变 —— 用户在两处看到的按钮是同一组。
  26. */
  27. export const RAIL_TABS: RailTabDef[] = [
  28. { key: 'tasks', label: '任务', icon: 'fa-list-check', hint: '名下的任务' },
  29. { key: 'files', label: '文件', icon: 'fa-paperclip', hint: '本次对话产出的文件' },
  30. { key: 'results', label: '成果', icon: 'fa-box', hint: '平台上报的交付成果' },
  31. { key: 'status', label: '状态', icon: 'fa-gauge-high', hint: '运行指标与最近动态' },
  32. ];
  33. const RAIL_TAB_KEY = 'fmode_rail_tab';
  34. /**
  35. * 读上次选中的标签。
  36. *
  37. * 默认「文件」—— 用户最关心这次对话产出了什么。
  38. */
  39. export function readRailTabPref(): RailTab {
  40. try {
  41. const v = localStorage.getItem(RAIL_TAB_KEY);
  42. if (v === 'tasks' || v === 'files' || v === 'results' || v === 'status') return v;
  43. } catch {
  44. /* 隐私模式 */
  45. }
  46. return 'files';
  47. }
  48. /** 记住用户的选择(跨刷新保持) */
  49. export function saveRailTabPref(tab: RailTab): void {
  50. try {
  51. localStorage.setItem(RAIL_TAB_KEY, tab);
  52. } catch {
  53. /* 隐私模式:只保留内存态 */
  54. }
  55. }
  56. /** 右栏宽度的本地存储键(日常页与群聊页共用同一偏好) */
  57. export const RAIL_W_KEY = 'fmode_rail_w';
  58. export const RAIL_W_MIN = 280;
  59. export const RAIL_W_MAX = 560;
  60. export const RAIL_W_DEFAULT = 320;
  61. /** 预览形态下的宽度上限:看文档/表格时 560 太窄 */
  62. export const RAIL_W_PREVIEW_MAX = 720;
  63. /**
  64. * 右栏宽度夹取 —— **纯函数**,便于单测。
  65. *
  66. * 下限 280:再窄右栏里「文件名 + 大小」会折行成两三行;
  67. * 上限 560:再宽就把对话区挤到不可读(主区最小可读宽约 480)。
  68. */
  69. export function clampRailWidth(w: number): number {
  70. if (!Number.isFinite(w)) return RAIL_W_DEFAULT;
  71. return Math.min(RAIL_W_MAX, Math.max(RAIL_W_MIN, Math.round(w)));
  72. }
  73. /** 预览形态下的宽度夹取(上限放宽到 720) */
  74. export function clampPreviewRailWidth(w: number): number {
  75. return Math.min(
  76. RAIL_W_PREVIEW_MAX,
  77. Math.max(RAIL_W_MIN, Math.round(Number.isFinite(w) ? w : RAIL_W_DEFAULT)),
  78. );
  79. }
  80. /** 按 mimeType 选文件图标(原生附件没有 fileType 字段) */
  81. export function fileIconByMime(mime: string): string {
  82. if (mime.startsWith('image/')) return 'fa-file-image';
  83. if (mime.includes('html')) return 'fa-file-code';
  84. if (mime.includes('pdf')) return 'fa-file-pdf';
  85. if (mime.includes('word') || mime.includes('officedocument.word')) return 'fa-file-word';
  86. if (mime.includes('sheet') || mime.includes('excel')) return 'fa-file-excel';
  87. if (mime.includes('zip')) return 'fa-file-zipper';
  88. return 'fa-file-lines';
  89. }
  90. /** 按成果文件后缀选图标 */
  91. export function fileIconByType(type?: string): string {
  92. const t = (type || '').toLowerCase();
  93. if (t.includes('pdf')) return 'fa-file-pdf';
  94. if (t.includes('doc')) return 'fa-file-word';
  95. if (t.includes('xls') || t.includes('sheet') || t.includes('csv')) return 'fa-file-excel';
  96. if (t.includes('ppt')) return 'fa-file-powerpoint';
  97. if (t.includes('zip') || t.includes('rar')) return 'fa-file-zipper';
  98. if (t.includes('png') || t.includes('jpg') || t.includes('jpeg') || t.includes('image'))
  99. return 'fa-file-image';
  100. if (t.includes('md') || t.includes('txt')) return 'fa-file-lines';
  101. return 'fa-file';
  102. }
  103. /** 字节数 → 人类可读 */
  104. export function fmtSize(bytes: number): string {
  105. if (!bytes) return '';
  106. /*
  107. * 负数 = 「大小未知」(2026-11-09)。云盘链接不带大小,
  108. * 显示「-1 B」会让用户以为文件坏了,这里当作「不显示」。
  109. */
  110. if (bytes < 0) return '';
  111. if (bytes < 1024) return `${bytes} B`;
  112. if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  113. return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
  114. }