Quellcode durchsuchen

Merge branch 'develop' of http://192.168.0.253:3000/bozzysadmb/ISCS-Client-V1.0 into develop

wyn vor 3 Monaten
Ursprung
Commit
94154242e1
3 geänderte Dateien mit 154 neuen und 21 gelöschten Zeilen
  1. 46 0
      src/api/WorkHandle.ts
  2. 3 0
      src/api/index.ts
  3. 105 21
      src/components/WorkJobDetail.tsx

+ 46 - 0
src/api/WorkHandle.ts

@@ -0,0 +1,46 @@
+import axiosInstance from '../utils/axios';
+
+/**
+ * 作业办理/归档相关接口
+ * baseURL 已包含 /admin-api,请求路径为 /isc/...
+ */
+
+/** 归档信息列表项(接口返回结构可能多样,使用宽松类型) */
+export interface ArchiveListItem {
+  [key: string]: any;
+}
+
+/** 分页响应 */
+export interface WorkflowWorkLogPageResult {
+  list?: ArchiveListItem[];
+  total?: number;
+  [key: string]: any;
+}
+
+/** 获取归档列表(工作流操作日志分页)请求参数 */
+export interface GetWorkflowWorkLogPageParams {
+  pageNo?: number;
+  pageSize?: number;
+  workId?: string | number; // 作业 id,归档信息列表按此查询
+  [key: string]: any;
+}
+
+export const workHandleApi = {
+  /**
+   * 获取归档信息列表(作业详情页「归档信息列表」)
+   * 请求: GET /admin-api/isc/workflow-work-log/getWorkflowWorkLogPage
+   * 传 pageNo: 1, pageSize: -1
+   */
+  getWorkflowWorkLogPage: (params?: GetWorkflowWorkLogPageParams) => {
+    return axiosInstance.get<WorkflowWorkLogPageResult>(
+      '/isc/workflow-work-log/getWorkflowWorkLogPage',
+      {
+        params: {
+          pageNo: 1,
+          pageSize: -1,
+          ...params,
+        },
+      }
+    );
+  },
+};

+ 3 - 0
src/api/index.ts

@@ -23,6 +23,7 @@ import { emailTemplateApi } from './emailTemplate';
 import { cockpitApi } from './cockpit';
 import { workflowDesignApi } from './WorkflowDesign';
 import { workJobApi } from './WorkJob';
+import { workHandleApi } from './WorkHandle';
 import { managerHomeApi } from './managerHome';
 import { in_site } from './notification/in_site';
 import { app_message } from './notification/app_message';
@@ -59,6 +60,7 @@ export { mailNotifyConfigApi } from './mailNotifyConfig';
 export { emailTemplateApi } from './emailTemplate';
 export { workflowDesignApi } from './WorkflowDesign';
 export { workJobApi } from './WorkJob';
+export { workHandleApi } from './WorkHandle';
 export { managerHomeApi } from './managerHome';
 export { in_site, type NotifyMessageVO, type NotifyMessageQueryParams } from './notification/in_site';
 export { app_message } from './notification/app_message';
@@ -96,6 +98,7 @@ export default {
   cockpit: cockpitApi,
   workflowDesign: workflowDesignApi,
   workJob: workJobApi,
+  workHandle: workHandleApi,
   managerHome: managerHomeApi,
   in_site: in_site,
   app_message: app_message,

+ 105 - 21
src/components/WorkJobDetail.tsx

@@ -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">