Files
writer-work-flow/apps/web/components/foreshadow/RegisterForm.tsx
2026-06-28 07:31:20 +02:00

186 lines
5.0 KiB
TypeScript
Raw 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 { useState, type ReactNode } from "react";
import { Plus } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { TextInput } from "@/components/ui/TextInput";
import type { RegisterInput } from "@/lib/foreshadow/board";
interface RegisterFormProps {
projectId: string;
busy: boolean;
onRegister: (input: RegisterInput) => Promise<boolean>;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
const EMPTY = {
code: "",
title: "",
plantedAt: "",
content: "",
expectedCloseFrom: "",
expectedCloseTo: "",
importance: "",
};
// 登记新伏笔UX §6.8)。受控表单 → 组装 RegisterInput成功后清空。
export function RegisterForm({
projectId,
busy,
onRegister,
open: controlledOpen,
onOpenChange,
}: RegisterFormProps) {
const [localOpen, setLocalOpen] = useState(false);
const [form, setForm] = useState({ ...EMPTY });
const open = controlledOpen ?? localOpen;
const setOpen = (next: boolean): void => {
if (controlledOpen === undefined) setLocalOpen(next);
onOpenChange?.(next);
};
const set = (key: keyof typeof EMPTY, value: string): void =>
setForm((prev) => ({ ...prev, [key]: value }));
const submit = async (): Promise<void> => {
const ok = await onRegister({
projectId,
code: form.code,
title: form.title,
plantedAt: toInt(form.plantedAt),
content: form.content || null,
expectedCloseFrom: toInt(form.expectedCloseFrom),
expectedCloseTo: toInt(form.expectedCloseTo),
importance: form.importance || null,
});
if (ok) {
setForm({ ...EMPTY });
setOpen(false);
}
};
if (!open) {
return (
<Button onClick={() => setOpen(true)} variant="primary" size="sm">
<Plus className="h-4 w-4" aria-hidden="true" />
</Button>
);
}
const canSubmit =
form.code.trim().length > 0 && form.title.trim().length > 0 && !busy;
return (
<form
onSubmit={(e) => {
e.preventDefault();
void submit();
}}
className="rounded border border-line bg-panel p-4"
>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
<FormSlot label="代号 *" id="f-code">
<TextInput
id="f-code"
value={form.code}
onChange={(e) => set("code", e.target.value)}
/>
</FormSlot>
<FormSlot label="标题 *" id="f-title" span2>
<TextInput
id="f-title"
value={form.title}
onChange={(e) => set("title", e.target.value)}
/>
</FormSlot>
<FormSlot label="埋设章" id="f-planted">
<TextInput
id="f-planted"
inputMode="numeric"
value={form.plantedAt}
onChange={(e) => set("plantedAt", e.target.value)}
/>
</FormSlot>
<FormSlot label="回收窗口起" id="f-from">
<TextInput
id="f-from"
inputMode="numeric"
value={form.expectedCloseFrom}
onChange={(e) => set("expectedCloseFrom", e.target.value)}
/>
</FormSlot>
<FormSlot label="回收窗口止" id="f-to">
<TextInput
id="f-to"
inputMode="numeric"
value={form.expectedCloseTo}
onChange={(e) => set("expectedCloseTo", e.target.value)}
/>
</FormSlot>
<FormSlot label="重要度" id="f-imp">
<TextInput
id="f-imp"
value={form.importance}
onChange={(e) => set("importance", e.target.value)}
placeholder="主线/支线"
/>
</FormSlot>
<FormSlot label="线索内容" id="f-content" span3>
<TextInput
id="f-content"
value={form.content}
onChange={(e) => set("content", e.target.value)}
/>
</FormSlot>
</div>
<div className="mt-3 flex items-center gap-2">
<Button
type="submit"
disabled={!canSubmit}
variant="primary"
size="sm"
>
{busy ? "登记中…" : "登记"}
</Button>
<Button
onClick={() => setOpen(false)}
variant="secondary"
size="sm"
>
</Button>
</div>
</form>
);
}
interface FormSlotProps {
label: string;
id: string;
span2?: boolean;
span3?: boolean;
children: ReactNode;
}
function FormSlot({ label, id, span2, span3, children }: FormSlotProps) {
const span = span3 ? "col-span-2 sm:col-span-3" : span2 ? "col-span-2" : "";
return (
<div className={span}>
<label htmlFor={id} className="block text-xs text-ink-soft">
{label}
</label>
<div className="mt-0.5">{children}</div>
</div>
);
}
function toInt(raw: string): number | null {
const trimmed = raw.trim();
if (trimmed.length === 0) return null;
const n = Number.parseInt(trimmed, 10);
return Number.isInteger(n) ? n : null;
}