CategoryDraggableModel.vue 19 KB

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