feat: Web端401队列拦截器 + 消息/任务/个人中心重写 + TabBar红点 + Admin页面完善
This commit is contained in:
@ -10,6 +10,7 @@ import {
|
||||
Warehouse,
|
||||
UserPlus,
|
||||
} from "lucide-react";
|
||||
import TaskFlowView from "./TaskFlowView";
|
||||
import {
|
||||
getTaskTree,
|
||||
receiveTask,
|
||||
@ -17,59 +18,8 @@ import {
|
||||
transferTask,
|
||||
} from "../../services/taskApi";
|
||||
import { useToast } from "../ui/Toast";
|
||||
import type { ProductScanResponse, TaskResponse, TaskStatus } from "../../types/api";
|
||||
import { TASK_STATUS } from "../../types/api";
|
||||
|
||||
// ============================================================
|
||||
// 状态 → 颜色/标签映射
|
||||
// ============================================================
|
||||
|
||||
const STATUS_CONFIG: Record<
|
||||
string,
|
||||
{ bg: string; text: string; ring: string; label: string }
|
||||
> = {
|
||||
[TASK_STATUS.PENDING]: {
|
||||
bg: "bg-yellow-50",
|
||||
text: "text-yellow-700",
|
||||
ring: "ring-yellow-400",
|
||||
label: "待接收",
|
||||
},
|
||||
[TASK_STATUS.WIP]: {
|
||||
bg: "bg-blue-50",
|
||||
text: "text-blue-700",
|
||||
ring: "ring-blue-400",
|
||||
label: "进行中",
|
||||
},
|
||||
[TASK_STATUS.COMPLETED]: {
|
||||
bg: "bg-green-50",
|
||||
text: "text-green-700",
|
||||
ring: "ring-green-400",
|
||||
label: "已完成",
|
||||
},
|
||||
[TASK_STATUS.REJECTED]: {
|
||||
bg: "bg-red-50",
|
||||
text: "text-red-700",
|
||||
ring: "ring-red-400",
|
||||
label: "已驳回",
|
||||
},
|
||||
[TASK_STATUS.ARCHIVED]: {
|
||||
bg: "bg-gray-50",
|
||||
text: "text-gray-600",
|
||||
ring: "ring-gray-300",
|
||||
label: "已入库",
|
||||
},
|
||||
};
|
||||
|
||||
function getStatusConfig(status: string) {
|
||||
return (
|
||||
STATUS_CONFIG[status] ?? {
|
||||
bg: "bg-gray-50",
|
||||
text: "text-gray-600",
|
||||
ring: "ring-gray-300",
|
||||
label: status,
|
||||
}
|
||||
);
|
||||
}
|
||||
import type { ProductScanResponse, TaskResponse } from "../../types/api";
|
||||
import { getStatusConfig } from "../../constants/task";
|
||||
|
||||
// ============================================================
|
||||
// 通用 Modal 容器
|
||||
@ -116,7 +66,7 @@ const Modal = memo(function Modal({
|
||||
// 确认接收弹窗
|
||||
// ============================================================
|
||||
|
||||
const ReceiveConfirmModal = memo(function ReceiveConfirmModal({
|
||||
export const ReceiveConfirmModal = memo(function ReceiveConfirmModal({
|
||||
open,
|
||||
task,
|
||||
submitting,
|
||||
@ -169,7 +119,7 @@ const ReceiveConfirmModal = memo(function ReceiveConfirmModal({
|
||||
// 品质驳回弹窗
|
||||
// ============================================================
|
||||
|
||||
const RejectModal = memo(function RejectModal({
|
||||
export const RejectModal = memo(function RejectModal({
|
||||
open,
|
||||
task,
|
||||
submitting,
|
||||
@ -250,7 +200,7 @@ const RejectModal = memo(function RejectModal({
|
||||
// 完工裂变转交弹窗
|
||||
// ============================================================
|
||||
|
||||
const TransferModal = memo(function TransferModal({
|
||||
export const TransferModal = memo(function TransferModal({
|
||||
open,
|
||||
task,
|
||||
submitting,
|
||||
@ -499,162 +449,14 @@ const TransferModal = memo(function TransferModal({
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 单个任务节点卡片
|
||||
// 操作弹窗目标类型
|
||||
// ============================================================
|
||||
|
||||
interface ModalTarget {
|
||||
export interface ModalTarget {
|
||||
task: TaskResponse;
|
||||
action: "receive" | "reject" | "transfer";
|
||||
}
|
||||
|
||||
const TaskNodeCard = memo(function TaskNodeCard({
|
||||
task,
|
||||
isLast,
|
||||
onAction,
|
||||
}: {
|
||||
task: TaskResponse;
|
||||
isLast: boolean;
|
||||
onAction: (target: ModalTarget) => void;
|
||||
}) {
|
||||
const { bg, text, ring, label } = getStatusConfig(task.status);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* 树形连接线 */}
|
||||
{task.child_tasks.length > 0 && (
|
||||
<>
|
||||
<div
|
||||
className="absolute left-4 top-full z-0 w-px bg-gray-200"
|
||||
style={{ height: "calc(100% - 2rem)" }}
|
||||
/>
|
||||
{task.child_tasks.length > 1 && (
|
||||
<div
|
||||
className="absolute left-4 z-0 h-px bg-gray-200"
|
||||
style={{
|
||||
top: "calc(100% + 1rem)",
|
||||
width: "calc(50% - 1rem)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 卡片本体 */}
|
||||
<div
|
||||
className={`relative z-10 mb-1 rounded-lg border bg-white px-3 py-2.5 shadow-sm transition-shadow hover:shadow-md ${ring} ${
|
||||
task.is_rework ? "ring-2 ring-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
{/* 左侧:任务名 + 标签 */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{task.is_rework && (
|
||||
<span className="inline-flex shrink-0 items-center gap-0.5 rounded bg-red-600 px-1.5 py-0.5 text-[10px] font-bold text-white animate-pulse">
|
||||
⚠ 返工
|
||||
</span>
|
||||
)}
|
||||
{task.child_tasks.length > 1 && (
|
||||
<span className="inline-flex shrink-0 items-center gap-0.5 rounded bg-purple-100 px-1.5 py-0.5 text-[10px] font-medium text-purple-700">
|
||||
<GitBranch className="h-2.5 w-2.5" />
|
||||
裂变×{task.child_tasks.length}
|
||||
</span>
|
||||
)}
|
||||
<span className="truncate text-sm font-semibold text-gray-800">
|
||||
{task.task_name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-1 flex items-center gap-2 text-[11px] text-gray-400">
|
||||
{task.assignee_id && <span>负责人: {task.assignee_id}</span>}
|
||||
{task.received_at && (
|
||||
<span>
|
||||
接收: {new Date(task.received_at).toLocaleDateString("zh-CN")}
|
||||
</span>
|
||||
)}
|
||||
{task.completed_at && (
|
||||
<span>
|
||||
完成:{" "}
|
||||
{new Date(task.completed_at).toLocaleDateString("zh-CN")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{task.reject_reason && (
|
||||
<p className="mt-1 text-[11px] text-red-500">
|
||||
驳回原因: {task.reject_reason}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧:状态标签 + 操作按钮 */}
|
||||
<div className="flex shrink-0 flex-col items-end gap-1">
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${bg} ${text}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{task.status === TASK_STATUS.PENDING && (
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
className="rounded border border-blue-200 px-1.5 py-0.5 text-[10px] font-medium text-blue-600 hover:bg-blue-50 transition-colors"
|
||||
onClick={() => onAction({ task, action: "receive" })}
|
||||
>
|
||||
接收
|
||||
</button>
|
||||
<button
|
||||
className="rounded border border-red-200 px-1.5 py-0.5 text-[10px] font-medium text-red-500 hover:bg-red-50 transition-colors"
|
||||
onClick={() => onAction({ task, action: "reject" })}
|
||||
>
|
||||
驳回
|
||||
</button>
|
||||
<button
|
||||
className="rounded border border-green-200 px-1.5 py-0.5 text-[10px] font-medium text-green-600 hover:bg-green-50 transition-colors"
|
||||
onClick={() => onAction({ task, action: "transfer" })}
|
||||
>
|
||||
转交
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{task.status === TASK_STATUS.WIP && (
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
className="rounded border border-red-200 px-1.5 py-0.5 text-[10px] font-medium text-red-500 hover:bg-red-50 transition-colors"
|
||||
onClick={() => onAction({ task, action: "reject" })}
|
||||
>
|
||||
驳回
|
||||
</button>
|
||||
<button
|
||||
className="rounded border border-green-200 px-1.5 py-0.5 text-[10px] font-medium text-green-600 hover:bg-green-50 transition-colors"
|
||||
onClick={() => onAction({ task, action: "transfer" })}
|
||||
>
|
||||
转交
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 递归渲染子任务 */}
|
||||
{task.child_tasks.length > 0 && (
|
||||
<div className="ml-8 border-l-2 border-gray-100 pl-4 pt-1">
|
||||
{task.child_tasks.map((child, idx) => (
|
||||
<TaskNodeCard
|
||||
key={child.id}
|
||||
task={child}
|
||||
isLast={idx === task.child_tasks.length - 1}
|
||||
onAction={onAction}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 主容器组件
|
||||
// ============================================================
|
||||
@ -688,7 +490,7 @@ export default function TaskTreeViewer() {
|
||||
e?.preventDefault();
|
||||
const trimmed = serial.trim();
|
||||
if (trimmed.length !== 16) {
|
||||
setError("请输入 16 位产品序列号");
|
||||
setError("请输入 16 位产品身份证");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -702,7 +504,7 @@ export default function TaskTreeViewer() {
|
||||
} catch (err: any) {
|
||||
const msg =
|
||||
err?.response?.status === 404
|
||||
? `未找到序列号 ${trimmed} 对应的产品`
|
||||
? `未找到产品身份证 ${trimmed} 对应的产品`
|
||||
: err?.response?.data?.detail ??
|
||||
err?.message ??
|
||||
"查询失败,请检查后端服务";
|
||||
@ -793,7 +595,7 @@ export default function TaskTreeViewer() {
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-bold text-gray-800">任务全景树</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
输入 16 位产品序列号,查看完整任务流转十字矩阵树状图
|
||||
输入 16 位产品身份证,查看完整任务流转十字矩阵树状图
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSearch} className="mt-4 flex items-center gap-2">
|
||||
@ -803,7 +605,7 @@ export default function TaskTreeViewer() {
|
||||
type="text"
|
||||
value={serial}
|
||||
onChange={(e) => setSerial(e.target.value)}
|
||||
placeholder="输入 16 位序列号,如 X20260801000001"
|
||||
placeholder="输入 16 位产品身份证,如 X20260801000001"
|
||||
maxLength={16}
|
||||
className="w-full rounded-lg border border-gray-200 py-2.5 pl-9 pr-3 font-mono text-sm tracking-widest placeholder:tracking-normal placeholder:font-sans focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-100"
|
||||
/>
|
||||
@ -855,7 +657,7 @@ export default function TaskTreeViewer() {
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-wrap items-center gap-3 rounded-xl bg-white p-4 shadow-sm">
|
||||
<div>
|
||||
<span className="text-xs text-gray-400">产品序列号</span>
|
||||
<span className="text-xs text-gray-400">产品身份证</span>
|
||||
<p className="font-mono text-base font-bold tracking-widest text-gray-800">
|
||||
{product.serial_number}
|
||||
</p>
|
||||
@ -894,25 +696,15 @@ export default function TaskTreeViewer() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ---- 任务树 ---- */}
|
||||
{/* ---- 任务流转泳道/卡片视图 ---- */}
|
||||
{product && !loading && (
|
||||
<>
|
||||
{product.task_tree && product.task_tree.length > 0 ? (
|
||||
<div className="rounded-xl bg-white p-6 shadow-sm">
|
||||
<h3 className="mb-4 flex items-center gap-2 text-sm font-semibold text-gray-500">
|
||||
<GitBranch className="h-4 w-4" />
|
||||
任务流转树状图
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{product.task_tree.map((task, idx) => (
|
||||
<TaskNodeCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
isLast={idx === product.task_tree!.length - 1}
|
||||
onAction={setModalTarget}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="rounded-xl bg-white p-6 shadow-sm overflow-hidden">
|
||||
<TaskFlowView
|
||||
tasks={product.task_tree}
|
||||
onAction={setModalTarget}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center rounded-xl bg-white py-16 text-gray-400 shadow-sm">
|
||||
|
||||
Reference in New Issue
Block a user