user-modal.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. <template>
  2. <hc-dialog v-model="isShow" ui="hc-tasks-user-modal" widths="1195px" title="选择任务人" :footer="!isView" @close="modalClose">
  3. <div class="card-div-1 h-full w-235px">
  4. <hc-body scrollbar padding="0">
  5. <div class="hc-process-item">
  6. <div class="process setup" :class="{ disabled: isView }" @click="processSetupClick">
  7. <div class="icon hc-flex-center">
  8. <i class="i-hugeicons-flowchart-01" />
  9. </div>
  10. <div class="name">流程设置</div>
  11. </div>
  12. </div>
  13. <template v-for="(item, index) in fixedData" :key="index">
  14. <div class="hc-process-item">
  15. <div
  16. class="process content"
  17. :class="fixedIndex === index ? 's-orange' : item.isDataAdd ? 's-gray' : item.isDataSave ? 's-blue' : 's-gray'"
  18. @click.capture="fixedItemClick(item, index)"
  19. >
  20. <div class="icon hc-flex-center" @click="fixedTypeClick(item)">
  21. <i :class="getProcessIcon(item)" />
  22. </div>
  23. <div class="input-box">
  24. <div class="width-name">{{ item.name }}</div>
  25. <input
  26. v-model="item.name"
  27. class="input"
  28. :readonly="isView"
  29. @click="isView ? null : fixedTypeClick(item)"
  30. >
  31. </div>
  32. <div v-if="!isView" class="del-icon hc-flex-center" @click="fixedDelClick(item, index)">
  33. <i class="i-ri-delete-bin-2-line" />
  34. </div>
  35. </div>
  36. </div>
  37. </template>
  38. <div class="hc-process-item">
  39. <div v-if="!isView" class="process add" @click="fixedAddClick">
  40. <div class="icon hc-flex-center">
  41. <i class="i-iconoir-plus" />
  42. </div>
  43. </div>
  44. </div>
  45. </hc-body>
  46. </div>
  47. <div v-if="fixedIndex === -1" class="card-div-no h-full flex-1">
  48. <hc-empty :src="HcLoadSvg" title="请选择流程后,设置流程人员" />
  49. </div>
  50. <template v-else>
  51. <div class="cards-wrapper">
  52. <div class="card-div-2 h-full w-235px">
  53. <hc-card scrollbar title="角色类型" :loading="signUserLoading">
  54. <template v-for="item in signUserList" :key="item.roleId">
  55. <div class="hc-tasks-user-role-item" :class="{ cur: roleItem.roleId === item.roleId }" @click="roleItemClick(item)">
  56. {{ item.roleName }}
  57. </div>
  58. </template>
  59. </hc-card>
  60. </div>
  61. <div v-if="roleItem.roleId" class="card-div-3 h-full w-265px">
  62. <hc-card v-if="positionList.length > 0" scrollbar>
  63. <template #header>
  64. <hc-search-input v-model="positionKey" placeholder="岗位搜索" icon="" @search="positionSearch" />
  65. </template>
  66. <template v-for="item in positionList" :key="item.roleId">
  67. <div class="hc-tasks-user-role-item" :class="{ cur: positionItem.roleId === item.roleId }" @click="positionItemClick(item)">
  68. <i class="i-ph-user-list-light mr-5px" />
  69. <span>{{ item.roleName }}</span>
  70. </div>
  71. </template>
  72. </hc-card>
  73. <div v-else class="card-empty-no">
  74. <hc-empty :src="HcLoadSvg" title="请先选择角色" />
  75. </div>
  76. </div>
  77. <div class="card-div-4 h-full w-235px">
  78. <hc-card v-if="signPfxFileList.length > 0">
  79. <template #header>
  80. <hc-search-input v-model="signUserKey" placeholder="人员搜索" icon="" @search="signUserSearch" />
  81. </template>
  82. <div class="hc-tasks-user-sign-pfx-box">
  83. <el-scrollbar ref="scrollRef">
  84. <template v-for="item in signPfxFileList" :key="item.name">
  85. <div v-if="item.data.length > 0" :id="`hc-sign-pfx-file-item-${item.name}`" class="hc-sign-pfx-file-item">
  86. <div class="hc-tasks-user-letter">{{ item.name }}</div>
  87. <template v-for="(items, index) in item.data" :key="index">
  88. <div class="hc-tasks-user-item" @click="signUserItemClick(items)">
  89. <i class="i-iconoir-user mr-5px" />
  90. <span>{{ items.certificateUserName }}</span>
  91. </div>
  92. </template>
  93. </div>
  94. </template>
  95. </el-scrollbar>
  96. </div>
  97. <div class="hc-tasks-user-letter-index">
  98. <div v-for="item in alphabet" :key="item" class="item" @click="alphabetClick(item)">{{ item }}</div>
  99. </div>
  100. </hc-card>
  101. <div v-else class="card-empty-no">
  102. <hc-empty :src="HcLoadSvg" title="请先选择岗位" />
  103. </div>
  104. </div>
  105. <div class="card-div-5 h-full w-205px">
  106. <hc-card v-if="fixedItem?.userList?.length > 0" scrollbar>
  107. <template #header>
  108. <span>已选择{{ fixedItem?.userList?.length || 0 }}人</span>
  109. </template>
  110. <template #extra>
  111. <el-button v-if="!isView" type="warning" size="small" @click="fixedUserSortClick">调整排序</el-button>
  112. </template>
  113. <div class="hc-tasks-user-cur-box" :class="`type-${fixedItem.type}`">
  114. <template v-for="(item, index) in fixedItem?.userList" :key="index">
  115. <div class="hc-tasks-user-item">
  116. <el-tag :closable="!isView" @close="fixedItemUserListDel(index)">
  117. <i class="i-ri-user-3-fill mr-5px" />
  118. <span>{{ item.userName }}</span>
  119. </el-tag>
  120. </div>
  121. </template>
  122. </div>
  123. <template #action>
  124. <el-button v-if="!isView" block size="default" type="success" @click="singleSaveClick">保存</el-button>
  125. </template>
  126. </hc-card>
  127. <div v-else class="card-empty-no">
  128. <hc-empty :src="HcLoadSvg" title="请先选择人员" />
  129. </div>
  130. </div>
  131. </div>
  132. </template>
  133. <template #footer>
  134. <el-button @click="modalClose">取消</el-button>
  135. <el-button type="primary" :loading="confirmLoading" @click="confirmClick">确定</el-button>
  136. </template>
  137. </hc-dialog>
  138. <!-- 流程设置 -->
  139. <HcProcessModal v-model="isProcessSetup" :data="fixedData" @finish="processSetupFinish" />
  140. <!-- 任务人排序 -->
  141. <HcSortModal v-model="isUserSort" :data="userSortData" @finish="userSortFinish" />
  142. </template>
  143. <script setup>
  144. import { nextTick, ref, watch } from 'vue'
  145. import { HcDelMsg } from 'hc-vue3-ui'
  146. import { pinyin } from 'pinyin-pro'
  147. import { deepClone, getArrValue, getObjValue, isNullES } from 'js-fast-way'
  148. import HcLoadSvg from '~src/assets/view/load.svg'
  149. import HcProcessModal from './process-modal.vue'
  150. import HcSortModal from './sort-modal.vue'
  151. import mainApi from '~api/tasks/flow'
  152. const props = defineProps({
  153. data: {
  154. type: Array,
  155. default: () => ([]),
  156. },
  157. datas: {
  158. type: Object,
  159. default: () => ({}),
  160. },
  161. isView:{
  162. type: Boolean,
  163. default: false,
  164. },
  165. })
  166. const emit = defineEmits(['finish', 'close'])
  167. //双向绑定
  168. const isShow = defineModel('modelValue', {
  169. default: false,
  170. })
  171. //监听参数
  172. const dataInfo = ref(props.datas)
  173. watch(() => props.datas, (data) => {
  174. dataInfo.value = getObjValue(data)
  175. }, { deep: true, immediate: true })
  176. const isView = ref(props.isView)
  177. watch(() => props.isView, (data) => {
  178. isView.value = data
  179. }, { deep: true, immediate: true })
  180. //监听数据
  181. const fixedData = ref([])
  182. watch(() => props.data, (data) => {
  183. const res = getArrValue(data)
  184. fixedData.value = deepClone(res)
  185. }, { deep: true, immediate: true })
  186. watch(isShow, (val) => {
  187. if (val) setInitData()
  188. })
  189. //初始化
  190. const setInitData = async () => {
  191. await nextTick()
  192. fixedData.value.forEach(item => {
  193. item.isDataSave = true
  194. item.flowTaskType = isNullES(item.flowTaskType) ? 1 : item.flowTaskType
  195. })
  196. }
  197. //流程被点击
  198. const fixedIndex = ref(-1)
  199. const fixedItem = ref({})
  200. const fixedItemClick = (item, index) => {
  201. item.isDataAdd = false
  202. fixedIndex.value = index
  203. fixedItem.value = item
  204. getAllRoleList()
  205. getsignPfxFileList()
  206. }
  207. const signPfxFileListLoading = ref(false)
  208. const getsignPfxFileList = async () => {
  209. signPfxFileListLoading.value = true
  210. const { contractId } = getObjValue(dataInfo.value)
  211. const { data } = await mainApi.findAllUserAndRoleList({ contractId, roleId:roleItem.value?.roleId || '' })
  212. let arr = getObjValue(data)
  213. setSignPfxUser(arr['userList'])
  214. signPfxFileListLoading.value = false
  215. }
  216. //获取流程图标
  217. const getProcessIcon = (item) => {
  218. return item.type === 1 ? 'i-hugeicons-workflow-square-03' : 'i-hugeicons-workflow-square-06'
  219. }
  220. //流程类型切换
  221. const fixedTypeClick = (item) => {
  222. if (isView.value) return
  223. item.type = item.type === 1 ? 2 : 1
  224. }
  225. //新增流程
  226. const fixedAddClick = () => {
  227. fixedData.value.push({
  228. type: 1,
  229. flowTaskType: 1,
  230. name: '流程审批名称',
  231. isDataAdd: true,
  232. isDataSave: false,
  233. userList: [],
  234. })
  235. }
  236. //删除流程
  237. const fixedDelClick = (item, index) => {
  238. HcDelMsg({
  239. title: '确认删除任务流程?',
  240. text: `确认是否需要删除【${item.name}】?`,
  241. }, (resolve) => {
  242. fixedData.value?.splice(index, 1)
  243. if (fixedIndex.value === index) {
  244. fixedIndex.value = -1
  245. }
  246. resolve()
  247. })
  248. }
  249. //角色列表
  250. const signUserLoading = ref(false)
  251. const signUserList = ref([])
  252. const getAllRoleList = async () => {
  253. signUserLoading.value = true
  254. const { contractId } = getObjValue(dataInfo.value)
  255. const { data } = await mainApi.queryAllRoleList({ contractId, type:1 })
  256. signUserList.value = getArrValue(data)
  257. signUserLoading.value = false
  258. }
  259. //角色被点击
  260. const roleItem = ref({})
  261. const roleItemClick = async (item) => {
  262. if (roleItem.value.roleId === item.roleId) {
  263. // 如果点击的是已选中的角色,则清空选择
  264. roleItem.value = {}
  265. positionList.value = []
  266. positionItem.value = {}
  267. roleItem.value = {}
  268. await getsignPfxFileList()
  269. return
  270. }
  271. roleItem.value = item
  272. const arr = getArrValue(item.childRoleList)
  273. positionList.value = deepClone(arr)
  274. setSignPfxUser(item.signPfxFileList)
  275. }
  276. //岗位搜索
  277. const positionKey = ref('')
  278. const positionList = ref([])
  279. const positionSearch = () => {
  280. const key = positionKey.value
  281. const list = getArrValue(roleItem.value?.childRoleList)
  282. const arr = deepClone(list)
  283. if (isNullES(key)) {
  284. positionList.value = arr
  285. return
  286. }
  287. positionList.value = arr.filter(({ roleName }) => roleName.toLowerCase().includes(key.toLowerCase()))
  288. }
  289. //岗位被点击
  290. const positionItem = ref({})
  291. const positionItemClick = async (item) => {
  292. if (positionItem.value.roleId === item.roleId) {
  293. // 如果点击的是已选中的岗位,则清空选择
  294. positionItem.value = {}
  295. await getsignPfxFileList()
  296. return
  297. }
  298. positionItem.value = item
  299. setSignPfxUser(item.signPfxFileList)
  300. }
  301. //设置任务人数据
  302. const alphabet = Array.from({ length: 26 }, (_, i) => String.fromCharCode(65 + i))
  303. const setSignPfxUser = (data) => {
  304. const list = deepClone(data)
  305. const arr = getArrValue(list)
  306. arr.forEach(item => {
  307. item.letter = getFirstLetter(item.certificateUserName)
  308. })
  309. signPfxFileData.value = deepClone(arr)
  310. signPfxFileList.value = alphabet.map(letter => ({
  311. name: letter,
  312. data: arr.filter(item => item.letter === letter),
  313. }))
  314. }
  315. //中文转姓氏拼音
  316. const getFirstLetter = (name) => {
  317. return pinyin(name.charAt(0), { pattern: 'first', toneType: 'none', surname: 'head' }).charAt(0).toUpperCase()
  318. }
  319. //搜索任务人
  320. const signPfxFileData = ref([])
  321. const signUserKey = ref('')
  322. const signPfxFileList = ref([])
  323. const signUserSearch = () => {
  324. const key = signUserKey.value
  325. const arr = deepClone(signPfxFileData.value)
  326. if (isNullES(key)) {
  327. setSignPfxUser(arr)
  328. return
  329. }
  330. // 判断是否为全英文
  331. const isAllEnglish = /^[A-Za-z]+$/.test(key)
  332. const letterName = getFirstLetter(key)
  333. //搜索筛选
  334. const filteredData = arr.filter(({ certificateUserName, letter }) => {
  335. if (isAllEnglish) {
  336. // 如果是英文,判断首字母是否一致
  337. return letter.toLowerCase().includes(letterName.toLowerCase())
  338. } else {
  339. // 如果是中文或其他字符,进行精准搜索
  340. return certificateUserName.toLowerCase().includes(key.toLowerCase())
  341. }
  342. })
  343. signPfxFileList.value = alphabet.map(letter => ({
  344. name: letter,
  345. data: filteredData.filter(item => item.letter === letter),
  346. }))
  347. }
  348. //滚动到相关位置
  349. const scrollRef = ref(null)
  350. const alphabetClick = (key) => {
  351. const ids = `hc-sign-pfx-file-item-${key}`
  352. const dom = document.getElementById(ids)
  353. if (isNullES(dom)) return
  354. scrollRef.value?.setScrollTop(dom.offsetTop - 20)
  355. }
  356. //任务人被点击
  357. const signUserItemClick = ({ certificateUserId, certificateUserName }) => {
  358. const arr = fixedData.value, index = fixedIndex.value
  359. const list = getArrValue(arr[index]?.userList)
  360. list.push({ userId: certificateUserId, userName: certificateUserName })
  361. fixedData.value[index].userList = list
  362. fixedItem.value = fixedData.value[index]
  363. }
  364. //删除选择的任务人
  365. const fixedItemUserListDel = (index) => {
  366. const arr = fixedData.value, i = fixedIndex.value
  367. const list = getArrValue(arr[i]?.userList)
  368. list.splice(index, 1)
  369. fixedData.value[i].userList = list
  370. fixedItem.value = fixedData.value[i]
  371. }
  372. //单个流程保存
  373. const singleSaveClick = () => {
  374. const arr = getArrValue(fixedItem.value?.userList)
  375. if (arr.length <= 0) {
  376. window.$message.warning('请选择对应的任务人员')
  377. return
  378. }
  379. window.$message.success('保存成功,全部完成后,请点击确定')
  380. const index = fixedIndex.value
  381. fixedData.value[index].isDataSave = true
  382. fixedData.value[index].isDataAdd = false
  383. }
  384. //流程设置
  385. const isProcessSetup = ref(false)
  386. const processSetupClick = () => {
  387. const arr = getArrValue(fixedData.value)
  388. if (arr.length <= 0) {
  389. window.$message.warning('请先创建任务流程')
  390. return
  391. }
  392. isProcessSetup.value = true
  393. }
  394. //流程设置完成
  395. const processSetupFinish = (data) => {
  396. isProcessSetup.value = false
  397. fixedData.value = getArrValue(data)
  398. fixedIndex.value = -1
  399. fixedItem.value = {}
  400. }
  401. //任务人排序
  402. const isUserSort = ref(false)
  403. const userSortData = ref([])
  404. const fixedUserSortClick = () => {
  405. const arr = fixedData.value, index = fixedIndex.value
  406. const list = getArrValue(arr[index]?.userList)
  407. if (list.length <= 0) {
  408. window.$message.warning('请先添加任务人')
  409. return
  410. }
  411. userSortData.value = list
  412. isUserSort.value = true
  413. }
  414. //任务人排序完成
  415. const userSortFinish = (data) => {
  416. const index = fixedIndex.value
  417. fixedData.value[index].userList = getArrValue(data)
  418. }
  419. //确定选择
  420. const confirmLoading = ref(false)
  421. const confirmClick = async () => {
  422. const list = deepClone(fixedData.value)
  423. if (list.length <= 0) {
  424. window.$message.warning('请先创建人物流程和选择任务人')
  425. return
  426. }
  427. //验证数组
  428. let isRes = true
  429. for (let i = 0; i < list.length; i++) {
  430. delete list[i].isDataAdd
  431. delete list[i].isDataSave
  432. const { name, userList } = list[i]
  433. if (userList.length <= 0) {
  434. isRes = false
  435. window.$message.warning(name + ',中没有选择任务人')
  436. break
  437. }
  438. }
  439. if (!isRes) return
  440. emit('finish', list)
  441. modalClose()
  442. }
  443. //关闭窗口
  444. const modalClose = () => {
  445. isShow.value = false
  446. emit('close')
  447. }
  448. </script>
  449. <style scoped>
  450. .cards-wrapper {
  451. display: flex;
  452. gap: 12px;
  453. width: 100%;
  454. height: 100%;
  455. }
  456. .card-div-2,
  457. .card-div-3,
  458. .card-div-4,
  459. .card-div-5 {
  460. height: 100%;
  461. }
  462. .card-div-2 {
  463. width: 235px;
  464. flex-shrink: 0;
  465. }
  466. .card-div-3 {
  467. width: 265px;
  468. flex-shrink: 0;
  469. }
  470. .card-div-4,
  471. .card-div-5 {
  472. flex: 1;
  473. min-width: 205px;
  474. }
  475. /* 添加禁用状态样式 */
  476. .disabled {
  477. cursor: not-allowed !important; /* 鼠标变为禁止样式 */
  478. opacity: 0.6; /* 降低透明度表示禁用 */
  479. pointer-events: none; /* 阻止鼠标事件穿透 */
  480. }
  481. /* 针对删除图标和添加按钮的额外样式调整 */
  482. .del-icon.disabled,
  483. .process.add.disabled {
  484. filter: grayscale(100%); /* 转为灰度进一步表示禁用 */
  485. }
  486. </style>