feat: 状态渲染修复+中文姓名映射+SUPERVISOR/SUPER_ADMIN权限控制(前端+后端)

This commit is contained in:
2026-08-07 17:26:14 +08:00
parent 592bde7cab
commit 65ff05fb2f
4 changed files with 110 additions and 43 deletions

View File

@ -92,15 +92,23 @@ const FlowCard = memo(function FlowCard({
task,
isActive,
onAction,
currentUser,
assigneeName,
}: {
task: TaskResponse;
isActive: boolean;
onAction: (target: ModalTarget) => void;
currentUser?: { username?: string; role?: string } | null;
assigneeName?: string;
}) {
const cfg = getStatusConfig(task.status);
const dwell = calcDwell(task.received_at, task.completed_at, task.status);
const isCompleted = task.status?.toUpperCase() === TASK_STATUS.COMPLETED;
const isArchived = task.status?.toUpperCase() === TASK_STATUS.ARCHIVED;
// 权限:本人或管理员/主管可操作
const isOwner = !!(currentUser?.username && task.assignee_id === currentUser.username);
const isManager = currentUser?.role === "SUPER_ADMIN" || currentUser?.role === "SUPERVISOR";
const canOperate = isOwner || isManager;
return (
<div
@ -137,7 +145,7 @@ const FlowCard = memo(function FlowCard({
</span>
{task.assignee_id && (
<span className="text-[10px] text-gray-400 truncate max-w-[80px]">
{task.assignee_id}
{assigneeName || task.assignee_id}
</span>
)}
</div>
@ -160,49 +168,75 @@ const FlowCard = memo(function FlowCard({
{task.received_at && `接收: ${new Date(task.received_at).toLocaleDateString("zh-CN")}`}
</p>
{/* 操作按钮 — 仅激活态显示 */}
{isActive && (
{/* 操作按钮 — 仅激活态 + 有权限时显示 */}
{isActive && canOperate && (
<div className="mt-2 flex gap-1.5 border-t border-gray-100 pt-2">
{task.status?.toUpperCase() === TASK_STATUS.PENDING && (
<>
<button
onClick={() => onAction({ task, action: "receive" })}
className="flex-1 rounded border border-blue-200 bg-blue-50 py-1 text-[10px] font-medium text-blue-600 hover:bg-blue-100"
>
<button onClick={() => onAction({ task, action: "receive" })}
className="flex-1 rounded border border-blue-200 bg-blue-50 py-1 text-[10px] font-medium text-blue-600 hover:bg-blue-100">
</button>
<button
onClick={() => onAction({ task, action: "reject" })}
className="rounded border border-red-200 bg-red-50 px-2 py-1 text-[10px] font-medium text-red-500 hover:bg-red-100"
>
<button onClick={() => onAction({ task, action: "reject" })}
className="rounded border border-red-200 bg-red-50 px-2 py-1 text-[10px] font-medium text-red-500 hover:bg-red-100">
</button>
<button
onClick={() => onAction({ task, action: "transfer" })}
className="rounded border border-green-200 bg-green-50 px-2 py-1 text-[10px] font-medium text-green-600 hover:bg-green-100"
>
<button onClick={() => onAction({ task, action: "transfer" })}
className="rounded border border-green-200 bg-green-50 px-2 py-1 text-[10px] font-medium text-green-600 hover:bg-green-100">
</button>
</>
)}
{task.status?.toUpperCase() === TASK_STATUS.WIP && (
<>
<button
onClick={() => onAction({ task, action: "reject" })}
className="flex-1 rounded border border-red-200 bg-red-50 py-1 text-[10px] font-medium text-red-500 hover:bg-red-100"
>
<button onClick={() => onAction({ task, action: "reject" })}
className="flex-1 rounded border border-red-200 bg-red-50 py-1 text-[10px] font-medium text-red-500 hover:bg-red-100">
</button>
<button
onClick={() => onAction({ task, action: "transfer" })}
className="flex-1 rounded border border-green-200 bg-green-50 py-1 text-[10px] font-medium text-green-600 hover:bg-green-100"
>
<button onClick={() => onAction({ task, action: "transfer" })}
className="flex-1 rounded border border-green-200 bg-green-50 py-1 text-[10px] font-medium text-green-600 hover:bg-green-100">
</button>
</>
)}
</div>
)}
{/* 非本人但管理员可见:干预提示 */}
{isActive && !isOwner && isManager && (
<div className="mt-2 border-t border-orange-100 pt-2">
<p className="text-[9px] text-orange-500 mb-1"> </p>
{task.status?.toUpperCase() === TASK_STATUS.PENDING && (
<div className="flex gap-1.5">
<button onClick={() => onAction({ task, action: "reject" })}
className="flex-1 rounded border border-orange-200 bg-orange-50 py-1 text-[10px] font-medium text-orange-600 hover:bg-orange-100">
</button>
<button onClick={() => onAction({ task, action: "transfer" })}
className="flex-1 rounded border border-orange-200 bg-orange-50 py-1 text-[10px] font-medium text-orange-600 hover:bg-orange-100">
</button>
</div>
)}
{task.status?.toUpperCase() === TASK_STATUS.WIP && (
<div className="flex gap-1.5">
<button onClick={() => onAction({ task, action: "reject" })}
className="flex-1 rounded border border-orange-200 bg-orange-50 py-1 text-[10px] font-medium text-orange-600 hover:bg-orange-100">
</button>
<button onClick={() => onAction({ task, action: "transfer" })}
className="flex-1 rounded border border-orange-200 bg-orange-50 py-1 text-[10px] font-medium text-orange-600 hover:bg-orange-100">
</button>
</div>
)}
</div>
)}
{/* 非本人非管理员:只读提示 */}
{isActive && !canOperate && (
<div className="mt-2 border-t border-gray-100 pt-2">
<p className="text-[9px] text-gray-400 text-center"></p>
</div>
)}
</div>
);
});
@ -212,9 +246,11 @@ const FlowCard = memo(function FlowCard({
interface TaskFlowViewProps {
tasks: TaskResponse[];
onAction: (target: ModalTarget) => void;
currentUser?: { username?: string; role?: string } | null;
assigneeNames?: Record<string, string>;
}
export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction }: TaskFlowViewProps) {
export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, currentUser, assigneeNames }: TaskFlowViewProps) {
// 按 depth 分组的层级数据
const depthMap = useMemo(() => {
if (!tasks || tasks.length === 0) return new Map<number, TaskResponse[]>();
@ -266,6 +302,8 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction }: Task
task={task}
isActive={isActive}
onAction={onAction}
currentUser={currentUser}
assigneeName={assigneeNames?.[task.assignee_id || ""]}
/>
</div>
);

View File

@ -16,6 +16,7 @@ import type { ProductResponse } from "../../types/admin";
import type { ProductScanResponse, TaskResponse } from "../../types/api";
import { useToast } from "../../components/ui/Toast";
import { getStatusConfig } from "../../constants/task";
import { useAuth } from "../../contexts/AuthContext";
const STATUS_TABS = [
{ key: "", label: "全部" },
@ -33,6 +34,7 @@ interface OrderGroup {
export default function AdminTasksPage() {
const { toast } = useToast();
const { user: currentUser } = useAuth();
// 搜索 & 筛选
const [keyword, setKeyword] = useState("");
@ -357,7 +359,14 @@ export default function AdminTasksPage() {
const productExpanded = expandedProducts.has(p.serial_number);
const isTreeLoading = treeLoading[p.serial_number];
const tree = taskTrees[p.serial_number];
const statusCfg = getStatusConfig(p.status);
// 综合状态:优先流转树状态,兜底产品状态
const treeStatus = tree
? (tree.task_tree?.some(t => t.status === "WIP") ? "WIP"
: tree.task_tree?.every(t => t.status === "COMPLETED" || t.status === "ARCHIVED") ? "COMPLETED"
: tree.task_tree?.[0]?.status)
: null;
const displayStatus = treeStatus || p.status;
const statusCfg = getStatusConfig(displayStatus);
return (
<div key={p.id}>
@ -386,7 +395,7 @@ export default function AdminTasksPage() {
🏭
</span>
) : (
p.current_location_id ?? "—"
p.current_location_name || p.current_location_id || "—"
)}
</div>
<div className="col-span-2 text-xs text-gray-400">
@ -420,6 +429,7 @@ export default function AdminTasksPage() {
<TaskFlowView
tasks={tree.task_tree}
onAction={setModalTarget}
currentUser={currentUser}
/>
) : (
<div className="flex flex-col items-center justify-center py-12 text-gray-400">