feat:前端修改(我认为ok)
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
|
||||
import { ListChecks } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { adminApi, errMessage, type TodoItem } from "@/lib/admin-api";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
EmptyState,
|
||||
ErrorState,
|
||||
LoadingBlock,
|
||||
Pagination,
|
||||
SearchInput,
|
||||
Select,
|
||||
TableWrap,
|
||||
Td,
|
||||
Th,
|
||||
formatTime,
|
||||
isOverdue,
|
||||
statusTone,
|
||||
useToast,
|
||||
} from "@/components/admin/ui";
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: "", label: "全部状态" },
|
||||
{ value: "待处理", label: "待处理" },
|
||||
{ value: "处理中", label: "处理中" },
|
||||
{ value: "已完成", label: "已完成" },
|
||||
];
|
||||
|
||||
const PRIORITY_TONE = { 高: "danger", 中: "warning", 低: "neutral" } as const;
|
||||
|
||||
export function TodosSection({ onOpenCustomer }: { onOpenCustomer?: (customerId: number) => void }) {
|
||||
const toast = useToast();
|
||||
const [items, setItems] = useState<TodoItem[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [status, setStatus] = useState("待处理");
|
||||
const [type, setType] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [busyId, setBusyId] = useState<number | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const result = await adminApi.todos({ status: status || undefined, todo_type: type || undefined, page, page_size: 10 });
|
||||
setItems(result.items ?? []);
|
||||
setTotal(result.total ?? 0);
|
||||
setTotalPages(result.total_pages ?? 0);
|
||||
} catch (e) {
|
||||
setError(errMessage(e, "待办加载失败"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [status, type, page]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
async function handle(todo: TodoItem, action: "process" | "done") {
|
||||
setBusyId(todo.id);
|
||||
try {
|
||||
await adminApi.handleTodo(todo.id, action);
|
||||
toast(action === "done" ? "待办已标记完成" : "待办已开始处理");
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast(errMessage(e, "操作失败"), "error");
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title="我的待办"
|
||||
description="投顾待办事项,支持按状态筛选和直接处理"
|
||||
bodyClassName="p-0"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-3 border-b border-slate-100 p-4">
|
||||
<Select value={status} onChange={(event) => { setStatus(event.target.value); setPage(1); }}>
|
||||
{STATUS_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<SearchInput
|
||||
className="w-56"
|
||||
placeholder="按待办类型筛选"
|
||||
value={type}
|
||||
onChange={(event) => { setType(event.target.value); setPage(1); }}
|
||||
/>
|
||||
<Button size="sm" className="ml-auto" onClick={() => void load()} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<LoadingBlock />
|
||||
) : error ? (
|
||||
<ErrorState message={error} onRetry={() => void load()} />
|
||||
) : items.length === 0 ? (
|
||||
<EmptyState text="没有符合条件的待办" />
|
||||
) : (
|
||||
<>
|
||||
<TableWrap>
|
||||
<thead>
|
||||
<tr>
|
||||
<Th>待办类型</Th>
|
||||
<Th>关联客户</Th>
|
||||
<Th>优先级</Th>
|
||||
<Th>状态</Th>
|
||||
<Th>截止时间</Th>
|
||||
<Th className="text-right">操作</Th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{items.map((todo) => (
|
||||
<tr key={todo.id} className="transition-colors hover:bg-slate-50">
|
||||
<Td className="font-medium text-slate-900">{todo.todo_type}</Td>
|
||||
<Td>
|
||||
{todo.customer_id ? (
|
||||
<button
|
||||
onClick={() => onOpenCustomer?.(todo.customer_id as number)}
|
||||
className="text-indigo-600 hover:underline"
|
||||
>
|
||||
客户 #{todo.customer_id}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-slate-400">--</span>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge tone={PRIORITY_TONE[todo.priority as keyof typeof PRIORITY_TONE] ?? "neutral"}>
|
||||
{todo.priority ?? "普通"}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge tone={statusTone(todo.status)}>{todo.status}</Badge>
|
||||
</Td>
|
||||
<Td className={isOverdue(todo.due_at) && todo.status !== "已完成" ? "text-red-500" : "text-slate-500"}>
|
||||
{formatTime(todo.due_at)}
|
||||
{isOverdue(todo.due_at) && todo.status !== "已完成" && " · 已逾期"}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex justify-end gap-2">
|
||||
{todo.status === "待处理" && (
|
||||
<Button size="sm" loading={busyId === todo.id} onClick={() => void handle(todo, "process")}>
|
||||
开始处理
|
||||
</Button>
|
||||
)}
|
||||
{todo.status !== "已完成" && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
loading={busyId === todo.id}
|
||||
onClick={() => void handle(todo, "done")}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
)}
|
||||
{todo.status === "已完成" && <ListChecks className="h-4 w-4 text-emerald-500" />}
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</TableWrap>
|
||||
<Pagination page={page} totalPages={totalPages} total={total} onChange={setPage} />
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user