feat:前端修改(我认为ok)
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user