fix(web): 链相位语义色/失败可重试 + 模板填入中文标签/删除可撤销/工具卡文案 + 模板库 PageHeader

This commit is contained in:
Yaojia Wang
2026-06-30 08:56:23 +02:00
parent 6bb3559e25
commit 164e98887e
10 changed files with 235 additions and 74 deletions

View File

@@ -2,34 +2,42 @@
import { useCallback, useState } from "react";
import { FileText } from "lucide-react";
import { useToast } from "@/components/Toast";
import { Button } from "@/components/ui/Button";
import { EmptyState } from "@/components/ui/EmptyState";
import { Field } from "@/components/ui/Field";
import { SectionHeader } from "@/components/ui/SectionHeader";
import { Select } from "@/components/ui/Select";
import { TextArea } from "@/components/ui/TextArea";
import { TextInput } from "@/components/ui/TextInput";
import { api } from "@/lib/api/client";
import type { TemplateResponse } from "@/lib/api/types";
import type { TemplateResponse, ToolDescriptorView } from "@/lib/api/types";
import {
buildTemplateCreateRequest,
EMPTY_TEMPLATE_DRAFT,
isTemplateDraftValid,
type TemplateDraft,
} from "@/lib/templates/templates";
import { toolKeyOptions } from "@/lib/toolbox/toolbox";
import { cardClass } from "@/lib/ui/variants";
interface TemplatesManagerProps {
// 初始模板列表Server Component 预取;失败给空数组 + loadError 文案)。
initial: TemplateResponse[];
// 工具描述符(供「关联工具」下拉:展示中文标题、值用 key。空 → 退化为「不关联」单选。
tools: ToolDescriptorView[];
}
// 提示词/模板库管理F3单用户本地版列出 / 新建 / 删除。
// 列表本地维护乐观更新失败回滚CRUD 走 OpenAPI 客户端。分享/市场不做(需多租户)。
export function TemplatesManager({ initial }: TemplatesManagerProps) {
export function TemplatesManager({ initial, tools }: TemplatesManagerProps) {
const toast = useToast();
const [templates, setTemplates] = useState<TemplateResponse[]>(initial);
const [draft, setDraft] = useState<TemplateDraft>(EMPTY_TEMPLATE_DRAFT);
const [creating, setCreating] = useState(false);
const toolOptions = toolKeyOptions(tools);
const setField = useCallback(
(field: keyof TemplateDraft, value: string): void => {
@@ -62,27 +70,54 @@ export function TemplatesManager({ initial }: TemplatesManagerProps) {
}
}, [draft, toast]);
// 撤销删除:以原模板内容重新入库(后端无恢复接口,新建即可;新 id 不影响用途)。
const restore = useCallback(
async (removed: TemplateResponse): Promise<void> => {
try {
const { data, error } = await api.POST("/templates", {
body: {
title: removed.title,
body: removed.body,
category: removed.category ?? null,
tool_key: removed.tool_key ?? null,
},
});
if (error || !data) {
toast("撤销失败,请重新新建。", "error");
return;
}
setTemplates((cur) => [data, ...cur]);
} catch {
toast("撤销请求异常,请检查网络。", "error");
}
},
[toast],
);
const onDelete = useCallback(
async (id: string): Promise<void> => {
async (removed: TemplateResponse): Promise<void> => {
const prev = templates;
// 乐观删除:先移除,失败回滚。
setTemplates((cur) => cur.filter((t) => t.id !== id));
setTemplates((cur) => cur.filter((t) => t.id !== removed.id));
try {
const { error } = await api.DELETE("/templates/{template_id}", {
params: { path: { template_id: id } },
params: { path: { template_id: removed.id } },
});
if (error) {
setTemplates(prev);
toast("删除模板失败,请重试。", "error");
return;
}
toast("已删除模板。", "success");
// 成功后给「撤销」action第三参误删可一键以原内容重新入库。
toast(`已删除模板「${removed.title}」。`, "success", {
action: { label: "撤销", onClick: () => void restore(removed) },
});
} catch {
setTemplates(prev);
toast("删除模板请求异常,请检查网络。", "error");
}
},
[templates, toast],
[templates, toast, restore],
);
return (
@@ -121,13 +156,19 @@ export function TemplatesManager({ initial }: TemplatesManagerProps) {
aria-label="分类"
/>
</Field>
<Field label="关联生成器 key(可选)" className="w-full sm:w-48">
<TextInput
type="text"
<Field label="关联工具(可选)" className="w-full sm:w-48">
<Select
value={draft.toolKey}
onChange={(e) => setField("toolKey", e.target.value)}
aria-label="关联生成器 key"
/>
aria-label="关联工具"
>
<option value=""></option>
{toolOptions.map((opt) => (
<option key={opt.key} value={opt.key}>
{opt.title}
</option>
))}
</Select>
</Field>
</div>
<Button
@@ -143,7 +184,11 @@ export function TemplatesManager({ initial }: TemplatesManagerProps) {
<section className="flex flex-col gap-3" aria-label="模板列表">
<h2 className="font-serif text-lg text-ink">{templates.length}</h2>
{templates.length === 0 ? (
<p className="text-sm text-ink-soft"></p>
<EmptyState
icon={FileText}
title="还没有模板"
description="用上面的表单保存一段常用提示词,之后在生成器里就能一键填入需求 / 原文。"
/>
) : (
<ul className="flex flex-col gap-2">
{templates.map((t) => (
@@ -164,7 +209,7 @@ export function TemplatesManager({ initial }: TemplatesManagerProps) {
</div>
<Button
type="button"
onClick={() => void onDelete(t.id)}
onClick={() => void onDelete(t)}
variant="danger"
size="sm"
className="shrink-0"