PMFlow/frontend/src/pages/TaskBriefTemplatePage.tsx

189 lines
8.1 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.

import { useEffect, useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { LayoutTemplate } from "lucide-react";
import { taskBriefApi, type TaskBriefTemplate } from "@/api/taskBriefs";
import { ConfigSubpageHeader } from "@/components/ai-config/ConfigSubpageHeader";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
export default function TaskBriefTemplatePage() {
const queryClient = useQueryClient();
const [selectedId, setSelectedId] = useState<string>("");
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [content, setContent] = useState("");
const templatesQuery = useQuery({
queryKey: ["task-brief-templates"],
queryFn: () => taskBriefApi.listTemplates(),
});
const templates = templatesQuery.data || [];
const selectedTemplate = useMemo(
() => templates.find((item) => item.id === selectedId) || null,
[templates, selectedId]
);
useEffect(() => {
if (!selectedTemplate) return;
setName(selectedTemplate.name || "");
setDescription(selectedTemplate.description || "");
setContent(selectedTemplate.content || "");
}, [selectedTemplate]);
const createMutation = useMutation({
mutationFn: (payload: { name: string; description: string; content: string }) =>
taskBriefApi.createTemplate({
name: payload.name,
description: payload.description || undefined,
content: payload.content,
}),
onSuccess: (created: TaskBriefTemplate) => {
queryClient.invalidateQueries({ queryKey: ["task-brief-templates"] });
setSelectedId(created.id);
},
});
const updateMutation = useMutation({
mutationFn: (payload: { id: string; name: string; description: string; content: string }) =>
taskBriefApi.updateTemplate(payload.id, {
name: payload.name,
description: payload.description || undefined,
content: payload.content,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["task-brief-templates"] });
},
});
const templatesErrorText = templatesQuery.error instanceof Error ? templatesQuery.error.message : "请检查后端服务。";
return (
<div className="space-y-6">
<ConfigSubpageHeader
title="任务包模板管理"
description="统一维护增量任务包模板,保障跨任务输出结构一致。"
icon={<LayoutTemplate className="h-5 w-5" />}
tone="violet"
meta={<span>{templatesQuery.isError ? "模板读取失败" : `模板总数 ${templates.length}`}</span>}
/>
<div className="grid gap-6 xl:grid-cols-[1.1fr,0.9fr]">
<Card className="border-slate-200/80 bg-white/90 shadow-sm backdrop-blur-sm dark:border-slate-800 dark:bg-slate-900/85">
<CardHeader>
<CardTitle className="text-base">模板编辑器</CardTitle>
<CardDescription>支持新建与覆盖更新。</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid gap-3 md:grid-cols-2">
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="模板名称" />
<Input
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="模板描述(可选)"
/>
</div>
<Textarea
rows={12}
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="模板正文"
/>
<div className="flex flex-wrap items-center gap-2">
<Button
className="bg-blue-600 text-white hover:bg-blue-700"
disabled={createMutation.isPending || !name.trim() || !content.trim()}
onClick={() => createMutation.mutate({ name, description, content })}
>
{createMutation.isPending ? "创建中..." : "新建模板"}
</Button>
<Button
variant="outline"
disabled={updateMutation.isPending || !selectedId || !name.trim() || !content.trim()}
onClick={() => updateMutation.mutate({ id: selectedId, name, description, content })}
>
{updateMutation.isPending ? "保存中..." : "保存当前模板"}
</Button>
</div>
{createMutation.isError || updateMutation.isError ? (
<p className="text-xs text-rose-600 dark:text-rose-300">
保存失败:
{createMutation.error instanceof Error
? createMutation.error.message
: updateMutation.error instanceof Error
? updateMutation.error.message
: "请重试"}
</p>
) : null}
</CardContent>
</Card>
<Card className="border-slate-200/80 bg-white/90 shadow-sm backdrop-blur-sm dark:border-slate-800 dark:bg-slate-900/85">
<CardHeader>
<CardTitle className="text-base">模板列表</CardTitle>
<CardDescription>点击“编辑”可回填到左侧编辑器。</CardDescription>
</CardHeader>
<CardContent>
{templatesQuery.isLoading ? (
<p className="text-sm text-slate-500 dark:text-slate-400">加载中...</p>
) : templatesQuery.isError ? (
<div className="rounded-xl border border-rose-200/70 bg-rose-50/80 p-4 dark:border-rose-900/40 dark:bg-rose-950/30">
<p className="text-sm font-medium text-rose-700 dark:text-rose-200">模板读取失败:{templatesErrorText}</p>
<Button
className="mt-3"
variant="outline"
size="sm"
onClick={() => void templatesQuery.refetch()}
disabled={templatesQuery.isFetching}
>
{templatesQuery.isFetching ? "重试中..." : "重试"}
</Button>
</div>
) : templates.length === 0 ? (
<p className="text-sm text-slate-500 dark:text-slate-400">暂无模板。</p>
) : (
<div className="overflow-x-auto rounded-xl border border-slate-200/90 dark:border-slate-700">
<Table>
<TableHeader>
<TableRow>
<TableHead>名称</TableHead>
<TableHead>描述</TableHead>
<TableHead>更新时间</TableHead>
<TableHead className="text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{templates.map((item) => (
<TableRow
key={item.id}
className={
item.id === selectedId
? "bg-blue-50/80 dark:bg-blue-950/35"
: "hover:bg-slate-50/70 dark:hover:bg-slate-800/60"
}
>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell className="max-w-[320px] truncate">{item.description || "-"}</TableCell>
<TableCell>{item.updated_at || "-"}</TableCell>
<TableCell className="text-right">
<Button size="sm" variant="outline" onClick={() => setSelectedId(item.id)}>
编辑
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}