feat:前端修改(我认为ok)
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { adminApi, errMessage, type WorkOrder } from "@/lib/admin-api";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
EmptyState,
|
||||
ErrorState,
|
||||
LoadingBlock,
|
||||
Modal,
|
||||
Pagination,
|
||||
Select,
|
||||
StatCard,
|
||||
TableWrap,
|
||||
Td,
|
||||
TextInput,
|
||||
Th,
|
||||
formatTime,
|
||||
statusTone,
|
||||
useToast,
|
||||
} from "@/components/admin/ui";
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: "", label: "全部状态" },
|
||||
{ value: "待处理", label: "待处理" },
|
||||
{ value: "处理中", label: "处理中" },
|
||||
{ value: "待审核", label: "待审核" },
|
||||
{ value: "已完成", label: "已完成" },
|
||||
{ value: "已驳回", label: "已驳回" },
|
||||
];
|
||||
|
||||
export function WorkOrdersSection() {
|
||||
const toast = useToast();
|
||||
const [items, setItems] = useState<WorkOrder[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [status, setStatus] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [busyId, setBusyId] = useState<number | null>(null);
|
||||
const [reviewing, setReviewing] = useState<WorkOrder | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const result = await adminApi.workOrders({ status: status || 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, page]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
async function run(id: number, task: () => Promise<unknown>, message: string) {
|
||||
setBusyId(id);
|
||||
try {
|
||||
await task();
|
||||
toast(message);
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast(errMessage(e, "操作失败"), "error");
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
}
|
||||
|
||||
const pending = items.filter((item) => item.status === "待处理").length;
|
||||
const reviewingCount = items.filter((item) => item.status === "待审核").length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<StatCard label="工单总数" value={total} hint={status ? `筛选:${status}` : "全部状态"} />
|
||||
<StatCard label="本页待认领" value={pending} hint="待处理状态" />
|
||||
<StatCard
|
||||
label="本页待复核"
|
||||
value={reviewingCount}
|
||||
hint={reviewingCount > 0 ? "等待复核结论" : "暂无待复核工单"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title="工单处理"
|
||||
description="认领 → 提交审核 → 复核,状态由后端流转控制"
|
||||
bodyClassName="p-0"
|
||||
className="mt-6"
|
||||
actions={
|
||||
<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>
|
||||
}
|
||||
>
|
||||
{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>状态</Th>
|
||||
<Th>创建时间</Th>
|
||||
<Th className="text-right">操作</Th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{items.map((order) => (
|
||||
<tr key={order.id} className="transition-colors hover:bg-slate-50">
|
||||
<Td className="font-medium text-slate-900">{order.work_order_no}</Td>
|
||||
<Td>
|
||||
{order.order_type}
|
||||
{order.sub_type && <span className="text-xs text-slate-400"> · {order.sub_type}</span>}
|
||||
</Td>
|
||||
<Td className="text-slate-500">#{order.customer_id ?? "--"}</Td>
|
||||
<Td>{order.current_node ?? "--"}</Td>
|
||||
<Td>
|
||||
<Badge
|
||||
tone={
|
||||
order.priority === "特急" ? "danger" : order.priority === "紧急" ? "warning" : "neutral"
|
||||
}
|
||||
>
|
||||
{order.priority}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge tone={statusTone(order.status)}>{order.status}</Badge>
|
||||
</Td>
|
||||
<Td className="text-xs text-slate-500">{formatTime(order.create_time)}</Td>
|
||||
<Td>
|
||||
<div className="flex justify-end gap-2">
|
||||
{order.status === "待处理" && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
loading={busyId === order.id}
|
||||
onClick={() => void run(order.id, () => adminApi.claimWorkOrder(order.id), "工单已认领")}
|
||||
>
|
||||
认领
|
||||
</Button>
|
||||
)}
|
||||
{order.status === "处理中" && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
loading={busyId === order.id}
|
||||
onClick={() =>
|
||||
void run(order.id, () => adminApi.submitWorkOrder(order.id), "已提交审核")
|
||||
}
|
||||
>
|
||||
提交审核
|
||||
</Button>
|
||||
)}
|
||||
{order.status === "待审核" && (
|
||||
<Button size="sm" variant="primary" onClick={() => setReviewing(order)}>
|
||||
复核
|
||||
</Button>
|
||||
)}
|
||||
{(order.status === "已完成" || order.status === "已驳回") && (
|
||||
<span className="text-xs text-slate-400">已归档</span>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</TableWrap>
|
||||
<Pagination page={page} totalPages={totalPages} total={total} onChange={setPage} />
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{reviewing && (
|
||||
<ReviewModal
|
||||
order={reviewing}
|
||||
onClose={() => setReviewing(null)}
|
||||
onDone={async (approve, comment) => {
|
||||
await run(
|
||||
reviewing.id,
|
||||
() => adminApi.reviewWorkOrder(reviewing.id, approve, comment),
|
||||
approve ? "复核通过" : "已驳回"
|
||||
);
|
||||
setReviewing(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewModal({
|
||||
order,
|
||||
onClose,
|
||||
onDone,
|
||||
}: {
|
||||
order: WorkOrder;
|
||||
onClose: () => void;
|
||||
onDone: (approve: boolean, comment: string) => Promise<void>;
|
||||
}) {
|
||||
const [comment, setComment] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
async function submit(approve: boolean) {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await onDone(approve, comment);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
title={`复核工单 ${order.work_order_no}`}
|
||||
onClose={onClose}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="danger" loading={submitting} onClick={() => void submit(false)}>
|
||||
驳回
|
||||
</Button>
|
||||
<Button variant="primary" loading={submitting} onClick={() => void submit(true)}>
|
||||
通过
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg bg-slate-50 p-3 text-xs text-slate-600">
|
||||
<p>工单类型:{order.order_type}</p>
|
||||
<p>关联客户:#{order.customer_id ?? "--"}</p>
|
||||
<p>优先级:{order.priority}</p>
|
||||
<p>当前节点:{order.current_node ?? "--"}</p>
|
||||
</div>
|
||||
{order.biz_content && (
|
||||
<div className="rounded-lg border border-slate-200 p-3 text-xs text-slate-600">
|
||||
<p className="mb-1 font-medium text-slate-500">业务内容</p>
|
||||
<pre className="whitespace-pre-wrap break-all text-[11px] leading-5 text-slate-500">
|
||||
{JSON.stringify(order.biz_content, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-slate-500">复核意见(可选)</label>
|
||||
<TextInput
|
||||
value={comment}
|
||||
onChange={(event) => setComment(event.target.value)}
|
||||
placeholder="填写复核说明"
|
||||
/>
|
||||
</div>
|
||||
{order.handler_id !== null && (
|
||||
<p className="text-xs text-amber-600">提示:若复核人与处理人为同一人,系统仅提示不阻断。</p>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user