189 lines
8.1 KiB
TypeScript
189 lines
8.1 KiB
TypeScript
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>
|
||
);
|
||
}
|