Files
writer-work-flow/apps/web/components/rules/RulesPage.tsx

153 lines
5.2 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 { Plus, Trash2 } from "lucide-react";
import { useMemo, useState } from "react";
import { AppShell } from "@/components/AppShell";
import { Button } from "@/components/ui/Button";
import { EmptyState } from "@/components/ui/EmptyState";
import { Field } from "@/components/ui/Field";
import { PageHeader } from "@/components/ui/PageHeader";
import { Select } from "@/components/ui/Select";
import { TextArea } from "@/components/ui/TextArea";
import type { ProjectResponse, RuleView } from "@/lib/api/types";
import {
RULE_LEVELS,
RULE_LEVEL_LABELS,
groupByLevel,
type RuleLevel,
} from "@/lib/rules/rules";
import { useRules } from "@/lib/rules/useRules";
import { cardClass } from "@/lib/ui/variants";
interface RulesPageProps {
project: ProjectResponse;
initialRules: RuleView[];
}
// 规则页UX §7四级规则列表 + 新增(乐观 + 回滚)。
export function RulesPage({ project, initialRules }: RulesPageProps) {
const { items, busy, add, remove } = useRules(initialRules);
const [level, setLevel] = useState<RuleLevel>("project");
const [content, setContent] = useState("");
const groups = useMemo(() => groupByLevel(items), [items]);
const onSubmit = async (e: React.FormEvent): Promise<void> => {
e.preventDefault();
const ok = await add(project.id, level, content);
if (ok) setContent("");
};
return (
<AppShell
title={`${project.title}`}
subtitle="规则"
projectId={project.id}
activeNav="rules"
>
<div className="mx-auto flex max-w-3xl flex-col gap-6 p-6">
<PageHeader
title="规则"
description="维护本作、题材、世界观和章节级硬约束,写作与审稿会共同引用这些规则。"
/>
<form
onSubmit={onSubmit}
className="rounded border border-line bg-panel p-4"
>
<h2 className="mb-3 font-serif text-lg text-ink"></h2>
<div className="mb-3 flex items-end gap-3">
<Field label="级别" htmlFor="rule-level">
<Select
id="rule-level"
value={level}
onChange={(e) => setLevel(e.target.value as RuleLevel)}
>
{RULE_LEVELS.map((lv) => (
<option key={lv} value={lv}>
{RULE_LEVEL_LABELS[lv]}
</option>
))}
</Select>
</Field>
</div>
<Field label="规则内容" htmlFor="rule-content">
<TextArea
id="rule-content"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="如:本作禁用现代科技词汇;称呼一律用「道友」"
rows={3}
/>
</Field>
<Button
type="submit"
disabled={busy || content.trim().length === 0}
className="mt-3"
variant="primary"
>
<Plus className="h-4 w-4" aria-hidden="true" />
{busy ? "保存中…" : "新增规则"}
</Button>
</form>
<div className="flex flex-col gap-4">
{RULE_LEVELS.map((lv) => (
<section key={lv}>
<h2 className="mb-2 font-serif text-sm text-ink">
{RULE_LEVEL_LABELS[lv]}
<span className="ml-2 text-xs text-ink-soft">
{groups[lv].length}
</span>
</h2>
{groups[lv].length === 0 ? (
<EmptyState
icon={Plus}
title="暂无规则"
description={`还没有${RULE_LEVEL_LABELS[lv]}规则。`}
action={
<Button
onClick={() => setLevel(lv)}
variant="secondary"
size="sm"
>
<Plus className="h-4 w-4" aria-hidden="true" />
{RULE_LEVEL_LABELS[lv]}
</Button>
}
className="px-4 py-6"
/>
) : (
<ul className="flex flex-col gap-2">
{groups[lv].map((rule) => (
<li
key={rule.id}
className={cardClass(
"flex items-start justify-between gap-3 p-3 text-sm text-ink",
)}
>
<span className="min-w-0 flex-1 break-words">
{rule.content}
</span>
<Button
variant="danger"
size="sm"
disabled={busy}
onClick={() => void remove(project.id, rule.id)}
aria-label="删除规则"
className="shrink-0"
>
<Trash2 className="h-3 w-3" aria-hidden="true" />
</Button>
</li>
))}
</ul>
)}
</section>
))}
</div>
</div>
</AppShell>
);
}