feat(files): add per-channel storage quota [CI]
ci/woodpecker/pr/web-build Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
justme committed 2026-10-14 20:42:08 +08:00
1 parent 441ed0e5f0
commit a6f078d009
19 files changed
+172 -15

No files matched your search

+15 -1
View File
@@ -10,11 +10,14 @@ import { ExplorerMoveDialog } from './ExplorerMoveDialog';
import { useExplorerInteractions } from './useExplorerInteractions';
import { ContextMenu } from '@/ui/ContextMenu';
import styles from './FileExplorer.module.css';
import { formatSize } from '@/lib/format';
interface Props {
scope: string;
threadId?: string;
threadName: string;
fileBytes: number;
maxFileBytes?: number;
folders: FolderInfo[];
folderId?: string;
ids: string[];
@@ -56,6 +59,8 @@ export function FileExplorer(props: Props) {
seen.add(current.id); path.unshift(current); current = props.folders.find((folder) => folder.id === current?.parentId);
}
const children = props.folders.filter((folder) => folder.parentId === props.folderId).sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
const maxFileBytes = props.maxFileBytes;
const usedRatio = maxFileBytes && maxFileBytes > 0 ? Math.min(1, props.fileBytes / maxFileBytes) : 0;
function beginFolder(id?: string) { const folder = props.folders.find((row) => row.id === id); setEdit(id ?? 'create'); setName(folder?.name ?? ''); setError(''); }
const interaction = useExplorerInteractions({ ...props, ids, onDownload: props.onDownloadItems, onDelete: props.onDeleteItems, onMove: props.onMoveItems,
onRenameFolder: (id) => beginFolder(id), onCreateFolder: () => beginFolder(), onUpload: () => input.current?.click(), onFolder: navigateFolder });
@@ -99,7 +104,16 @@ export function FileExplorer(props: Props) {
}}><option value="__choose">移动到…</option><option value="__root">根目录</option>{props.folders.map((folder) => <option value={folder.id} key={folder.id}>{folder.name}</option>)}</select> : null}
</div> : null}
{error || interaction.error ? <p className={styles.error} role="alert">{error || interaction.error}</p> : null}
<footer className={styles.status}>{children.length} 个文件夹 · {ids.length} 个文件<span>双击打开 · 右键操作 · 拖动移动</span></footer>
<footer className={styles.status}>{children.length} 个文件夹 · {ids.length} 个文件<span>双击打开 · 右键操作 · 拖动移动</span></footer>
<div className={styles.quota} role="group" aria-label="当前频道文件空间">
<div className={styles.quotaLabel}><span>频道文件空间</span><span>{formatSize(props.fileBytes)} / {maxFileBytes ? formatSize(maxFileBytes) : '读取中'}</span></div>
<div className={styles.quotaTrack} role="progressbar" aria-label="文件空间使用量" aria-valuemin={0}
aria-valuemax={maxFileBytes ?? 0} aria-valuenow={Math.min(props.fileBytes, maxFileBytes ?? 0)}>
<span className={props.fileBytes >= (maxFileBytes ?? Number.POSITIVE_INFINITY) ? styles.quotaFull : undefined}
style={{ width: `${usedRatio * 100}%` }} />
</div>
{maxFileBytes ? <span className={styles.quotaRemaining}>剩余 {formatSize(Math.max(0, maxFileBytes - props.fileBytes))}</span> : null}
</div>
{interaction.menu ? <ContextMenu point={interaction.menu} actions={interaction.actions} onClose={interaction.dismiss} /> : null}
{interaction.hover && ids.includes(interaction.hover.id) && !interaction.menu ? <ExplorerHover key={interaction.hover.id} {...interaction.hover} /> : null}
{interaction.moveIds ? <ExplorerMoveDialog folders={props.folders} count={interaction.moveIds.length} currentFolderId={props.folderId} onClose={() => interaction.setMoveIds(undefined)}