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

147 lines
5.2 KiB
TypeScript
Raw Permalink 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 { Badge } from "@/components/ui/Badge";
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, adding, deletingId, 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={adding || content.trim().length === 0}
className="mt-3"
variant="primary"
>
<Plus className="h-4 w-4" aria-hidden="true" />
{adding ? "保存中…" : "新增规则"}
</Button>
</form>
{items.length === 0 ? (
<EmptyState
icon={Plus}
title="还没有任何规则"
description="从上方表单添加第一条,写作与审稿都会共同引用这些规则。"
/>
) : (
<div className="flex flex-col gap-4">
{RULE_LEVELS.map((lv) => (
<section key={lv}>
<h2 className="mb-2 flex items-center gap-2 font-serif text-sm text-ink">
{RULE_LEVEL_LABELS[lv]}
<Badge variant="neutral">{groups[lv].length}</Badge>
</h2>
{groups[lv].length === 0 ? (
<p className="px-1 text-xs text-ink-soft">
{RULE_LEVEL_LABELS[lv]}
</p>
) : (
<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={deletingId === rule.id}
onClick={() => void remove(project.id, rule.id)}
aria-label="删除规则"
className="shrink-0"
>
<Trash2 className="h-3 w-3" aria-hidden="true" />
{deletingId === rule.id ? "删除中…" : "删除"}
</Button>
</li>
))}
</ul>
)}
</section>
))}
</div>
)}
</div>
</AppShell>
);
}