- checkboxClass/radioClass(accent-cinnabar + 统一焦点环)+ 同名测试 - Checkbox/Radio 原子件:无 label 只渲染受控原生 input 便于就地替换 - 采用:ChainStarter 链类型单选、GeneratorRunner 入库勾选、CharacterCardItem 角色勾选
113 lines
3.8 KiB
TypeScript
113 lines
3.8 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { Play } from "lucide-react";
|
||
|
||
import { Button } from "@/components/ui/Button";
|
||
import { Field } from "@/components/ui/Field";
|
||
import { Radio } from "@/components/ui/Radio";
|
||
import { SectionHeader } from "@/components/ui/SectionHeader";
|
||
import { TextInput } from "@/components/ui/TextInput";
|
||
import { CHAIN_KINDS, type ChainKind } from "@/lib/chain/chain";
|
||
|
||
interface ChainStarterProps {
|
||
// 发起一条链(链类型 + 起始章 + 章数);父层负责 POST run + 轮询。
|
||
onStart: (chainKey: ChainKind, startChapterNo: number, count: number) => void;
|
||
// 链正在运行/等待裁决时禁用(避免重复发起)。
|
||
disabled: boolean;
|
||
}
|
||
|
||
const DEFAULT_START = 1;
|
||
const DEFAULT_COUNT = 3;
|
||
const MAX_COUNT = 50;
|
||
const DEFAULT_CHAIN: ChainKind = "draft_volume";
|
||
|
||
// 链发起表单(净新):选起始章号 + 连续写几章 → 调 onStart。
|
||
// count 1..50(对齐后端 ChainRunRequest Field 约束;前端先拦一道,越界后端 422 兜底)。
|
||
export function ChainStarter({ onStart, disabled }: ChainStarterProps) {
|
||
const [chainKey, setChainKey] = useState<ChainKind>(DEFAULT_CHAIN);
|
||
const [start, setStart] = useState(String(DEFAULT_START));
|
||
const [count, setCount] = useState(String(DEFAULT_COUNT));
|
||
|
||
const startNo = Number.parseInt(start, 10);
|
||
const countNo = Number.parseInt(count, 10);
|
||
const valid =
|
||
Number.isInteger(startNo) &&
|
||
startNo >= 1 &&
|
||
Number.isInteger(countNo) &&
|
||
countNo >= 1 &&
|
||
countNo <= MAX_COUNT;
|
||
|
||
return (
|
||
<form
|
||
className="flex flex-col gap-4 rounded border border-line bg-panel p-5"
|
||
aria-label="发起多章链"
|
||
onSubmit={(e) => {
|
||
e.preventDefault();
|
||
if (valid && !disabled) onStart(chainKey, startNo, countNo);
|
||
}}
|
||
>
|
||
<SectionHeader
|
||
title="连续写多章"
|
||
description="从指定章起循环「写章 → 四审 → 验收」;遇未决冲突会暂停等你裁决再续跑。"
|
||
/>
|
||
<fieldset className="flex flex-col gap-2">
|
||
<legend className="text-sm text-ink-soft">链类型</legend>
|
||
<div className="flex flex-wrap gap-4">
|
||
{CHAIN_KINDS.map((kind) => (
|
||
<label
|
||
key={kind.key}
|
||
className="flex items-start gap-2 text-sm text-ink"
|
||
>
|
||
<Radio
|
||
name="chain-kind"
|
||
value={kind.key}
|
||
checked={chainKey === kind.key}
|
||
onChange={() => setChainKey(kind.key)}
|
||
className="mt-1"
|
||
aria-label={kind.label}
|
||
/>
|
||
<span>
|
||
<span className="block">{kind.label}</span>
|
||
<span className="block text-xs text-ink-soft">
|
||
{kind.description}
|
||
</span>
|
||
</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</fieldset>
|
||
<div className="flex flex-wrap gap-4">
|
||
<Field label="起始章号" className="w-32">
|
||
<TextInput
|
||
type="number"
|
||
min={1}
|
||
value={start}
|
||
onChange={(e) => setStart(e.target.value)}
|
||
aria-label="起始章号"
|
||
/>
|
||
</Field>
|
||
<Field label={`连续章数(1..${MAX_COUNT})`} className="w-40">
|
||
<TextInput
|
||
type="number"
|
||
min={1}
|
||
max={MAX_COUNT}
|
||
value={count}
|
||
onChange={(e) => setCount(e.target.value)}
|
||
aria-label="连续章数"
|
||
/>
|
||
</Field>
|
||
</div>
|
||
<Button
|
||
type="submit"
|
||
disabled={disabled || !valid}
|
||
variant="primary"
|
||
className="self-start"
|
||
>
|
||
<Play className="h-4 w-4" aria-hidden="true" />
|
||
{disabled ? "运行中…" : "发起多章链"}
|
||
</Button>
|
||
</form>
|
||
);
|
||
}
|