Преглед на файлове

Merge branch 'master' of http://39.164.159.226:3000/hnsh-smart-steward/smart-steward-admin

闪电 преди 1 месец
родител
ревизия
5a83f027d4
променени са 31 файла, в които са добавени 2327 реда и са изтрити 952 реда
  1. 2 2
      apps/web-ele/.env.development
  2. 10 0
      apps/web-ele/src/api/system/infoEntry/aidKitInfo/model.ts
  3. 78 0
      apps/web-ele/src/api/system/infoEntry/certificateSetting/certificateSetting.ts
  4. 40 0
      apps/web-ele/src/api/system/infoEntry/certificateSetting/model.ts
  5. 45 1
      apps/web-ele/src/api/workflow/instance/index.ts
  6. 15 0
      apps/web-ele/src/api/workflow/task/index.ts
  7. 7 0
      apps/web-ele/src/api/workflow/task/model.d.ts
  8. 3 3
      apps/web-ele/src/views/Archive/HSSEManage/safetyHazardLedger/drawer.vue
  9. 2 0
      apps/web-ele/src/views/oilstation/base/config-data.tsx
  10. 2 2
      apps/web-ele/src/views/oilstation/idphoto/idphoto-data.tsx
  11. 1 0
      apps/web-ele/src/views/oilstation/idphoto/index.vue
  12. 1 1
      apps/web-ele/src/views/system/infoEntry/aidKitInfo/aidKitInfo-data.tsx
  13. 86 75
      apps/web-ele/src/views/system/infoEntry/certificateSetting/certificateSetting-data.tsx
  14. 9 18
      apps/web-ele/src/views/system/infoEntry/certificateSetting/certificateSetting-drawer.vue
  15. 29 78
      apps/web-ele/src/views/system/infoEntry/certificateSetting/index.vue
  16. 56 5
      apps/web-ele/src/views/system/infoEntry/emergencyInfo/emergencyInfo-data.tsx
  17. 3 1
      apps/web-ele/src/views/system/infoEntry/emergencyInfo/index.vue
  18. 670 324
      apps/web-ele/src/views/workflow/components/actions/flow-actions.vue
  19. 57 5
      apps/web-ele/src/views/workflow/components/approval-card.vue
  20. 4 3
      apps/web-ele/src/views/workflow/components/approval-details.vue
  21. 25 10
      apps/web-ele/src/views/workflow/components/approval-panel.vue
  22. 143 53
      apps/web-ele/src/views/workflow/components/approval-timeline-item.vue
  23. 32 9
      apps/web-ele/src/views/workflow/components/approval-timeline.vue
  24. 78 15
      apps/web-ele/src/views/workflow/leave/leave-description.vue
  25. 141 45
      apps/web-ele/src/views/workflow/order/creatOrder.vue
  26. 3 8
      apps/web-ele/src/views/workflow/processDefinition/index.vue
  27. 287 0
      apps/web-ele/src/views/workflow/task/delayedReview.vue
  28. 131 72
      apps/web-ele/src/views/workflow/task/myDocument.vue
  29. 118 65
      apps/web-ele/src/views/workflow/task/taskCopyList.vue
  30. 119 72
      apps/web-ele/src/views/workflow/task/taskFinish.vue
  31. 130 85
      apps/web-ele/src/views/workflow/task/taskWaiting.vue

+ 2 - 2
apps/web-ele/.env.development

@@ -4,8 +4,8 @@ VITE_PORT=8081
4 4
 VITE_BASE=/
5 5
 
6 6
 # 接口地址
7
-# VITE_GLOB_API_URL=http://39.164.159.226:8088
8
-VITE_GLOB_API_URL=http://192.168.1.7:8080
7
+VITE_GLOB_API_URL=http://39.164.159.226:8088
8
+# VITE_GLOB_API_URL=http://192.168.1.7:8080
9 9
 # VITE_GLOB_API_URL=http://192.168.1.170:8080
10 10
 # VITE_GLOB_FLOW_URL=http://192.168.31.160:8082
11 11
 VITE_GLOB_FLOW_URL=http://192.168.1.21:8082

+ 10 - 0
apps/web-ele/src/api/system/infoEntry/aidKitInfo/model.ts

@@ -11,4 +11,14 @@ export interface AidKitInfoModel {
11 11
   standardQuantity?: number;
12 12
   // 单位
13 13
   unit?: string;
14
+  // 创建人
15
+  createBy?: string;
16
+  // 创建时间
17
+  createTime?: string;
18
+  // 更新人
19
+  updateBy?: string;
20
+  // 更新时间
21
+  updateTime?: string;
22
+  // 备注
23
+  remark?: string;
14 24
 }

+ 78 - 0
apps/web-ele/src/api/system/infoEntry/certificateSetting/certificateSetting.ts

@@ -0,0 +1,78 @@
1
+import type { CertificateSettingModel } from './model';
2
+
3
+import type { BaseResult } from '#/api/base-result';
4
+
5
+import { commonExport } from '#/api/helper';
6
+import { requestClient } from '#/api/request';
7
+
8
+// API路径常量枚举
9
+enum Api {
10
+  // 导出
11
+  export = '/sysLicenseRule/rule/export',
12
+  // 列表查询
13
+  list = '/sysLicenseRule/rule/list',
14
+  // 基础路径
15
+  root = '/sysLicenseRule/rule',
16
+}
17
+
18
+/**
19
+ * 查询证照规则列表
20
+ * @param params 查询参数
21
+ * @returns 分页结果
22
+ */
23
+export function certificateSettingList(params: any) {
24
+  return requestClient.get<BaseResult<CertificateSettingModel[]>>(Api.list, {
25
+    params,
26
+  });
27
+}
28
+
29
+/**
30
+ * 获取证照规则详细信息
31
+ * @param id 证照规则ID
32
+ * @returns 证照规则详情
33
+ */
34
+export function getCertificateSettingDetail(id: number) {
35
+  return requestClient.get<CertificateSettingModel>(`${Api.root}/${id}`);
36
+}
37
+
38
+/**
39
+ * 新增证照规则
40
+ * @param data 证照规则数据
41
+ * @returns 操作结果
42
+ */
43
+export function addCertificateSetting(data: CertificateSettingModel) {
44
+  return requestClient.post(Api.root, data, {
45
+    successMessageMode: 'message',
46
+  });
47
+}
48
+
49
+/**
50
+ * 修改证照规则
51
+ * @param data 证照规则数据
52
+ * @returns 操作结果
53
+ */
54
+export function updateCertificateSetting(data: CertificateSettingModel) {
55
+  return requestClient.put(Api.root, data, {
56
+    successMessageMode: 'message',
57
+  });
58
+}
59
+
60
+/**
61
+ * 删除证照规则
62
+ * @param ids 证照规则ID数组
63
+ * @returns 操作结果
64
+ */
65
+export function deleteCertificateSetting(ids: number[]) {
66
+  return requestClient.delete(`${Api.root}/${ids.join(',')}`, {
67
+    successMessageMode: 'message',
68
+  });
69
+}
70
+
71
+/**
72
+ * 导出证照规则列表
73
+ * @param data 导出参数
74
+ * @returns 导出结果
75
+ */
76
+export function exportCertificateSetting(data: Partial<CertificateSettingModel>) {
77
+  return commonExport(Api.export, data);
78
+}

+ 40 - 0
apps/web-ele/src/api/system/infoEntry/certificateSetting/model.ts

@@ -0,0 +1,40 @@
1
+export interface CertificateSettingModel {
2
+  // 主键id
3
+  id?: number;
4
+  // 年审是否转任务:0-否,1-是
5
+  annualReviewToTask?: number;
6
+  // 年审xxlJob的id
7
+  annualReviewXxlJobId?: number;
8
+  // 创建人
9
+  createBy?: string;
10
+  // 创建时间
11
+  createTime?: string;
12
+  // 删除标记
13
+  delFlag?: number;
14
+  // 证照名称
15
+  licenseName?: string;
16
+  // 备注
17
+  remark?: string;
18
+  // 换证提前时间
19
+  renewAdvanceMonths?: number;
20
+  // 换证频率
21
+  renewFrequency?: string;
22
+  // 更换要求
23
+  renewRequirements?: string;
24
+  // 换证是否转任务:0-否,1-是
25
+  renewToTask?: number;
26
+  // 换证xxlJob的id
27
+  renewXxlJobId?: number;
28
+  // 油站集合
29
+  stationCollection?: any[];
30
+  // 任务截止时间
31
+  taskEndMonth?: number;
32
+  // 任务来源
33
+  taskSource?: number;
34
+  // 任务开始时间
35
+  taskStartMonth?: number;
36
+  // 更新人
37
+  updateBy?: string;
38
+  // 更新时间
39
+  updateTime?: string;
40
+}

+ 45 - 1
apps/web-ele/src/api/workflow/instance/index.ts

@@ -87,6 +87,50 @@ export function pageByCurrent(params?: PageQuery) {
87 87
 }
88 88
 
89 89
 /**
90
+ * 获取当前登录人的待处理工单
91
+ * @param params
92
+ * @returns PageResult<TaskInfo>
93
+ */
94
+export function pageByPending(params?: PageQuery) {
95
+  return requestClient.get<PageResult<TaskInfo>>('/workOrder/order/list/pending',
96
+    { params },
97
+  );
98
+}
99
+
100
+/**
101
+ * 获取当前登录人的抄送工单
102
+ * @param params
103
+ * @returns PageResult<TaskInfo>
104
+ */
105
+export function pageByCc(params?: PageQuery) {
106
+  return requestClient.get<PageResult<TaskInfo>>('/workOrder/order/list/cc',
107
+    { params },
108
+  );
109
+}
110
+
111
+/**
112
+ * 获取当前登录人的已完成工单
113
+ * @param params
114
+ * @returns PageResult<TaskInfo>
115
+ */
116
+export function pageByCompleted(params?: PageQuery) {
117
+  return requestClient.get<PageResult<TaskInfo>>('/workOrder/order/list/over',
118
+    { params },
119
+  );
120
+}
121
+
122
+/**
123
+ * 获取当前登录人的延时审批工单
124
+ * @param params
125
+ * @returns PageResult<TaskInfo>
126
+ */
127
+export function pageByDelay(params?: PageQuery) {
128
+  return requestClient.get<PageResult<TaskInfo>>('/workOrder/order/list/delay',
129
+    { params },
130
+  );
131
+}
132
+
133
+/**
90 134
  * 获取工单详情
91 135
  * @param id 工单id
92 136
  * @returns TaskInfo
@@ -144,4 +188,4 @@ export function updateFlowVariable(data: {
144 188
     '/workflow/instance/updateVariable',
145 189
     data,
146 190
   );
147
-}
191
+}

+ 15 - 0
apps/web-ele/src/api/workflow/task/index.ts

@@ -23,6 +23,21 @@ export function startWorkFlow(data: StartWorkFlowReqData) {
23 23
 }
24 24
 
25 25
 /**
26
+ * 提交工单
27
+ * @param taskId 任务id
28
+ */
29
+export function submitOrder(taskId: string) {
30
+  return requestClient.put<void>(`/workOrder/order/submit/${taskId}`);
31
+}
32
+
33
+/**
34
+ * 删除工单
35
+ * @param taskId 任务id
36
+ */
37
+export function deleteOrder(taskId: string) {
38
+  return requestClient.delete<void>(`/workOrder/order/${taskId}`);
39
+}
40
+/**
26 41
  * 办理任务
27 42
  * @param data
28 43
  */

+ 7 - 0
apps/web-ele/src/api/workflow/task/model.d.ts

@@ -5,6 +5,9 @@ export interface ButtonWithPermission {
5 5
 }
6 6
 
7 7
 export interface TaskInfo {
8
+  status: string;
9
+  taskId: string;
10
+  orderId: string;
8 11
   id: string;
9 12
   categoryName: string;
10 13
   createTime: string;
@@ -39,6 +42,10 @@ export interface TaskInfo {
39 42
   createByName: string;
40 43
   targetNodeName?: string;
41 44
   buttonList: ButtonWithPermission[];
45
+  /**
46
+   * 操作按钮,逗号分隔的字符串,如:handle,delay,cc,back
47
+   */
48
+  button?: string;
42 49
 }
43 50
 
44 51
 export interface CompleteTaskReqData {

+ 3 - 3
apps/web-ele/src/views/Archive/HSSEManage/safetyHazardLedger/drawer.vue

@@ -238,16 +238,16 @@ const resetForm = (formEl: FormInstance | undefined) => {
238 238
             width="80%"
239 239
             :close-on-click-modal="true"
240 240
           >
241
-            <div style="text-align: center; padding: 12px 8px">
241
+            <div style=" padding: 12px 8px;text-align: center">
242 242
               <img
243 243
                 :src="ruleForm.cploadImage.dialogImageUrl"
244 244
                 alt="Preview Image"
245 245
                 style="
246
+                  display: inline-block;
246 247
                   width: 90vw;
247 248
                   max-height: 80vh;
248
-                  display: inline-block;
249
-                  object-fit: contain;
250 249
                   cursor: zoom-in;
250
+                  object-fit: contain;
251 251
                 "
252 252
               />
253 253
             </div>

+ 2 - 0
apps/web-ele/src/views/oilstation/base/config-data.tsx

@@ -23,6 +23,7 @@ export const queryFormSchema: FormSchemaGetter = () => [
23 23
     label: '所属片区',
24 24
     componentProps: {
25 25
       placeholder: '请选择所属片区',
26
+      clearable: true,
26 27
       api: async () => {
27 28
         const resp = await selectAllSysStationAreaList();
28 29
         const data = resp || [];
@@ -151,6 +152,7 @@ export const drawerFormSchema: FormSchemaGetter = () => [
151 152
       },
152 153
       labelField: 'label',
153 154
       valueField: 'value',
155
+      clearable: true,
154 156
     },
155 157
     rules: 'required',
156 158
   },

+ 2 - 2
apps/web-ele/src/views/oilstation/idphoto/idphoto-data.tsx

@@ -4,7 +4,7 @@ import type { VxeGridProps } from '#/adapter/vxe-table';
4 4
 import { DictEnum } from '@vben/constants';
5 5
 
6 6
 import { stationInfoList } from '#/api/system/infoEntry/stationInfo/stationInfo';
7
-import { getSingleImageUploadConfig } from '#/components/upload';
7
+import { getSingleImageUploadConfig,getFileUploadConfig } from '#/components/upload';
8 8
 // import { uploadFile } from '#/api/upload';
9 9
 import { getDictOptions } from '#/utils/dict';
10 10
 import { renderDict } from '#/utils/render';
@@ -352,7 +352,7 @@ export const drawerFormSchema: FormSchemaGetter = () => [
352 352
     label: '证件扫描件',
353 353
     componentProps: (row: any) => {
354 354
       return {
355
-        ...getSingleImageUploadConfig(),
355
+        ...getFileUploadConfig('.pdf,.docx,.doc,.txt', 'text', 1, '/common/upload'),
356 356
         onSuccess: (res: any) => {
357 357
           if (res.code === 200) {
358 358
             row.certificateScanPath = res.data.fileName;

+ 1 - 0
apps/web-ele/src/views/oilstation/idphoto/index.vue

@@ -118,6 +118,7 @@ function openDrawer() {
118 118
 
119 119
 // 编辑证照
120 120
 function handleEdit(row: any) {
121
+  console.log(drawerApi);
121 122
   drawerApi.setData({ ...row, isUpdate: true }).open();
122 123
 }
123 124
 

+ 1 - 1
apps/web-ele/src/views/system/infoEntry/aidKitInfo/aidKitInfo-data.tsx

@@ -96,4 +96,4 @@ export const drawerFormSchema: FormSchemaGetter = () => [
96 96
     },
97 97
     rules: 'required',
98 98
   },
99
-];
99
+];

+ 86 - 75
apps/web-ele/src/views/system/infoEntry/certificateSetting/certificateSetting-data.tsx

@@ -1,10 +1,13 @@
1 1
 import type { FormSchemaGetter } from '#/adapter/form';
2 2
 import type { VxeGridProps } from '#/adapter/vxe-table';
3 3
 
4
+import { requestClient } from '#/api/request';
5
+import { getDictOptions } from '#/utils/dict';
6
+
4 7
 export const queryFormSchema: FormSchemaGetter = () => [
5 8
   {
6 9
     component: 'Input',
7
-    fieldName: 'certificateName',
10
+    fieldName: 'licenseName',
8 11
     label: '证照名称',
9 12
     componentProps: {
10 13
       placeholder: '请输入证照名称',
@@ -12,37 +15,27 @@ export const queryFormSchema: FormSchemaGetter = () => [
12 15
   },
13 16
   {
14 17
     component: 'Select',
15
-    fieldName: 'annualInspection',
16
-    label: '年审',
17
-    componentProps: {
18
-      placeholder: '请选择年审',
19
-      options: [
20
-        { label: '转任务', value: '转任务' },
21
-        { label: '无', value: '无' },
22
-      ],
23
-    },
24
-  },
25
-  {
26
-    component: 'Select',
27
-    fieldName: 'annualInspectionTask',
28
-    label: '年审(转任务计算)',
18
+    fieldName: 'annualReviewToTask',
19
+    label: '年审转任务',
29 20
     componentProps: {
30
-      placeholder: '请选择年审(转任务计算)',
21
+      placeholder: '请选择年审转任务',
22
+      clearable: true,
31 23
       options: [
32
-        { label: '按时间', value: '按时间' },
33
-        { label: '按周期', value: '按周期' },
24
+        { label: '是', value: 1 },
25
+        { label: '否', value: 0 },
34 26
       ],
35 27
     },
36 28
   },
37 29
   {
38 30
     component: 'Select',
39
-    fieldName: 'renewal',
40
-    label: '换证',
31
+    fieldName: 'renewToTask',
32
+    label: '换证转任务',
41 33
     componentProps: {
42
-      placeholder: '请选择换证',
34
+      placeholder: '请选择换证转任务',
35
+      clearable: true,
43 36
       options: [
44
-        { label: '转任务', value: '转任务' },
45
-        { label: '无', value: '无' },
37
+        { label: '是', value: 1 },
38
+        { label: '否', value: 0 },
46 39
       ],
47 40
     },
48 41
   },
@@ -54,27 +47,43 @@ export const tableColumns: VxeGridProps['columns'] = [
54 47
     width: 80,
55 48
   },
56 49
   {
57
-    field: 'certificateName',
50
+    field: 'licenseName',
58 51
     title: '证照名称',
59 52
     minWidth: 150,
60 53
   },
61 54
   {
62
-    field: 'annualInspection',
63
-    title: '年审',
55
+    field: 'annualReviewToTask',
56
+    title: '年审是否转任务',
64 57
     minWidth: 120,
58
+    formatter: (params: { cellValue: number }) => {
59
+      return params.cellValue === 1 ? '是' : '否';
60
+    },
65 61
   },
66 62
   {
67
-    field: 'annualInspectionTask',
68
-    title: '年审(转任务计算)',
69
-    minWidth: 180,
63
+    field: 'renewToTask',
64
+    title: '换证是否转任务',
65
+    minWidth: 120,
66
+    formatter: (params: { cellValue: number }) => {
67
+      return params.cellValue === 1 ? '是' : '否';
68
+    },
70 69
   },
71 70
   {
72
-    field: 'renewal',
73
-    title: '换证',
71
+    field: 'renewFrequency',
72
+    title: '换证频率',
74 73
     minWidth: 120,
74
+   formatter: (params: { cellValue: string; column: any; row: any }) => {
75
+      const options =getDictOptions('renew_frequency');
76
+      const option = options.find((item) => item.value === params.cellValue);
77
+      return option ? option.label : params.cellValue;
78
+    },
75 79
   },
76 80
   {
77
-    field: 'replacementRequirements',
81
+    field: 'renewAdvanceMonths',
82
+    title: '换证提前时间/月',
83
+    minWidth: 120,
84
+  },
85
+  {
86
+    field: 'renewRequirements',
78 87
     title: '更换要求',
79 88
     minWidth: 200,
80 89
   },
@@ -98,7 +107,7 @@ export const drawerFormSchema: FormSchemaGetter = () => [
98 107
   },
99 108
   {
100 109
     component: 'Input',
101
-    fieldName: 'certificateName',
110
+    fieldName: 'licenseName',
102 111
     label: '证照名称',
103 112
     componentProps: {
104 113
       placeholder: '请输入证照名称',
@@ -108,92 +117,93 @@ export const drawerFormSchema: FormSchemaGetter = () => [
108 117
   },
109 118
   {
110 119
     component: 'RadioGroup',
111
-    fieldName: 'annualInspection',
112
-    label: '年审',
120
+    fieldName: 'annualReviewToTask',
121
+    label: '年审转任务',
113 122
     componentProps: {
114 123
       options: [
115
-        { label: '转任务', value: '转任务' },
116
-        { label: '转任务计算', value: '转任务计算' },
117
-        { label: '无', value: '无' },
124
+        { label: '是', value: 1 },
125
+        { label: '否', value: 0 },
118 126
       ],
119 127
     },
120 128
     rules: 'required',
121 129
   },
122 130
   {
123
-    component: 'DatePicker',
124
-    fieldName: 'taskStartTime',
131
+    component: 'Select',
132
+    fieldName: 'taskStartMonth',
125 133
     label: '任务开始时间',
126 134
     componentProps: {
127 135
       placeholder: '请选择任务开始时间',
128
-      type: 'date',
129
-      format: 'YYYY-MM-DD',
130
-      valueFormat: 'YYYY-MM-DD',
136
+      options: Array.from({ length: 12 }, (_, i) => ({
137
+        label: `${i + 1}月`,
138
+        value: i + 1,
139
+      })),
131 140
     },
132 141
   },
133 142
   {
134
-    component: 'DatePicker',
135
-    fieldName: 'taskEndTime',
136
-    label: '任务结束时间',
143
+    component: 'Select',
144
+    fieldName: 'taskEndMonth',
145
+    label: '任务截止时间',
137 146
     componentProps: {
138
-      placeholder: '请选择任务结束时间',
139
-      type: 'date',
140
-      format: 'YYYY-MM-DD',
141
-      valueFormat: 'YYYY-MM-DD',
147
+      placeholder: '请选择任务截止时间',
148
+      options: Array.from({ length: 12 }, (_, i) => ({
149
+        label: `${i + 1}月`,
150
+        value: i + 1,
151
+      })),
142 152
     },
143 153
   },
144 154
   {
145 155
     component: 'RadioGroup',
146
-    fieldName: 'renewal',
147
-    label: '换证',
156
+    fieldName: 'renewToTask',
157
+    label: '换证转任务',
148 158
     componentProps: {
149 159
       options: [
150
-        { label: '转任务', value: '转任务' },
151
-        { label: '无', value: '无' },
160
+        { label: '是', value: 1 },
161
+        { label: '否', value: 0 },
152 162
       ],
153 163
     },
154 164
     rules: 'required',
155 165
   },
156 166
   {
157 167
     component: 'Select',
158
-    fieldName: 'renewalFrequency',
159
-    label: '换证频率',
160 168
     componentProps: {
161 169
       placeholder: '请选择换证频率',
162
-      options: [
163
-        { label: '每年', value: '每年' },
164
-        { label: '每两年', value: '每两年' },
165
-        { label: '每三年', value: '每三年' },
166
-        { label: '每五年', value: '每五年' },
167
-      ],
170
+      options: getDictOptions('renew_frequency'),
168 171
     },
172
+    fieldName: 'renewFrequency',
173
+    label: '换证频率',
169 174
   },
170 175
   {
171
-    component: 'Input',
172
-    fieldName: 'renewalAdvanceTime',
173
-    label: '换证提前时间',
176
+    component: 'InputNumber',
177
+    fieldName: 'renewAdvanceMonths',
178
+    label: '换证提前时间/月',
174 179
     componentProps: {
175
-      placeholder: '请输入换证提前时间(天)',
176
-      type: 'number',
180
+      placeholder: '请输入换证提前时间(月)',
177 181
       min: 0,
178 182
     },
179 183
   },
180 184
   {
181
-    component: 'Select',
185
+    component: 'ApiSelect',
182 186
     fieldName: 'taskSource',
183 187
     label: '任务来源',
184 188
     componentProps: {
185 189
       placeholder: '请选择任务来源',
186
-      options: [
187
-        { label: '系统', value: '系统' },
188
-        { label: '手动', value: '手动' },
189
-        { label: '外部', value: '外部' },
190
-      ],
190
+      api: async () => {
191
+        const resp: any = await requestClient.get('/system/user/allList');
192
+        return Array.isArray(resp)
193
+          ? resp.map((item: any) => ({
194
+              label: item.nickName || item.userName,
195
+              value: item.userId.toString(),
196
+            }))
197
+          : [];
198
+      },
199
+      filterable: true,
200
+      labelField: 'label',
201
+      valueField: 'value',
191 202
     },
192
-    rules: 'required',
193 203
   },
194 204
   {
195 205
     component: 'Input',
196
-    fieldName: 'replacementRequirements',
206
+    fieldName: 'renewRequirements',
197 207
     label: '更换要求',
198 208
     componentProps: {
199 209
       placeholder: '请输入更换要求',
@@ -203,3 +213,4 @@ export const drawerFormSchema: FormSchemaGetter = () => [
203 213
     },
204 214
   },
205 215
 ];
216
+

+ 9 - 18
apps/web-ele/src/views/system/infoEntry/certificateSetting/certificateSetting-drawer.vue

@@ -3,6 +3,8 @@ import { onMounted, ref } from 'vue';
3 3
 
4 4
 import { useVbenDrawer, useVbenForm } from '@vben/common-ui';
5 5
 
6
+import { addCertificateSetting, getCertificateSettingDetail, updateCertificateSetting } from '#/api/system/infoEntry/certificateSetting/certificateSetting';
7
+
6 8
 import { drawerFormSchema } from './certificateSetting-data';
7 9
 
8 10
 const emit = defineEmits<{
@@ -26,21 +28,9 @@ const [Drawer, drawerApi] = useVbenDrawer({
26 28
       const { id, isUpdate } = drawerApi.getData();
27 29
       isUpdateRef.value = isUpdate;
28 30
       if (isUpdate && id) {
29
-        // 模拟获取详情数据
30
-        const mockDetail = {
31
-          id,
32
-          certificateName: `证照规则${id}`,
33
-          annualInspection: '转任务',
34
-          annualInspectionTask: '按时间',
35
-          taskStartTime: '2023-01-01',
36
-          taskEndTime: '2023-12-31',
37
-          renewal: '转任务',
38
-          renewalFrequency: '每年',
39
-          renewalAdvanceTime: 30,
40
-          taskSource: '系统',
41
-          replacementRequirements: '这是一个模拟的更换要求',
42
-        };
43
-        await formApi.setValues(mockDetail);
31
+        const res = await getCertificateSettingDetail(id);
32
+        const detailData = res?.data || res;
33
+        await formApi.setValues(detailData);
44 34
       }
45 35
     } catch (error) {
46 36
       console.error('获取证照规则详情失败:', error);
@@ -57,9 +47,10 @@ const [Drawer, drawerApi] = useVbenDrawer({
57 47
         return;
58 48
       }
59 49
       const data = await formApi.getValues();
60
-      // 模拟提交数据
61
-      console.log('提交数据:', data);
62
-      // 模拟成功
50
+      
51
+      await (isUpdateRef.value
52
+        ? updateCertificateSetting(data)
53
+        : addCertificateSetting(data));
63 54
       emit('reload');
64 55
       drawerApi.close();
65 56
     } catch (error) {

+ 29 - 78
apps/web-ele/src/views/system/infoEntry/certificateSetting/index.vue

@@ -11,6 +11,9 @@ import { ElMessageBox } from 'element-plus';
11 11
 
12 12
 import { useVbenVxeGrid } from '#/adapter/vxe-table';
13 13
 
14
+import { certificateSettingList, deleteCertificateSetting, exportCertificateSetting } from '#/api/system/infoEntry/certificateSetting/certificateSetting';
15
+import { commonDownloadExcel } from '#/utils/file/download';
16
+
14 17
 import { queryFormSchema, tableColumns } from './certificateSetting-data';
15 18
 import CertificateSettingDrawerComp from './certificateSetting-drawer.vue';
16 19
 
@@ -25,53 +28,6 @@ const formOptions: VbenFormProps = {
25 28
   wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
26 29
 };
27 30
 
28
-// 模拟数据
29
-const mockData = {
30
-  rows: [
31
-    {
32
-      id: 1,
33
-      certificateName: '营业执照',
34
-      annualInspection: '转任务',
35
-      annualInspectionTask: '按时间',
36
-      renewal: '转任务',
37
-      replacementRequirements: '每年进行一次审核,确保信息准确',
38
-    },
39
-    {
40
-      id: 2,
41
-      certificateName: '税务登记证',
42
-      annualInspection: '转任务计算',
43
-      annualInspectionTask: '按周期',
44
-      renewal: '无',
45
-      replacementRequirements: '无需定期更换,信息变更时更新',
46
-    },
47
-    {
48
-      id: 3,
49
-      certificateName: '组织机构代码证',
50
-      annualInspection: '无',
51
-      annualInspectionTask: '',
52
-      renewal: '转任务',
53
-      replacementRequirements: '有效期内使用,到期前30天办理换证',
54
-    },
55
-    {
56
-      id: 4,
57
-      certificateName: '安全生产许可证',
58
-      annualInspection: '转任务',
59
-      annualInspectionTask: '按时间',
60
-      renewal: '转任务',
61
-      replacementRequirements: '每3年更换一次,提前60天申请',
62
-    },
63
-    {
64
-      id: 5,
65
-      certificateName: '消防许可证',
66
-      annualInspection: '转任务计算',
67
-      annualInspectionTask: '按周期',
68
-      renewal: '无',
69
-      replacementRequirements: '每年进行消防检查,合格后延续使用',
70
-    },
71
-  ],
72
-  total: 5,
73
-};
74
-
75 31
 const gridOptions: VxeGridProps = {
76 32
   checkboxConfig: {
77 33
     highlight: true,
@@ -84,32 +40,12 @@ const gridOptions: VxeGridProps = {
84 40
   proxyConfig: {
85 41
     ajax: {
86 42
       query: async ({ page }, formValues = {}) => {
87
-        // 模拟API请求
88
-        console.log('查询参数:', formValues, page);
89
-        // 模拟过滤
90
-        let filteredRows = [...mockData.rows];
91
-        if (formValues.certificateName) {
92
-          filteredRows = filteredRows.filter(row => 
93
-            row.certificateName.includes(formValues.certificateName)
94
-          );
95
-        }
96
-        if (formValues.annualInspection) {
97
-          filteredRows = filteredRows.filter(row => row.annualInspection === formValues.annualInspection);
98
-        }
99
-        if (formValues.annualInspectionTask) {
100
-          filteredRows = filteredRows.filter(row => row.annualInspectionTask === formValues.annualInspectionTask);
101
-        }
102
-        if (formValues.renewal) {
103
-          filteredRows = filteredRows.filter(row => row.renewal === formValues.renewal);
104
-        }
105
-        // 模拟分页
106
-        const start = (page.currentPage - 1) * page.pageSize;
107
-        const end = start + page.pageSize;
108
-        const paginatedRows = filteredRows.slice(start, end);
109
-        return { 
110
-          items: paginatedRows, 
111
-          total: filteredRows.length 
112
-        };
43
+        const resp = await certificateSettingList({
44
+          ...formValues,
45
+          pageNum: page.currentPage,
46
+          pageSize: page.pageSize,
47
+        });
48
+        return { items: resp.rows, total: resp.total };
113 49
       },
114 50
     },
115 51
   },
@@ -121,7 +57,7 @@ const gridOptions: VxeGridProps = {
121 57
     refresh: true,
122 58
     zoom: true,
123 59
   },
124
-  id: 'system-checklis-labelManage-index',
60
+  id: 'system-infoEntry-certificateSetting-index',
125 61
 };
126 62
 
127 63
 const [BasicTable, basicTableApi] = useVbenVxeGrid({
@@ -140,13 +76,14 @@ function openDrawer() {
140 76
 
141 77
 // 编辑证照规则
142 78
 function handleEdit(row: any) {
79
+  console.log(row);
80
+  // row.taskSource = row.taskSource
143 81
   drawerApi.setData({ ...row, isUpdate: true }).open();
144 82
 }
145 83
 
146 84
 // 单个删除证照规则
147 85
 async function confirmEvent(row: any) {
148
-  // 模拟删除操作
149
-  console.log('删除证照规则:', row.id);
86
+  await deleteCertificateSetting([row.id]);
150 87
   await basicTableApi.reload();
151 88
 }
152 89
 
@@ -163,11 +100,19 @@ function deleteHandle() {
163 100
     cancelButtonText: '取消',
164 101
     type: 'warning',
165 102
   }).then(async () => {
166
-    // 模拟批量删除操作
167
-    console.log('批量删除证照规则:', ids);
103
+    await deleteCertificateSetting(ids);
168 104
     await basicTableApi.reload();
169 105
   });
170 106
 }
107
+
108
+// 导出证照规则数据
109
+async function exportHandle() {
110
+  await commonDownloadExcel(
111
+    exportCertificateSetting,
112
+    '证照规则数据',
113
+    basicTableApi.formApi.form.values,
114
+  );
115
+}
171 116
 </script>
172 117
 
173 118
 <template>
@@ -176,6 +121,12 @@ function deleteHandle() {
176 121
       <template #toolbar-tools>
177 122
         <ElSpace>
178 123
           <ElButton
124
+            type="warning"
125
+            @click="exportHandle"
126
+          >
127
+            导出
128
+          </ElButton>
129
+          <ElButton
179 130
             type="danger"
180 131
             :disabled="
181 132
               !(basicTableApi?.grid?.getCheckboxRecords?.()?.length > 0)

+ 56 - 5
apps/web-ele/src/views/system/infoEntry/emergencyInfo/emergencyInfo-data.tsx

@@ -1,6 +1,7 @@
1 1
 import type { FormSchemaGetter } from '#/adapter/form';
2 2
 import type { VxeGridProps } from '#/adapter/vxe-table';
3 3
 
4
+import { selectAllSysStationAreaList } from '#/api/system/infoEntry/stationInfo/stationInfo';
4 5
 import { getDictOptions } from '#/utils/dict';
5 6
 
6 7
 // 字典标识常量
@@ -13,13 +14,48 @@ const DICT_KEYS = {
13 14
 const getEmergencyProtectionTypes = () =>
14 15
   getDictOptions(DICT_KEYS.EMERGENCY_PROTECTION_TYPE);
15 16
 
17
+// 缓存片区列表
18
+let areaList: any[] = [];
19
+
20
+// 获取片区列表
21
+const getAreaList = async () => {
22
+  if (areaList.length === 0) {
23
+    const resp = await selectAllSysStationAreaList();
24
+    areaList = Array.isArray(resp) ? resp : [];
25
+  }
26
+  return areaList;
27
+};
28
+
29
+// 获取片区名称
30
+const getAreaName = (areaId: string | number) => {
31
+  const area = areaList.find(item => item.id === areaId);
32
+  return area ? area.areaName : areaId;
33
+};
34
+
35
+// 导出获取片区列表的函数
36
+export const loadAreaList = async () => {
37
+  await getAreaList();
38
+};
39
+
16 40
 export const queryFormSchema: FormSchemaGetter = () => [
17 41
   {
18
-    component: 'Input',
42
+    component: 'ApiSelect',
19 43
     fieldName: 'belongArea',
20 44
     label: '我方油站',
21 45
     componentProps: {
22
-      placeholder: '请输入我方油站',
46
+      placeholder: '请选择我方油站',
47
+      api: async () => {
48
+        const resp = await selectAllSysStationAreaList();
49
+        const data = resp || [];
50
+        return Array.isArray(data)
51
+          ? data.map((item: any) => ({
52
+              label: item.areaName,
53
+              value: item.id,
54
+            }))
55
+          : [];
56
+      },
57
+      labelField: 'label',
58
+      valueField: 'value',
23 59
     },
24 60
   },
25 61
   {
@@ -43,7 +79,9 @@ export const tableColumns: VxeGridProps['columns'] = [
43 79
     title: '我方油站',
44 80
     minWidth: 120,
45 81
     formatter: (params: { cellValue: string; column: any; row: any }) => {
46
-      return params.cellValue;
82
+      // 确保片区列表已加载
83
+      
84
+      return getAreaName(params.cellValue);
47 85
     },
48 86
   },
49 87
   {
@@ -102,11 +140,24 @@ export const drawerFormSchema: FormSchemaGetter = () => [
102 140
     fieldName: 'id',
103 141
   },
104 142
   {
105
-    component: 'Input',
143
+    component: 'ApiSelect',
106 144
     fieldName: 'belongArea',
107 145
     label: '我方油站',
108 146
     componentProps: {
109
-      placeholder: '请输入我方油站',
147
+      placeholder: '请选择我方油站',
148
+      api: async () => {
149
+        const resp = await selectAllSysStationAreaList();
150
+        const data = resp || [];
151
+        return Array.isArray(data)
152
+          ? data.map((item: any) => ({
153
+              label: item.areaName,
154
+              value: item.id,
155
+            }))
156
+          : [];
157
+      },
158
+      labelField: 'label',
159
+      valueField: 'value',
160
+      clearable: true,
110 161
     },
111 162
     rules: 'required',
112 163
   },

+ 3 - 1
apps/web-ele/src/views/system/infoEntry/emergencyInfo/index.vue

@@ -18,7 +18,7 @@ import {
18 18
 import { getDictOptions } from '#/utils/dict';
19 19
 import { commonDownloadExcel } from '#/utils/file/download';
20 20
 
21
-import { queryFormSchema, tableColumns } from './emergencyInfo-data';
21
+import { queryFormSchema, tableColumns, loadAreaList } from './emergencyInfo-data';
22 22
 import EmergencyInfoDrawerComp from './emergencyInfo-drawer.vue';
23 23
 
24 24
 // 字典标识常量
@@ -86,6 +86,8 @@ const protectionTypes = ref<any[]>([]);
86 86
 onMounted(async () => {
87 87
   // 获取字典数据
88 88
   protectionTypes.value = getDictOptions(DICT_KEYS.EMERGENCY_PROTECTION_TYPE);
89
+  // 加载片区列表
90
+  await loadAreaList();
89 91
 });
90 92
 
91 93
 // 新增应急防护用品

Файловите разлики са ограничени, защото са твърде много
+ 670 - 324
apps/web-ele/src/views/workflow/components/actions/flow-actions.vue


+ 57 - 5
apps/web-ele/src/views/workflow/components/approval-card.vue

@@ -6,7 +6,7 @@ import { computed } from 'vue';
6 6
 import { VbenAvatar } from '@vben/common-ui';
7 7
 import { DictEnum } from '@vben/constants';
8 8
 
9
-import { ElDescriptions, ElDescriptionsItem, ElTooltip } from 'element-plus';
9
+import { ElDescriptions, ElDescriptionsItem, ElTooltip, ElTag } from 'element-plus';
10 10
 
11 11
 import { renderDict } from '#/utils/render';
12 12
 
@@ -31,8 +31,40 @@ function handleClick() {
31 31
 }
32 32
 
33 33
 const diffUpdateTimeString = computed(() => {
34
-  return getDiffTimeString(props.info.updateTime);
34
+  return getDiffTimeString(props.info.updateTime || props.info.createTime);
35 35
 });
36
+
37
+// 处理数字类型的状态显示
38
+function renderStatusNumber(status: number) {
39
+  const statusMap = {
40
+    0: { label: '待处理', type: 'warning' },
41
+    1: { label: '处理中', type: 'info' },
42
+    2: { label: '已处理', type: 'success' },
43
+    3: { label: '已退回', type: 'danger' },
44
+  };
45
+  
46
+  const statusInfo = statusMap[status as keyof typeof statusMap] || { label: '未知状态', type: 'default' };
47
+  return {
48
+    label: statusInfo.label,
49
+    type: statusInfo.type
50
+  };
51
+}
52
+
53
+// 处理数字类型的审批状态显示
54
+function renderApproveStatusNumber(approveStatus: number) {
55
+  const statusMap = {
56
+    0: { label: '未审批', type: 'info' },
57
+    1: { label: '已同意', type: 'success' },
58
+    2: { label: '已拒绝', type: 'danger' },
59
+    3: { label: '已废除', type: 'warning' },
60
+  };
61
+  
62
+  const statusInfo = statusMap[approveStatus as keyof typeof statusMap] || { label: '未知状态', type: 'default' };
63
+  return {
64
+    label: statusInfo.label,
65
+    type: statusInfo.type
66
+  };
67
+}
36 68
 </script>
37 69
 
38 70
 <template>
@@ -49,10 +81,26 @@ const diffUpdateTimeString = computed(() => {
49 81
       size="large"
50 82
     >
51 83
       <template #extra>
52
-        <component :is="renderDict(info.status, DictEnum.TICKET_STATUS)" />
84
+        <div class="flex gap-2">
85
+          <!-- 根据status的类型选择不同的渲染方式 -->
86
+          <template v-if="typeof info.status === 'number'">
87
+            <ElTag :type="renderStatusNumber(info.status).type">
88
+              {{ renderStatusNumber(info.status).label }}
89
+            </ElTag>
90
+          </template>
91
+          <component v-else :is="renderDict(info.status, DictEnum.TICKET_STATUS)" />
92
+          
93
+          <!-- 显示审批状态 -->
94
+          <template v-if="typeof info.approveStatus === 'number'">
95
+            <ElTag :type="renderApproveStatusNumber(info.approveStatus).type">
96
+              {{ renderApproveStatusNumber(info.approveStatus).label }}
97
+            </ElTag>
98
+          </template>
99
+          <component v-else-if="info.approveStatus" :is="renderDict(info.approveStatus, DictEnum.TICKET_STATUS)" />
100
+        </div>
53 101
       </template>
54 102
       <ElDescriptionsItem>
55
-        <div class="font-bold">{{ info.content }}</div>
103
+        <div class="font-bold">{{ info.content || info.delayReason }}</div>
56 104
       </ElDescriptionsItem>
57 105
       <ElDescriptionsItem label="提交时间">
58 106
         {{ info.createTime }}
@@ -73,7 +121,7 @@ const diffUpdateTimeString = computed(() => {
73 121
         </span>
74 122
       </div>
75 123
       <div class="text-nowrap opacity-50">
76
-        <ElTooltip placement="top" :content="`更新时间: ${info.updateTime}`">
124
+        <ElTooltip placement="top" :content="`更新时间: ${info.updateTime || info.createTime}`">
77 125
           <div class="flex items-center gap-1">
78 126
             <span class="icon-[mdi--clock-outline] size-[16px]"></span>
79 127
             <span>{{ diffUpdateTimeString }}前更新</span>
@@ -93,3 +141,7 @@ const diffUpdateTimeString = computed(() => {
93 141
   padding-bottom: 8px !important;
94 142
 }
95 143
 </style>
144
+
145
+
146
+
147
+

+ 4 - 3
apps/web-ele/src/views/workflow/components/approval-details.vue

@@ -14,7 +14,8 @@ defineOptions({
14 14
 });
15 15
 
16 16
 defineProps<{
17
-  task: TaskInfo;
17
+  task?: TaskInfo;
18
+  workOrderDetail?: any;
18 19
 }>();
19 20
 </script>
20 21
 
@@ -27,8 +28,8 @@ defineProps<{
27 28
     <!-- <component :is="flowComponentsMap[task.formPath as FlowComponentsMapMapKey]" :business-id="task.businessId" /> -->
28 29
     <component
29 30
       :is="flowComponentsMap['/workflow/leaveEdit/index']"
30
-      :task="task"
31
+      :work-order-detail="workOrderDetail"
31 32
     />
32
-    <ApprovalTimeline :id="task.id" />
33
+    <ApprovalTimeline :id="workOrderDetail?.id" />
33 34
   </div>
34 35
 </template>

+ 25 - 10
apps/web-ele/src/views/workflow/components/approval-panel.vue

@@ -42,9 +42,17 @@ interface Props {
42 42
    */
43 43
   task?: TaskInfo;
44 44
   /**
45
+   * 工单详情
46
+   */
47
+  workOrderDetail?: any;
48
+  /**
45 49
    * 审批类型
46 50
    */
47 51
   type: ApprovalType;
52
+  /**
53
+   * 按钮权限(可选,外部传入覆盖内部计算)
54
+   */
55
+  buttonPermissions?: Record<string, boolean>;
48 56
 }
49 57
 
50 58
 /**
@@ -55,6 +63,11 @@ const onlyForBtnPermissionTask = ref<TaskInfo>();
55 63
  * 按钮权限
56 64
  */
57 65
 const buttonPermissions = computed(() => {
66
+  // 如果外部传入了buttonPermissions,优先使用
67
+  if (props.buttonPermissions) {
68
+    return props.buttonPermissions;
69
+  }
70
+  
58 71
   const record: Record<string, boolean> = {};
59 72
   if (!onlyForBtnPermissionTask.value) {
60 73
     return record;
@@ -135,14 +148,14 @@ async function handleCopy(text: string) {
135 148
 <template>
136 149
   <div :class="cn('thin-scrollbar', 'flex flex-1 overflow-y-hidden')">
137 150
     <ElCard
138
-      v-if="task"
151
+      v-if="workOrderDetail"
139 152
       :body-style="{ overflowY: 'auto', height: '100%' }"
140 153
       :loading="loading"
141 154
       class="thin-scrollbar flex-1 overflow-y-hidden"
142 155
       size="small"
143 156
     >
144 157
       <template #extra>
145
-        <el-button size="small" @click="() => handleLoadInfo(task)">
158
+        <el-button size="small" @click="() => handleLoadInfo(workOrderDetail)">   
146 159
           <div class="flex items-center justify-center">
147 160
             <span class="icon-[material-symbols--refresh] size-24px"></span>
148 161
           </div>
@@ -153,36 +166,36 @@ async function handleCopy(text: string) {
153 166
         <div class="flex flex-col gap-3">
154 167
           <div class="flex items-center gap-2">
155 168
             <div class="text-2xl font-bold">
156
-              {{ task.orderNo }}
169
+              {{ workOrderDetail.orderNo }}
157 170
             </div>
158
-            <el-icon @click="() => handleCopy(task.orderNo)">
171
+            <el-icon @click="() => handleCopy(workOrderDetail.orderNo)">
159 172
               <CopyDocument />
160 173
             </el-icon>
161 174
             <div>
162 175
               <component
163
-                :is="renderDict(task.status, DictEnum.TICKET_STATUS)"
176
+                :is="renderDict(workOrderDetail.status, DictEnum.TICKET_STATUS)"
164 177
               />
165 178
             </div>
166 179
           </div>
167 180
 
168 181
           <div class="flex items-center gap-2">
169 182
             <VbenAvatar
170
-              :alt="task?.createBy ?? ''"
183
+              :alt="workOrderDetail?.createBy ?? ''" 
171 184
               class="bg-primary size-[28px] rounded-full text-white"
172 185
               src=""
173 186
             />
174 187
 
175
-            <span>{{ task.createBy }}</span>
188
+            <span>{{ workOrderDetail.createBy }}</span>
176 189
 
177 190
             <div class="flex items-center opacity-50">
178 191
               <div class="flex items-center gap-1">
179 192
                 <span class="icon-[bxs--category-alt] size-[16px]"></span>
180
-                流程分类: {{ task.ticketTypeName }}
193
+                流程分类: {{ workOrderDetail.ticketTypeName }}
181 194
               </div>
182 195
 
183 196
               <div class="flex items-center gap-1">
184 197
                 <span class="icon-[mdi--clock-outline] size-[16px]"></span>
185
-                提交时间: {{ task.createTime }}
198
+                提交时间: {{ workOrderDetail.createTime }}
186 199
               </div>
187 200
             </div>
188 201
           </div>
@@ -192,7 +205,7 @@ async function handleCopy(text: string) {
192 205
           <ElTabPane key="1" label="工单详情">
193 206
             <ApprovalDetails
194 207
               :current-flow-info="currentFlowInfo"
195
-              :task="task"
208
+              :work-order-detail="workOrderDetail"
196 209
             />
197 210
           </ElTabPane>
198 211
 
@@ -208,6 +221,7 @@ async function handleCopy(text: string) {
208 221
         v-if="showFooter"
209 222
         :type="type"
210 223
         :task="task"
224
+        :work-order-detail="workOrderDetail"
211 225
         :button-permissions="buttonPermissions"
212 226
         @reload="$emit('reload')"
213 227
       />
@@ -218,3 +232,4 @@ async function handleCopy(text: string) {
218 232
     </slot>
219 233
   </div>
220 234
 </template>
235
+

+ 143 - 53
apps/web-ele/src/views/workflow/components/approval-timeline-item.vue

@@ -1,15 +1,14 @@
1 1
 <script setup lang="ts">
2 2
 import type { Flow } from '#/api/workflow/instance/model';
3 3
 
4
-import { computed, onMounted, ref } from 'vue';
4
+import { computed, ref } from 'vue';
5 5
 
6 6
 import { VbenAvatar } from '@vben/common-ui';
7 7
 import { DictEnum } from '@vben/constants';
8 8
 import { cn } from '@vben/utils';
9 9
 
10
-// import { Message, User, Users } from '@element-plus/icons-vue';
11
-// import { Avatar } from 'element-plus';
12
-// import { ossInfo } from '#/api/system/oss';
10
+import { Message, User, Timer, Document } from '@element-plus/icons-vue';
11
+import { ElAvatar, ElTimelineItem, ElTag } from 'element-plus';
13 12
 import { renderDict } from '#/utils/render';
14 13
 
15 14
 defineOptions({
@@ -19,7 +18,6 @@ defineOptions({
19 18
 const props = defineProps<{ item: Flow }>();
20 19
 
21 20
 interface AttachmentInfo {
22
-  ossId: string;
23 21
   url: string;
24 22
   name: string;
25 23
 }
@@ -27,34 +25,53 @@ interface AttachmentInfo {
27 25
 /**
28 26
  * 处理附件信息
29 27
  */
30
-const attachmentInfo = ref<AttachmentInfo[]>([]);
31
-onMounted(async () => {
32
-  if (!props.item.ext) {
33
-    return null;
34
-  }
35
-  // const resp = await ossInfo(props.item.ext.split(','));
36
-  // attachmentInfo.value = resp.map((item) => ({
37
-  //   ossId: item.ossId,
38
-  //   url: item.url,
39
-  //   name: item.originalName,
40
-  // }));
28
+const processAttachments = computed(() => {
29
+  const records = props.item.workOrderProcessRecords?.[0];
30
+  if (!records?.attachments) return [];
31
+  const names = records.attachments.split(',');
32
+  const urls = records.attachmentsUrl || [];
33
+  return names.map((name, index) => ({
34
+    name,
35
+    url: urls[index] || ''
36
+  }));
37
+});
38
+
39
+const delayAttachments = computed(() => {
40
+  const delayData = props.item.workOrderDelay?.[0];
41
+  if (!delayData?.applyFile) return [];
42
+  const names = delayData.applyFile.split(',');
43
+  const urls = delayData.applyFileUrl || [];
44
+  return names.map((name, index) => ({
45
+    name,
46
+    url: urls[index] || ''
47
+  }));
41 48
 });
42 49
 
43 50
 const isMultiplePerson = computed(
44
-  () => props.item.approver?.split(',').length > 1,
51
+  () => props.item.operatorName?.split(',').length > 1,
45 52
 );
53
+
54
+// 处理审批状态显示
55
+function renderApproveStatus(status: number) {
56
+  const statusMap = {
57
+    0: { label: '未审批', type: 'info' },
58
+    1: { label: '已同意', type: 'success' },
59
+    2: { label: '已拒绝', type: 'danger' },
60
+    3: { label: '已废除', type: 'warning' },
61
+  };
62
+  
63
+  return statusMap[status] || { label: '未知状态', type: 'default' };
64
+}
46 65
 </script>
47 66
 
48 67
 <template>
49 68
   <ElTimelineItem>
50 69
     <template #dot>
51 70
       <div class="relative rounded-full border">
52
-        <ElAvatar class="bg-primary-400" v-if="isMultiplePerson" :size="36">
53
-          <!-- <Users /> -->
54
-        </ElAvatar>
71
+        <ElAvatar class="bg-primary-400" v-if="isMultiplePerson" :size="36"/>
55 72
         <VbenAvatar
56 73
           v-else
57
-          :alt="item?.approveName ?? 'unknown'"
74
+          :alt="props.item?.operatorName ?? 'unknown'"
58 75
           class="bg-primary size-[36px] rounded-full text-white"
59 76
           src=""
60 77
         />
@@ -69,54 +86,127 @@ const isMultiplePerson = computed(
69 86
         ></div>
70 87
       </div>
71 88
     </template>
72
-    <div class="mb-5 ml-2 flex flex-col gap-1">
89
+    <div class="mb-5 ml-2 flex flex-col gap-1" style="padding-left: 15px;">
73 90
       <div class="flex items-center gap-1">
74
-        <div class="font-bold">{{ item.nodeName }}</div>
75
-        <component :is="renderDict(item.flowStatus, DictEnum.WF_TASK_STATUS)" />
91
+        <div class="font-bold">{{ props.item.operationContent }}</div>
92
+        <component :is="renderDict(props.item.status, DictEnum.TICKET_STATUS)" />
76 93
       </div>
77 94
 
78 95
       <div :class="cn('mt-2 flex flex-wrap gap-2')" v-if="isMultiplePerson">
79 96
         <!-- 如果昵称中带, 这里的处理是不准确的 -->
80 97
         <div
81 98
           :class="cn('bg-foreground/5 flex items-center rounded-full', 'p-1')"
82
-          v-for="(name, index) in item.approveName.split(',')"
83
-          :key="index"
84
-        >
85
-          <Avatar
86
-            class="bg-primary-400 flex items-center justify-center"
87
-            :size="24"
88
-          >
89
-            <User />
90
-          </Avatar>
99
+          v-for="(name, index) in props.item.approveName.split(',')"
100
+          :key="index">
91 101
           <span class="px-1">{{ name }}</span>
92 102
         </div>
93 103
       </div>
94
-      <div v-else>{{ item.approveName }}</div>
104
+      <div>{{ props.item.operatorName }}</div>
95 105
 
96
-      <div>{{ item.updateTime }}</div>
106
+      <div>{{ props.item.operationTime }}</div>
107
+      
108
+      <!-- 处理工单内容 -->
109
+      <div v-if="props.item.operationType === 'PROCESS' && (props.item.workOrderProcessRecords?.[0]?.processContent || processAttachments.length > 0)" class="mt-2 rounded-lg border p-3">
110
+        <div v-if="props.item.workOrderProcessRecords?.[0]?.processContent" class="flex flex-wrap gap-2">
111
+          <div class="flex items-center gap-1 font-medium">
112
+            <el-icon :size="16"><Message /></el-icon>
113
+            <span>处理内容:</span>
114
+          </div>
115
+          <div class="text-foreground/75 break-all flex-1">{{ props.item.workOrderProcessRecords[0].processContent }}</div>
116
+        </div>
117
+        <div v-if="processAttachments.length > 0" class="mt-2 flex flex-wrap gap-2">
118
+          <div class="font-medium">附件:</div>
119
+          <div class="flex flex-wrap gap-2">
120
+            <a
121
+              v-for="(attachment, index) in processAttachments"
122
+              :key="index"
123
+              :href="attachment.url"
124
+              class="text-primary"
125
+              target="_blank"
126
+            >
127
+              <div class="flex items-center gap-1">
128
+                <span class="icon-[mingcute--attachment-line] size-[14px]"></span>
129
+                <span>{{ attachment.name }}</span>
130
+              </div>
131
+            </a>
132
+          </div>
133
+        </div>
134
+      </div>
135
+      
136
+      <!-- 申请延时内容 -->
137
+      <div v-else-if="props.item.operationType === 'APPLY_DELAY' && props.item.workOrderDelay?.[0] && (props.item.workOrderDelay[0].delayReason || props.item.workOrderDelay[0].delayDuration || delayAttachments.length > 0)" class="mt-2 rounded-lg border p-3">
138
+        <div v-if="props.item.workOrderDelay[0].delayReason" class="flex flex-wrap gap-2">
139
+          <div class="flex items-center gap-1 font-medium">
140
+            <el-icon :size="16"><Message /></el-icon>
141
+            <span>延时原因:</span>
142
+          </div>
143
+          <div class="text-foreground/75 break-all flex-1">{{ props.item.workOrderDelay[0].delayReason }}</div>
144
+        </div>
145
+        <div v-if="props.item.workOrderDelay[0].delayDuration" class="mt-2 flex flex-wrap gap-2">
146
+          <div class="flex items-center gap-1 font-medium">
147
+            <el-icon :size="16"><Timer /></el-icon>
148
+            <span>延时时长:</span>
149
+          </div>
150
+          <div class="text-foreground/75">{{ props.item.workOrderDelay[0].delayDuration }} 小时</div>
151
+        </div>
152
+        <div v-if="delayAttachments.length > 0" class="mt-2 flex flex-wrap gap-2">
153
+          <div class="font-medium">附件:</div>
154
+          <div class="flex flex-wrap gap-2">
155
+            <a
156
+              v-for="(attachment, index) in delayAttachments"
157
+              :key="index"
158
+              :href="attachment.url"
159
+              class="text-primary"
160
+              target="_blank"
161
+            >
162
+              <div class="flex items-center gap-1">
163
+                <span class="icon-[mingcute--attachment-line] size-[14px]"></span>
164
+                <span>{{ attachment.name }}</span>
165
+              </div>
166
+            </a>
167
+          </div>
168
+        </div>
169
+      </div>
170
+      
171
+      <!-- 审核延时内容 -->
172
+      <div v-else-if="props.item.operationType === 'AUDIT_DELAY' && props.item.workOrderDelay?.[0] && (props.item.workOrderDelay[0].approveRemark || props.item.workOrderDelay[0].approveStatus !== undefined || props.item.workOrderDelay[0].approveTime)" class="mt-2 rounded-lg border p-3">
173
+        <div v-if="props.item.workOrderDelay[0].approveRemark" class="flex flex-wrap gap-2">
174
+          <div class="flex items-center gap-1 font-medium">
175
+            <el-icon :size="16"><Message /></el-icon>
176
+            <span>审批意见:</span>
177
+          </div>
178
+          <div class="text-foreground/75 break-all flex-1">{{ props.item.workOrderDelay[0].approveRemark }}</div>
179
+        </div>
180
+        <div v-if="props.item.workOrderDelay[0].approveStatus !== undefined" class="mt-2 flex flex-wrap gap-2">
181
+          <div class="flex items-center gap-1 font-medium">
182
+            <el-icon :size="16"><User /></el-icon>
183
+            <span>审核状态:</span>
184
+          </div>
185
+          <ElTag :type="renderApproveStatus(props.item.workOrderDelay[0].approveStatus).type">
186
+            {{ renderApproveStatus(props.item.workOrderDelay[0].approveStatus).label }}
187
+          </ElTag>
188
+        </div>
189
+        <div v-if="props.item.workOrderDelay[0].approveTime" class="mt-2 flex flex-wrap gap-2">
190
+          <div class="flex items-center gap-1 font-medium">
191
+            <el-icon :size="16"><Timer /></el-icon>
192
+            <span>新截止时间:</span>
193
+          </div>
194
+          <div class="text-foreground/75">{{ props.item.workOrderDelay[0].approveTime }}</div>
195
+        </div>
196
+      </div>
197
+      
198
+      <!-- 原有消息内容 -->
97 199
       <div
98
-        v-if="item.message"
99
-        class="rounded-lg border px-3 py-1"
200
+        v-if="props.item.message && !['PROCESS', 'APPLY_DELAY', 'AUDIT_DELAY'].includes(props.item.operationType)"
201
+        class="mt-2 rounded-lg border px-3 py-1"
100 202
         :class="cn('flex gap-2')"
101 203
       >
102 204
         <Message />
103
-        <div class="text-foreground/75 break-all">{{ item.message }}</div>
104
-      </div>
105
-      <div v-if="attachmentInfo.length > 0" class="flex flex-wrap gap-2">
106
-        <!-- 这里下载的文件名不是原始文件名 -->
107
-        <a
108
-          v-for="attachment in attachmentInfo"
109
-          :key="attachment.ossId"
110
-          :href="attachment.url"
111
-          class="text-primary"
112
-          target="_blank"
113
-        >
114
-          <div class="flex items-center gap-1">
115
-            <span class="icon-[mingcute--attachment-line] size-[18px]"></span>
116
-            <span>{{ attachment.name }}</span>
117
-          </div>
118
-        </a>
205
+        <div class="text-foreground/75 break-all">{{ props.item.message }}</div>
119 206
       </div>
120 207
     </div>
121 208
   </ElTimelineItem>
122 209
 </template>
210
+
211
+
212
+

+ 32 - 9
apps/web-ele/src/views/workflow/components/approval-timeline.vue

@@ -1,5 +1,5 @@
1 1
 <script setup lang="ts">
2
-import { onMounted, shallowRef } from 'vue';
2
+import { onMounted, shallowRef, watch } from 'vue';
3 3
 
4 4
 import { ElEmpty, ElMessage, ElTimeline } from 'element-plus';
5 5
 
@@ -8,30 +8,53 @@ import { requestClient } from '#/api/request';
8 8
 import ApprovalTimelineItem from './approval-timeline-item.vue';
9 9
 
10 10
 interface Props {
11
-  id: string;
11
+  id?: string | number; // 修改为同时接受字符串和数字类型
12 12
 }
13 13
 
14 14
 const props = defineProps<Props>();
15 15
 const list = shallowRef([]);
16 16
 
17
-onMounted(async () => {
18
-  if (!props.id) return;
17
+// 提取数据加载逻辑为单独函数
18
+async function loadData() {
19
+  // 使用更严格的检查,确保id为有效字符串或数字时才执行请求
20
+  if (!props.id || props.id === 'undefined' || props.id === 0) {
21
+    list.value = [];
22
+    return;
23
+  }
19 24
 
20 25
   try {
26
+    // 将id转换为字符串类型,确保API请求参数类型正确
27
+    const orderId = String(props.id);
21 28
     const resp = await requestClient.get('/workOrderRecord/record/list', {
22
-      params: { orderId: props.id },
29
+      params: { orderId },
23 30
     });
24
-    list.value = resp.data;
25
-    console.log(resp.data);
31
+    list.value = resp.rows || [];
26 32
   } catch {
27 33
     ElMessage.error('获取工单记录失败');
28 34
   }
35
+}
36
+
37
+// 组件挂载时加载数据
38
+onMounted(() => {
39
+  console.log(props.id);
40
+  loadData();
41
+});
42
+
43
+// 监听id属性变化,当id变化时重新加载数据
44
+watch(() => props.id, () => {
45
+  console.log(props.id);
46
+  loadData();
29 47
 });
30 48
 </script>
31 49
 
32 50
 <template>
33
-  <ElTimeline v-if="id">
34
-    <ApprovalTimelineItem v-for="item in list" :key="item.id" :item="item" />
51
+  <ElTimeline v-if="id" style="padding-top: 20px; margin-top: 20px; border-top: 1px solid #e5e5e5;">
52
+    <ApprovalTimelineItem v-for="item in list" :key="item.id" :item="item"  style="padding-left: 15px;"/>
35 53
   </ElTimeline>
36 54
   <ElEmpty v-else />
37 55
 </template>
56
+
57
+
58
+
59
+
60
+

+ 78 - 15
apps/web-ele/src/views/workflow/leave/leave-description.vue

@@ -1,50 +1,113 @@
1 1
 <script setup lang="ts">
2 2
 import type { LeaveVO } from '../leave/api/model';
3 3
 
4
-import { onMounted, shallowRef } from 'vue';
5
-
6 4
 import dayjs from 'dayjs';
5
+import { ref, watchEffect } from 'vue';
6
+import { ElImage, ElImageViewer } from 'element-plus';
7 7
 
8 8
 defineOptions({
9 9
   name: 'LeaveDescription',
10 10
   inheritAttrs: false,
11 11
 });
12 12
 
13
-const props = defineProps<{ task: LeaveVO }>();
13
+const props = defineProps<{ workOrderDetail: LeaveVO }>();
14
+
15
+// 附件数组,使用ref存储以保持引用稳定
16
+const attachmentsArray = ref<string[]>([]);
17
+
18
+// 图片预览相关
19
+const previewVisible = ref(false);
20
+const previewIndex = ref(0);
14 21
 
15
-const data = shallowRef<LeaveVO>();
16
-onMounted(async () => {
17
-  data.value = props.task;
22
+// 监听附件URL变化,更新附件数组
23
+watchEffect(() => {
24
+  if (!props.workOrderDetail?.attachmentsUrl) {
25
+    attachmentsArray.value = [];
26
+    return;
27
+  }
28
+  
29
+  let urls: string[] = [];
30
+  
31
+  // 如果已经是数组,确保每个元素都是字符串
32
+  if (Array.isArray(props.workOrderDetail.attachmentsUrl)) {
33
+    urls = props.workOrderDetail.attachmentsUrl.map(item => String(item));
34
+  } else {
35
+    try {
36
+      // 尝试解析JSON字符串
37
+      const parsed = JSON.parse(String(props.workOrderDetail.attachmentsUrl));
38
+      if (Array.isArray(parsed)) {
39
+        urls = parsed.map(item => String(item));
40
+      } else {
41
+        urls = [String(parsed)];
42
+      }
43
+    } catch (e) {
44
+      // 解析失败,将整个字符串作为单个URL
45
+      urls = [String(props.workOrderDetail.attachmentsUrl)];
46
+    }
47
+  }
48
+  
49
+  // 过滤掉空字符串
50
+  attachmentsArray.value = urls.filter(url => url.trim() !== '');
18 51
 });
19 52
 
20
-function formatDate(date: string) {
21
-  return dayjs(date).format('YYYY-MM-DD');
53
+// 图片预览函数
54
+function handlePreview(index: number) {
55
+  previewIndex.value = index;
56
+  previewVisible.value = true;
57
+}
58
+
59
+// 关闭预览
60
+function closePreview() {
61
+  previewVisible.value = false;
22 62
 }
23 63
 </script>
24 64
 
25 65
 <template>
26 66
   <div class="rounded-[6px] border p-2">
27
-    <ElDescriptions v-if="data" :column="1">
67
+    <ElDescriptions v-if="props.workOrderDetail" :column="4" class="mb-2">
28 68
       <ElDescriptionsItem label="工单类型:">
29
-        {{ data.ticketTypeName }}
69
+        {{ props.workOrderDetail.ticketTypeName }}
30 70
       </ElDescriptionsItem>
31 71
       <ElDescriptionsItem label="场站:">
32
-        {{ data.stationName || '无' }}
72
+        {{ props.workOrderDetail.stationName || '无' }}
33 73
       </ElDescriptionsItem>
34 74
       <ElDescriptionsItem label="联系人:">
35
-        {{ data.contact || '无' }}
75
+        {{ props.workOrderDetail.contact || '无' }}
36 76
       </ElDescriptionsItem>
37 77
       <ElDescriptionsItem label="联系电话:">
38
-        {{ data.phone || '无' }}
78
+        {{ props.workOrderDetail.phone || '无' }}
39 79
       </ElDescriptionsItem>
80
+    </ElDescriptions>
81
+    <ElDescriptions v-if="props.workOrderDetail" :column="1">
40 82
       <ElDescriptionsItem label="提报内容:">
41
-        {{ data.content || '无' }}
83
+        {{ props.workOrderDetail.content || '无' }}
42 84
       </ElDescriptionsItem>
43 85
       <ElDescriptionsItem label="附件:">
44
-        {{ data.attachment || '无' }}
86
+        <div v-if="attachmentsArray.length > 0" class="flex flex-wrap gap-2">
87
+          <div
88
+            v-for="(url, index) in attachmentsArray"
89
+            :key="index"
90
+            class="w-20 h-20 cursor-pointer rounded overflow-hidden"
91
+            @click="handlePreview(index)"
92
+          >
93
+            <ElImage
94
+              :src="url"
95
+              fit="cover"
96
+              class="w-full h-full"
97
+            />
98
+          </div>
99
+        </div>
100
+        <span v-else>无</span>
45 101
       </ElDescriptionsItem>
46 102
     </ElDescriptions>
47 103
 
48 104
     <ElSkeleton active v-else :rows="4" />
105
+    <!-- 图片预览组件 -->
106
+  <ElImageViewer
107
+    v-if="previewVisible"
108
+    :url-list="attachmentsArray"
109
+    :initial-index="previewIndex"
110
+    @close="closePreview"
111
+  />
49 112
   </div>
50 113
 </template>

+ 141 - 45
apps/web-ele/src/views/workflow/order/creatOrder.vue

@@ -1,12 +1,41 @@
1 1
 <script lang="ts" setup>
2
-import { ref, reactive } from 'vue';
2
+import { onMounted, reactive, ref, watch } from 'vue';
3
+import { useRoute, useRouter } from 'vue-router';
4
+
3 5
 import { Page } from '@vben/common-ui';
4
-import { ElForm, ElFormItem, ElInput, ElSelect, ElUpload, ElSwitch, ElButton, ElMessage, ElTreeSelect } from 'element-plus';
6
+
5 7
 import { UploadFilled } from '@element-plus/icons-vue';
6
-import { categoryTree } from '#/api/workflow/category/index';
8
+import {
9
+  ElButton,
10
+  ElForm,
11
+  ElFormItem,
12
+  ElInput,
13
+  ElMessage,
14
+  ElSelect,
15
+  ElSwitch,
16
+  ElTreeSelect,
17
+  ElUpload,
18
+} from 'element-plus';
19
+
20
+import { requestClient } from '#/api/request';
7 21
 import { selectAllSysStation } from '#/api/system/infoEntry/stationInfo/stationInfo';
22
+import { categoryTree } from '#/api/workflow/category/index';
23
+import { getWorkOrderDetail } from '#/api/workflow/instance/index';
8 24
 import { getSingleImageUploadConfig } from '#/components/upload/config';
9
-import { requestClient } from '#/api/request';
25
+
26
+// 接收props参数
27
+const props = defineProps<{
28
+  id?: string;
29
+}>();
30
+
31
+// 定义emit事件
32
+const emit = defineEmits<{
33
+  close: [];
34
+}>();
35
+
36
+// 路由相关
37
+const route = useRoute();
38
+const router = useRouter();
10 39
 
11 40
 // 表单数据
12 41
 const formData = reactive({
@@ -16,19 +45,25 @@ const formData = reactive({
16 45
   phone: '',
17 46
   content: '',
18 47
   attachments: '',
19
-  isSubmit: false
48
+  isSubmit: false,
20 49
 });
21 50
 
22 51
 // 表单规则
23 52
 const formRules = reactive({
24
-  ticketType: [{ required: true, message: '请选择工单类型', trigger: 'change' }],
53
+  ticketType: [
54
+    { required: true, message: '请选择工单类型', trigger: 'change' },
55
+  ],
25 56
   stationId: [{ required: true, message: '请选择场站', trigger: 'change' }],
26 57
   contact: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
27 58
   phone: [
28 59
     { required: true, message: '请输入联系电话', trigger: 'blur' },
29
-    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
60
+    {
61
+      pattern: /^1[3-9]\d{9}$/,
62
+      message: '请输入正确的手机号码',
63
+      trigger: 'blur',
64
+    },
30 65
   ],
31
-  content: [{ required: true, message: '请输入提报内容', trigger: 'blur' }]
66
+  content: [{ required: true, message: '请输入提报内容', trigger: 'blur' }],
32 67
 });
33 68
 
34 69
 // 工单类型选项
@@ -70,7 +105,7 @@ const handleUploadSuccess = (response, file, fileList) => {
70 105
   if (response.code === 200) {
71 106
     // 将文件路径添加到attachments字段
72 107
     console.log(response, file, fileList);
73
-    const paths = fileList.map(f => f.response.data.fileName).join(',');
108
+    const paths = fileList.map((f) => f.response.data.fileName).join(',');
74 109
     formData.attachments = paths;
75 110
   } else {
76 111
     ElMessage.error('文件上传失败');
@@ -85,35 +120,95 @@ const handleUploadError = (error) => {
85 120
 
86 121
 // 移除文件回调
87 122
 const handleRemove = (file, fileList) => {
88
-  // 更新attachments字段
89
-  const paths = fileList.map(f => f.response.data.url).join(',');
90
-  formData.attachments = paths;
123
+  // 更新attachments字段(文件名)
124
+  const fileNames = fileList.map((f) => f.response.data.fileName).join(',');
125
+  formData.attachments = fileNames;
126
+};
127
+
128
+// 获取工单详情
129
+const getOrderDetail = async (orderId?: string) => {
130
+  const id = orderId || props.id || route.query.id;
131
+  if (!id) return;
132
+
133
+  try {
134
+    const detail = await getWorkOrderDetail(id as string);
135
+    console.log('工单详情:', detail);
136
+    // 填充表单数据
137
+    formData.ticketType = Number(detail.ticketType) || '';
138
+    formData.stationId = Number(detail.stationId) || '';
139
+    formData.contact = detail.contact || '';
140
+    formData.phone = detail.phone || '';
141
+    formData.content = detail.content || '';
142
+    formData.attachments = detail.attachments || '';
143
+    formData.isSubmit = detail.isSubmit || false;
144
+
145
+    // 处理附件文件列表
146
+    if (detail.attachmentsUrl && Array.isArray(detail.attachmentsUrl)) {
147
+      const fileNames = formData.attachments
148
+        ? formData.attachments.split(',')
149
+        : [];
150
+      fileList.value = detail.attachmentsUrl.map((url, index) => ({
151
+        name: fileNames[index] || `file-${index}`,
152
+        url,
153
+        response: {
154
+          data: { url, fileName: fileNames[index] || `file-${index}` },
155
+        },
156
+      }));
157
+    }
158
+  } catch (error) {
159
+    ElMessage.error('获取工单详情失败');
160
+    console.error('获取工单详情失败:', error);
161
+  }
91 162
 };
92 163
 
164
+// 监听props.id变化,获取工单详情
165
+watch(
166
+  () => props.id,
167
+  (newId) => {
168
+    if (newId) {
169
+      getOrderDetail(newId);
170
+    }
171
+  },
172
+  { immediate: true },
173
+);
174
+
93 175
 // 表单提交
94 176
 const handleSubmit = async () => {
95 177
   if (!formRef.value) return;
96
-  
178
+
97 179
   try {
98 180
     await formRef.value.validate();
99 181
     console.log(formData);
100
-    // 调用创建工单接口
101
-    await requestClient.post('/workOrder/order', formData);
102
-    
103
-    ElMessage.success('工单创建成功');
104
-    
105
-    // 重置表单
106
-    formRef.value.resetFields();
107
-    fileList.value = [];
108
-    formData.attachments = '';
182
+    const id = props.id || route.query.id;
183
+
184
+    if (id) {
185
+      // 更新工单
186
+      await requestClient.put(`/workOrder/order/${id}`, formData);
187
+      ElMessage.success('工单更新成功');
188
+
189
+      // 如果是在drawer中打开的,需要关闭drawer
190
+      if (props.id) {
191
+        // 触发close事件通知drawer关闭
192
+        emit('close');
193
+      }
194
+    } else {
195
+      // 创建工单
196
+      await requestClient.post('/workOrder/order', formData);
197
+      ElMessage.success('工单创建成功');
198
+
199
+      // 重置表单
200
+      formRef.value.resetFields();
201
+      fileList.value = [];
202
+      formData.attachments = '';
203
+    }
109 204
   } catch (error) {
110 205
     if (error.name === 'ValidationError') {
111 206
       // 表单验证失败,Element Plus会自动提示错误信息
112 207
       return;
113 208
     }
114
-    
115
-    ElMessage.error('工单创建失败');
116
-    console.error('工单创建失败:', error);
209
+
210
+    ElMessage.error(id ? '工单更新失败' : '工单创建失败');
211
+    console.error('工单操作失败:', error);
117 212
   }
118 213
 };
119 214
 
@@ -121,10 +216,22 @@ const handleSubmit = async () => {
121 216
 const initData = () => {
122 217
   getTicketTypes();
123 218
   getStations();
219
+  // 只有在非drawer模式下(没有props.id)才调用getOrderDetail
220
+  // drawer模式下由watch监听props.id变化来获取数据
221
+  if (!props.id) {
222
+    getOrderDetail();
223
+  }
124 224
 };
125 225
 
126 226
 // 页面加载时初始化数据
127
-initData();
227
+onMounted(() => {
228
+  initData();
229
+});
230
+
231
+// 暴露方法给父组件
232
+defineExpose({
233
+  handleSubmit,
234
+});
128 235
 </script>
129 236
 
130 237
 <template>
@@ -147,7 +254,7 @@ initData();
147 254
             style="width: 100%"
148 255
             :props="{ label: 'label', value: 'id', children: 'children' }"
149 256
             filterable
150
-          ></ElTreeSelect>
257
+          />
151 258
         </ElFormItem>
152 259
 
153 260
         <!-- 场站 -->
@@ -162,24 +269,18 @@ initData();
162 269
               :key="option.id"
163 270
               :label="option.stationName"
164 271
               :value="option.id"
165
-            ></ElOption>
272
+            />
166 273
           </ElSelect>
167 274
         </ElFormItem>
168 275
 
169 276
         <!-- 联系人 -->
170 277
         <ElFormItem label="联系人" prop="contact">
171
-          <ElInput
172
-            v-model="formData.contact"
173
-            placeholder="请输入联系人"
174
-          ></ElInput>
278
+          <ElInput v-model="formData.contact" placeholder="请输入联系人" />
175 279
         </ElFormItem>
176 280
 
177 281
         <!-- 联系电话 -->
178 282
         <ElFormItem label="联系电话" prop="phone">
179
-          <ElInput
180
-            v-model="formData.phone"
181
-            placeholder="请输入联系电话"
182
-          ></ElInput>
283
+          <ElInput v-model="formData.phone" placeholder="请输入联系电话" />
183 284
         </ElFormItem>
184 285
 
185 286
         <!-- 提报内容 -->
@@ -189,7 +290,7 @@ initData();
189 290
             type="textarea"
190 291
             :rows="4"
191 292
             placeholder="请输入提报内容"
192
-          ></ElInput>
293
+          />
193 294
         </ElFormItem>
194 295
 
195 296
         <!-- 提报附件 -->
@@ -216,11 +317,11 @@ initData();
216 317
 
217 318
         <!-- 是否提交 -->
218 319
         <ElFormItem label="是否提交">
219
-          <ElSwitch v-model="formData.isSubmit"></ElSwitch>
320
+          <ElSwitch v-model="formData.isSubmit" />
220 321
         </ElFormItem>
221 322
 
222
-        <!-- 提交按钮 -->
223
-        <ElFormItem>
323
+        <!-- 提交按钮 - 仅在创建模式下显示 -->
324
+        <ElFormItem v-if="!props.id">
224 325
           <ElButton type="primary" @click="handleSubmit">创建工单</ElButton>
225 326
         </ElFormItem>
226 327
       </ElForm>
@@ -239,8 +340,3 @@ initData();
239 340
   max-width: 600px;
240 341
 }
241 342
 </style>
242
-
243
-
244
-
245
-
246
-

+ 3 - 8
apps/web-ele/src/views/workflow/processDefinition/index.vue

@@ -347,6 +347,7 @@ const [ProcessDefinitionDeployDrawerComponent, deployDrawerApi] = useVbenDrawer(
347 347
               <ElPopconfirm title="确认删除" @confirm="confirmEvent(row)">
348 348
                 <template #reference>
349 349
                   <ElButton
350
+                    v-if="!row.isPublish && row.useExpression === 'N'"
350 351
                     size="small"
351 352
                     type="danger"
352 353
                     plain
@@ -411,15 +412,9 @@ const [ProcessDefinitionDeployDrawerComponent, deployDrawerApi] = useVbenDrawer(
411 412
                   </ElButton>
412 413
                 </template>
413 414
               </ElPopconfirm>
414
-              <ElButton
415
-                size="small"
416
-                type="primary"
417
-                plain
418
-                @click="handleExportXml(row)"
419
-                v-access:code="['workflow:definition:export']"
420
-              >
415
+              <!-- <ElButton size="small" type="primary" plain @click="handleExportXml(row)" v-access:code="['workflow:definition:export']" >
421 416
                 导出流程
422
-              </ElButton>
417
+              </ElButton> -->
423 418
             </ElSpace>
424 419
           </template>
425 420
         </BasicTable>

+ 287 - 0
apps/web-ele/src/views/workflow/task/delayedReview.vue

@@ -0,0 +1,287 @@
1
+<!-- eslint-disable no-use-before-define -->
2
+<script setup lang="ts">
3
+import type { TaskInfo } from '#/api/workflow/task/model';
4
+
5
+import { computed, onMounted, ref, useTemplateRef } from 'vue';
6
+
7
+import { Page } from '@vben/common-ui';
8
+import { useTabs } from '@vben/hooks';
9
+
10
+import { Refresh, Search } from '@element-plus/icons-vue';
11
+import { ElButton, ElEmpty, ElInput, ElMessage, ElTooltip } from 'element-plus';
12
+import { cloneDeep, debounce } from 'lodash-es';
13
+
14
+import { getWorkOrderDetail, pageByDelay } from '#/api/workflow/instance';
15
+
16
+import { ApprovalCard, ApprovalPanel } from '../components';
17
+import { bottomOffset } from './constant';
18
+
19
+const emptyImage = ElEmpty.PRESENTED_IMAGE_SIMPLE;
20
+
21
+const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
22
+const taskTotal = ref(0);
23
+const page = ref(1);
24
+const loading = ref(false);
25
+
26
+const defaultFormData = {
27
+  searchKeyword: '', // 通用搜索关键词
28
+  nodeName: '', // 任务名称
29
+  flowCode: '', // 流程定义编码
30
+  category: null as null | number, // 流程分类
31
+};
32
+const formData = ref(cloneDeep(defaultFormData));
33
+
34
+/**
35
+ * 是否已经加载全部数据 即 taskList.length === taskTotal
36
+ */
37
+const isLoadComplete = computed(
38
+  () => taskList.value.length === taskTotal.value,
39
+);
40
+
41
+// 卡片父容器的ref
42
+const cardContainerRef = useTemplateRef('cardContainerRef');
43
+
44
+/**
45
+ * @param resetFields 是否清空查询参数
46
+ */
47
+async function reload(resetFields: boolean = false) {
48
+  // 需要先滚动到顶部
49
+  cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
50
+
51
+  page.value = 1;
52
+  currentTask.value = undefined;
53
+  taskTotal.value = 0;
54
+  lastSelectId.value = '';
55
+
56
+  if (resetFields) {
57
+    formData.value = cloneDeep(defaultFormData);
58
+  }
59
+
60
+  loading.value = true;
61
+  try {
62
+    // 根据搜索关键词类型动态决定搜索字段
63
+    const searchParams = { ...formData.value };
64
+    const { searchKeyword, ...otherParams } = searchParams;
65
+
66
+    // 如果有搜索关键词
67
+    if (searchKeyword) {
68
+      // 检查是否为数字
69
+      if (/^\d+$/.test(searchKeyword)) {
70
+        // 数字搜索orderNo
71
+        otherParams.orderNo = searchKeyword;
72
+      } else {
73
+        // 文字搜索content
74
+        otherParams.content = searchKeyword;
75
+      }
76
+    }
77
+
78
+    const resp = await pageByDelay({
79
+      pageSize: 10,
80
+      pageNum: page.value,
81
+      ...otherParams,
82
+    });
83
+    taskList.value = resp.rows.map((item) => ({
84
+      ...item,
85
+      active: false,
86
+      id: String(item.id),
87
+    }));
88
+    taskTotal.value = resp.total;
89
+  } catch {
90
+    ElMessage.error('加载任务列表失败');
91
+  } finally {
92
+    loading.value = false;
93
+  }
94
+
95
+  // 默认选中第一个
96
+  if (taskList.value.length > 0) {
97
+    const firstTask = taskList.value[0]!;
98
+    currentTask.value = firstTask;
99
+    currentTaskButtonData.value = firstTask.button;
100
+    handleCardClick(firstTask);
101
+  }
102
+}
103
+
104
+onMounted(reload);
105
+
106
+const handleScroll = debounce(async (e: Event) => {
107
+  if (!e.target) {
108
+    return;
109
+  }
110
+  // e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
111
+  // e.target.clientHeight 是元素的可视高度。
112
+  // e.target.scrollHeight 是元素的总高度。
113
+  const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
114
+  // 判断是否滚动到底部
115
+  const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
116
+
117
+  // 滚动到底部且没有加载完成
118
+  if (isBottom && !isLoadComplete.value) {
119
+    loading.value = true;
120
+    try {
121
+      page.value += 1;
122
+
123
+      // 根据搜索关键词类型动态决定搜索字段
124
+      const searchParams = { ...formData.value };
125
+      const { searchKeyword, ...otherParams } = searchParams;
126
+
127
+      // 如果有搜索关键词
128
+      if (searchKeyword) {
129
+        // 检查是否为数字
130
+        if (/^\d+$/.test(searchKeyword)) {
131
+          // 数字搜索orderNo
132
+          otherParams.orderNo = searchKeyword;
133
+        } else {
134
+          // 文字搜索content
135
+          otherParams.content = searchKeyword;
136
+        }
137
+      }
138
+
139
+      const resp = await pageByDelay({
140
+        pageSize: 10,
141
+        pageNum: page.value,
142
+        ...otherParams,
143
+      });
144
+      taskList.value.push(
145
+        ...resp.rows.map((item) => ({
146
+          ...item,
147
+          active: false,
148
+          id: String(item.id),
149
+        })),
150
+      );
151
+    } catch {
152
+      ElMessage.error('加载更多任务失败');
153
+    } finally {
154
+      loading.value = false;
155
+    }
156
+  }
157
+}, 200);
158
+
159
+const lastSelectId = ref('');
160
+const currentTask = ref<TaskInfo>();
161
+const currentWorkOrderDetail = ref<any>();
162
+const currentTaskButtonData = ref<any>('');
163
+async function handleCardClick(item: TaskInfo) {
164
+  const { taskId, orderId } = item;
165
+  // 点击的是同一个
166
+  if (lastSelectId.value === taskId) {
167
+    return;
168
+  }
169
+  currentTask.value = item;
170
+  currentTaskButtonData.value = item.button;
171
+  // 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
172
+  taskList.value.forEach((item) => {
173
+    item.active = item.taskId === taskId;
174
+  });
175
+  lastSelectId.value = taskId;
176
+  // 调用获取工单详情的接口
177
+  try {
178
+    const detail = await getWorkOrderDetail(orderId);
179
+    // 保存工单详情
180
+    currentWorkOrderDetail.value = detail;
181
+  } catch (error) {
182
+    console.error('获取工单详情失败:', error);
183
+    ElMessage.error('获取工单详情失败');
184
+  }
185
+}
186
+
187
+const { refreshTab } = useTabs();
188
+
189
+// 按钮权限控制
190
+const currentTaskButtonPermissions = computed(() => {
191
+  // 如果没有当前任务,返回空对象
192
+  if (!currentTaskButtonData.value) {
193
+    return {};
194
+  }
195
+  // 初始化权限对象
196
+  const permissions: Record<string, boolean> = {};
197
+  const button = currentTaskButtonData.value;
198
+  if (button) {
199
+    const buttonArray = button.split(',');
200
+    // 设置对应按钮的权限
201
+    permissions.approveDelay = buttonArray.includes('approve_delay');
202
+  }
203
+  console.log('currentTaskButtonPermissions', permissions);
204
+  return permissions;
205
+});
206
+</script>
207
+
208
+<template>
209
+  <Page :auto-content-height="true">
210
+    <div class="flex h-full gap-2">
211
+      <div
212
+        class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
213
+      >
214
+        <!-- 搜索条件 -->
215
+        <div
216
+          class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
217
+        >
218
+          <div class="grid grid-cols-1 gap-2">
219
+            <div class="flex items-center gap-1">
220
+              <ElInput
221
+                v-model="formData.searchKeyword"
222
+                placeholder="输入数字搜索工单号,输入文字搜索内容"
223
+                clearable
224
+                class="w-full"
225
+                :suffix-icon="Search"
226
+                @keyup.enter="reload(false)"
227
+              />
228
+              <ElTooltip placement="top" content="重置">
229
+                <ElButton @click="reload(true)" :icon="Refresh" />
230
+              </ElTooltip>
231
+            </div>
232
+          </div>
233
+        </div>
234
+        <div
235
+          ref="cardContainerRef"
236
+          class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
237
+          @scroll="handleScroll"
238
+        >
239
+          <template v-if="taskList.length > 0">
240
+            <ApprovalCard
241
+              v-for="item in taskList"
242
+              :key="item.id"
243
+              :info="item"
244
+              class="mx-2"
245
+              @click="handleCardClick(item)"
246
+            />
247
+          </template>
248
+          <ElEmpty v-else :image="emptyImage" description="暂无数据" />
249
+          <div
250
+            v-if="isLoadComplete && taskList.length > 0"
251
+            class="flex items-center justify-center text-[14px] opacity-50"
252
+          >
253
+            没有更多数据了
254
+          </div>
255
+          <!-- 遮罩loading层 -->
256
+          <div
257
+            v-if="loading"
258
+            class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
259
+          ></div>
260
+        </div>
261
+        <!-- total显示 -->
262
+        <div
263
+          class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
264
+        >
265
+          <div class="flex items-center justify-center">
266
+            共 {{ taskTotal }} 条记录
267
+          </div>
268
+        </div>
269
+      </div>
270
+      <ApprovalPanel
271
+        :task="currentTask"
272
+        :work-order-detail="currentWorkOrderDetail"
273
+        :button-permissions="currentTaskButtonPermissions"
274
+        type="delay"
275
+        @reload="refreshTab"
276
+      />
277
+    </div>
278
+  </Page>
279
+</template>
280
+
281
+<style lang="scss" scoped>
282
+.thin-scrollbar {
283
+  &::-webkit-scrollbar {
284
+    width: 5px;
285
+  }
286
+}
287
+</style>

+ 131 - 72
apps/web-ele/src/views/workflow/task/myDocument.vue

@@ -5,17 +5,26 @@ import type { TaskInfo } from '#/api/workflow/task/model';
5 5
 import { computed, onMounted, ref, useTemplateRef } from 'vue';
6 6
 
7 7
 import { Page } from '@vben/common-ui';
8
+import { DictEnum } from '@vben/constants';
8 9
 import { useTabs } from '@vben/hooks';
9 10
 
10
-import { Filter, Refresh, Search } from '@element-plus/icons-vue';
11
-import { ElButton, ElEmpty, ElForm, ElFormItem, ElInput, ElMessage, ElPopover, ElTooltip } from 'element-plus';
12
-
11
+import { Refresh, Search } from '@element-plus/icons-vue';
12
+import {
13
+  ElButton,
14
+  ElEmpty,
15
+  ElInput,
16
+  ElMessage,
17
+  ElOption,
18
+  ElSelect,
19
+  ElTooltip,
20
+} from 'element-plus';
13 21
 import { cloneDeep, debounce } from 'lodash-es';
14 22
 
15
-import { pageByCurrent, getWorkOrderDetail } from '#/api/workflow/instance';
23
+import { pageByCurrent } from '#/api/workflow/instance';
24
+import { getDictOptions } from '#/utils/dict';
16 25
 
17
-import { bottomOffset } from './constant';
18 26
 import { ApprovalCard, ApprovalPanel } from '../components';
27
+import { bottomOffset } from './constant';
19 28
 
20 29
 const emptyImage = ElEmpty.PRESENTED_IMAGE_SIMPLE;
21 30
 
@@ -25,10 +34,8 @@ const page = ref(1);
25 34
 const loading = ref(false);
26 35
 
27 36
 const defaultFormData = {
28
-  flowName: '', // 流程定义名称
29
-  nodeName: '', // 任务名称
30
-  flowCode: '', // 流程定义编码
31
-  category: null as null | number, // 流程分类
37
+  searchKeyword: '', // 通用搜索关键词
38
+  status: null as null | number, // 工单状态
32 39
 };
33 40
 const formData = ref(cloneDeep(defaultFormData));
34 41
 
@@ -39,6 +46,9 @@ const isLoadComplete = computed(
39 46
   () => taskList.value.length === taskTotal.value,
40 47
 );
41 48
 
49
+// 获取状态下拉框选项
50
+const statusOptions = getDictOptions(DictEnum.TICKET_STATUS);
51
+
42 52
 // 卡片父容器的ref
43 53
 const cardContainerRef = useTemplateRef('cardContainerRef');
44 54
 
@@ -50,7 +60,7 @@ async function reload(resetFields: boolean = false) {
50 60
   cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
51 61
 
52 62
   page.value = 1;
53
-  currentTask.value = undefined;
63
+  currentWorkOrderDetail.value = undefined;
54 64
   taskTotal.value = 0;
55 65
   lastSelectId.value = '';
56 66
 
@@ -60,13 +70,32 @@ async function reload(resetFields: boolean = false) {
60 70
 
61 71
   loading.value = true;
62 72
   try {
73
+    // 根据搜索关键词类型动态决定搜索字段
74
+    const searchParams = { ...formData.value };
75
+    const { searchKeyword, ...otherParams } = searchParams;
76
+
77
+    // 如果有搜索关键词
78
+    if (searchKeyword) {
79
+      // 检查是否为数字
80
+      if (/^\d+$/.test(searchKeyword)) {
81
+        // 数字搜索orderNo
82
+        otherParams.orderNo = searchKeyword;
83
+      } else {
84
+        // 文字搜索content
85
+        otherParams.content = searchKeyword;
86
+      }
87
+    }
88
+
63 89
     const resp = await pageByCurrent({
64 90
       pageSize: 10,
65 91
       pageNum: page.value,
66
-      ...formData.value,
92
+      ...otherParams,
67 93
     });
68
-    console.log(resp);
69
-    taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
94
+    taskList.value = resp.rows.map((item) => ({
95
+      ...item,
96
+      active: false,
97
+      id: String(item.id),
98
+    }));
70 99
     taskTotal.value = resp.total;
71 100
   } catch {
72 101
     ElMessage.error('加载任务列表失败');
@@ -77,7 +106,7 @@ async function reload(resetFields: boolean = false) {
77 106
   // 默认选中第一个
78 107
   if (taskList.value.length > 0) {
79 108
     const firstTask = taskList.value[0]!;
80
-    currentTask.value = firstTask;
109
+    currentWorkOrderDetail.value = firstTask;
81 110
     handleCardClick(firstTask);
82 111
   }
83 112
 }
@@ -100,14 +129,34 @@ const handleScroll = debounce(async (e: Event) => {
100 129
     loading.value = true;
101 130
     try {
102 131
       page.value += 1;
132
+
133
+      // 根据搜索关键词类型动态决定搜索字段
134
+      const searchParams = { ...formData.value };
135
+      const { searchKeyword, ...otherParams } = searchParams;
136
+
137
+      // 如果有搜索关键词
138
+      if (searchKeyword) {
139
+        // 检查是否为数字
140
+        if (/^\d+$/.test(searchKeyword)) {
141
+          // 数字搜索orderNo
142
+          otherParams.orderNo = searchKeyword;
143
+        } else {
144
+          // 文字搜索content
145
+          otherParams.content = searchKeyword;
146
+        }
147
+      }
148
+
103 149
       const resp = await pageByCurrent({
104 150
         pageSize: 10,
105 151
         pageNum: page.value,
106
-        ...formData.value,
152
+        ...otherParams,
107 153
       });
108
-      console.log(resp);
109 154
       taskList.value.push(
110
-        ...resp.rows.map((item) => ({ ...item, active: false })),
155
+        ...resp.rows.map((item) => ({
156
+          ...item,
157
+          active: false,
158
+          id: String(item.id),
159
+        })),
111 160
       );
112 161
     } catch {
113 162
       ElMessage.error('加载更多任务失败');
@@ -118,33 +167,49 @@ const handleScroll = debounce(async (e: Event) => {
118 167
 }, 200);
119 168
 
120 169
 const lastSelectId = ref('');
121
-const currentTask = ref<TaskInfo>();
170
+const currentWorkOrderDetail = ref<any>();
122 171
 async function handleCardClick(item: TaskInfo) {
123 172
   const { id } = item;
124 173
   // 点击的是同一个
125 174
   if (lastSelectId.value === id) {
126 175
     return;
127 176
   }
128
-  currentTask.value = item;
177
+  currentWorkOrderDetail.value = item;
129 178
   // 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
130 179
   taskList.value.forEach((item) => {
131 180
     item.active = item.id === id;
132 181
   });
133 182
   lastSelectId.value = id;
134
-  console.log(lastSelectId.value);
135
-  
136
-  // 调用获取工单详情的接口
137
-  try {
138
-    const detail = await getWorkOrderDetail(id);
139
-    console.log('工单详情:', detail);
140
-    // 可以在这里使用获取到的工单详情数据
141
-  } catch (error) {
142
-    console.error('获取工单详情失败:', error);
143
-    ElMessage.error('获取工单详情失败');
144
-  }
145 183
 }
146 184
 
147 185
 const { refreshTab } = useTabs();
186
+
187
+// 按钮权限控制
188
+const currentTaskButtonPermissions = computed(() => {
189
+  // 如果没有当前任务,返回空对象
190
+  if (!currentWorkOrderDetail.value) {
191
+    return {};
192
+  }
193
+  // 根据任务状态判断工单类型
194
+  const { status } = currentWorkOrderDetail.value;
195
+  // 初始化权限对象
196
+  const permissions: Record<string, boolean> = {};
197
+  // 新工单 (status: "news")
198
+  if (status === 'new') {
199
+    // 新工单可以进行的操作
200
+    permissions.edit = true; // 编辑
201
+    permissions.submit = true; // 提交
202
+    permissions.delete = true; // 删除
203
+  }
204
+  // 已处理工单 (status: "processed")
205
+  else if (status === 'processed') {
206
+    // 已处理工单可以进行的操作
207
+    permissions.edit = false; // 编辑
208
+    permissions.submit = false; // 提交
209
+    permissions.delete = false; // 删除
210
+  }
211
+  return permissions;
212
+});
148 213
 </script>
149 214
 
150 215
 <template>
@@ -157,46 +222,36 @@ const { refreshTab } = useTabs();
157 222
         <div
158 223
           class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
159 224
         >
160
-          <div class="flex items-center gap-1">
161
-            <ElInput
162
-              v-model="formData.flowName"
163
-              placeholder="流程名称搜索"
164
-              clearable
165
-              class="w-full"
166
-              :suffix-icon="Search"
167
-              @keyup.enter="reload(false)"
168
-            />
169
-            <ElTooltip placement="top" content="重置">
170
-              <ElButton @click="reload(true)" :icon="Refresh" />
171
-            </ElTooltip>
172
-            <ElPopover placement="top-end" trigger="click">
173
-              <template #reference>
174
-                <ElButton :icon="Filter"  style="margin-left: 0;" />
175
-              </template>
176
-              <template #default>
177
-                <ElForm
178
-                  :model="formData"
179
-                  label-width="80px"
180
-                  class="w-[300px]"
181
-                  @submit.prevent="reload(false)"
182
-                >
183
-                  <ElFormItem label="任务名称">
184
-                    <ElInput v-model="formData.nodeName" placeholder="请输入" />
185
-                  </ElFormItem>
186
-                  <ElFormItem label="流程编码">
187
-                    <ElInput v-model="formData.flowCode" placeholder="请输入" />
188
-                  </ElFormItem>
189
-                  <ElFormItem>
190
-                    <div class="flex justify-end gap-2">
191
-                      <ElButton type="primary" @click="reload(false)">
192
-                        搜索
193
-                      </ElButton>
194
-                      <ElButton @click="reload(true)"> 重置 </ElButton>
195
-                    </div>
196
-                  </ElFormItem>
197
-                </ElForm>
198
-              </template>
199
-            </ElPopover>
225
+          <div class="grid grid-cols-1 gap-2">
226
+            <div class="flex items-center gap-1">
227
+              <ElInput
228
+                v-model="formData.searchKeyword"
229
+                placeholder="输入数字搜索工单号,输入文字搜索内容"
230
+                clearable
231
+                class="w-full"
232
+                :suffix-icon="Search"
233
+                @keyup.enter="reload(false)"
234
+              />
235
+            </div>
236
+            <div class="flex items-center gap-1">
237
+              <ElSelect
238
+                v-model="formData.status"
239
+                placeholder="状态"
240
+                clearable
241
+                class="w-full"
242
+                @change="reload(false)"
243
+              >
244
+                <ElOption
245
+                  v-for="option in statusOptions"
246
+                  :key="option.value"
247
+                  :label="option.label"
248
+                  :value="option.value"
249
+                />
250
+              </ElSelect>
251
+              <ElTooltip placement="top" content="重置">
252
+                <ElButton @click="reload(true)" :icon="Refresh" />
253
+              </ElTooltip>
254
+            </div>
200 255
           </div>
201 256
         </div>
202 257
         <div
@@ -235,7 +290,12 @@ const { refreshTab } = useTabs();
235 290
           </div>
236 291
         </div>
237 292
       </div>
238
-      <ApprovalPanel :task="currentTask" type="myself" @reload="refreshTab" />
293
+      <ApprovalPanel
294
+        :work-order-detail="currentWorkOrderDetail"
295
+        :button-permissions="currentTaskButtonPermissions"
296
+        type="myself"
297
+        @reload="refreshTab"
298
+      />
239 299
     </div>
240 300
   </Page>
241 301
 </template>
@@ -251,4 +311,3 @@ const { refreshTab } = useTabs();
251 311
   @apply thin-scrollbar;
252 312
 }
253 313
 </style>
254
-

+ 118 - 65
apps/web-ele/src/views/workflow/task/taskCopyList.vue

@@ -5,12 +5,13 @@ import type { TaskInfo } from '#/api/workflow/task/model';
5 5
 import { computed, onMounted, ref, useTemplateRef } from 'vue';
6 6
 
7 7
 import { Page } from '@vben/common-ui';
8
+import { useTabs } from '@vben/hooks';
8 9
 
9
-import { ElEmpty, ElMessage } from 'element-plus';
10
-
10
+import { Refresh, Search } from '@element-plus/icons-vue';
11
+import { ElButton, ElEmpty, ElInput, ElMessage, ElTooltip } from 'element-plus';
11 12
 import { cloneDeep, debounce } from 'lodash-es';
12 13
 
13
-import { pageByTaskCopy } from '#/api/workflow/task';
14
+import { getWorkOrderDetail, pageByCc } from '#/api/workflow/instance';
14 15
 
15 16
 import { ApprovalCard, ApprovalPanel } from '../components';
16 17
 import { bottomOffset } from './constant';
@@ -23,7 +24,7 @@ const page = ref(1);
23 24
 const loading = ref(false);
24 25
 
25 26
 const defaultFormData = {
26
-  flowName: '', // 流程定义名称
27
+  searchKeyword: '', // 通用搜索关键词
27 28
   nodeName: '', // 任务名称
28 29
   flowCode: '', // 流程定义编码
29 30
   category: null as null | number, // 流程分类
@@ -58,12 +59,32 @@ async function reload(resetFields: boolean = false) {
58 59
 
59 60
   loading.value = true;
60 61
   try {
61
-    const resp = await pageByTaskCopy({
62
+    // 根据搜索关键词类型动态决定搜索字段
63
+    const searchParams = { ...formData.value };
64
+    const { searchKeyword, ...otherParams } = searchParams;
65
+
66
+    // 如果有搜索关键词
67
+    if (searchKeyword) {
68
+      // 检查是否为数字
69
+      if (/^\d+$/.test(searchKeyword)) {
70
+        // 数字搜索orderNo
71
+        otherParams.orderNo = searchKeyword;
72
+      } else {
73
+        // 文字搜索content
74
+        otherParams.content = searchKeyword;
75
+      }
76
+    }
77
+
78
+    const resp = await pageByCc({
62 79
       pageSize: 10,
63 80
       pageNum: page.value,
64
-      ...formData.value,
81
+      ...otherParams,
65 82
     });
66
-    taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
83
+    taskList.value = resp.rows.map((item) => ({
84
+      ...item,
85
+      active: false,
86
+      id: String(item.id),
87
+    }));
67 88
     taskTotal.value = resp.total;
68 89
   } catch {
69 90
     ElMessage.error('加载任务列表失败');
@@ -75,6 +96,7 @@ async function reload(resetFields: boolean = false) {
75 96
   if (taskList.value.length > 0) {
76 97
     const firstTask = taskList.value[0]!;
77 98
     currentTask.value = firstTask;
99
+    currentTaskButtonData.value = firstTask.button;
78 100
     handleCardClick(firstTask);
79 101
   }
80 102
 }
@@ -97,13 +119,34 @@ const handleScroll = debounce(async (e: Event) => {
97 119
     loading.value = true;
98 120
     try {
99 121
       page.value += 1;
100
-      const resp = await pageByTaskCopy({
122
+
123
+      // 根据搜索关键词类型动态决定搜索字段
124
+      const searchParams = { ...formData.value };
125
+      const { searchKeyword, ...otherParams } = searchParams;
126
+
127
+      // 如果有搜索关键词
128
+      if (searchKeyword) {
129
+        // 检查是否为数字
130
+        if (/^\d+$/.test(searchKeyword)) {
131
+          // 数字搜索orderNo
132
+          otherParams.orderNo = searchKeyword;
133
+        } else {
134
+          // 文字搜索content
135
+          otherParams.content = searchKeyword;
136
+        }
137
+      }
138
+
139
+      const resp = await pageByCc({
101 140
         pageSize: 10,
102 141
         pageNum: page.value,
103
-        ...formData.value,
142
+        ...otherParams,
104 143
       });
105 144
       taskList.value.push(
106
-        ...resp.rows.map((item) => ({ ...item, active: false })),
145
+        ...resp.rows.map((item) => ({
146
+          ...item,
147
+          active: false,
148
+          id: String(item.id),
149
+        })),
107 150
       );
108 151
     } catch {
109 152
       ElMessage.error('加载更多任务失败');
@@ -115,19 +158,58 @@ const handleScroll = debounce(async (e: Event) => {
115 158
 
116 159
 const lastSelectId = ref('');
117 160
 const currentTask = ref<TaskInfo>();
161
+const currentWorkOrderDetail = ref<any>();
162
+const currentTaskButtonData = ref<any>('');
118 163
 async function handleCardClick(item: TaskInfo) {
119
-  const { id } = item;
164
+  const { taskId, orderId } = item;
120 165
   // 点击的是同一个
121
-  if (lastSelectId.value === id) {
166
+  if (lastSelectId.value === taskId) {
122 167
     return;
123 168
   }
124 169
   currentTask.value = item;
170
+  currentTaskButtonData.value = item.button;
125 171
   // 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
126 172
   taskList.value.forEach((item) => {
127
-    item.active = item.id === id;
173
+    item.active = item.taskId === taskId;
128 174
   });
129
-  lastSelectId.value = id;
175
+  lastSelectId.value = taskId;
176
+  // 调用获取工单详情的接口
177
+  try {
178
+    const detail = await getWorkOrderDetail(orderId);
179
+    // 保存工单详情
180
+    currentWorkOrderDetail.value = detail;
181
+  } catch (error) {
182
+    console.error('获取工单详情失败:', error);
183
+    ElMessage.error('获取工单详情失败');
184
+  }
130 185
 }
186
+
187
+const { refreshTab } = useTabs();
188
+
189
+// 按钮权限控制
190
+const currentTaskButtonPermissions = computed(() => {
191
+  // 如果没有当前任务,返回空对象
192
+  if (!currentTaskButtonData.value) {
193
+    return {};
194
+  }
195
+  // 初始化权限对象
196
+  const permissions: Record<string, boolean> = {};
197
+
198
+  // 获取 button 字段
199
+  const button = currentTaskButtonData.value;
200
+  // 如果 button 字段存在,解析为数组
201
+  if (button) {
202
+    const buttonArray = button.split(',');
203
+    console.log('buttonArray', buttonArray);
204
+    // 设置对应按钮的权限
205
+    permissions.handle = buttonArray.includes('handle');
206
+    permissions.delay = buttonArray.includes('delay');
207
+    permissions.cc = buttonArray.includes('cc');
208
+    permissions.back = buttonArray.includes('back');
209
+  }
210
+  console.log('currentTaskButtonPermissions', permissions);
211
+  return permissions;
212
+});
131 213
 </script>
132 214
 
133 215
 <template>
@@ -140,49 +222,20 @@ async function handleCardClick(item: TaskInfo) {
140 222
         <div
141 223
           class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
142 224
         >
143
-          <div class="flex items-center gap-1">
144
-            <ElInput
145
-              v-model="formData.flowName"
146
-              placeholder="流程名称搜索"
147
-              clearable
148
-              class="w-full"
149
-              @keyup.enter="reload(false)"
150
-            />
151
-            <ElTooltip placement="top" content="重置">
152
-              <ElButton @click="reload(true)" circle>
153
-                <Refresh />
154
-              </ElButton>
155
-            </ElTooltip>
156
-            <ElPopover placement="top-end" trigger="click">
157
-              <template #reference>
158
-                <ElButton circle>
159
-                  <Filter />
160
-                </ElButton>
161
-              </template>
162
-              <template #default>
163
-                <ElForm
164
-                  :model="formData"
165
-                  label-width="80px"
166
-                  class="w-[300px]"
167
-                  @submit.prevent="reload(false)"
168
-                >
169
-                  <ElFormItem label="任务名称">
170
-                    <ElInput v-model="formData.nodeName" placeholder="请输入" />
171
-                  </ElFormItem>
172
-                  <ElFormItem label="流程编码">
173
-                    <ElInput v-model="formData.flowCode" placeholder="请输入" />
174
-                  </ElFormItem>
175
-                  <ElFormItem>
176
-                    <div class="flex justify-end gap-2">
177
-                      <ElButton type="primary" @click="reload(false)">
178
-                        搜索
179
-                      </ElButton>
180
-                      <ElButton @click="reload(true)"> 重置 </ElButton>
181
-                    </div>
182
-                  </ElFormItem>
183
-                </ElForm>
184
-              </template>
185
-            </ElPopover>
225
+          <div class="grid grid-cols-1 gap-2">
226
+            <div class="flex items-center gap-1">
227
+              <ElInput
228
+                v-model="formData.searchKeyword"
229
+                placeholder="输入数字搜索工单号,输入文字搜索内容"
230
+                clearable
231
+                class="w-full"
232
+                :suffix-icon="Search"
233
+                @keyup.enter="reload(false)"
234
+              />
235
+              <ElTooltip placement="top" content="重置">
236
+                <ElButton @click="reload(true)" :icon="Refresh" />
237
+              </ElTooltip>
238
+            </div>
186 239
           </div>
187 240
         </div>
188 241
         <div
@@ -210,9 +263,7 @@ async function handleCardClick(item: TaskInfo) {
210 263
           <div
211 264
             v-if="loading"
212 265
             class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
213
-          >
214
-            <Spin tip="加载中..." />
215
-          </div>
266
+          ></div>
216 267
         </div>
217 268
         <!-- total显示 -->
218 269
         <div
@@ -223,7 +274,13 @@ async function handleCardClick(item: TaskInfo) {
223 274
           </div>
224 275
         </div>
225 276
       </div>
226
-      <ApprovalPanel :task="currentTask" type="readonly" />
277
+      <ApprovalPanel
278
+        :task="currentTask"
279
+        :work-order-detail="currentWorkOrderDetail"
280
+        :button-permissions="currentTaskButtonPermissions"
281
+        type="waiting"
282
+        @reload="refreshTab"
283
+      />
227 284
     </div>
228 285
   </Page>
229 286
 </template>
@@ -234,8 +291,4 @@ async function handleCardClick(item: TaskInfo) {
234 291
     width: 5px;
235 292
   }
236 293
 }
237
-
238
-:deep(.el-card__body) {
239
-  @apply thin-scrollbar;
240
-}
241 294
 </style>

+ 119 - 72
apps/web-ele/src/views/workflow/task/taskFinish.vue

@@ -5,22 +5,13 @@ import type { TaskInfo } from '#/api/workflow/task/model';
5 5
 import { computed, onMounted, ref, useTemplateRef } from 'vue';
6 6
 
7 7
 import { Page } from '@vben/common-ui';
8
+import { useTabs } from '@vben/hooks';
8 9
 
9
-import { Filter, Refresh } from '@element-plus/icons-vue';
10
-import {
11
-  ElButton,
12
-  ElEmpty,
13
-  ElForm,
14
-  ElFormItem,
15
-  ElInput,
16
-  ElMessage,
17
-  ElPopover,
18
-  ElTooltip,
19
-} from 'element-plus';
20
-
10
+import { Refresh, Search } from '@element-plus/icons-vue';
11
+import { ElButton, ElEmpty, ElInput, ElMessage, ElTooltip } from 'element-plus';
21 12
 import { cloneDeep, debounce } from 'lodash-es';
22 13
 
23
-import { pageByTaskFinish } from '#/api/workflow/task';
14
+import { getWorkOrderDetail, pageByCompleted } from '#/api/workflow/instance';
24 15
 
25 16
 import { ApprovalCard, ApprovalPanel } from '../components';
26 17
 import { bottomOffset } from './constant';
@@ -33,9 +24,10 @@ const page = ref(1);
33 24
 const loading = ref(false);
34 25
 
35 26
 const defaultFormData = {
36
-  flowName: '', // 流程定义名称
27
+  searchKeyword: '', // 通用搜索关键词
37 28
   nodeName: '', // 任务名称
38 29
   flowCode: '', // 流程定义编码
30
+  category: null as null | number, // 流程分类
39 31
 };
40 32
 const formData = ref(cloneDeep(defaultFormData));
41 33
 
@@ -67,22 +59,44 @@ async function reload(resetFields: boolean = false) {
67 59
 
68 60
   loading.value = true;
69 61
   try {
70
-    const resp = await pageByTaskFinish({
62
+    // 根据搜索关键词类型动态决定搜索字段
63
+    const searchParams = { ...formData.value };
64
+    const { searchKeyword, ...otherParams } = searchParams;
65
+
66
+    // 如果有搜索关键词
67
+    if (searchKeyword) {
68
+      // 检查是否为数字
69
+      if (/^\d+$/.test(searchKeyword)) {
70
+        // 数字搜索orderNo
71
+        otherParams.orderNo = searchKeyword;
72
+      } else {
73
+        // 文字搜索content
74
+        otherParams.content = searchKeyword;
75
+      }
76
+    }
77
+
78
+    const resp = await pageByCompleted({
71 79
       pageSize: 10,
72 80
       pageNum: page.value,
73
-      ...formData.value,
81
+      ...otherParams,
74 82
     });
75
-    taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
83
+    taskList.value = resp.rows.map((item) => ({
84
+      ...item,
85
+      active: false,
86
+      id: String(item.id),
87
+    }));
76 88
     taskTotal.value = resp.total;
77 89
   } catch {
78 90
     ElMessage.error('加载任务列表失败');
79 91
   } finally {
80 92
     loading.value = false;
81 93
   }
94
+
82 95
   // 默认选中第一个
83 96
   if (taskList.value.length > 0) {
84 97
     const firstTask = taskList.value[0]!;
85 98
     currentTask.value = firstTask;
99
+    currentTaskButtonData.value = firstTask.button;
86 100
     handleCardClick(firstTask);
87 101
   }
88 102
 }
@@ -105,13 +119,34 @@ const handleScroll = debounce(async (e: Event) => {
105 119
     loading.value = true;
106 120
     try {
107 121
       page.value += 1;
108
-      const resp = await pageByTaskFinish({
122
+
123
+      // 根据搜索关键词类型动态决定搜索字段
124
+      const searchParams = { ...formData.value };
125
+      const { searchKeyword, ...otherParams } = searchParams;
126
+
127
+      // 如果有搜索关键词
128
+      if (searchKeyword) {
129
+        // 检查是否为数字
130
+        if (/^\d+$/.test(searchKeyword)) {
131
+          // 数字搜索orderNo
132
+          otherParams.orderNo = searchKeyword;
133
+        } else {
134
+          // 文字搜索content
135
+          otherParams.content = searchKeyword;
136
+        }
137
+      }
138
+
139
+      const resp = await pageByCompleted({
109 140
         pageSize: 10,
110 141
         pageNum: page.value,
111
-        ...formData.value,
142
+        ...otherParams,
112 143
       });
113 144
       taskList.value.push(
114
-        ...resp.rows.map((item) => ({ ...item, active: false })),
145
+        ...resp.rows.map((item) => ({
146
+          ...item,
147
+          active: false,
148
+          id: String(item.id),
149
+        })),
115 150
       );
116 151
     } catch {
117 152
       ElMessage.error('加载更多任务失败');
@@ -123,19 +158,58 @@ const handleScroll = debounce(async (e: Event) => {
123 158
 
124 159
 const lastSelectId = ref('');
125 160
 const currentTask = ref<TaskInfo>();
161
+const currentWorkOrderDetail = ref<any>();
162
+const currentTaskButtonData = ref<any>('');
126 163
 async function handleCardClick(item: TaskInfo) {
127
-  const { id } = item;
164
+  const { taskId, orderId } = item;
128 165
   // 点击的是同一个
129
-  if (lastSelectId.value === id) {
166
+  if (lastSelectId.value === taskId) {
130 167
     return;
131 168
   }
132 169
   currentTask.value = item;
170
+  currentTaskButtonData.value = item.button;
133 171
   // 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
134 172
   taskList.value.forEach((item) => {
135
-    item.active = item.id === id;
173
+    item.active = item.taskId === taskId;
136 174
   });
137
-  lastSelectId.value = id;
175
+  lastSelectId.value = taskId;
176
+  // 调用获取工单详情的接口
177
+  try {
178
+    const detail = await getWorkOrderDetail(orderId);
179
+    // 保存工单详情
180
+    currentWorkOrderDetail.value = detail;
181
+  } catch (error) {
182
+    console.error('获取工单详情失败:', error);
183
+    ElMessage.error('获取工单详情失败');
184
+  }
138 185
 }
186
+
187
+const { refreshTab } = useTabs();
188
+
189
+// 按钮权限控制
190
+const currentTaskButtonPermissions = computed(() => {
191
+  // 如果没有当前任务,返回空对象
192
+  if (!currentTaskButtonData.value) {
193
+    return {};
194
+  }
195
+  // 初始化权限对象
196
+  const permissions: Record<string, boolean> = {};
197
+
198
+  // 获取 button 字段
199
+  const button = currentTaskButtonData.value;
200
+  // 如果 button 字段存在,解析为数组
201
+  if (button) {
202
+    const buttonArray = button.split(',');
203
+    console.log('buttonArray', buttonArray);
204
+    // 设置对应按钮的权限
205
+    permissions.handle = buttonArray.includes('handle');
206
+    permissions.delay = buttonArray.includes('delay');
207
+    permissions.cc = buttonArray.includes('cc');
208
+    permissions.back = buttonArray.includes('back');
209
+  }
210
+  console.log('currentTaskButtonPermissions', permissions);
211
+  return permissions;
212
+});
139 213
 </script>
140 214
 
141 215
 <template>
@@ -148,49 +222,20 @@ async function handleCardClick(item: TaskInfo) {
148 222
         <div
149 223
           class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
150 224
         >
151
-          <div class="flex items-center gap-1">
152
-            <ElInput
153
-              v-model="formData.flowName"
154
-              placeholder="流程名称搜索"
155
-              clearable
156
-              class="w-full"
157
-              @keyup.enter="reload(false)"
158
-            />
159
-            <ElTooltip placement="top" content="重置">
160
-              <ElButton @click="reload(true)" circle>
161
-                <Refresh />
162
-              </ElButton>
163
-            </ElTooltip>
164
-            <ElPopover placement="top-end" trigger="click">
165
-              <template #reference>
166
-                <ElButton circle>
167
-                  <Filter />
168
-                </ElButton>
169
-              </template>
170
-              <template #default>
171
-                <ElForm
172
-                  :model="formData"
173
-                  label-width="80px"
174
-                  class="w-[300px]"
175
-                  @submit.prevent="reload(false)"
176
-                >
177
-                  <ElFormItem label="任务名称">
178
-                    <ElInput v-model="formData.nodeName" placeholder="请输入" />
179
-                  </ElFormItem>
180
-                  <ElFormItem label="流程编码">
181
-                    <ElInput v-model="formData.flowCode" placeholder="请输入" />
182
-                  </ElFormItem>
183
-                  <ElFormItem>
184
-                    <div class="flex justify-end gap-2">
185
-                      <ElButton type="primary" @click="reload(false)">
186
-                        搜索
187
-                      </ElButton>
188
-                      <ElButton @click="reload(true)"> 重置 </ElButton>
189
-                    </div>
190
-                  </ElFormItem>
191
-                </ElForm>
192
-              </template>
193
-            </ElPopover>
225
+          <div class="grid grid-cols-1 gap-2">
226
+            <div class="flex items-center gap-1">
227
+              <ElInput
228
+                v-model="formData.searchKeyword"
229
+                placeholder="输入数字搜索工单号,输入文字搜索内容"
230
+                clearable
231
+                class="w-full"
232
+                :suffix-icon="Search"
233
+                @keyup.enter="reload(false)"
234
+              />
235
+              <ElTooltip placement="top" content="重置">
236
+                <ElButton @click="reload(true)" :icon="Refresh" />
237
+              </ElTooltip>
238
+            </div>
194 239
           </div>
195 240
         </div>
196 241
         <div
@@ -229,7 +274,13 @@ async function handleCardClick(item: TaskInfo) {
229 274
           </div>
230 275
         </div>
231 276
       </div>
232
-      <ApprovalPanel :task="currentTask" type="readonly" />
277
+      <ApprovalPanel
278
+        :task="currentTask"
279
+        :work-order-detail="currentWorkOrderDetail"
280
+        :button-permissions="currentTaskButtonPermissions"
281
+        type="waiting"
282
+        @reload="refreshTab"
283
+      />
233 284
     </div>
234 285
   </Page>
235 286
 </template>
@@ -240,8 +291,4 @@ async function handleCardClick(item: TaskInfo) {
240 291
     width: 5px;
241 292
   }
242 293
 }
243
-
244
-:deep(.el-card__body) {
245
-  @apply thin-scrollbar;
246
-}
247 294
 </style>

+ 130 - 85
apps/web-ele/src/views/workflow/task/taskWaiting.vue

@@ -7,21 +7,11 @@ import { computed, onMounted, ref, useTemplateRef } from 'vue';
7 7
 import { Page } from '@vben/common-ui';
8 8
 import { useTabs } from '@vben/hooks';
9 9
 
10
-import { Filter, Refresh } from '@element-plus/icons-vue';
11
-import {
12
-  ElButton,
13
-  ElEmpty,
14
-  ElForm,
15
-  ElFormItem,
16
-  ElInput,
17
-  ElMessage,
18
-  ElPopover,
19
-  ElTooltip,
20
-} from 'element-plus';
21
-
10
+import { Refresh, Search } from '@element-plus/icons-vue';
11
+import { ElButton, ElEmpty, ElInput, ElMessage, ElTooltip } from 'element-plus';
22 12
 import { cloneDeep, debounce } from 'lodash-es';
23 13
 
24
-import { pageByTaskWait } from '#/api/workflow/task';
14
+import { getWorkOrderDetail, pageByPending } from '#/api/workflow/instance';
25 15
 
26 16
 import { ApprovalCard, ApprovalPanel } from '../components';
27 17
 import { bottomOffset } from './constant';
@@ -34,9 +24,10 @@ const page = ref(1);
34 24
 const loading = ref(false);
35 25
 
36 26
 const defaultFormData = {
37
-  flowName: '', // 流程定义名称
27
+  searchKeyword: '', // 通用搜索关键词
38 28
   nodeName: '', // 任务名称
39 29
   flowCode: '', // 流程定义编码
30
+  category: null as null | number, // 流程分类
40 31
 };
41 32
 const formData = ref(cloneDeep(defaultFormData));
42 33
 
@@ -68,22 +59,44 @@ async function reload(resetFields: boolean = false) {
68 59
 
69 60
   loading.value = true;
70 61
   try {
71
-    const resp = await pageByTaskWait({
62
+    // 根据搜索关键词类型动态决定搜索字段
63
+    const searchParams = { ...formData.value };
64
+    const { searchKeyword, ...otherParams } = searchParams;
65
+
66
+    // 如果有搜索关键词
67
+    if (searchKeyword) {
68
+      // 检查是否为数字
69
+      if (/^\d+$/.test(searchKeyword)) {
70
+        // 数字搜索orderNo
71
+        otherParams.orderNo = searchKeyword;
72
+      } else {
73
+        // 文字搜索content
74
+        otherParams.content = searchKeyword;
75
+      }
76
+    }
77
+
78
+    const resp = await pageByPending({
72 79
       pageSize: 10,
73 80
       pageNum: page.value,
74
-      ...formData.value,
81
+      ...otherParams,
75 82
     });
76
-    taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
83
+    taskList.value = resp.rows.map((item) => ({
84
+      ...item,
85
+      active: false,
86
+      id: String(item.id),
87
+    }));
77 88
     taskTotal.value = resp.total;
78 89
   } catch {
79 90
     ElMessage.error('加载任务列表失败');
80 91
   } finally {
81 92
     loading.value = false;
82 93
   }
94
+
83 95
   // 默认选中第一个
84 96
   if (taskList.value.length > 0) {
85 97
     const firstTask = taskList.value[0]!;
86 98
     currentTask.value = firstTask;
99
+    currentTaskButtonData.value = firstTask.button;
87 100
     handleCardClick(firstTask);
88 101
   }
89 102
 }
@@ -104,91 +117,121 @@ const handleScroll = debounce(async (e: Event) => {
104 117
   // 滚动到底部且没有加载完成
105 118
   if (isBottom && !isLoadComplete.value) {
106 119
     loading.value = true;
107
-    page.value += 1;
108
-    const resp = await pageByTaskWait({
109
-      pageSize: 10,
110
-      pageNum: page.value,
111
-      ...formData.value,
112
-    });
113
-    taskList.value.push(
114
-      ...resp.rows.map((item) => ({ ...item, active: false })),
115
-    );
116
-    loading.value = false;
120
+    try {
121
+      page.value += 1;
122
+
123
+      // 根据搜索关键词类型动态决定搜索字段
124
+      const searchParams = { ...formData.value };
125
+      const { searchKeyword, ...otherParams } = searchParams;
126
+
127
+      // 如果有搜索关键词
128
+      if (searchKeyword) {
129
+        // 检查是否为数字
130
+        if (/^\d+$/.test(searchKeyword)) {
131
+          // 数字搜索orderNo
132
+          otherParams.orderNo = searchKeyword;
133
+        } else {
134
+          // 文字搜索content
135
+          otherParams.content = searchKeyword;
136
+        }
137
+      }
138
+
139
+      const resp = await pageByPending({
140
+        pageSize: 10,
141
+        pageNum: page.value,
142
+        ...otherParams,
143
+      });
144
+      taskList.value.push(
145
+        ...resp.rows.map((item) => ({
146
+          ...item,
147
+          active: false,
148
+          id: String(item.id),
149
+        })),
150
+      );
151
+    } catch {
152
+      ElMessage.error('加载更多任务失败');
153
+    } finally {
154
+      loading.value = false;
155
+    }
117 156
   }
118 157
 }, 200);
119 158
 
120 159
 const lastSelectId = ref('');
121 160
 const currentTask = ref<TaskInfo>();
161
+const currentWorkOrderDetail = ref<any>();
162
+const currentTaskButtonData = ref<any>('');
122 163
 async function handleCardClick(item: TaskInfo) {
123
-  const { id } = item;
164
+  const { taskId, orderId } = item;
124 165
   // 点击的是同一个
125
-  if (lastSelectId.value === id) {
166
+  if (lastSelectId.value === taskId) {
126 167
     return;
127 168
   }
128 169
   currentTask.value = item;
170
+  currentTaskButtonData.value = item.button;
129 171
   // 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
130 172
   taskList.value.forEach((item) => {
131
-    item.active = item.id === id;
173
+    item.active = item.taskId === taskId;
132 174
   });
133
-  lastSelectId.value = id;
175
+  lastSelectId.value = taskId;
176
+  // 调用获取工单详情的接口
177
+  try {
178
+    const detail = await getWorkOrderDetail(orderId);
179
+    // 保存工单详情
180
+    currentWorkOrderDetail.value = detail;
181
+  } catch (error) {
182
+    console.error('获取工单详情失败:', error);
183
+    ElMessage.error('获取工单详情失败');
184
+  }
134 185
 }
135 186
 
136 187
 const { refreshTab } = useTabs();
188
+
189
+// 按钮权限控制
190
+const currentTaskButtonPermissions = computed(() => {
191
+  // 如果没有当前任务,返回空对象
192
+  if (!currentTaskButtonData.value) {
193
+    return {};
194
+  }
195
+  // 初始化权限对象
196
+  const permissions: Record<string, boolean> = {};
197
+
198
+  // 获取 button 字段
199
+  const button = currentTaskButtonData.value;
200
+  // 如果 button 字段存在,解析为数组
201
+  if (button) {
202
+    const buttonArray = button.split(',');
203
+    console.log('buttonArray', buttonArray);
204
+    // 设置对应按钮的权限
205
+    permissions.handle = buttonArray.includes('handle');
206
+    permissions.delay = buttonArray.includes('delay');
207
+    permissions.cc = buttonArray.includes('cc');
208
+    permissions.back = buttonArray.includes('back');
209
+  }
210
+  console.log('currentTaskButtonPermissions', permissions);
211
+  return permissions;
212
+});
137 213
 </script>
138 214
 
139 215
 <template>
140 216
   <Page :auto-content-height="true">
141 217
     <div class="flex h-full gap-2">
142
-      <div
143
-        class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
144
-      >
218
+      <div class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg">
145 219
         <!-- 搜索条件 -->
146
-        <div
147
-          class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
148
-        >
149
-          <div class="flex items-center gap-1">
150
-            <ElInput
151
-              v-model="formData.flowName"
152
-              placeholder="流程名称搜索"
153
-              clearable
154
-              class="w-full"
155
-              @keyup.enter="reload(false)"
156
-            />
157
-            <ElTooltip placement="top" content="重置">
158
-              <ElButton @click="reload(true)" circle>
159
-                <Refresh />
160
-              </ElButton>
161
-            </ElTooltip>
162
-            <ElPopover placement="top-end" trigger="click">
163
-              <template #reference>
164
-                <ElButton circle>
165
-                  <Filter />
166
-                </ElButton>
167
-              </template>
168
-              <template #default>
169
-                <ElForm
170
-                  :model="formData"
171
-                  label-width="80px"
172
-                  class="w-[300px]"
173
-                  @submit.prevent="reload(false)"
174
-                >
175
-                  <ElFormItem label="任务名称">
176
-                    <ElInput v-model="formData.nodeName" placeholder="请输入" />
177
-                  </ElFormItem>
178
-                  <ElFormItem label="流程编码">
179
-                    <ElInput v-model="formData.flowCode" placeholder="请输入" />
180
-                  </ElFormItem>
181
-                  <ElFormItem>
182
-                    <div class="flex justify-end gap-2">
183
-                      <ElButton type="primary" @click="reload(false)">
184
-                        搜索
185
-                      </ElButton>
186
-                      <ElButton @click="reload(true)"> 重置 </ElButton>
187
-                    </div>
188
-                  </ElFormItem>
189
-                </ElForm>
190
-              </template>
191
-            </ElPopover>
220
+        <div class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2" >
221
+          <div class="grid grid-cols-1 gap-2">
222
+            <div class="flex items-center gap-1">
223
+              <ElInput
224
+                v-model="formData.searchKeyword"
225
+                placeholder="输入数字搜索工单号,输入文字搜索内容"
226
+                clearable
227
+                class="w-full"
228
+                :suffix-icon="Search"
229
+                @keyup.enter="reload(false)"
230
+              />
231
+              <ElTooltip placement="top" content="重置">
232
+                <ElButton @click="reload(true)" :icon="Refresh" />
233
+              </ElTooltip>
234
+            </div>
192 235
           </div>
193 236
         </div>
194 237
         <div
@@ -227,7 +270,13 @@ const { refreshTab } = useTabs();
227 270
           </div>
228 271
         </div>
229 272
       </div>
230
-      <ApprovalPanel :task="currentTask" type="approve" @reload="refreshTab" />
273
+      <ApprovalPanel
274
+        :task="currentTask"
275
+        :work-order-detail="currentWorkOrderDetail"
276
+        :button-permissions="currentTaskButtonPermissions"
277
+        type="waiting"
278
+        @reload="refreshTab"
279
+      />
231 280
     </div>
232 281
   </Page>
233 282
 </template>
@@ -238,8 +287,4 @@ const { refreshTab } = useTabs();
238 287
     width: 5px;
239 288
   }
240 289
 }
241
-
242
-:deep(.el-card__body) {
243
-  @apply thin-scrollbar;
244
-}
245 290
 </style>