Files
Kokorone/apps/web/app/room/RoomView.tsx
jiantw83andClaude Sonnet 5 1bea06e3cd feat(遊戲化): 新增大廳、附身、時間流、後日談進度等遊戲化系統功能
新增角色大廳(lobby)邀約與通話、附身(possession)、時鐘(clock)抽象、
時間流與睡眠負債、後日談進度、行為選擇與旁觀反應等模組,並補齊對應冒煙
測試(R~V)與單元測試;同步調整 web/mobile 對應頁面與元件。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 09:10:26 +08:00

97 lines
3.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { createRoom, listCharacters, type CharacterSummary, type RoomMode } from "../../lib/api";
const USER_ID = "seed-user-primary";
// K-7 建房介面:建立後導向 /room/[roomId](T19-P2 起的通用房間畫面,含 SSE/立繪舞台/語音/靜音,
// 見 T19-P3),不再自己維護一份較舊、功能較少的房間顯示邏輯。
export function RoomView() {
const router = useRouter();
const [characters, setCharacters] = useState<CharacterSummary[]>([]);
const [selectedCharacterIds, setSelectedCharacterIds] = useState<string[]>([]);
const [mode, setMode] = useState<RoomMode>("GROUP");
const [topicSeed, setTopicSeed] = useState("");
const [pending, setPending] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
void listCharacters().then(setCharacters);
}, []);
async function handleCreateRoom() {
if (selectedCharacterIds.length === 0 || pending) return;
setPending(true);
setError(null);
try {
const created = await createRoom(USER_ID, selectedCharacterIds, mode, topicSeed || undefined);
router.push(`/room/${created.id}`);
} catch {
setError("房間建立失敗,請確認 api 服務是否已啟動");
setPending(false);
}
}
return (
<div className="flex flex-col gap-3 rounded-2xl border p-4" style={{ borderColor: "var(--line)" }}>
<h2 className="text-sm font-bold" style={{ color: "var(--ink)" }}>
建立房間
</h2>
<div className="flex flex-wrap gap-2 text-xs">
{characters.map((character) => {
const checked = selectedCharacterIds.includes(character.id);
return (
<label
key={character.id}
className="flex items-center gap-1 rounded-full border px-2 py-1"
style={{ borderColor: "var(--line)", color: "var(--ink)" }}
>
<input
type="checkbox"
checked={checked}
onChange={() =>
setSelectedCharacterIds((prev) => (checked ? prev.filter((id) => id !== character.id) : [...prev, character.id]))
}
/>
{character.aliases[0]?.formalName ?? character.id}
</label>
);
})}
</div>
<div className="flex items-center gap-3 text-xs">
<label className="flex items-center gap-1" style={{ color: "var(--ink)" }}>
<input type="radio" checked={mode === "GROUP"} onChange={() => setMode("GROUP")} />
群聊
</label>
<label className="flex items-center gap-1" style={{ color: "var(--ink)" }}>
<input type="radio" checked={mode === "SELF_CHAT"} onChange={() => setMode("SELF_CHAT")} />
角色自聊(旁觀)
</label>
</div>
<input
value={topicSeed}
onChange={(event) => setTopicSeed(event.target.value)}
placeholder="話題種子(選填)"
className="rounded-full border bg-transparent px-3 py-1.5 text-xs"
style={{ borderColor: "var(--line)", color: "var(--ink)" }}
/>
<button
type="button"
onClick={() => void handleCreateRoom()}
disabled={pending || selectedCharacterIds.length === 0}
className="self-start rounded-full px-4 py-1.5 text-xs font-bold text-white disabled:opacity-50"
style={{ background: "var(--accent-bright)" }}
>
建立房間
</button>
{error && (
<p className="text-xs" style={{ color: "var(--accent)" }}>
{error}
</p>
)}
</div>
);
}