CategoryDraggableModel.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. <template>
  2. <div class="flex items-center h-50px" v-memo="[categoryInfo.name, isCategorySorting]">
  3. <!-- 头部:分类名 -->
  4. <div class="flex items-center">
  5. <el-tooltip content="拖动排序" v-if="isCategorySorting">
  6. <Icon
  7. :size="22"
  8. icon="ic:round-drag-indicator"
  9. class="ml-10px category-drag-icon cursor-move text-#8a909c"
  10. />
  11. </el-tooltip>
  12. <h3 class="ml-20px mr-8px text-18px">{{ categoryInfo.name }}</h3>
  13. <div class="color-gray-600 text-16px"> ({{ categoryInfo.modelList?.length || 0 }}) </div>
  14. </div>
  15. <!-- 头部:操作 -->
  16. <div class="flex-1 flex" v-show="!isCategorySorting">
  17. <div
  18. v-if="categoryInfo.modelList.length > 0"
  19. class="ml-20px flex items-center"
  20. :class="[
  21. 'transition-transform duration-300 cursor-pointer',
  22. isExpand ? 'rotate-180' : 'rotate-0'
  23. ]"
  24. @click="isExpand = !isExpand"
  25. >
  26. <Icon icon="ep:arrow-down-bold" color="#999" />
  27. </div>
  28. <div class="ml-auto flex items-center" :class="isModelSorting ? 'mr-15px' : 'mr-45px'">
  29. <template v-if="!isModelSorting">
  30. <el-button
  31. v-if="categoryInfo.modelList.length > 0"
  32. link
  33. type="info"
  34. class="mr-20px"
  35. @click.stop="handleModelSort"
  36. >
  37. <Icon icon="fa:sort-amount-desc" class="mr-5px" />
  38. 排序
  39. </el-button>
  40. <el-button v-else link type="info" class="mr-20px" @click.stop="openModelForm('create')">
  41. <Icon icon="fa:plus" class="mr-5px" />
  42. 新建
  43. </el-button>
  44. <el-dropdown
  45. @command="(command) => handleCategoryCommand(command, categoryInfo)"
  46. placement="bottom"
  47. >
  48. <el-button link type="info">
  49. <Icon icon="ep:setting" class="mr-5px" />
  50. 分类
  51. </el-button>
  52. <template #dropdown>
  53. <el-dropdown-menu>
  54. <el-dropdown-item command="handleRename"> 重命名 </el-dropdown-item>
  55. <el-dropdown-item command="handleDeleteCategory"> 删除该类 </el-dropdown-item>
  56. </el-dropdown-menu>
  57. </template>
  58. </el-dropdown>
  59. </template>
  60. <template v-else>
  61. <el-button @click.stop="handleModelSortCancel"> 取 消 </el-button>
  62. <el-button type="primary" @click.stop="handleModelSortSubmit"> 保存排序 </el-button>
  63. </template>
  64. </div>
  65. </div>
  66. </div>
  67. <!-- 模型列表 -->
  68. <el-collapse-transition>
  69. <div v-show="isExpand">
  70. <el-table
  71. v-if="modelList && modelList.length > 0"
  72. :class="categoryInfo.name"
  73. ref="tableRef"
  74. :data="modelList"
  75. row-key="id"
  76. :header-cell-style="tableHeaderStyle"
  77. :cell-style="tableCellStyle"
  78. :row-style="{ height: '68px' }"
  79. >
  80. <el-table-column label="流程名" prop="name" min-width="150">
  81. <template #default="{ row }">
  82. <div class="flex items-center">
  83. <el-tooltip content="拖动排序" v-if="isModelSorting">
  84. <Icon
  85. icon="ic:round-drag-indicator"
  86. class="drag-icon cursor-move text-#8a909c mr-10px"
  87. />
  88. </el-tooltip>
  89. <el-image v-if="row.icon" :src="row.icon" class="h-38px w-38px mr-10px rounded" />
  90. {{ row.name }}
  91. </div>
  92. </template>
  93. </el-table-column>
  94. <el-table-column label="可见范围" prop="startUserIds" min-width="150">
  95. <template #default="{ row }">
  96. <el-text v-if="!row.startUsers?.length"> 全部可见 </el-text>
  97. <el-text v-else-if="row.startUsers.length === 1">
  98. {{ row.startUsers[0].nickname }}
  99. </el-text>
  100. <el-text v-else>
  101. <el-tooltip
  102. class="box-item"
  103. effect="dark"
  104. placement="top"
  105. :content="row.startUsers.map((user: any) => user.nickname).join('、')"
  106. >
  107. {{ row.startUsers[0].nickname }}等 {{ row.startUsers.length }} 人可见
  108. </el-tooltip>
  109. </el-text>
  110. </template>
  111. </el-table-column>
  112. <el-table-column label="表单信息" prop="formType" min-width="150">
  113. <template #default="scope">
  114. <el-button
  115. v-if="scope.row.formType === BpmModelFormType.NORMAL"
  116. type="primary"
  117. link
  118. @click="handleFormDetail(scope.row)"
  119. >
  120. <span>{{ scope.row.formName }}</span>
  121. </el-button>
  122. <el-button
  123. v-else-if="scope.row.formType === BpmModelFormType.CUSTOM"
  124. type="primary"
  125. link
  126. @click="handleFormDetail(scope.row)"
  127. >
  128. <span>{{ scope.row.formCustomCreatePath }}</span>
  129. </el-button>
  130. <label v-else>暂无表单</label>
  131. </template>
  132. </el-table-column>
  133. <el-table-column label="最后发布" prop="deploymentTime" min-width="250">
  134. <template #default="scope">
  135. <div class="flex items-center">
  136. <span v-if="scope.row.processDefinition" class="w-150px">
  137. {{ formatDate(scope.row.processDefinition.deploymentTime) }}
  138. </span>
  139. <el-tag v-if="scope.row.processDefinition">
  140. v{{ scope.row.processDefinition.version }}
  141. </el-tag>
  142. <el-tag v-else type="warning">未部署</el-tag>
  143. <el-tag
  144. v-if="scope.row.processDefinition?.suspensionState === 2"
  145. type="warning"
  146. class="ml-10px"
  147. >
  148. 已停用
  149. </el-tag>
  150. </div>
  151. </template>
  152. </el-table-column>
  153. <el-table-column label="操作" width="200" fixed="right">
  154. <template #default="scope">
  155. <el-button
  156. link
  157. type="primary"
  158. @click="openModelForm('update', scope.row.id)"
  159. v-if="hasPermiUpdate"
  160. :disabled="!isManagerUser(scope.row)"
  161. >
  162. 修改
  163. </el-button>
  164. <el-button
  165. link
  166. type="primary"
  167. @click="openModelForm('copy', scope.row.id)"
  168. v-if="hasPermiUpdate"
  169. :disabled="!isManagerUser(scope.row)"
  170. >
  171. 复制
  172. </el-button>
  173. <el-button
  174. link
  175. class="!ml-5px"
  176. type="primary"
  177. @click="handleDeploy(scope.row)"
  178. v-if="hasPermiDeploy"
  179. :disabled="!isManagerUser(scope.row)"
  180. >
  181. 发布
  182. </el-button>
  183. <el-dropdown
  184. class="!align-middle ml-5px"
  185. @command="(command) => handleModelCommand(command, scope.row)"
  186. v-if="hasPermiMore"
  187. >
  188. <el-button type="primary" link>更多</el-button>
  189. <template #dropdown>
  190. <el-dropdown-menu>
  191. <el-dropdown-item command="handleDefinitionList" v-if="hasPermiPdQuery">
  192. 历史
  193. </el-dropdown-item>
  194. <el-dropdown-item
  195. command="handleReport"
  196. v-if="
  197. checkPermi(['bpm:process-instance:manager-query']) &&
  198. scope.row.processDefinition
  199. "
  200. :disabled="!isManagerUser(scope.row)"
  201. >
  202. 报表
  203. </el-dropdown-item>
  204. <el-dropdown-item
  205. command="handleChangeState"
  206. v-if="hasPermiUpdate && scope.row.processDefinition"
  207. :disabled="!isManagerUser(scope.row)"
  208. >
  209. {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
  210. </el-dropdown-item>
  211. <el-dropdown-item
  212. type="danger"
  213. command="handleClean"
  214. v-if="checkPermi(['bpm:model:clean'])"
  215. :disabled="!isManagerUser(scope.row)"
  216. >
  217. 清理
  218. </el-dropdown-item>
  219. <el-dropdown-item
  220. type="danger"
  221. command="handleDelete"
  222. v-if="hasPermiDelete"
  223. :disabled="!isManagerUser(scope.row)"
  224. >
  225. 删除
  226. </el-dropdown-item>
  227. </el-dropdown-menu>
  228. </template>
  229. </el-dropdown>
  230. </template>
  231. </el-table-column>
  232. </el-table>
  233. </div>
  234. </el-collapse-transition>
  235. <!-- 弹窗:重命名分类 -->
  236. <Dialog :fullscreen="false" class="rename-dialog" v-model="renameCategoryVisible" width="400">
  237. <template #title>
  238. <div class="pl-10px font-bold text-18px"> 重命名分类 </div>
  239. </template>
  240. <div class="px-30px">
  241. <el-input v-model="renameCategoryForm.name" />
  242. </div>
  243. <template #footer>
  244. <div class="pr-25px pb-25px">
  245. <el-button @click="renameCategoryVisible = false">取 消</el-button>
  246. <el-button type="primary" @click="handleRenameConfirm">确 定</el-button>
  247. </div>
  248. </template>
  249. </Dialog>
  250. </template>
  251. <script lang="ts" setup>
  252. import { CategoryApi, CategoryVO } from '@/api/bpm/category'
  253. import Sortable from 'sortablejs'
  254. import { formatDate } from '@/utils/formatTime'
  255. import * as ModelApi from '@/api/bpm/model'
  256. import * as FormApi from '@/api/bpm/form'
  257. import { setConfAndFields2 } from '@/utils/formCreate'
  258. import { BpmModelFormType } from '@/utils/constants'
  259. import { checkPermi } from '@/utils/permission'
  260. import { useUserStoreWithOut } from '@/store/modules/user'
  261. import { useAppStore } from '@/store/modules/app'
  262. import { cloneDeep, isEqual } from 'lodash-es'
  263. import { useTagsView } from '@/hooks/web/useTagsView'
  264. import { useDebounceFn } from '@vueuse/core'
  265. defineOptions({ name: 'BpmModel' })
  266. // 优化 Props 类型定义
  267. interface UserInfo {
  268. nickname: string
  269. [key: string]: any
  270. }
  271. interface ProcessDefinition {
  272. deploymentTime: string
  273. version: number
  274. suspensionState: number
  275. }
  276. interface ModelInfo {
  277. id: number
  278. name: string
  279. icon?: string
  280. startUsers?: UserInfo[]
  281. processDefinition?: ProcessDefinition
  282. formType?: number
  283. formId?: number
  284. formName?: string
  285. formCustomCreatePath?: string
  286. managerUserIds?: number[]
  287. [key: string]: any
  288. }
  289. interface CategoryInfoProps {
  290. id: number
  291. name: string
  292. modelList: ModelInfo[]
  293. }
  294. const props = defineProps<{
  295. categoryInfo: CategoryInfoProps
  296. isCategorySorting: boolean
  297. }>()
  298. const emit = defineEmits(['success'])
  299. const message = useMessage() // 消息弹窗
  300. const { t } = useI18n() // 国际化
  301. const { push } = useRouter() // 路由
  302. const userStore = useUserStoreWithOut() // 用户信息缓存
  303. const isDark = computed(() => useAppStore().getIsDark) // 是否黑暗模式
  304. const router = useRouter() // 路由
  305. const isModelSorting = ref(false) // 是否正处于排序状态
  306. const originalData = ref<ModelInfo[]>([]) // 原始数据
  307. const modelList = ref<ModelInfo[]>([]) // 模型列表
  308. const isExpand = ref(false) // 是否处于展开状态
  309. // 使用 computed 优化表格样式计算
  310. const tableHeaderStyle = computed(() => ({
  311. backgroundColor: isDark.value ? '' : '#edeff0',
  312. paddingLeft: '10px'
  313. }))
  314. const tableCellStyle = computed(() => ({
  315. paddingLeft: '10px'
  316. }))
  317. /** 权限校验:通过 computed 解决列表的卡顿问题 */
  318. const hasPermiUpdate = computed(() => {
  319. return checkPermi(['bpm:model:update'])
  320. })
  321. const hasPermiDelete = computed(() => {
  322. return checkPermi(['bpm:model:delete'])
  323. })
  324. const hasPermiDeploy = computed(() => {
  325. return checkPermi(['bpm:model:deploy'])
  326. })
  327. const hasPermiMore = computed(() => {
  328. return checkPermi(['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete'])
  329. })
  330. const hasPermiPdQuery = computed(() => {
  331. return checkPermi(['bpm:process-definition:query'])
  332. })
  333. /** '更多'操作按钮 */
  334. const handleModelCommand = (command: string, row: any) => {
  335. switch (command) {
  336. case 'handleDefinitionList':
  337. handleDefinitionList(row)
  338. break
  339. case 'handleDelete':
  340. handleDelete(row)
  341. break
  342. case 'handleChangeState':
  343. handleChangeState(row)
  344. break
  345. case 'handleClean':
  346. handleClean(row)
  347. break
  348. case 'handleReport':
  349. router.push({
  350. name: 'BpmProcessInstanceReport',
  351. query: {
  352. processDefinitionId: row.processDefinition.id,
  353. processDefinitionKey: row.key
  354. }
  355. })
  356. break
  357. default:
  358. break
  359. }
  360. }
  361. /** '分类'操作按钮 */
  362. const handleCategoryCommand = async (command: string, row: any) => {
  363. switch (command) {
  364. case 'handleRename':
  365. renameCategoryForm.value = await CategoryApi.getCategory(row.id)
  366. renameCategoryVisible.value = true
  367. break
  368. case 'handleDeleteCategory':
  369. await handleDeleteCategory()
  370. break
  371. default:
  372. break
  373. }
  374. }
  375. /** 删除按钮操作 */
  376. const handleDelete = async (row: any) => {
  377. try {
  378. // 删除的二次确认
  379. await message.delConfirm()
  380. // 发起删除
  381. await ModelApi.deleteModel(row.id)
  382. message.success(t('common.delSuccess'))
  383. // 刷新列表
  384. emit('success')
  385. } catch {}
  386. }
  387. /** 清理按钮操作 */
  388. const handleClean = async (row: any) => {
  389. try {
  390. // 清理的二次确认
  391. await message.confirm('是否确认清理流程名字为"' + row.name + '"的数据项?')
  392. // 发起清理
  393. await ModelApi.cleanModel(row.id)
  394. message.success('清理成功')
  395. // 刷新列表
  396. emit('success')
  397. } catch {}
  398. }
  399. /** 更新状态操作 */
  400. const handleChangeState = async (row: any) => {
  401. const state = row.processDefinition.suspensionState
  402. const newState = state === 1 ? 2 : 1
  403. try {
  404. // 修改状态的二次确认
  405. const id = row.id
  406. debugger
  407. const statusState = state === 1 ? '停用' : '启用'
  408. const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
  409. await message.confirm(content)
  410. // 发起修改状态
  411. await ModelApi.updateModelState(id, newState)
  412. message.success(statusState + '成功')
  413. // 刷新列表
  414. emit('success')
  415. } catch {}
  416. }
  417. /** 发布流程 */
  418. const handleDeploy = async (row: any) => {
  419. try {
  420. // 删除的二次确认
  421. await message.confirm('是否部署该流程!!')
  422. // 发起部署
  423. await ModelApi.deployModel(row.id)
  424. message.success(t('部署成功'))
  425. // 刷新列表
  426. emit('success')
  427. } catch {}
  428. }
  429. /** 跳转到指定流程定义列表 */
  430. const handleDefinitionList = (row: any) => {
  431. push({
  432. name: 'BpmProcessDefinition',
  433. query: {
  434. key: row.key
  435. }
  436. })
  437. }
  438. /** 流程表单的详情按钮操作 */
  439. const formDetailVisible = ref(false)
  440. const formDetailPreview = ref({
  441. rule: [],
  442. option: {}
  443. })
  444. const handleFormDetail = async (row: any) => {
  445. if (row.formType == 10) {
  446. // 设置表单
  447. const data = await FormApi.getForm(row.formId)
  448. setConfAndFields2(formDetailPreview, data.conf, data.fields)
  449. // 弹窗打开
  450. formDetailVisible.value = true
  451. } else {
  452. await push({
  453. path: row.formCustomCreatePath
  454. })
  455. }
  456. }
  457. /** 判断是否可以操作 */
  458. const isManagerUser = (row: any) => {
  459. const userId = userStore.getUser.id
  460. return row.managerUserIds && row.managerUserIds.includes(userId)
  461. }
  462. /** 处理模型的排序 **/
  463. const handleModelSort = () => {
  464. // 保存初始数据
  465. originalData.value = cloneDeep(props.categoryInfo.modelList)
  466. isModelSorting.value = true
  467. initSort()
  468. }
  469. /** 处理模型的排序提交 */
  470. const handleModelSortSubmit = async () => {
  471. // 保存排序
  472. const ids = modelList.value.map((item: any) => item.id)
  473. await ModelApi.updateModelSortBatch(ids)
  474. // 刷新列表
  475. isModelSorting.value = false
  476. message.success('排序模型成功')
  477. emit('success')
  478. }
  479. /** 处理模型的排序取消 */
  480. const handleModelSortCancel = () => {
  481. // 恢复初始数据
  482. modelList.value = cloneDeep(originalData.value)
  483. isModelSorting.value = false
  484. }
  485. /** 创建拖拽实例 */
  486. const tableRef = ref()
  487. const initSort = useDebounceFn(() => {
  488. const table = document.querySelector(`.${props.categoryInfo.name} .el-table__body-wrapper tbody`)
  489. if (!table) return
  490. Sortable.create(table, {
  491. group: 'shared',
  492. animation: 150,
  493. draggable: '.el-table__row',
  494. handle: '.drag-icon',
  495. onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
  496. if (oldDraggableIndex !== newDraggableIndex) {
  497. modelList.value.splice(
  498. newDraggableIndex,
  499. 0,
  500. modelList.value.splice(oldDraggableIndex, 1)[0]
  501. )
  502. }
  503. }
  504. })
  505. }, 200)
  506. /** 更新 modelList 模型列表 */
  507. const updateModeList = useDebounceFn(() => {
  508. const newModelList = props.categoryInfo.modelList
  509. if (!isEqual(modelList.value, newModelList)) {
  510. modelList.value = cloneDeep(newModelList)
  511. if (newModelList?.length > 0) {
  512. isExpand.value = true
  513. }
  514. }
  515. }, 100)
  516. /** 重命名弹窗确定 */
  517. const renameCategoryVisible = ref(false)
  518. const renameCategoryForm = ref({
  519. name: ''
  520. })
  521. const handleRenameConfirm = async () => {
  522. if (renameCategoryForm.value?.name.length === 0) {
  523. return message.warning('请输入名称')
  524. }
  525. // 发起修改
  526. await CategoryApi.updateCategory(renameCategoryForm.value as CategoryVO)
  527. message.success('重命名成功')
  528. // 刷新列表
  529. renameCategoryVisible.value = false
  530. emit('success')
  531. }
  532. /** 删除分类 */
  533. const handleDeleteCategory = async () => {
  534. try {
  535. if (props.categoryInfo.modelList.length > 0) {
  536. return message.warning('该分类下仍有流程定义,不允许删除')
  537. }
  538. await message.confirm('确认删除分类吗?')
  539. // 发起删除
  540. await CategoryApi.deleteCategory(props.categoryInfo.id)
  541. message.success(t('common.delSuccess'))
  542. // 刷新列表
  543. emit('success')
  544. } catch {}
  545. }
  546. /** 添加流程模型弹窗 */
  547. const tagsView = useTagsView()
  548. const openModelForm = async (type: string, id?: number) => {
  549. if (type === 'create') {
  550. await push({ name: 'BpmModelCreate' })
  551. } else {
  552. await push({
  553. name: 'BpmModelUpdate',
  554. params: { id, type }
  555. })
  556. // 设置标题
  557. if (type === 'copy') {
  558. tagsView.setTitle('复制流程')
  559. }
  560. }
  561. }
  562. watchEffect(() => {
  563. if (props.categoryInfo?.modelList) {
  564. updateModeList()
  565. }
  566. if (props.isCategorySorting) {
  567. isExpand.value = false
  568. }
  569. })
  570. </script>
  571. <style lang="scss">
  572. .rename-dialog.el-dialog {
  573. padding: 0 !important;
  574. .el-dialog__header {
  575. border-bottom: none;
  576. }
  577. .el-dialog__footer {
  578. border-top: none !important;
  579. }
  580. }
  581. </style>
  582. <style lang="scss" scoped>
  583. .category-draggable-model {
  584. :deep(.el-table__cell) {
  585. overflow: hidden;
  586. border-bottom: none !important;
  587. }
  588. // 优化表格渲染性能
  589. :deep(.el-table__body) {
  590. will-change: transform;
  591. transform: translateZ(0);
  592. }
  593. }
  594. </style>