miaofuhao 3 天之前
父节点
当前提交
2d29818527

+ 11 - 0
apps/web-ele/src/api/workflow/instance/index.ts

@@ -120,6 +120,17 @@ export function pageByCompleted(params?: PageQuery) {
120 120
 }
121 121
 
122 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
+/**
123 134
  * 获取工单详情
124 135
  * @param id 工单id
125 136
  * @returns TaskInfo

+ 175 - 42
apps/web-ele/src/views/workflow/components/actions/flow-actions.vue

@@ -3,20 +3,28 @@ import type { ApprovalType } from '../type';
3 3
 
4 4
 import type { TaskInfo } from '#/api/workflow/task/model';
5 5
 
6
-import { computed, ref, reactive } from 'vue';
6
+import { computed, reactive, ref } from 'vue';
7 7
 
8 8
 import { cn } from '@vben/utils';
9 9
 
10 10
 import { UploadFilled } from '@element-plus/icons-vue';
11
-import { ElButton, ElDrawer, ElForm, ElFormItem, ElInput, ElMessage, ElUpload, ElMessageBox, ElSpace, ElTreeSelect, ElSelect, ElOption } from 'element-plus';
12
-
13 11
 import {
14
-  backProcess,
15
-  deleteOrder,
16
-  submitOrder,
17
-  taskOperation,
18
-} from '#/api/workflow/task';
12
+  ElButton,
13
+  ElDrawer,
14
+  ElForm,
15
+  ElFormItem,
16
+  ElInput,
17
+  ElMessage,
18
+  ElMessageBox,
19
+  ElOption,
20
+  ElSelect,
21
+  ElSpace,
22
+  ElTreeSelect,
23
+  ElUpload,
24
+} from 'element-plus';
25
+
19 26
 import { requestClient } from '#/api/request';
27
+import { backProcess, deleteOrder, submitOrder } from '#/api/workflow/task';
20 28
 import { getSingleImageUploadConfig } from '#/components/upload/config';
21 29
 import CreatOrder from '#/views/workflow/order/creatOrder.vue';
22 30
 
@@ -92,6 +100,15 @@ const userList = ref([]);
92 100
 const selectedDeptId = ref('');
93 101
 const selectedUsers = ref([]);
94 102
 
103
+// 延时审批的抽屉
104
+const approveDelayDrawerVisible = ref(false);
105
+const approveDelayFormData = reactive({
106
+  approveRemark: '',
107
+  approveStatus: '1',
108
+  delayId: 0,
109
+  newDeadline: '',
110
+});
111
+
95 112
 // 编辑
96 113
 const handleEdit = () => {
97 114
   drawerId.value = props.task!.id;
@@ -189,7 +206,8 @@ function handleRemove() {
189 206
     cancelButtonText: '取消',
190 207
     type: 'warning',
191 208
     center: true,
192
-  }).then(async () => {
209
+  })
210
+    .then(async () => {
193 211
       await deleteOrder([props.task!.id]);
194 212
       emit('reload');
195 213
     })
@@ -223,7 +241,7 @@ function handleApprove() {
223 241
   handleFormData.processContent = '';
224 242
   handleFormData.taskId = props.task!.taskId;
225 243
   handleFileList.value = [];
226
-  
244
+
227 245
   // 打开抽屉
228 246
   handleDrawerVisible.value = true;
229 247
 }
@@ -236,7 +254,7 @@ function handleDelay() {
236 254
   delayFormData.delayDuration = 0;
237 255
   delayFormData.delayReason = '';
238 256
   delayFileList.value = [];
239
-  
257
+
240 258
   // 打开抽屉
241 259
   delayDrawerVisible.value = true;
242 260
 }
@@ -249,10 +267,10 @@ function handleCopy() {
249 267
   selectedDeptId.value = '';
250 268
   selectedUsers.value = [];
251 269
   userList.value = [];
252
-  
270
+
253 271
   // 加载部门树
254 272
   loadDeptTree();
255
-  
273
+
256 274
   // 打开抽屉
257 275
   ccDrawerVisible.value = true;
258 276
 }
@@ -261,11 +279,7 @@ function handleCopy() {
261 279
 async function loadDeptTree() {
262 280
   try {
263 281
     const response = await requestClient.get('/system/user/deptTree');
264
-    if (response.code === 200) {
265
-      deptTree.value = response.data;
266
-    } else {
267
-      ElMessage.error('加载部门树失败');
268
-    }
282
+    deptTree.value = response;
269 283
   } catch (error) {
270 284
     ElMessage.error('加载部门树失败');
271 285
     console.error('加载部门树失败:', error);
@@ -275,9 +289,12 @@ async function loadDeptTree() {
275 289
 // 加载人员列表
276 290
 async function loadUserList(deptId) {
277 291
   try {
278
-    const response = await requestClient.get(`/system/user/list?deptId=${deptId}&pageNum=1&pageSize=20`);
279
-    if (response.code === 200) {
280
-      userList.value = response.data.list || [];
292
+    const response = await requestClient.get(
293
+      `/system/user/list?deptId=${deptId}&pageNum=1&pageSize=999`,
294
+    );
295
+    console.log('加载人员列表响应:', response);
296
+    if (response.rows && response.rows.length > 0) {
297
+      userList.value = response.rows || [];
281 298
     } else {
282 299
       ElMessage.error('加载人员列表失败');
283 300
     }
@@ -318,14 +335,14 @@ async function handleDelaySubmit() {
318 335
       ElMessage.error('请输入有效的延时时长(至少1小时)');
319 336
       return;
320 337
     }
321
-    
338
+
322 339
     // 调用延时API
323 340
     await requestClient.put('/workOrder/order/delay', delayFormData);
324 341
     ElMessage.success('延时申请提交成功');
325
-    
342
+
326 343
     // 关闭抽屉
327 344
     delayDrawerVisible.value = false;
328
-    
345
+
329 346
     // 重新加载数据
330 347
     emit('reload');
331 348
   } catch (error) {
@@ -346,14 +363,14 @@ async function handleCCSubmit() {
346 363
       ElMessage.error('请选择人员');
347 364
       return;
348 365
     }
349
-    
366
+
350 367
     // 调用抄送API
351 368
     await requestClient.put('/workOrder/order/cc', ccFormData);
352 369
     ElMessage.success('抄送成功');
353
-    
370
+
354 371
     // 关闭抽屉
355 372
     ccDrawerVisible.value = false;
356
-    
373
+
357 374
     // 重新加载数据
358 375
     emit('reload');
359 376
   } catch (error) {
@@ -381,9 +398,48 @@ async function handleBack() {
381 398
   }
382 399
 }
383 400
 
401
+// 延时审批
402
+function handleApproveDelay() {
403
+  // 重置表单数据
404
+  approveDelayFormData.approveRemark = '';
405
+  approveDelayFormData.approveStatus = '1';
406
+  approveDelayFormData.delayId = props.task!.taskId;
407
+  approveDelayFormData.newDeadline = '';
408
+
409
+  // 打开抽屉
410
+  approveDelayDrawerVisible.value = true;
411
+}
384 412
 
413
+// 延时审批提交
414
+async function handleApproveDelaySubmit() {
415
+  try {
416
+    // 验证表单
417
+    if (!approveDelayFormData.approveRemark.trim()) {
418
+      ElMessage.error('请输入审批意见');
419
+      return;
420
+    }
421
+    if (!approveDelayFormData.newDeadline) {
422
+      ElMessage.error('请选择新截止时间');
423
+      return;
424
+    }
425
+    console.log('延时审批表单数据:', approveDelayFormData);
426
+    // 调用延时审批API
427
+    await requestClient.put(
428
+      '/workOrder/order/approveDelay',
429
+      approveDelayFormData,
430
+    );
431
+    ElMessage.success('延时审批操作成功');
385 432
 
433
+    // 关闭抽屉
434
+    approveDelayDrawerVisible.value = false;
386 435
 
436
+    // 重新加载数据
437
+    emit('reload');
438
+  } catch (error) {
439
+    ElMessage.error('延时审批操作失败');
440
+    console.error('延时审批失败:', error);
441
+  }
442
+}
387 443
 </script>
388 444
 
389 445
 <template>
@@ -445,6 +501,15 @@ async function handleBack() {
445 501
           退回
446 502
         </ElButton>
447 503
       </ElSpace>
504
+      <ElSpace v-if="type === 'delay'">
505
+        <ElButton
506
+          v-if="buttonPermissions?.approveDelay"
507
+          type="primary"
508
+          @click="handleApproveDelay"
509
+        >
510
+          延时审批
511
+        </ElButton>
512
+      </ElSpace>
448 513
     </div>
449 514
   </div>
450 515
 
@@ -464,7 +529,9 @@ async function handleBack() {
464 529
         @close="handleCloseDrawer"
465 530
       />
466 531
     </div>
467
-    <div class="absolute bottom-0 left-0 right-0 border-t border-gray-200 p-4 bg-white">
532
+    <div
533
+      class="absolute bottom-0 left-0 right-0 border-t border-gray-200 bg-white p-4"
534
+    >
468 535
       <div class="flex justify-end space-x-2">
469 536
         <ElButton @click="drawerVisible = false">取消</ElButton>
470 537
         <ElButton type="primary" @click="handleConfirmEdit">保存</ElButton>
@@ -477,7 +544,8 @@ async function handleBack() {
477 544
     v-model="handleDrawerVisible"
478 545
     title="处理工单"
479 546
     width="800px"
480
-    :close-on-click-modal="false">
547
+    :close-on-click-modal="false"
548
+  >
481 549
     <div class="drawer-content p-4 pb-20">
482 550
       <ElForm
483 551
         :model="handleFormData"
@@ -517,7 +585,9 @@ async function handleBack() {
517 585
         </ElFormItem>
518 586
       </ElForm>
519 587
     </div>
520
-    <div class="absolute bottom-0 left-0 right-0 border-t border-gray-200 p-4 bg-white">
588
+    <div
589
+      class="absolute bottom-0 left-0 right-0 border-t border-gray-200 bg-white p-4"
590
+    >
521 591
       <div class="flex justify-end space-x-2">
522 592
         <ElButton @click="handleDrawerVisible = false">取消</ElButton>
523 593
         <ElButton type="primary" @click="handleSubmitForm">提交</ElButton>
@@ -530,7 +600,8 @@ async function handleBack() {
530 600
     v-model="delayDrawerVisible"
531 601
     title="延时申请"
532 602
     width="800px"
533
-    :close-on-click-modal="false">
603
+    :close-on-click-modal="false"
604
+  >
534 605
     <div class="drawer-content p-4 pb-20">
535 606
       <ElForm
536 607
         :model="delayFormData"
@@ -580,7 +651,9 @@ async function handleBack() {
580 651
         </ElFormItem>
581 652
       </ElForm>
582 653
     </div>
583
-    <div class="absolute bottom-0 left-0 right-0 border-t border-gray-200 p-4 bg-white">
654
+    <div
655
+      class="absolute bottom-0 left-0 right-0 border-t border-gray-200 bg-white p-4"
656
+    >
584 657
       <div class="flex justify-end space-x-2">
585 658
         <ElButton @click="delayDrawerVisible = false">取消</ElButton>
586 659
         <ElButton type="primary" @click="handleDelaySubmit">提交</ElButton>
@@ -593,17 +666,15 @@ async function handleBack() {
593 666
     v-model="ccDrawerVisible"
594 667
     title="抄送"
595 668
     width="800px"
596
-    :close-on-click-modal="false">
669
+    :close-on-click-modal="false"
670
+  >
597 671
     <div class="drawer-content p-4 pb-20">
598
-      <ElForm
599
-        :model="ccFormData"
600
-        label-width="120px"
601
-        class="cc-order-form"
602
-      >
672
+      <ElForm :model="ccFormData" label-width="120px" class="cc-order-form">
603 673
         <!-- 部门选择 -->
604 674
         <ElFormItem label="选择部门" required>
605 675
           <ElTreeSelect
606 676
             v-model="selectedDeptId"
677
+            check-strictly
607 678
             :data="deptTree"
608 679
             node-key="id"
609 680
             :props="{ label: 'label', children: 'children' }"
@@ -622,19 +693,81 @@ async function handleBack() {
622 693
           >
623 694
             <ElOption
624 695
               v-for="user in userList"
625
-              :key="user.id"
626
-              :label="user.nickname || user.username"
627
-              :value="user.id"
696
+              :key="user.userId"
697
+              :label="
698
+                user.nickName + (user.userName ? `(${user.userName})` : '')
699
+              "
700
+              :value="user.userId"
628 701
             />
629 702
           </ElSelect>
630 703
         </ElFormItem>
631 704
       </ElForm>
632 705
     </div>
633
-    <div class="absolute bottom-0 left-0 right-0 border-t border-gray-200 p-4 bg-white">
706
+    <div
707
+      class="absolute bottom-0 left-0 right-0 border-t border-gray-200 bg-white p-4"
708
+    >
634 709
       <div class="flex justify-end space-x-2">
635 710
         <ElButton @click="ccDrawerVisible = false">取消</ElButton>
636 711
         <ElButton type="primary" @click="handleCCSubmit">提交</ElButton>
637 712
       </div>
638 713
     </div>
639 714
   </ElDrawer>
715
+
716
+  <!-- 延时审批的抽屉组件 -->
717
+  <ElDrawer
718
+    v-model="approveDelayDrawerVisible"
719
+    title="延时审批"
720
+    width="800px"
721
+    :close-on-click-modal="false"
722
+  >
723
+    <div class="drawer-content p-4 pb-20">
724
+      <ElForm
725
+        :model="approveDelayFormData"
726
+        label-width="120px"
727
+        class="approve-delay-form"
728
+      >
729
+        <!-- 审批意见 -->
730
+        <ElFormItem label="审批意见" required>
731
+          <ElInput
732
+            v-model="approveDelayFormData.approveRemark"
733
+            type="textarea"
734
+            :rows="4"
735
+            placeholder="请输入审批意见"
736
+          />
737
+        </ElFormItem>
738
+
739
+        <!-- 审核状态 -->
740
+        <ElFormItem label="审核状态" required>
741
+          <ElSelect
742
+            v-model="approveDelayFormData.approveStatus"
743
+            placeholder="请选择审核状态"
744
+          >
745
+            <ElOption label="审核通过" value="1" />
746
+            <ElOption label="审核拒绝" value="2" />
747
+          </ElSelect>
748
+        </ElFormItem>
749
+
750
+        <!-- 新截止时间 -->
751
+        <ElFormItem label="新截止时间" required>
752
+          <ElDatePicker
753
+            v-model="approveDelayFormData.newDeadline"
754
+            type="datetime"
755
+            placeholder="请选择新截止时间"
756
+            format="YYYY-MM-DD HH:mm:ss"
757
+            value-format="YYYY-MM-DD HH:mm:ss"
758
+          />
759
+        </ElFormItem>
760
+      </ElForm>
761
+    </div>
762
+    <div
763
+      class="absolute bottom-0 left-0 right-0 border-t border-gray-200 bg-white p-4"
764
+    >
765
+      <div class="flex justify-end space-x-2">
766
+        <ElButton @click="approveDelayDrawerVisible = false">取消</ElButton>
767
+        <ElButton type="primary" @click="handleApproveDelaySubmit">
768
+          提交
769
+        </ElButton>
770
+      </div>
771
+    </div>
772
+  </ElDrawer>
640 773
 </template>

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

@@ -0,0 +1,282 @@
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
+
198
+  permissions.approveDelay = true;
199
+  return permissions;
200
+});
201
+</script>
202
+
203
+<template>
204
+  <Page :auto-content-height="true">
205
+    <div class="flex h-full gap-2">
206
+      <div
207
+        class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
208
+      >
209
+        <!-- 搜索条件 -->
210
+        <div
211
+          class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
212
+        >
213
+          <div class="grid grid-cols-1 gap-2">
214
+            <div class="flex items-center gap-1">
215
+              <ElInput
216
+                v-model="formData.searchKeyword"
217
+                placeholder="输入数字搜索工单号,输入文字搜索内容"
218
+                clearable
219
+                class="w-full"
220
+                :suffix-icon="Search"
221
+                @keyup.enter="reload(false)"
222
+              />
223
+              <ElTooltip placement="top" content="重置">
224
+                <ElButton @click="reload(true)" :icon="Refresh" />
225
+              </ElTooltip>
226
+            </div>
227
+          </div>
228
+        </div>
229
+        <div
230
+          ref="cardContainerRef"
231
+          class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
232
+          @scroll="handleScroll"
233
+        >
234
+          <template v-if="taskList.length > 0">
235
+            <ApprovalCard
236
+              v-for="item in taskList"
237
+              :key="item.id"
238
+              :info="item"
239
+              class="mx-2"
240
+              @click="handleCardClick(item)"
241
+            />
242
+          </template>
243
+          <ElEmpty v-else :image="emptyImage" description="暂无数据" />
244
+          <div
245
+            v-if="isLoadComplete && taskList.length > 0"
246
+            class="flex items-center justify-center text-[14px] opacity-50"
247
+          >
248
+            没有更多数据了
249
+          </div>
250
+          <!-- 遮罩loading层 -->
251
+          <div
252
+            v-if="loading"
253
+            class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
254
+          ></div>
255
+        </div>
256
+        <!-- total显示 -->
257
+        <div
258
+          class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
259
+        >
260
+          <div class="flex items-center justify-center">
261
+            共 {{ taskTotal }} 条记录
262
+          </div>
263
+        </div>
264
+      </div>
265
+      <ApprovalPanel
266
+        :task="currentTask"
267
+        :work-order-detail="currentWorkOrderDetail"
268
+        :button-permissions="currentTaskButtonPermissions"
269
+        type="delay"
270
+        @reload="refreshTab"
271
+      />
272
+    </div>
273
+  </Page>
274
+</template>
275
+
276
+<style lang="scss" scoped>
277
+.thin-scrollbar {
278
+  &::-webkit-scrollbar {
279
+    width: 5px;
280
+  }
281
+}
282
+</style>

+ 61 - 20
apps/web-ele/src/views/workflow/task/taskCopyList.vue

@@ -9,13 +9,12 @@ import { useTabs } from '@vben/hooks';
9 9
 
10 10
 import { Refresh, Search } from '@element-plus/icons-vue';
11 11
 import { ElButton, ElEmpty, ElInput, ElMessage, ElTooltip } from 'element-plus';
12
-
13 12
 import { cloneDeep, debounce } from 'lodash-es';
14 13
 
15
-import { pageByCc, getWorkOrderDetail } from '#/api/workflow/instance';
14
+import { getWorkOrderDetail, pageByCc } from '#/api/workflow/instance';
16 15
 
17
-import { bottomOffset } from './constant';
18 16
 import { ApprovalCard, ApprovalPanel } from '../components';
17
+import { bottomOffset } from './constant';
19 18
 
20 19
 const emptyImage = ElEmpty.PRESENTED_IMAGE_SIMPLE;
21 20
 
@@ -63,7 +62,7 @@ async function reload(resetFields: boolean = false) {
63 62
     // 根据搜索关键词类型动态决定搜索字段
64 63
     const searchParams = { ...formData.value };
65 64
     const { searchKeyword, ...otherParams } = searchParams;
66
-    
65
+
67 66
     // 如果有搜索关键词
68 67
     if (searchKeyword) {
69 68
       // 检查是否为数字
@@ -75,13 +74,17 @@ async function reload(resetFields: boolean = false) {
75 74
         otherParams.content = searchKeyword;
76 75
       }
77 76
     }
78
-    
77
+
79 78
     const resp = await pageByCc({
80 79
       pageSize: 10,
81 80
       pageNum: page.value,
82 81
       ...otherParams,
83 82
     });
84
-    taskList.value = resp.rows.map((item) => ({ ...item, active: false, id: String(item.id) }));
83
+    taskList.value = resp.rows.map((item) => ({
84
+      ...item,
85
+      active: false,
86
+      id: String(item.id),
87
+    }));
85 88
     taskTotal.value = resp.total;
86 89
   } catch {
87 90
     ElMessage.error('加载任务列表失败');
@@ -93,6 +96,7 @@ async function reload(resetFields: boolean = false) {
93 96
   if (taskList.value.length > 0) {
94 97
     const firstTask = taskList.value[0]!;
95 98
     currentTask.value = firstTask;
99
+    currentTaskButtonData.value = firstTask.button;
96 100
     handleCardClick(firstTask);
97 101
   }
98 102
 }
@@ -115,11 +119,11 @@ const handleScroll = debounce(async (e: Event) => {
115 119
     loading.value = true;
116 120
     try {
117 121
       page.value += 1;
118
-      
122
+
119 123
       // 根据搜索关键词类型动态决定搜索字段
120 124
       const searchParams = { ...formData.value };
121 125
       const { searchKeyword, ...otherParams } = searchParams;
122
-      
126
+
123 127
       // 如果有搜索关键词
124 128
       if (searchKeyword) {
125 129
         // 检查是否为数字
@@ -131,14 +135,18 @@ const handleScroll = debounce(async (e: Event) => {
131 135
           otherParams.content = searchKeyword;
132 136
         }
133 137
       }
134
-      
138
+
135 139
       const resp = await pageByCc({
136 140
         pageSize: 10,
137 141
         pageNum: page.value,
138 142
         ...otherParams,
139 143
       });
140 144
       taskList.value.push(
141
-        ...resp.rows.map((item) => ({ ...item, active: false, id: String(item.id) })),
145
+        ...resp.rows.map((item) => ({
146
+          ...item,
147
+          active: false,
148
+          id: String(item.id),
149
+        })),
142 150
       );
143 151
     } catch {
144 152
       ElMessage.error('加载更多任务失败');
@@ -150,24 +158,26 @@ const handleScroll = debounce(async (e: Event) => {
150 158
 
151 159
 const lastSelectId = ref('');
152 160
 const currentTask = ref<TaskInfo>();
161
+const currentWorkOrderDetail = ref<any>();
162
+const currentTaskButtonData = ref<any>('');
153 163
 async function handleCardClick(item: TaskInfo) {
154 164
   const { taskId, orderId } = item;
155 165
   // 点击的是同一个
156
-  if (lastSelectId.value === orderId) {
166
+  if (lastSelectId.value === taskId) {
157 167
     return;
158 168
   }
159 169
   currentTask.value = item;
170
+  currentTaskButtonData.value = item.button;
160 171
   // 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
161 172
   taskList.value.forEach((item) => {
162 173
     item.active = item.taskId === taskId;
163 174
   });
164
-  lastSelectId.value = orderId;
165
-  
175
+  lastSelectId.value = taskId;
166 176
   // 调用获取工单详情的接口
167 177
   try {
168 178
     const detail = await getWorkOrderDetail(orderId);
169
-    // 可以在这里使用获取到的工单详情数据
170
-    currentTask.value = detail;
179
+    // 保存工单详情
180
+    currentWorkOrderDetail.value = detail;
171 181
   } catch (error) {
172 182
     console.error('获取工单详情失败:', error);
173 183
     ElMessage.error('获取工单详情失败');
@@ -175,6 +185,31 @@ async function handleCardClick(item: TaskInfo) {
175 185
 }
176 186
 
177 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
+});
178 213
 </script>
179 214
 
180 215
 <template>
@@ -197,7 +232,7 @@ const { refreshTab } = useTabs();
197 232
                 :suffix-icon="Search"
198 233
                 @keyup.enter="reload(false)"
199 234
               />
200
-               <ElTooltip placement="top" content="重置">
235
+              <ElTooltip placement="top" content="重置">
201 236
                 <ElButton @click="reload(true)" :icon="Refresh" />
202 237
               </ElTooltip>
203 238
             </div>
@@ -231,13 +266,21 @@ const { refreshTab } = useTabs();
231 266
           ></div>
232 267
         </div>
233 268
         <!-- total显示 -->
234
-        <div class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2">
269
+        <div
270
+          class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
271
+        >
235 272
           <div class="flex items-center justify-center">
236 273
             共 {{ taskTotal }} 条记录
237 274
           </div>
238 275
         </div>
239 276
       </div>
240
-      <ApprovalPanel :task="currentTask" type="myself" @reload="refreshTab" />
277
+      <ApprovalPanel
278
+        :task="currentTask"
279
+        :work-order-detail="currentWorkOrderDetail"
280
+        :button-permissions="currentTaskButtonPermissions"
281
+        type="waiting"
282
+        @reload="refreshTab"
283
+      />
241 284
     </div>
242 285
   </Page>
243 286
 </template>
@@ -249,5 +292,3 @@ const { refreshTab } = useTabs();
249 292
   }
250 293
 }
251 294
 </style>
252
-
253
-

+ 61 - 19
apps/web-ele/src/views/workflow/task/taskFinish.vue

@@ -9,13 +9,12 @@ import { useTabs } from '@vben/hooks';
9 9
 
10 10
 import { Refresh, Search } from '@element-plus/icons-vue';
11 11
 import { ElButton, ElEmpty, ElInput, ElMessage, ElTooltip } from 'element-plus';
12
-
13 12
 import { cloneDeep, debounce } from 'lodash-es';
14 13
 
15
-import { pageByCompleted, getWorkOrderDetail } from '#/api/workflow/instance';
14
+import { getWorkOrderDetail, pageByCompleted } from '#/api/workflow/instance';
16 15
 
17
-import { bottomOffset } from './constant';
18 16
 import { ApprovalCard, ApprovalPanel } from '../components';
17
+import { bottomOffset } from './constant';
19 18
 
20 19
 const emptyImage = ElEmpty.PRESENTED_IMAGE_SIMPLE;
21 20
 
@@ -63,7 +62,7 @@ async function reload(resetFields: boolean = false) {
63 62
     // 根据搜索关键词类型动态决定搜索字段
64 63
     const searchParams = { ...formData.value };
65 64
     const { searchKeyword, ...otherParams } = searchParams;
66
-    
65
+
67 66
     // 如果有搜索关键词
68 67
     if (searchKeyword) {
69 68
       // 检查是否为数字
@@ -75,13 +74,17 @@ async function reload(resetFields: boolean = false) {
75 74
         otherParams.content = searchKeyword;
76 75
       }
77 76
     }
78
-    
77
+
79 78
     const resp = await pageByCompleted({
80 79
       pageSize: 10,
81 80
       pageNum: page.value,
82 81
       ...otherParams,
83 82
     });
84
-    taskList.value = resp.rows.map((item) => ({ ...item, active: false, id: String(item.id) }));
83
+    taskList.value = resp.rows.map((item) => ({
84
+      ...item,
85
+      active: false,
86
+      id: String(item.id),
87
+    }));
85 88
     taskTotal.value = resp.total;
86 89
   } catch {
87 90
     ElMessage.error('加载任务列表失败');
@@ -93,6 +96,7 @@ async function reload(resetFields: boolean = false) {
93 96
   if (taskList.value.length > 0) {
94 97
     const firstTask = taskList.value[0]!;
95 98
     currentTask.value = firstTask;
99
+    currentTaskButtonData.value = firstTask.button;
96 100
     handleCardClick(firstTask);
97 101
   }
98 102
 }
@@ -115,11 +119,11 @@ const handleScroll = debounce(async (e: Event) => {
115 119
     loading.value = true;
116 120
     try {
117 121
       page.value += 1;
118
-      
122
+
119 123
       // 根据搜索关键词类型动态决定搜索字段
120 124
       const searchParams = { ...formData.value };
121 125
       const { searchKeyword, ...otherParams } = searchParams;
122
-      
126
+
123 127
       // 如果有搜索关键词
124 128
       if (searchKeyword) {
125 129
         // 检查是否为数字
@@ -131,14 +135,18 @@ const handleScroll = debounce(async (e: Event) => {
131 135
           otherParams.content = searchKeyword;
132 136
         }
133 137
       }
134
-      
138
+
135 139
       const resp = await pageByCompleted({
136 140
         pageSize: 10,
137 141
         pageNum: page.value,
138 142
         ...otherParams,
139 143
       });
140 144
       taskList.value.push(
141
-        ...resp.rows.map((item) => ({ ...item, active: false, id: String(item.id) })),
145
+        ...resp.rows.map((item) => ({
146
+          ...item,
147
+          active: false,
148
+          id: String(item.id),
149
+        })),
142 150
       );
143 151
     } catch {
144 152
       ElMessage.error('加载更多任务失败');
@@ -150,24 +158,26 @@ const handleScroll = debounce(async (e: Event) => {
150 158
 
151 159
 const lastSelectId = ref('');
152 160
 const currentTask = ref<TaskInfo>();
161
+const currentWorkOrderDetail = ref<any>();
162
+const currentTaskButtonData = ref<any>('');
153 163
 async function handleCardClick(item: TaskInfo) {
154 164
   const { taskId, orderId } = item;
155 165
   // 点击的是同一个
156
-  if (lastSelectId.value === orderId) {
166
+  if (lastSelectId.value === taskId) {
157 167
     return;
158 168
   }
159 169
   currentTask.value = item;
170
+  currentTaskButtonData.value = item.button;
160 171
   // 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
161 172
   taskList.value.forEach((item) => {
162 173
     item.active = item.taskId === taskId;
163 174
   });
164
-  lastSelectId.value = orderId;
165
-  
175
+  lastSelectId.value = taskId;
166 176
   // 调用获取工单详情的接口
167 177
   try {
168 178
     const detail = await getWorkOrderDetail(orderId);
169
-    // 可以在这里使用获取到的工单详情数据
170
-    currentTask.value = detail;
179
+    // 保存工单详情
180
+    currentWorkOrderDetail.value = detail;
171 181
   } catch (error) {
172 182
     console.error('获取工单详情失败:', error);
173 183
     ElMessage.error('获取工单详情失败');
@@ -175,6 +185,31 @@ async function handleCardClick(item: TaskInfo) {
175 185
 }
176 186
 
177 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
+});
178 213
 </script>
179 214
 
180 215
 <template>
@@ -197,7 +232,7 @@ const { refreshTab } = useTabs();
197 232
                 :suffix-icon="Search"
198 233
                 @keyup.enter="reload(false)"
199 234
               />
200
-               <ElTooltip placement="top" content="重置">
235
+              <ElTooltip placement="top" content="重置">
201 236
                 <ElButton @click="reload(true)" :icon="Refresh" />
202 237
               </ElTooltip>
203 238
             </div>
@@ -231,13 +266,21 @@ const { refreshTab } = useTabs();
231 266
           ></div>
232 267
         </div>
233 268
         <!-- total显示 -->
234
-        <div class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2">
269
+        <div
270
+          class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
271
+        >
235 272
           <div class="flex items-center justify-center">
236 273
             共 {{ taskTotal }} 条记录
237 274
           </div>
238 275
         </div>
239 276
       </div>
240
-      <ApprovalPanel :task="currentTask" type="myself" @reload="refreshTab" />
277
+      <ApprovalPanel
278
+        :task="currentTask"
279
+        :work-order-detail="currentWorkOrderDetail"
280
+        :button-permissions="currentTaskButtonPermissions"
281
+        type="waiting"
282
+        @reload="refreshTab"
283
+      />
241 284
     </div>
242 285
   </Page>
243 286
 </template>
@@ -249,4 +292,3 @@ const { refreshTab } = useTabs();
249 292
   }
250 293
 }
251 294
 </style>
252
-