Files
writer-work-flow/apps/web/components/foreshadow/RegisterForm.tsx

163 lines
4.5 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 } from "react";
import { Plus } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { Field } from "@/components/ui/Field";
import { TextArea } from "@/components/ui/TextArea";
import { TextInput } from "@/components/ui/TextInput";
import { cardClass } from "@/lib/ui/variants";
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 missingRequired =
form.code.trim().length === 0 || form.title.trim().length === 0;
const canSubmit = !missingRequired && !busy;
return (
<form
onSubmit={(e) => {
e.preventDefault();
void submit();
}}
className={cardClass("p-4")}
>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
<Field label="代号" required>
<TextInput
value={form.code}
onChange={(e) => set("code", e.target.value)}
/>
</Field>
<Field label="标题" required className="col-span-2">
<TextInput
value={form.title}
onChange={(e) => set("title", e.target.value)}
/>
</Field>
<Field label="埋设章">
<TextInput
inputMode="numeric"
value={form.plantedAt}
onChange={(e) => set("plantedAt", e.target.value)}
/>
</Field>
<Field label="回收窗口起">
<TextInput
inputMode="numeric"
value={form.expectedCloseFrom}
onChange={(e) => set("expectedCloseFrom", e.target.value)}
/>
</Field>
<Field label="回收窗口止">
<TextInput
inputMode="numeric"
value={form.expectedCloseTo}
onChange={(e) => set("expectedCloseTo", e.target.value)}
/>
</Field>
<Field label="重要度">
<TextInput
value={form.importance}
onChange={(e) => set("importance", e.target.value)}
placeholder="主线/支线"
/>
</Field>
<Field label="线索内容" className="col-span-2 sm:col-span-3">
<TextArea
rows={2}
value={form.content}
onChange={(e) => set("content", e.target.value)}
/>
</Field>
</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>
{missingRequired && !busy ? (
<span className="text-xs text-ink-soft"></span>
) : null}
</div>
</form>
);
}
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;
}