|
|
@@ -35,6 +35,7 @@ import ReactFlow, {
|
|
|
} from 'reactflow';
|
|
|
import 'reactflow/dist/style.css';
|
|
|
import { workJobApi, WorkJobVO, WorkflowWorkNodeDO } from '../api/WorkJob';
|
|
|
+import { workHandleApi } from '../api/WorkHandle';
|
|
|
import { formatDateWithFormat } from '../utils/formatTime';
|
|
|
import { dictDataApi, DictDataVO } from '../api/DictData';
|
|
|
import { userApi } from '../api/user';
|
|
|
@@ -61,6 +62,7 @@ interface FlowRecord {
|
|
|
taskNode: string; // 任务节点
|
|
|
nodeType?: string; // 节点类型(用于归档列表边框区分)
|
|
|
nodeIcon?: string; // 节点图标文件名(与作业流程一致,用于归档列表展示)
|
|
|
+ avatar?: string; // 头像 URL(归档列表优先展示)
|
|
|
executor: string; // 任务负责人
|
|
|
startTime?: string; // 开始时间
|
|
|
endTime?: string; // 结束时间
|
|
|
@@ -69,6 +71,49 @@ interface FlowRecord {
|
|
|
duration?: string; // 耗时
|
|
|
}
|
|
|
|
|
|
+// 将归档接口返回项映射为 FlowRecord(getWorkflowWorkLogPage 返回:id, nodeName, type, nodeIcon, nickName, approvalStatus, createTime, taskStartTime, taskFinishTime, taskContent)
|
|
|
+function mapArchiveItemToFlowRecord(item: any, index: number): FlowRecord {
|
|
|
+ const rawStatus = String(item?.taskStatus ?? item?.status ?? item?.approvalStatus ?? '').toLowerCase();
|
|
|
+ let taskStatus: FlowRecord['taskStatus'] = 'pending';
|
|
|
+ if (['completed', 'complete', 'approved', 'done', '1'].some((s) => rawStatus.includes(s))) taskStatus = 'completed';
|
|
|
+ else if (['in_progress', 'running', 'processing', '0'].some((s) => rawStatus.includes(s))) taskStatus = 'in_progress';
|
|
|
+
|
|
|
+ const formatTime = (v: any): string | undefined => {
|
|
|
+ if (v == null || v === '') return undefined;
|
|
|
+ if (typeof v === 'number') return formatDateWithFormat(v);
|
|
|
+ if (v instanceof Date) return formatDateWithFormat(v);
|
|
|
+ if (typeof v === 'string') {
|
|
|
+ const n = Number(v);
|
|
|
+ if (!Number.isNaN(n)) return formatDateWithFormat(n);
|
|
|
+ const d = new Date(v);
|
|
|
+ return Number.isNaN(d.getTime()) ? v : formatDateWithFormat(d);
|
|
|
+ }
|
|
|
+ return undefined;
|
|
|
+ };
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: String(item?.id ?? item?.recordId ?? item?.handleId ?? index),
|
|
|
+ taskNode: String(item?.taskNode ?? item?.nodeName ?? item?.name ?? item?.title ?? '未知节点'),
|
|
|
+ nodeType: item?.nodeType ?? item?.type,
|
|
|
+ nodeIcon: item?.nodeIcon ?? item?.icon,
|
|
|
+ avatar: item?.avatar,
|
|
|
+ executor: String(
|
|
|
+ item?.executor ?? item?.executorName ?? item?.nickName ?? item?.workerName ?? item?.userName ?? item?.operatorName ?? ''
|
|
|
+ ),
|
|
|
+ startTime: formatTime(item?.taskStartTime ?? item?.startTime ?? item?.createTime ?? item?.handleTime),
|
|
|
+ endTime: formatTime(item?.taskFinishTime ?? item?.endTime ?? item?.finishTime ?? item?.completeTime),
|
|
|
+ taskStatus,
|
|
|
+ executionDescription:
|
|
|
+ item?.taskContent ??
|
|
|
+ item?.executionDescription ??
|
|
|
+ item?.approvalOpinion ??
|
|
|
+ item?.description ??
|
|
|
+ item?.remark ??
|
|
|
+ '',
|
|
|
+ duration: item?.duration != null ? String(item.duration) : undefined,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
// 流程节点数据结构
|
|
|
interface WorkflowNode {
|
|
|
uuid: string;
|
|
|
@@ -1700,6 +1745,9 @@ export default function WorkJobDetail() {
|
|
|
|
|
|
// 是否展开归档信息面板(展开时左侧作业流程/作业信息/当前任务压缩,右侧显示归档信息列表)
|
|
|
const [showArchivePanel, setShowArchivePanel] = useState(false);
|
|
|
+ // 归档信息列表(来自 /isc/workflow-work-log/getWorkflowWorkLogPage,页面初始化时请求)
|
|
|
+ const [archiveList, setArchiveList] = useState<FlowRecord[]>([]);
|
|
|
+ const [archiveLoading, setArchiveLoading] = useState(false);
|
|
|
|
|
|
// 节点信息相关状态
|
|
|
const [formList, setFormList] = useState<any[]>([]);
|
|
|
@@ -1803,10 +1851,32 @@ export default function WorkJobDetail() {
|
|
|
loadData();
|
|
|
}, []);
|
|
|
|
|
|
+ // 获取归档信息列表(workId 传作业的 id 值,在作业详情加载后调用)
|
|
|
+ const loadArchiveList = async () => {
|
|
|
+ if (!jobId) return;
|
|
|
+ try {
|
|
|
+ setArchiveLoading(true);
|
|
|
+ const res = await workHandleApi.getWorkflowWorkLogPage({
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: -1,
|
|
|
+ workId: Number(jobId),
|
|
|
+ });
|
|
|
+ const data = (res as any)?.data ?? res;
|
|
|
+ const list: any[] = Array.isArray(data) ? data : (data?.list ?? data?.records ?? data?.rows ?? []) ?? [];
|
|
|
+ setArchiveList(list.map((it: any, i: number) => mapArchiveItemToFlowRecord(it, i)));
|
|
|
+ } catch (e: any) {
|
|
|
+ console.error('获取归档信息列表失败:', e);
|
|
|
+ setArchiveList([]);
|
|
|
+ } finally {
|
|
|
+ setArchiveLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
// 获取作业详情
|
|
|
useEffect(() => {
|
|
|
getJobStatusDictList();
|
|
|
if (jobId) {
|
|
|
+ setArchiveList([]); // 切换作业时先清空,再拉取新数据
|
|
|
loadJobDetail();
|
|
|
}
|
|
|
}, [jobId]);
|
|
|
@@ -2267,7 +2337,10 @@ export default function WorkJobDetail() {
|
|
|
const data = (response as any)?.data || response;
|
|
|
setJobDetail(data);
|
|
|
console.log('作业详情数据:', data);
|
|
|
-
|
|
|
+
|
|
|
+ // 用作业 id 作为 workId 拉取归档信息列表
|
|
|
+ loadArchiveList();
|
|
|
+
|
|
|
// 收集所有的 workerUserId
|
|
|
const userIds: (number | string)[] = [];
|
|
|
if (data?.workflowWorkNodeDOList && Array.isArray(data.workflowWorkNodeDOList)) {
|
|
|
@@ -2709,11 +2782,10 @@ export default function WorkJobDetail() {
|
|
|
return statusMap[status] || 'bg-gray-100 text-gray-700';
|
|
|
};
|
|
|
|
|
|
- // 根据节点类型返回归档记录卡片样式(边框 + 类型标签,图标与作业流程一致由下方单独渲染)
|
|
|
- // 类型标签统一使用背景色+文字色,避免有的只有文字变色
|
|
|
- const getArchiveRecordStyle = (nodeType?: string): { borderClass: string; typeLabel: string; iconBg: string; typeLabelStyle: React.CSSProperties } => {
|
|
|
+ // 根据节点类型返回归档记录卡片样式(边框 + 类型标签 + 卡片背景色,图标与作业流程一致由下方单独渲染)
|
|
|
+ const getArchiveRecordStyle = (nodeType?: string): { borderClass: string; typeLabel: string; iconBg: string; typeLabelStyle: React.CSSProperties; cardBgStyle: React.CSSProperties } => {
|
|
|
const type = (nodeType || 'default').toLowerCase();
|
|
|
- const base = 'rounded-xl border-l-4 p-4 mb-3 bg-white shadow-sm hover:shadow-md transition-shadow';
|
|
|
+ const base = 'rounded-xl border-l-4 p-4 mb-3 shadow-sm hover:shadow-md transition-shadow';
|
|
|
const configs: Record<string, { border: string; label: string; iconBg: string; bg: string; text: string }> = {
|
|
|
createjob: { border: 'border-l-blue-500', label: '创建作业', iconBg: 'bg-blue-100 text-blue-600', bg: '#dbeafe', text: '#2563eb' },
|
|
|
review: { border: 'border-l-orange-500', label: '审核', iconBg: 'bg-orange-100 text-orange-600', bg: '#ffedd5', text: '#ea580c' },
|
|
|
@@ -2726,7 +2798,8 @@ export default function WorkJobDetail() {
|
|
|
};
|
|
|
const c = configs[type] || { border: 'border-l-gray-400', label: '节点', iconBg: 'bg-gray-100 text-gray-600', bg: '#f3f4f6', text: '#6b7280' };
|
|
|
const typeLabelStyle: React.CSSProperties = { backgroundColor: c.bg, color: c.text };
|
|
|
- return { borderClass: `${base} ${c.border}`, typeLabel: c.label, iconBg: c.iconBg, typeLabelStyle };
|
|
|
+ const cardBgStyle: React.CSSProperties = { backgroundColor: c.bg };
|
|
|
+ return { borderClass: `${base} ${c.border}`, typeLabel: c.label, iconBg: c.iconBg, typeLabelStyle, cardBgStyle };
|
|
|
};
|
|
|
|
|
|
// 归档列表:渲染与作业流程一致的节点图标(优先自定义 nodeIcon 图片,否则用 getNodeIcon 按类型+状态)
|
|
|
@@ -3057,29 +3130,40 @@ export default function WorkJobDetail() {
|
|
|
<p className="text-xs text-gray-500 mt-1">当前作业执行流水记录,按节点类型区分展示</p>
|
|
|
</div>
|
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
|
- {flowRecords.length === 0 ? (
|
|
|
+ {archiveLoading ? (
|
|
|
+ <div className="flex items-center justify-center py-8 text-gray-500 text-sm">
|
|
|
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
|
+ 加载归档信息中...
|
|
|
+ </div>
|
|
|
+ ) : archiveList.length === 0 ? (
|
|
|
<div className="text-center text-gray-400 py-8 text-sm">暂无执行记录</div>
|
|
|
) : (
|
|
|
<div className="space-y-1">
|
|
|
- {flowRecords.map((record) => {
|
|
|
+ {archiveList.map((record) => {
|
|
|
const style = getArchiveRecordStyle(record.nodeType);
|
|
|
return (
|
|
|
- <div key={record.id} className={style.borderClass}>
|
|
|
+ <div key={record.id} className={style.borderClass} style={style.cardBgStyle}>
|
|
|
<div className="flex items-start gap-3">
|
|
|
- <div className="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center [&_svg]:w-5 [&_svg]:h-5 [&_svg]:text-current" style={style.typeLabelStyle}>
|
|
|
- {renderArchiveNodeIcon(record)}
|
|
|
+ <div
|
|
|
+ className="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center [&_svg]:w-5 [&_svg]:h-5 [&_svg]:text-current overflow-hidden"
|
|
|
+ style={record.avatar ? undefined : style.typeLabelStyle}
|
|
|
+ >
|
|
|
+ {record.avatar && (
|
|
|
+ <img
|
|
|
+ src={record.avatar}
|
|
|
+ alt={record.executor || record.taskNode}
|
|
|
+ className="w-10 h-10 rounded-full object-cover"
|
|
|
+ onError={(e) => {
|
|
|
+ (e.target as HTMLImageElement).style.display = 'none';
|
|
|
+ (e.target as HTMLImageElement).nextElementSibling?.classList.remove('hidden');
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ <span className={record.avatar ? 'hidden' : ''}>{renderArchiveNodeIcon(record)}</span>
|
|
|
</div>
|
|
|
<div className="flex-1 min-w-0">
|
|
|
- <div className="flex items-center justify-between gap-2 flex-wrap">
|
|
|
- <div className="flex items-center gap-2 flex-wrap">
|
|
|
- <span className="font-semibold text-gray-900">{record.taskNode}</span>
|
|
|
- <span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium" style={style.typeLabelStyle}>
|
|
|
- {style.typeLabel}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <span className={`inline-flex px-2.5 py-1 rounded-full text-xs font-medium ${getFlowRecordStatusClassName(record.taskStatus)}`}>
|
|
|
- {getFlowRecordStatusText(record.taskStatus)}
|
|
|
- </span>
|
|
|
+ <div className="flex items-center gap-2 flex-wrap">
|
|
|
+ <span className="font-semibold text-gray-900">{record.taskNode}</span>
|
|
|
</div>
|
|
|
{(record.executor || record.startTime) && (
|
|
|
<div className="mt-2.5 flex items-center gap-3 flex-wrap">
|