Files
Kokorone/apps/web/app/settings/AdultModeSection.tsx
T
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

104 lines
3.7 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import {
checkAdultContent,
fetchRomanceEligibility,
setAdultModeConsent,
type AdultContentCheckResult,
type RomanceEligibility,
} from "../../lib/api";
const CHARACTER_ID = "seed-character-genki";
const USER_ID = "seed-user-primary";
// L-8 成人模式 opt-in 介面:只做開關與既有 API 串接。
// 「成年使用者驗證」的實際機制計畫未指定(需人工確認),這裡先讓 verifiedAdult 等同使用者自我宣告,
// 待正式驗證流程拍板後再另行補項——不在這裡臆測或假造一套驗證邏輯。
export function AdultModeSection() {
const [optedIn, setOptedIn] = useState(false);
const [eligibility, setEligibility] = useState<RomanceEligibility | null>(null);
const [checkResult, setCheckResult] = useState<AdultContentCheckResult | null>(null);
const [pending, setPending] = useState(false);
const [error, setError] = useState<string | null>(null);
async function refresh() {
setError(null);
try {
const [eligibilityResult, checkResultValue] = await Promise.all([
fetchRomanceEligibility(CHARACTER_ID),
checkAdultContent(CHARACTER_ID, USER_ID),
]);
setEligibility(eligibilityResult);
setCheckResult(checkResultValue);
} catch {
setError("無法連線到 api 服務,成人模式狀態暫時無法顯示");
}
}
useEffect(() => {
void refresh();
}, []);
async function handleToggle(next: boolean) {
setPending(true);
setError(null);
try {
await setAdultModeConsent(USER_ID, next, next);
setOptedIn(next);
await refresh();
} catch {
setError("成人模式設定失敗,請確認 api 服務是否已啟動");
} finally {
setPending(false);
}
}
return (
<section 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>
<p className="text-xs" style={{ color: "var(--ink-soft)" }}>
系統預設為全年齡純愛尺度;開啟後也只有正史成年角色能解鎖,正史未成年、後日談推演才成年的角色永久限純愛尺度。
</p>
<label className="flex items-center gap-2 text-sm" style={{ color: "var(--ink)" }}>
<input
type="checkbox"
checked={optedIn}
disabled={pending}
onChange={(event) => void handleToggle(event.target.checked)}
/>
主動開啟成人模式(正式的成年使用者驗證機制尚待人工確認,目前僅為自我宣告已成年)
</label>
{error && (
<p className="text-xs" style={{ color: "var(--accent)" }}>
{error}
</p>
)}
{eligibility && (
<div className="flex flex-col gap-1 text-xs" style={{ color: "var(--ink-soft)" }}>
<span>戀愛軸資格:{eligibility.romanceAxisEligible ? "可用" : "尚未解鎖"}</span>
<span>成人模式角色資格:{eligibility.adultModeEligible ? "符合" : "不符合"}</span>
{eligibility.isEpilogueAdult && <span>此角色為後日談推演成年,永久限純愛尺度。</span>}
</div>
)}
{checkResult && (
<p
className="rounded-lg px-3 py-2 text-xs"
style={{
background: checkResult.allowed ? "var(--chip-bg)" : "var(--bg-soft)",
color: checkResult.allowed ? "var(--accent)" : "var(--ink-soft)",
}}
>
{checkResult.allowed ? "目前可使用成人內容。" : `目前無法使用成人內容:${checkResult.reason}`}
</p>
)}
</section>
);
}