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

+6 -5
View File
@@ -31,6 +31,7 @@ export function Workspace({ onCreateChannel }: { onCreateChannel: () => void })
const channel = useWorkspace((s) => s.channel);
const threads = useWorkspace((s) => s.threads);
const groups = useWorkspace((s) => s.groups);
const setChannelHistory = useWorkspace(s => s.setChannelHistory);
const createGroup = useWorkspace((s) => s.createGroup);
const renameGroup = useWorkspace((s) => s.renameGroup);
const groupThread = useWorkspace((s) => s.groupThread);
@@ -64,7 +65,7 @@ export function Workspace({ onCreateChannel }: { onCreateChannel: () => void })
const activeThreadId = activeThread?.id;
const activeFolders = folders.filter((folder) => folder.threadId === activeThreadId);
const activeFolderId = activeFolders.some((folder) => folder.id === folderId) ? folderId : undefined;
const enabled = Boolean(activeThreadId && channel?.canUpload !== false);
const enabled = Boolean(activeThreadId && channel?.canUpload);
const files = useWorkspace(useShallow((s) => s.order.filter((id) => {
const item = s.byId[id]; return item?.kind === 'file' && !item.metadataHidden && item.threadId === activeThreadId
&& (item.folderId ?? undefined) === activeFolderId && matchesFilter(item, filter);
@@ -101,19 +102,19 @@ export function Workspace({ onCreateChannel }: { onCreateChannel: () => void })
function drop(event: DragEvent) { event.preventDefault(); setDragging(false); requestFiles([...event.dataTransfer.files]); }
function deleteItem(id: string) { void deleteFiles([id]).catch((cause: unknown) => useWorkspace.setState({ error: cause instanceof Error ? cause.message : '删除失败' })); }
const navigation = <><ChannelSwitcher id={channel?.id} onCreate={onCreateChannel} />
<ThreadSidebar threads={threads} groups={groups} selectedThreadId={activeThreadId} enabled={channel?.canUpload !== false} onSelectThread={selectThread} onCreateThread={createThread} onRenameThread={renameThread}
<ThreadSidebar threads={threads} groups={groups} selectedThreadId={activeThreadId} enabled={Boolean(channel?.canManageStructure)} owner={channel?.role === 'owner'} onHistory={setChannelHistory} onSelectThread={selectThread} onCreateThread={createThread} onRenameThread={renameThread}
onCreateGroup={createGroup} onRenameGroup={renameGroup} onGroupThread={groupThread} />
{desktop ? <Notepad /> : null}<Button variant="ghost" icon="scan" onClick={() => setScanOpen(true)}>加入空间</Button></>;
const chat = <ChatPane threadName={activeThread?.name ?? '选择频道'} ids={messages} enabled={enabled} onSend={(text) => sendText(text, activeThreadId)} onDelete={deleteItem} onOpenFile={setPreviewId} onFiles={requestFiles} />;
const explorer = <>
<FilterBar filter={filter} searchOpen={searchOpen} onChange={setFilter} />
<FileExplorer key={activeThreadId} scope={`${channel?.id}:${activeThreadId}`} threadId={activeThreadId} threadName={activeThread?.name ?? '选择频道'} folders={activeFolders} folderId={activeFolderId} ids={files}
selectedId={files.includes(selectedId ?? '') ? selectedId : undefined} checked={checked} enabled={enabled} onFolder={selectFolder} onSelect={setSelectedId} onOpen={setPreviewId} onToggle={toggle} onSelection={setChecked}
selectedId={files.includes(selectedId ?? '') ? selectedId : undefined} checked={checked} enabled={enabled} structureEnabled={Boolean(activeThreadId && channel?.canManageStructure)} onFolder={selectFolder} onSelect={setSelectedId} onOpen={setPreviewId} onToggle={toggle} onSelection={setChecked}
onFiles={(incoming, target) => requestFiles(incoming, { folderId: target })}
onCreateFolder={(name) => activeThreadId ? createFolder(activeThreadId, name, activeFolderId) : Promise.reject(new Error('请先选择频道'))}
onRenameFolder={(id, name) => activeThreadId ? renameFolder(id, activeThreadId, name) : Promise.reject(new Error('请先选择频道'))}
onDownloadItems={downloadItems} onDeleteItems={deleteFiles} onMoveItems={async (ids, target) => { if (activeThreadId) { await moveItems(ids, activeThreadId, target); setChecked(new Set()); setSelectedId(undefined); } }} />
{restricted.length ? <div className={styles.legacy}><span>尚未同步目录的旧文件</span>{restricted.map((id) => <Button variant="ghost" key={id} onClick={() => setPreviewId(id)}>查看受限文件</Button>)}</div> : null}
{restricted.length ? <div className={styles.pendingCatalog}><span>等待同步文件目录</span>{restricted.map((id) => <Button variant="ghost" key={id} onClick={() => setPreviewId(id)}>查看受限文件</Button>)}</div> : null}
</>;
return <div className={desktop ? styles.desktop : styles.mobile} style={fileWidth ? { '--files-width': `${fileWidth}px` } as CSSProperties : undefined}
onDropCapture={() => setDragging(false)} onDragEndCapture={() => setDragging(false)}
@@ -124,7 +125,7 @@ export function Workspace({ onCreateChannel }: { onCreateChannel: () => void })
{error ? <div className={styles.error} role="alert">{error}</div> : null}
{panelOpen ? <Sheet title="设备与账号邀请" onClose={() => setPanelOpen(false)}><DevicePanel /></Sheet> : null}
{settingsOpen && channel ? <ChannelSettingsSheet channel={channel} onClose={() => setSettingsOpen(false)} onSave={updateSettings} /> : null}
{upload ? <UploadOptionsSheet files={upload.files} encrypted={channel?.mode === 'enc'} defaultMaxDownloads={channel?.maxDownloads ?? 10} onClose={() => setUpload(undefined)} onUpload={confirmUpload} /> : null}
{upload && channel?.maxDownloads !== undefined ? <UploadOptionsSheet files={upload.files} encrypted={channel?.mode === 'enc'} defaultMaxDownloads={channel.maxDownloads} onClose={() => setUpload(undefined)} onUpload={confirmUpload} /> : null}
<Suspense fallback={null}>{previewId ? <PreviewSheet id={previewId} onClose={() => setPreviewId(undefined)} onDelete={deleteItem} /> : null}
{scanOpen ? <ScanSheet onClose={() => setScanOpen(false)} onChannel={(resource) => navigate(channelPath(resource.id))} onShare={(resource) => navigate(sharePath(resource.id))} /> : null}</Suspense>
{dragging ? <div className={styles.drop}>松开以上传到当前频道</div> : null}