Files
Mutual_Fund/frontend/components/admin/sections/drafts.tsx
T

200 lines
6.9 KiB
TypeScript
Raw Normal View History

2026-09-14 20:31:06 +08:00
"use client";
import { FileSignature, Save, Send, Trash2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { adminApi, errMessage, type Draft } from "@/lib/admin-api";
import {
Badge,
Button,
Card,
EmptyState,
ErrorState,
LoadingBlock,
Pagination,
TextInput,
cn,
formatTime,
statusTone,
useToast,
} from "@/components/admin/ui";
function draftKey(draft: Draft) {
return String(draft.draft_id ?? draft.id ?? "");
}
export function DraftsSection() {
const toast = useToast();
const [items, setItems] = useState<Draft[]>([]);
const [total, setTotal] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [activeKey, setActiveKey] = useState("");
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [dirty, setDirty] = useState(false);
const [busy, setBusy] = useState(false);
const load = useCallback(async () => {
setLoading(true);
setError("");
try {
const result = await adminApi.drafts({ page, page_size: 20 });
setItems(result.items ?? []);
setTotal(result.total ?? 0);
setTotalPages(result.total_pages ?? 0);
} catch (e) {
setError(errMessage(e, "草稿列表加载失败"));
} finally {
setLoading(false);
}
}, [page]);
useEffect(() => {
void load();
}, [load]);
async function choose(draft: Draft) {
if (dirty && !window.confirm("当前草稿有未保存的修改,确认切换?")) return;
setBusy(true);
try {
const detail = await adminApi.draft(draftKey(draft));
setActiveKey(draftKey(draft));
setTitle(detail.title ?? "");
setContent(detail.content ?? "");
setDirty(false);
} catch (e) {
toast(errMessage(e, "草稿详情加载失败"), "error");
} finally {
setBusy(false);
}
}
async function run(action: "save" | "send" | "discard") {
if (!activeKey) return;
if (action === "send" && !window.confirm("确认发送该草稿?发送后将进入终审并触达客户。")) return;
if (action === "discard" && !window.confirm("确认废弃该草稿?废弃后不可恢复。")) return;
setBusy(true);
try {
if (action === "save") await adminApi.saveDraft(activeKey, { title, content: content });
if (action === "send") await adminApi.sendDraft(activeKey);
if (action === "discard") await adminApi.discardDraft(activeKey);
toast(action === "save" ? "草稿已保存" : action === "send" ? "草稿已发送" : "草稿已废弃");
setDirty(false);
if (action !== "save") {
setActiveKey("");
setTitle("");
setContent("");
}
await load();
} catch (e) {
toast(errMessage(e, "操作失败"), "error");
} finally {
setBusy(false);
}
}
return (
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.2fr)]">
<Card title="草稿列表" description="由投顾 Agent 生成,可编辑后发送" bodyClassName="p-0">
{loading ? (
<LoadingBlock />
) : error ? (
<ErrorState message={error} onRetry={() => void load()} />
) : items.length === 0 ? (
<EmptyState text="暂无草稿" />
) : (
<>
<ul className="max-h-[520px] divide-y divide-slate-100 overflow-y-auto">
{items.map((draft) => (
<li key={draftKey(draft) || draft.title}>
<button
onClick={() => void choose(draft)}
className={cn(
"w-full px-4 py-3 text-left text-sm transition-colors",
draftKey(draft) === activeKey
? "bg-indigo-50 text-indigo-700"
: "text-slate-600 hover:bg-slate-50"
)}
>
<div className="flex items-center justify-between gap-2">
<span className="truncate font-medium">{draft.title ?? "未命名草稿"}</span>
<Badge tone={statusTone(draft.status)}>{draft.status ?? "草稿"}</Badge>
</div>
<p className="mt-1 truncate text-xs text-slate-400">
{draft.customer_id ? `客户 #${draft.customer_id} · ` : ""}
{formatTime(draft.update_time ?? draft.create_time)}
</p>
</button>
</li>
))}
</ul>
<Pagination page={page} totalPages={totalPages} total={total} onChange={setPage} />
</>
)}
</Card>
<Card
title={activeKey ? "编辑草稿" : "选择草稿"}
description={activeKey ? `草稿 ID:${activeKey}` : "从左侧列表选择一份草稿进行编辑"}
actions={
activeKey ? (
<>
<Button size="sm" variant="primary" loading={busy} onClick={() => void run("save")}>
<Save className="h-3.5 w-3.5" />
保存
</Button>
<Button size="sm" loading={busy} onClick={() => void run("send")}>
<Send className="h-3.5 w-3.5" />
发送
</Button>
<Button size="sm" variant="danger" loading={busy} onClick={() => void run("discard")}>
<Trash2 className="h-3.5 w-3.5" />
废弃
</Button>
</>
) : undefined
}
>
{!activeKey ? (
<EmptyState text="暂无选中的草稿" />
) : (
<div className="space-y-4">
<div>
<label className="mb-1 block text-xs text-slate-500">草稿标题</label>
<TextInput
value={title}
onChange={(event) => {
setTitle(event.target.value);
setDirty(true);
}}
placeholder="请输入草稿标题"
/>
</div>
<div>
<label className="mb-1 block text-xs text-slate-500">草稿内容</label>
<textarea
value={content}
onChange={(event) => {
setContent(event.target.value);
setDirty(true);
}}
className="min-h-[360px] w-full rounded-lg border border-slate-200 px-3 py-2 text-sm leading-6 text-slate-700 transition-colors focus:border-indigo-400 focus:outline-none"
placeholder="请输入建议内容"
/>
</div>
{dirty && (
<p className="flex items-center gap-2 text-xs text-amber-600">
<FileSignature className="h-3.5 w-3.5" />
有未保存的修改
</p>
)}
</div>
)}
</Card>
</div>
);
}