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

200 lines
6.9 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.
"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>
);
}