feat(web): add account workspaces and signed content flows

This commit is contained in:
justme committed 2026-10-14 04:06:03 +08:00
1 parent 7b8d61d1ba
commit b5cf3d5958
117 files changed
+2789 -1674

No files matched your search

+5
View File
@@ -23,11 +23,16 @@ export function formatTransferProgress(state: {
}
const timeFmt = new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false });
const dateFmt = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' });
export function formatTime(ts: number): string {
return timeFmt.format(ts);
}
export function formatDate(ts: number): string {
return dateFmt.format(ts);
}
export function formatAgo(ts: number, now = Date.now()): string {
const m = Math.max(0, Math.round((now - ts) / 60000));
if (m < 1) return '刚刚';
-16
View File
@@ -1,16 +0,0 @@
import { describe, expect, it } from 'vitest';
import { readSender } from './sender';
describe('sender display snapshots', () => {
it('accepts account identity and safe local avatar data', () => {
expect(readSender({ username: 'alice', displayName: '小白', avatar: 'data:image/webp;base64,AA==' })).toEqual({ username: 'alice', displayName: '小白', avatar: 'data:image/webp;base64,AA==' });
expect(readSender({ username: 'alice', displayName: ' Alice ' })).toEqual({ username: 'alice', displayName: 'Alice' });
});
it.each([
{ username: 'alice', displayName: 'Alice', avatar: 'https://tracker.test/avatar.png' },
{ username: 'alice', displayName: 'Alice', avatar: 'data:image/svg+xml;base64,AA==' },
{ username: 'alice', displayName: 'Alice', avatar: `data:image/png;base64,${'A'.repeat(8000)}` },
{ username: 'alice', displayName: '\u0000Alice' },
{ username: 'alice', displayName: '' }, { username: 'invalid user', displayName: 'Alice' }, null,
])('rejects invalid or executable identity data %#', (value) => { expect(readSender(value)).toBeUndefined(); });
});
-10
View File
@@ -1,10 +0,0 @@
import type { SenderInfo } from '@/core-bridge/types';
export function readSender(value: unknown): SenderInfo | undefined {
if (!value || typeof value !== 'object') return undefined;
const row = value as Record<string, unknown>;
if (typeof row.username !== 'string' || !/^[a-z0-9_-]{3,32}$/.test(row.username)
|| typeof row.displayName !== 'string' || !row.displayName.trim() || row.displayName.length > 48 || /[\u0000-\u001f]/.test(row.displayName)
|| row.avatar !== undefined && (typeof row.avatar !== 'string' || row.avatar.length > 8000 || !/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/]+=*$/.test(row.avatar))) return undefined;
return { username: row.username, displayName: row.displayName.trim(), ...(row.avatar ? { avatar: row.avatar as string } : {}) };
}
+11
View File
@@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest';
import { ttlLabel, withCurrent } from './settingOptions';
describe('server setting presentation', () => {
it('formats arbitrary deployment-provided retention times', () => {
expect(ttlLabel(90)).toBe('90 秒'); expect(ttlLabel(120)).toBe('2 分钟'); expect(ttlLabel(3600)).toBe('1 小时');
});
it('keeps existing settings visible when deployment choices change', () => {
expect(withCurrent([60, 120, 120], 90)).toEqual([60, 90, 120]);
});
});
+9
View File
@@ -0,0 +1,9 @@
export function ttlLabel(seconds: number): string {
if (seconds % 86400 === 0) return `${seconds / 86400} 天`;
if (seconds % 3600 === 0) return `${seconds / 3600} 小时`;
if (seconds % 60 === 0) return `${seconds / 60} 分钟`;
return `${seconds} 秒`;
}
export function withCurrent(values: number[], current: number): number[] {
return [...new Set([...values, current])].sort((a, b) => a - b);
}