feat(journal): delete sessions from the ui

Add a confirmation dialog that spells out the removal is irreversible, and expose delete from the GM session dropdown and the lobby session list.
This commit is contained in:
2026-09-13 13:36:42 +08:00
parent a65ca2ab93
commit 421af125bb
6 changed files with 176 additions and 22 deletions
+37 -7
View File
@@ -16,6 +16,7 @@ import {
setMyRole, setMyRole,
useJournalStream, useJournalStream,
} from "../stores/journalStream"; } from "../stores/journalStream";
import { DeleteSessionDialog } from "./DeleteSessionDialog";
export const ConnectDialog: Component = () => { export const ConnectDialog: Component = () => {
const stream = useJournalStream(); const stream = useJournalStream();
@@ -28,6 +29,10 @@ export const ConnectDialog: Component = () => {
const [busy, setBusy] = createSignal(false); const [busy, setBusy] = createSignal(false);
const [error, setError] = createSignal<string | null>(null); const [error, setError] = createSignal<string | null>(null);
const [autoJoined, setAutoJoined] = createSignal(false); const [autoJoined, setAutoJoined] = createSignal(false);
const [sessionToDelete, setSessionToDelete] = createSignal<{
id: string;
name: string;
} | null>(null);
const ensureBroker = async () => { const ensureBroker = async () => {
if (stream.connected) return; if (stream.connected) return;
@@ -145,13 +150,27 @@ export const ConnectDialog: Component = () => {
<div class="max-h-40 overflow-y-auto border border-gray-200 rounded divide-y divide-gray-100"> <div class="max-h-40 overflow-y-auto border border-gray-200 rounded divide-y divide-gray-100">
<For each={Object.entries(manifest().sessions)}> <For each={Object.entries(manifest().sessions)}>
{([id, meta]) => ( {([id, meta]) => (
<button <div class="flex items-center">
onClick={() => handleJoin(id)} <button
disabled={busy()} onClick={() => handleJoin(id)}
class="w-full text-left px-2 py-1.5 text-xs text-gray-700 hover:bg-gray-50 disabled:opacity-50" disabled={busy()}
> class="flex-1 min-w-0 text-left px-2 py-1.5 text-xs text-gray-700 hover:bg-gray-50 disabled:opacity-50 truncate"
{meta.name || id} >
</button> {meta.name || id}
</button>
<Show when={role() === "gm"}>
<button
onClick={() =>
setSessionToDelete({ id, name: meta.name || id })
}
disabled={busy()}
class="shrink-0 px-2 py-1.5 text-[10px] text-gray-300 hover:text-red-500 disabled:opacity-50"
title="删除会话"
>
🗑
</button>
</Show>
</div>
)} )}
</For> </For>
<Show when={Object.keys(manifest().sessions).length === 0}> <Show when={Object.keys(manifest().sessions).length === 0}>
@@ -187,6 +206,17 @@ export const ConnectDialog: Component = () => {
<Show when={error()}> <Show when={error()}>
<p class="text-red-500 text-sm text-center">{error()}</p> <p class="text-red-500 text-sm text-center">{error()}</p>
</Show> </Show>
<Show when={sessionToDelete()}>
{(target) => (
<DeleteSessionDialog
sessionId={target().id}
sessionName={target().name}
active={stream.sessionId === target().id}
onClose={() => setSessionToDelete(null)}
/>
)}
</Show>
</div> </div>
</Show> </Show>
); );
@@ -0,0 +1,85 @@
/**
* DeleteSessionDialog — destructive confirmation before removing a session.
*
* Deletion removes the session directory on the server (meta + full message
* log), so the warning spells out that it cannot be undone.
*/
import { Component, createSignal, Show } from "solid-js";
import { deleteSession } from "../stores/journalStream";
interface DeleteSessionDialogProps {
sessionId: string;
sessionName: string;
/** Whether this is the session the user is currently in. */
active: boolean;
onClose: () => void;
}
export const DeleteSessionDialog: Component<DeleteSessionDialogProps> = (
props,
) => {
const [deleting, setDeleting] = createSignal(false);
const [error, setError] = createSignal<string | null>(null);
const handleDelete = async () => {
setDeleting(true);
setError(null);
try {
await deleteSession(props.sessionId);
props.onClose();
} catch (e) {
setError(e instanceof Error ? e.message : "删除失败");
} finally {
setDeleting(false);
}
};
return (
<div
class="fixed inset-0 z-[60] flex items-center justify-center bg-black/20"
onClick={props.onClose}
>
<div
class="bg-white rounded-lg border border-gray-200 shadow-xl w-[300px] p-4 space-y-3"
onClick={(e) => e.stopPropagation()}
>
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-red-600">删除会话</h3>
<button
onClick={props.onClose}
class="text-gray-400 hover:text-gray-600 text-sm"
>
✕
</button>
</div>
<p class="text-xs text-gray-600">
确定要删除会话{" "}
<span class="font-semibold">{props.sessionName}</span>
(<span class="font-mono">{props.sessionId}</span>)吗?
</p>
<p class="text-xs text-red-500">
此操作不可恢复:该会话的全部消息记录、玩家名单和派生状态都会从服务器删除。
{props.active && " 你当前正在该会话中,删除后将返回会话列表。"}
</p>
<Show when={error()}>
<p class="text-xs text-red-500">{error()}</p>
</Show>
<div class="flex justify-end gap-1">
<button
onClick={props.onClose}
class="px-3 py-1 text-xs text-gray-600 hover:text-gray-800"
>
取消
</button>
<button
onClick={handleDelete}
disabled={deleting()}
class="bg-red-600 text-white rounded px-3 py-1 text-xs font-medium hover:bg-red-700 disabled:opacity-50"
>
{deleting() ? "删除中…" : "删除"}
</button>
</div>
</div>
</div>
);
};
+5 -1
View File
@@ -8,6 +8,7 @@ import { SessionDropdown } from "./SessionDropdown";
interface JournalHeaderProps { interface JournalHeaderProps {
onClose: () => void; onClose: () => void;
onCreateSession: () => void; onCreateSession: () => void;
onDeleteSession: (id: string, name: string) => void;
} }
export const JournalHeader: Component<JournalHeaderProps> = (props) => { export const JournalHeader: Component<JournalHeaderProps> = (props) => {
@@ -54,7 +55,10 @@ export const JournalHeader: Component<JournalHeaderProps> = (props) => {
</p> </p>
{/* Clickable subtitle — session dropdown (GM only) */} {/* Clickable subtitle — session dropdown (GM only) */}
<Show when={stream.myRole === "gm"}> <Show when={stream.myRole === "gm"}>
<SessionDropdown onCreate={props.onCreateSession} /> <SessionDropdown
onCreate={props.onCreateSession}
onDelete={props.onDeleteSession}
/>
</Show> </Show>
</div> </div>
</Show> </Show>
+18
View File
@@ -15,6 +15,7 @@ import { JournalHeader } from "./JournalHeader";
import { ConnectDialog } from "./ConnectDialog"; import { ConnectDialog } from "./ConnectDialog";
import { InviteDialog } from "./InviteDialog"; import { InviteDialog } from "./InviteDialog";
import { CreateSessionDialog } from "./CreateSessionDialog"; import { CreateSessionDialog } from "./CreateSessionDialog";
import { DeleteSessionDialog } from "./DeleteSessionDialog";
import { VariableView } from "./VariableView"; import { VariableView } from "./VariableView";
export interface JournalPanelProps { export interface JournalPanelProps {
@@ -26,6 +27,10 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
const stream = useJournalStream(); const stream = useJournalStream();
const [showInvite, setShowInvite] = createSignal(false); const [showInvite, setShowInvite] = createSignal(false);
const [showCreateSession, setShowCreateSession] = createSignal(false); const [showCreateSession, setShowCreateSession] = createSignal(false);
const [sessionToDelete, setSessionToDelete] = createSignal<{
id: string;
name: string;
} | null>(null);
const [viewMode, setViewMode] = createSignal<"stream" | "variables">("stream"); const [viewMode, setViewMode] = createSignal<"stream" | "variables">("stream");
// Player list (exclude gm and observer) // Player list (exclude gm and observer)
@@ -56,6 +61,7 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
<JournalHeader <JournalHeader
onClose={props.onClose} onClose={props.onClose}
onCreateSession={() => setShowCreateSession(true)} onCreateSession={() => setShowCreateSession(true)}
onDeleteSession={(id, name) => setSessionToDelete({ id, name })}
/> />
{/* Player list tabs */} {/* Player list tabs */}
@@ -126,6 +132,18 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
<Show when={showCreateSession()}> <Show when={showCreateSession()}>
<CreateSessionDialog onClose={() => setShowCreateSession(false)} /> <CreateSessionDialog onClose={() => setShowCreateSession(false)} />
</Show> </Show>
{/* Delete session modal */}
<Show when={sessionToDelete()}>
{(target) => (
<DeleteSessionDialog
sessionId={target().id}
sessionName={target().name}
active={stream.sessionId === target().id}
onClose={() => setSessionToDelete(null)}
/>
)}
</Show>
</JournalContext.Provider> </JournalContext.Provider>
</aside> </aside>
</> </>
+30 -14
View File
@@ -10,6 +10,7 @@ import {
interface SessionDropdownProps { interface SessionDropdownProps {
onCreate: () => void; onCreate: () => void;
onDelete: (id: string, name: string) => void;
} }
export const SessionDropdown: Component<SessionDropdownProps> = (props) => { export const SessionDropdown: Component<SessionDropdownProps> = (props) => {
@@ -54,22 +55,37 @@ export const SessionDropdown: Component<SessionDropdownProps> = (props) => {
> >
<For each={Object.entries(manifest().sessions)}> <For each={Object.entries(manifest().sessions)}>
{([id, meta]) => ( {([id, meta]) => (
<button <div
onClick={(e) => { class={`flex items-center ${
e.stopPropagation(); id === stream.sessionId ? "bg-blue-50" : ""
handleSessionSelect(id);
}}
class={`w-full text-left px-3 py-1 text-xs hover:bg-gray-100 flex items-center gap-1 ${
id === stream.sessionId
? "bg-blue-50 text-blue-700"
: "text-gray-700"
}`} }`}
> >
<span class="truncate flex-1">{meta.name || id}</span> <button
<Show when={id === stream.sessionId}> onClick={(e) => {
<span class="text-blue-500 shrink-0">✓</span> e.stopPropagation();
</Show> handleSessionSelect(id);
</button> }}
class={`flex-1 min-w-0 text-left px-3 py-1 text-xs hover:bg-gray-100 flex items-center gap-1 ${
id === stream.sessionId ? "text-blue-700" : "text-gray-700"
}`}
>
<span class="truncate flex-1">{meta.name || id}</span>
<Show when={id === stream.sessionId}>
<span class="text-blue-500 shrink-0">✓</span>
</Show>
</button>
<button
onClick={(e) => {
e.stopPropagation();
setDropdownOpen(false);
props.onDelete(id, meta.name || id);
}}
class="shrink-0 px-2 py-1 text-[10px] text-gray-300 hover:text-red-500"
title="删除会话"
>
🗑
</button>
</div>
)} )}
</For> </For>
<div class="border-t border-gray-100 my-0.5" /> <div class="border-t border-gray-100 my-0.5" />
+1
View File
@@ -45,6 +45,7 @@ export { ConnectDialog } from "./ConnectDialog";
export { InviteDialog } from "./InviteDialog"; export { InviteDialog } from "./InviteDialog";
export { SessionDropdown } from "./SessionDropdown"; export { SessionDropdown } from "./SessionDropdown";
export { CreateSessionDialog } from "./CreateSessionDialog"; export { CreateSessionDialog } from "./CreateSessionDialog";
export { DeleteSessionDialog } from "./DeleteSessionDialog";
export { StreamView } from "./StreamView"; export { StreamView } from "./StreamView";
export { StreamMessageCard } from "./StreamMessage"; export { StreamMessageCard } from "./StreamMessage";
export { ComposePanel } from "./ComposePanel"; export { ComposePanel } from "./ComposePanel";