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