feat(web): space roles, upload queue with pipelined frames, background resume and faster sending

This commit is contained in:
justme committed 2026-10-16 19:42:33 +08:00
1 parent 96c4335cb5
commit a574583943
39 files changed
+1515 -194

No files matched your search

+21 -8
View File
@@ -10,16 +10,29 @@ export function formatSize(bytes: number): string {
return `${v >= 100 ? v.toFixed(0) : v.toFixed(1)} ${units[i]}`;
}
export function formatTransferProgress(state: {
export interface TransferState {
progress: number;
phase?: 'encrypting' | 'uploading';
phase?: 'queued' | 'encrypting' | 'uploading';
speedBytesPerSecond?: number;
}): string {
const phase = state.phase === 'encrypting' ? '加密中' : state.phase === 'uploading' ? '上传中' : '传输中';
const speed = state.speedBytesPerSecond && state.speedBytesPerSecond > 0
? ` · ${formatSize(state.speedBytesPerSecond)}/s`
: '';
return `${phase} ${Math.round(state.progress * 100)}%${speed}`;
etaSeconds?: number;
}
export function formatEta(seconds: number): string {
if (seconds < 60) return `剩 ${Math.max(1, Math.round(seconds))} 秒`;
if (seconds < 3600) return `剩 ${Math.ceil(seconds / 60)} 分钟`;
const hours = Math.floor(seconds / 3600);
return `剩 ${hours} 小时 ${Math.ceil((seconds % 3600) / 60)} 分钟`;
}
/** 传输状态的一行文字:排队中 / 加密中 / 上传中 42% · 3.2 MB/s · 剩 12 秒 */
export function formatTransferProgress(state: TransferState): string {
if (state.phase === 'queued') return '排队中';
const percent = Math.round(state.progress * 100);
if (state.phase === 'encrypting' && percent === 0) return '加密中…';
const parts = [`${state.phase === 'encrypting' ? '加密中' : '上传中'} ${percent}%`];
if (state.speedBytesPerSecond && state.speedBytesPerSecond > 0) parts.push(`${formatSize(state.speedBytesPerSecond)}/s`);
if (state.etaSeconds !== undefined) parts.push(formatEta(state.etaSeconds));
return parts.join(' · ');
}
const timeFmt = new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false });
+35
View File
@@ -0,0 +1,35 @@
import type { RoleInfo } from '@/core-bridge/types';
export const ROLE_COLORS = ['gray', 'blue', 'green', 'amber', 'red', 'purple'] as const;
export type RoleColor = (typeof ROLE_COLORS)[number];
/** 与后端 WorkspacePermission 一致 */
export const PERMISSIONS = [
{ id: 'members.remove', label: '移除成员', hint: '移除成员、撤销邀请;不能移除空间主和同样有此权限的人' },
{ id: 'structure.manage', label: '管理频道和分组', hint: '重命名频道、调整频道所属分组、新建和删除分组。新建频道涉及密钥,仍只有空间主' },
] as const;
export interface Badge { key: string; text: string; color: RoleColor | 'owner' }
const hasText = (value: string | null | undefined): value is string => Boolean(value && value.trim());
/** 聊天里名字前后要显示的文字:空间主固定一个,其余按身份组的创建顺序 */
export function badgesOf(roles: readonly RoleInfo[], username: string | undefined, creator: string | undefined): { prefix: Badge[]; suffix: Badge[] } {
const prefix: Badge[] = []; const suffix: Badge[] = [];
if (!username) return { prefix, suffix };
if (username === creator) suffix.push({ key: 'owner', text: '空间主', color: 'owner' });
for (const role of roles) {
if (!role.members.includes(username)) continue;
const color = (ROLE_COLORS as readonly string[]).includes(role.color ?? '') ? (role.color as RoleColor) : 'gray';
if (hasText(role.prefix)) prefix.push({ key: role.id + ':p', text: role.prefix, color });
if (hasText(role.suffix)) suffix.push({ key: role.id + ':s', text: role.suffix, color });
}
return { prefix, suffix };
}
export function permissionsOf(roles: readonly RoleInfo[], username: string | undefined, owner: boolean): ReadonlySet<string> {
if (owner) return new Set(PERMISSIONS.map((item) => item.id));
const granted = new Set<string>();
if (username) for (const role of roles) if (role.members.includes(username)) for (const id of role.permissions) granted.add(id);
return granted;
}