Bez popisu

allTaskWaiting.vue 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. <!-- eslint-disable no-use-before-define -->
  2. <script setup lang="ts">
  3. import type { User } from '#/api/system/user/model';
  4. import type { TaskInfo } from '#/api/workflow/task/model';
  5. import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
  6. import { Page } from '@vben/common-ui';
  7. import { useTabs } from '@vben/hooks';
  8. <<<<<<< HEAD
  9. import { Filter, Refresh } from '@element-plus/icons-vue';
  10. import {
  11. ElButton,
  12. ElForm,
  13. ElFormItem,
  14. ElInput,
  15. ElMessage,
  16. ElPopover,
  17. ElTooltip,
  18. } from 'element-plus';
  19. =======
  20. // import {
  21. // Empty,
  22. // Form,
  23. // FormItem,
  24. // Input,
  25. // InputSearch,
  26. // Popover,
  27. // Segmented,
  28. // Spin,
  29. // Tooltip,
  30. // TreeSelect,
  31. // } from 'ant-design-vue';
  32. >>>>>>> 0a5e399be8a3dc56c82fd915b6765781a5d30748
  33. import { cloneDeep, debounce, uniqueId } from 'lodash-es';
  34. import { categoryTree } from '#/api/workflow/category';
  35. import { pageByAllTaskFinish, pageByAllTaskWait } from '#/api/workflow/task';
  36. import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
  37. import { bottomOffset } from './constant';
  38. const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
  39. /**
  40. * 流程监控 - 待办任务页面的id不唯一 改为前端处理
  41. */
  42. interface TaskItem extends TaskInfo {
  43. active: boolean;
  44. randomId: string;
  45. }
  46. const taskList = ref<TaskItem[]>([]);
  47. const taskTotal = ref(0);
  48. const page = ref(1);
  49. const loading = ref(false);
  50. const typeOptions = [
  51. { label: '待办任务', value: 'todo' },
  52. { label: '已办任务', value: 'done' },
  53. ];
  54. const currentType = ref('todo');
  55. const currentApi = computed(() => {
  56. if (currentType.value === 'todo') {
  57. return pageByAllTaskWait;
  58. }
  59. return pageByAllTaskFinish;
  60. });
  61. const approvalType = computed(() => {
  62. if (currentType.value === 'done') {
  63. return 'readonly';
  64. }
  65. return 'admin';
  66. });
  67. async function handleTypeChange() {
  68. // 需要先滚动到顶部
  69. cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
  70. page.value = 1;
  71. taskList.value = [];
  72. await nextTick();
  73. await reload(true);
  74. }
  75. const defaultFormData = {
  76. flowName: '', // 流程定义名称
  77. nodeName: '', // 任务名称
  78. flowCode: '', // 流程定义编码
  79. createByIds: [] as string[], // 创建人
  80. category: null as null | number, // 流程分类
  81. };
  82. const formData = ref(cloneDeep(defaultFormData));
  83. /**
  84. * 是否已经加载全部数据 即 taskList.length === taskTotal
  85. */
  86. const isLoadComplete = computed(
  87. () => taskList.value.length === taskTotal.value,
  88. );
  89. // 卡片父容器的ref
  90. const cardContainerRef = useTemplateRef('cardContainerRef');
  91. /**
  92. * @param resetFields 是否清空查询参数
  93. */
  94. async function reload(resetFields: boolean = false) {
  95. // 需要先滚动到顶部
  96. cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
  97. page.value = 1;
  98. currentTask.value = undefined;
  99. taskTotal.value = 0;
  100. lastSelectId.value = '';
  101. if (resetFields) {
  102. formData.value = cloneDeep(defaultFormData);
  103. selectedUserList.value = [];
  104. }
  105. loading.value = true;
  106. const resp = await currentApi.value({
  107. pageSize: 10,
  108. pageNum: page.value,
  109. ...formData.value,
  110. });
  111. taskList.value = resp.rows.map((item) => ({
  112. ...item,
  113. active: false,
  114. randomId: uniqueId(),
  115. }));
  116. taskTotal.value = resp.total;
  117. loading.value = false;
  118. // 默认选中第一个
  119. if (taskList.value.length > 0) {
  120. const firstTask = taskList.value[0]!;
  121. currentTask.value = firstTask;
  122. handleCardClick(firstTask);
  123. }
  124. }
  125. onMounted(reload);
  126. const handleScroll = debounce(async (e: Event) => {
  127. if (!e.target) {
  128. return;
  129. }
  130. // e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
  131. // e.target.clientHeight 是元素的可视高度。
  132. // e.target.scrollHeight 是元素的总高度。
  133. const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
  134. // 判断是否滚动到底部
  135. const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
  136. console.log('scrollTop + clientHeight', scrollTop + clientHeight);
  137. console.log('scrollHeight', scrollHeight);
  138. // 滚动到底部且没有加载完成
  139. if (isBottom && !isLoadComplete.value) {
  140. loading.value = true;
  141. try {
  142. page.value += 1;
  143. const resp = await currentApi.value({
  144. pageSize: 10,
  145. pageNum: page.value,
  146. ...formData.value,
  147. });
  148. taskList.value.push(
  149. ...resp.rows.map((item) => ({
  150. ...item,
  151. active: false,
  152. randomId: uniqueId(),
  153. })),
  154. );
  155. } catch {
  156. ElMessage.error('加载更多任务失败');
  157. } finally {
  158. loading.value = false;
  159. }
  160. }
  161. }, 200);
  162. const lastSelectId = ref('');
  163. const currentTask = ref<TaskInfo>();
  164. async function handleCardClick(item: TaskItem) {
  165. const { randomId } = item;
  166. // 点击的是同一个
  167. if (lastSelectId.value === randomId) {
  168. return;
  169. }
  170. currentTask.value = item;
  171. // 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
  172. taskList.value.forEach((item) => {
  173. item.active = item.randomId === randomId;
  174. });
  175. lastSelectId.value = randomId;
  176. }
  177. const { refreshTab } = useTabs();
  178. // 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示
  179. const popoverOpen = ref(false);
  180. const selectedUserList = ref<User[]>([]);
  181. function handleFinish(userList: User[]) {
  182. popoverOpen.value = true;
  183. selectedUserList.value = userList;
  184. formData.value.createByIds = userList.map((item) => item.userId);
  185. }
  186. const treeData = ref<any[]>([]);
  187. onMounted(async () => {
  188. // menu
  189. const tree = await categoryTree();
  190. addFullName(tree, 'label', ' / ');
  191. treeData.value = tree;
  192. });
  193. </script>
  194. <template>
  195. <Page :auto-content-height="true">
  196. <div class="flex h-full gap-2">
  197. <div
  198. class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
  199. >
  200. <!-- 搜索条件 -->
  201. <div
  202. class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
  203. >
  204. <div class="mb-2 flex items-center gap-1">
  205. <ElButton
  206. :type="currentType === 'todo' ? 'primary' : 'default'"
  207. @click="
  208. currentType = 'todo';
  209. handleTypeChange();
  210. "
  211. >
  212. 待办任务
  213. </ElButton>
  214. <ElButton
  215. :type="currentType === 'done' ? 'primary' : 'default'"
  216. @click="
  217. currentType = 'done';
  218. handleTypeChange();
  219. "
  220. >
  221. 已办任务
  222. </ElButton>
  223. </div>
  224. <div class="flex items-center gap-1">
  225. <ElInput
  226. v-model="formData.flowName"
  227. placeholder="流程名称搜索"
  228. clearable
  229. class="w-full"
  230. @keyup.enter="reload(false)"
  231. />
  232. <ElTooltip placement="top" content="重置">
  233. <ElButton @click="reload(true)" circle>
  234. <Refresh />
  235. </ElButton>
  236. </ElTooltip>
  237. <ElPopover placement="top-end" trigger="click">
  238. <template #reference>
  239. <ElButton circle>
  240. <Filter />
  241. </ElButton>
  242. </template>
  243. <template #default>
  244. <ElForm
  245. :model="formData"
  246. label-width="80px"
  247. class="w-[300px]"
  248. @submit.prevent="reload(false)"
  249. >
  250. <ElFormItem label="申请人">
  251. <CopyComponent
  252. v-model:user-list="selectedUserList"
  253. @cancel="() => (popoverOpen = true)"
  254. @finish="handleFinish"
  255. />
  256. </ElFormItem>
  257. <ElFormItem label="流程分类">
  258. <ElInput v-model="formData.category" placeholder="请选择" />
  259. </ElFormItem>
  260. <ElFormItem label="任务名称">
  261. <ElInput v-model="formData.nodeName" placeholder="请输入" />
  262. </ElFormItem>
  263. <ElFormItem label="流程编码">
  264. <ElInput v-model="formData.flowCode" placeholder="请输入" />
  265. </ElFormItem>
  266. <ElFormItem>
  267. <div class="flex justify-end gap-2">
  268. <ElButton type="primary" @click="reload(false)">
  269. 搜索
  270. </ElButton>
  271. <ElButton @click="reload(true)"> 重置 </ElButton>
  272. </div>
  273. </ElFormItem>
  274. </ElForm>
  275. </template>
  276. </ElPopover>
  277. </div>
  278. </div>
  279. <div
  280. ref="cardContainerRef"
  281. class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
  282. @scroll="handleScroll"
  283. >
  284. <template v-if="taskList.length > 0">
  285. <ApprovalCard
  286. v-for="item in taskList"
  287. :key="item.randomId"
  288. :info="item"
  289. class="mx-2"
  290. row-key="randomId"
  291. @click="handleCardClick(item)"
  292. />
  293. </template>
  294. <Empty v-else :image="emptyImage" />
  295. <div
  296. v-if="isLoadComplete && taskList.length > 0"
  297. class="flex items-center justify-center text-[14px] opacity-50"
  298. >
  299. 没有更多数据了
  300. </div>
  301. <!-- 遮罩loading层 -->
  302. <div
  303. v-if="loading"
  304. class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
  305. >
  306. <Spin tip="加载中..." />
  307. </div>
  308. </div>
  309. <!-- total显示 -->
  310. <div
  311. class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
  312. >
  313. <div class="flex items-center justify-center">
  314. 共 {{ taskTotal }} 条记录
  315. </div>
  316. </div>
  317. </div>
  318. <ApprovalPanel
  319. :task="currentTask"
  320. :type="approvalType"
  321. @reload="refreshTab"
  322. />
  323. </div>
  324. </Page>
  325. </template>
  326. <style lang="scss" scoped>
  327. .thin-scrollbar {
  328. &::-webkit-scrollbar {
  329. width: 5px;
  330. }
  331. }
  332. :deep(.el-card__body) {
  333. @apply thin-scrollbar;
  334. }
  335. </style>