feat(web): tidy the channel sidebar and let owners delete groups

This commit is contained in:
justme committed 2026-10-16 18:30:58 +08:00
1 parent 6310f93469
commit 6d6490055e
12 files changed
+66 -13

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
.pad {
margin-top: var(--space-2);
padding: var(--space-1) var(--space-2);
padding: var(--space-1) var(--space-3);
border-top: 1px solid var(--line);
}
@@ -105,6 +105,15 @@
border: 0;
}
.dangerZone {
display: flex;
flex-direction: column;
gap: var(--space-2);
align-items: flex-start;
padding-top: var(--space-2);
border-top: 1px solid var(--line);
}
.modalActions {
justify-content: flex-end;
margin-top: var(--space-2);
@@ -147,11 +156,19 @@
.heading {
min-height: 32px;
padding-inline: var(--space-1);
padding-inline: var(--space-1) 0;
color: var(--text-dim);
font-size: 12px;
}
.heading button {
width: 26px;
min-width: 26px;
height: 26px;
min-height: 26px;
padding: 0;
}
.folderHeading {
padding-top: var(--space-2);
border-top: 1px solid var(--line);
@@ -167,9 +184,8 @@
list-style: none;
}
/* 频道行缩进到分组标题文字之下 */
.threads {
padding-inline-start: var(--space-4);
padding-inline-start: 0;
}
.nestedThreads {
@@ -219,11 +235,13 @@
gap: var(--space-2);
min-width: 0;
min-height: 32px;
padding: 0 var(--space-2);
padding: 0 var(--space-2) 0 var(--space-1);
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text-dim);
font: inherit;
font-size: 14px;
text-align: left;
cursor: pointer;
}
@@ -259,12 +277,14 @@
.thread.selected {
background: var(--accent-soft);
color: var(--text);
font-weight: 600;
font-weight: 500;
}
.thread > span:first-child {
width: 14px;
color: var(--text-dim);
font-size: 16px;
font-size: 14px;
text-align: center;
}
.empty,
+20 -2
View File
@@ -17,6 +17,7 @@ interface Props {
onRenameThread: (id: string, name: string) => Promise<void>;
onCreateGroup: (name: string) => Promise<string>;
onRenameGroup: (id: string, name: string) => Promise<void>;
onDeleteGroup: (id: string) => Promise<void>;
onGroupThread: (id: string, groupId?: string) => Promise<void>;
}
type Editor = { kind: 'channel' | 'group'; id?: string; groupId?: string };
@@ -29,16 +30,24 @@ export function ThreadSidebar(props: Props) {
const [chatHistory, setChatHistory] = useState(true);
const [filesHistory, setFilesHistory] = useState(true);
const [busy, setBusy] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
const [error, setError] = useState('');
const [collapsed, setCollapsed] = useState<ReadonlySet<string>>(new Set());
const knownGroups = new Set(props.groups.map((group) => group.id));
const ungrouped = props.threads.filter((thread) => !thread.groupId || !knownGroups.has(thread.groupId));
function begin(target: Editor, initial = '') {
setEdit(target); setName(initial); setGroupId(target.groupId ?? ''); setError('');
setEdit(target); setName(initial); setGroupId(target.groupId ?? ''); setError(''); setConfirmDelete(false);
const channel = props.threads.find(t => t.id === target.id); setChatHistory(channel?.chatHistory ?? true); setFilesHistory(channel?.filesHistory ?? true);
}
function toggle(id: string) { setCollapsed((previous) => { const next = new Set(previous); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }
function select(id: string) { props.onSelectThread(id); setEdit(undefined); }
async function removeGroup() {
if (!edit?.id || busy || !props.enabled) return;
setBusy(true); setError('');
try { await props.onDeleteGroup(edit.id); setEdit(undefined); }
catch (cause) { setError(cause instanceof Error ? cause.message : '删除失败'); }
finally { setBusy(false); }
}
async function submit(event: FormEvent) {
event.preventDefault(); if (!edit || !name.trim() || busy || !props.enabled) return;
setBusy(true); setError('');
@@ -57,7 +66,7 @@ export function ThreadSidebar(props: Props) {
finally { setBusy(false); }
}
return <section className={styles.panel} aria-label="空间频道导航">
<div className={styles.heading}><strong>频道</strong><Button variant="ghost" icon="folder" label="新建频道分组" disabled={!props.enabled || busy} onClick={() => begin({ kind: 'group' })} /></div>
<div className={styles.heading}><strong>频道</strong><Button variant="ghost" icon="plus" label="新建频道分组" disabled={!props.enabled || busy} onClick={() => begin({ kind: 'group' })} /></div>
{(ungrouped.length > 0 || props.groups.length === 0) ? <section className={styles.group} aria-label="未分组频道">
<div className={styles.groupHeading}>
<button className={styles.groupToggle} type="button" aria-label="未分组频道" aria-expanded={!collapsed.has(UNGROUPED)} onClick={() => toggle(UNGROUPED)}>
@@ -94,6 +103,15 @@ export function ThreadSidebar(props: Props) {
<label className={styles.check}><input type="checkbox" checked={chatHistory} onChange={event => setChatHistory(event.target.checked)} />新成员可读历史聊天</label>
<label className={styles.check}><input type="checkbox" checked={filesHistory} onChange={event => setFilesHistory(event.target.checked)} />新成员可读历史文件</label>
</> : null}
{edit.kind === 'group' && edit.id ? <div className={styles.dangerZone}>
{confirmDelete ? <>
<p className={styles.hint}>分组会被删除,其中的频道回到「未分组」,频道和消息都不会丢。</p>
<div className={styles.groupActions}>
<Button variant="ghost" disabled={busy} onClick={() => setConfirmDelete(false)}>先不删</Button>
<Button variant="danger" disabled={busy} onClick={() => { void removeGroup(); }}>确认删除分组</Button>
</div>
</> : <Button variant="ghost" icon="trash" disabled={busy} onClick={() => setConfirmDelete(true)}>删除分组</Button>}
</div> : null}
{error ? <p className={styles.error} role="alert">{error}</p> : null}
<div className={`${styles.groupActions} ${styles.modalActions}`}>
<Button variant="ghost" disabled={busy} onClick={() => setEdit(undefined)}>取消</Button>
+2 -1
View File
@@ -43,6 +43,7 @@ export function Workspace({ onCreateChannel }: { onCreateChannel: () => void })
const setChannelHistory = useWorkspace(s => s.setChannelHistory);
const createGroup = useWorkspace((s) => s.createGroup);
const renameGroup = useWorkspace((s) => s.renameGroup);
const deleteGroup = useWorkspace((s) => s.deleteGroup);
const groupThread = useWorkspace((s) => s.groupThread);
const folders = useWorkspace((s) => s.folders);
const sendText = useWorkspace((s) => s.sendText);
@@ -127,7 +128,7 @@ export function Workspace({ onCreateChannel }: { onCreateChannel: () => void })
const rail = <SpaceRail id={channel?.id} onCreate={onCreateChannel} onJoin={() => setScanOpen(true)} />;
const threadSidebar = <ThreadSidebar threads={threads} groups={groups} selectedThreadId={activeThreadId} enabled={Boolean(channel?.canManageStructure)} owner={channel?.role === 'owner'} onHistory={setChannelHistory}
onSelectThread={(id) => { selectThread(id); setDrawerOpen(false); setMobileView('chat'); }} onCreateThread={createThread} onRenameThread={renameThread}
onCreateGroup={createGroup} onRenameGroup={renameGroup} onGroupThread={groupThread} />;
onCreateGroup={createGroup} onRenameGroup={renameGroup} onDeleteGroup={deleteGroup} onGroupThread={groupThread} />;
const navigation = <><SpaceHeader />{threadSidebar}{desktop ? <Notepad /> : null}</>;
const chat = <ChatPane threadName={activeThread?.name ?? '选择频道'} ids={messages} enabled={enabled} onSend={(text) => sendText(text, activeThreadId)} onDelete={deleteItem} onOpenFile={setPreviewId} onFiles={requestFiles} />;
const explorer = <>