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>
);