index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. <template>
  2. <!-- 这是引入后展示的样式 -->
  3. <a-flex wrap="wrap" gap="small" v-if="props.userShow">
  4. <div
  5. class="user-container"
  6. v-for="(user, index) in userObj"
  7. :key="user.id"
  8. @mouseover="onMouseEnter(index)"
  9. @mouseleave="onMouseLeave(index)"
  10. >
  11. <span class="user-delete">
  12. <CloseCircleFilled
  13. :class="index === deleteShow ? 'show-delete-icon' : ''"
  14. class="delete-icon"
  15. @click="deleteUser(user)"
  16. />
  17. <a-avatar :src="user.avatar" />
  18. </span>
  19. <span class="user-name">{{ user.name }}</span>
  20. </div>
  21. <a-button shape="circle" @click="openModal" v-if="(props.radioModel ? userObj.length !== 1 : true) && addShow">
  22. <PlusOutlined />
  23. </a-button>
  24. <slot name="button"></slot>
  25. </a-flex>
  26. <!-- 以下是弹窗内容 -->
  27. <a-modal
  28. v-model:open="visible"
  29. title="用户选择"
  30. :width="1000"
  31. :mask-closable="false"
  32. :destroy-on-close="true"
  33. @ok="handleOk"
  34. @cancel="handleClose"
  35. >
  36. <a-row :gutter="10">
  37. <a-col :span="7">
  38. <a-card size="small" :loading="cardLoading" class="selectorTreeDiv">
  39. <a-tree
  40. v-if="treeData"
  41. v-model:expandedKeys="defaultExpandedKeys"
  42. :tree-data="treeData"
  43. :field-names="treeFieldNames"
  44. @select="treeSelect"
  45. >
  46. </a-tree>
  47. </a-card>
  48. </a-col>
  49. <a-col :span="11">
  50. <div class="table-operator xn-mb10">
  51. <a-form ref="searchFormRef" name="advanced_search" class="ant-advanced-search-form" :model="searchFormState">
  52. <a-row :gutter="24">
  53. <a-col :span="12">
  54. <a-form-item name="searchKey">
  55. <a-input v-model:value="searchFormState.searchKey" placeholder="请输入用户名" />
  56. </a-form-item>
  57. </a-col>
  58. <a-col :span="12">
  59. <a-button type="primary" class="xn-mr-10" @click="loadData()"> 查询 </a-button>
  60. <a-button @click="reset()"> 重置 </a-button>
  61. </a-col>
  62. </a-row>
  63. </a-form>
  64. </div>
  65. <div class="user-table">
  66. <a-table
  67. ref="tableRef"
  68. size="small"
  69. :columns="commons"
  70. :data-source="tableData"
  71. :expand-row-by-click="true"
  72. :loading="pageLoading"
  73. bordered
  74. :pagination="false"
  75. >
  76. <template #title>
  77. <span>待选择列表 {{ tableRecordNum }} 条</span>
  78. <div v-if="!radioModel" class="xn-fdr">
  79. <a-button type="dashed" size="small" @click="addAllPageRecord">添加当前数据</a-button>
  80. </div>
  81. </template>
  82. <template #bodyCell="{ column, record }">
  83. <template v-if="column.dataIndex === 'avatar'">
  84. <a-avatar :src="record.avatar" style="margin-bottom: -5px; margin-top: -5px" />
  85. </template>
  86. <template v-if="column.dataIndex === 'action'">
  87. <a-button type="dashed" size="small" @click="addRecord(record)"><PlusOutlined /></a-button>
  88. </template>
  89. <template v-if="column.dataIndex === 'category'">
  90. {{ $TOOL.dictTypeData('ROLE_CATEGORY', record.category) }}
  91. </template>
  92. </template>
  93. </a-table>
  94. <div class="mt-2">
  95. <a-pagination
  96. v-if="!isEmpty(tableData)"
  97. v-model:current="current"
  98. v-model:page-size="pageSize"
  99. :total="total"
  100. size="small"
  101. showSizeChanger
  102. @change="paginationChange"
  103. />
  104. </div>
  105. </div>
  106. </a-col>
  107. <a-col :span="6">
  108. <div class="user-table">
  109. <a-table
  110. ref="selectedTable"
  111. size="small"
  112. :columns="selectedCommons"
  113. :data-source="selectedData"
  114. :expand-row-by-click="true"
  115. :loading="selectedTableListLoading"
  116. bordered
  117. >
  118. <template #title>
  119. <span>已选择: {{ selectedData.length }}</span>
  120. <div v-if="!radioModel" class="xn-fdr">
  121. <a-button type="dashed" danger size="small" @click="delAllRecord">全部移除</a-button>
  122. </div>
  123. </template>
  124. <template #bodyCell="{ column, record }">
  125. <template v-if="column.dataIndex === 'action'">
  126. <a-button type="dashed" danger size="small" @click="delRecord(record)"><MinusOutlined /></a-button>
  127. </template>
  128. </template>
  129. </a-table>
  130. </div>
  131. </a-col>
  132. </a-row>
  133. </a-modal>
  134. </template>
  135. <script setup name="userSelector">
  136. import { message } from 'ant-design-vue'
  137. import { remove, isEmpty, cloneDeep } from 'lodash-es'
  138. import userCenterApi from '@/api/sys/userCenterApi'
  139. // 弹窗是否打开
  140. const visible = ref(false)
  141. const deleteShow = ref('')
  142. // 主表格common
  143. const commons = [
  144. {
  145. title: '操作',
  146. dataIndex: 'action',
  147. align: 'center',
  148. width: 50
  149. },
  150. {
  151. title: '头像',
  152. dataIndex: 'avatar',
  153. width: 50
  154. },
  155. {
  156. title: '用户名',
  157. dataIndex: 'name',
  158. ellipsis: true
  159. },
  160. {
  161. title: '账号',
  162. dataIndex: 'account'
  163. }
  164. ]
  165. // 选中表格的表格common
  166. const selectedCommons = [
  167. {
  168. title: '操作',
  169. dataIndex: 'action',
  170. align: 'center',
  171. width: 50
  172. },
  173. {
  174. title: '用户名',
  175. dataIndex: 'name',
  176. ellipsis: true
  177. }
  178. ]
  179. const props = defineProps({
  180. radioModel: {
  181. type: Boolean,
  182. default: () => false
  183. },
  184. dataIsConverterFlw: {
  185. type: Boolean,
  186. default: () => false
  187. },
  188. orgTreeApi: {
  189. type: Function
  190. },
  191. userPageApi: {
  192. type: Function
  193. },
  194. userListByIdListApi: {
  195. type: Function
  196. },
  197. value: {
  198. default: () => ''
  199. },
  200. dataType: {
  201. type: String,
  202. default: () => 'string'
  203. },
  204. userShow: {
  205. type: Boolean,
  206. default: () => true
  207. },
  208. addShow: {
  209. type: Boolean,
  210. default: () => true
  211. }
  212. })
  213. // 主表格的ref 名称
  214. const tableRef = ref()
  215. // 选中表格的ref 名称
  216. const selectedTable = ref()
  217. const tableRecordNum = ref()
  218. const searchFormState = ref({})
  219. const searchFormRef = ref()
  220. const cardLoading = ref(true)
  221. const pageLoading = ref(false)
  222. const selectedTableListLoading = ref(false)
  223. // 替换treeNode 中 title,key,children
  224. const treeFieldNames = { children: 'children', title: 'name', key: 'id' }
  225. // 获取机构树数据
  226. const treeData = ref()
  227. // 默认展开二级树的节点id
  228. const defaultExpandedKeys = ref([])
  229. const emit = defineEmits(['update:value', 'onBack'])
  230. const tableData = ref([])
  231. const selectedData = ref([])
  232. const recordIds = ref([])
  233. // 分页相关
  234. const current = ref(0) // 当前页数
  235. const pageSize = ref(20) // 每页条数
  236. const total = ref(0) // 数据总数
  237. // 获取选中列表的api
  238. const userListByIdList = (param) => {
  239. if (typeof props.userListByIdListApi === 'function') {
  240. return props.userListByIdListApi(param)
  241. } else {
  242. return userCenterApi.userCenterGetUserListByIdList(param).then((data) => {
  243. return Promise.resolve(data)
  244. })
  245. }
  246. }
  247. // 打开弹框
  248. const showUserPlusModal = (ids = []) => {
  249. const data = goDataConverter(ids)
  250. recordIds.value = data
  251. getUserAvatarById(data)
  252. openModal()
  253. }
  254. const onMouseEnter = (index) => {
  255. deleteShow.value = index
  256. }
  257. const onMouseLeave = (index) => {
  258. deleteShow.value = ''
  259. }
  260. const openModal = () => {
  261. if (typeof props.orgTreeApi !== 'function') {
  262. message.warning('未配置选择器需要的orgTreeApi接口')
  263. return
  264. }
  265. if (typeof props.userPageApi !== 'function') {
  266. message.warning('未配置选择器需要的userPageApi接口')
  267. return
  268. }
  269. visible.value = true
  270. // 获取机构树
  271. props
  272. .orgTreeApi()
  273. .then((data) => {
  274. if (data !== null) {
  275. treeData.value = data
  276. // 默认展开2级
  277. treeData.value.forEach((item) => {
  278. // 因为0的顶级
  279. if (item.parentId === '0') {
  280. defaultExpandedKeys.value.push(item.id)
  281. // 取到下级ID
  282. if (item.children) {
  283. item.children.forEach((items) => {
  284. defaultExpandedKeys.value.push(items.id)
  285. })
  286. }
  287. }
  288. })
  289. }
  290. })
  291. .finally(() => {
  292. cardLoading.value = false
  293. })
  294. searchFormState.value.size = pageSize.value
  295. loadData()
  296. if (isEmpty(recordIds.value)) {
  297. return
  298. }
  299. const param = {
  300. idList: recordIds.value
  301. }
  302. selectedTableListLoading.value = true
  303. userListByIdList(param)
  304. .then((data) => {
  305. selectedData.value = data
  306. })
  307. .finally(() => {
  308. selectedTableListLoading.value = false
  309. })
  310. }
  311. // 点击头像删除用户
  312. const deleteUser = (user) => {
  313. // 删除显示的
  314. remove(userObj.value, (item) => item.id === user.id)
  315. // 删除缓存的
  316. remove(recordIds.value, (item) => item === user.id)
  317. const value = []
  318. const showUser = []
  319. userObj.value.forEach((item) => {
  320. const obj = {
  321. id: item.id,
  322. name: item.name
  323. }
  324. value.push(item.id)
  325. // 拷贝一份obj数据
  326. const objClone = cloneDeep(obj)
  327. objClone.avatar = item.avatar
  328. showUser.push(objClone)
  329. })
  330. userObj.value = showUser
  331. // 判断是否做数据的转换为工作流需要的
  332. const resultData = outDataConverter(value)
  333. emit('update:value', resultData)
  334. emit('onBack', resultData)
  335. }
  336. // 查询主表格数据
  337. const loadData = () => {
  338. pageLoading.value = true
  339. props
  340. .userPageApi(searchFormState.value)
  341. .then((data) => {
  342. current.value = data.current
  343. // pageSize.value = data.size
  344. total.value = data.total
  345. // 重置、赋值
  346. tableData.value = []
  347. tableRecordNum.value = 0
  348. tableData.value = data.records
  349. if (data.records) {
  350. tableRecordNum.value = data.records.length
  351. } else {
  352. tableRecordNum.value = 0
  353. }
  354. })
  355. .finally(() => {
  356. pageLoading.value = false
  357. })
  358. }
  359. // pageSize改变回调分页事件
  360. const paginationChange = (page, pageSize) => {
  361. searchFormState.value.current = page
  362. searchFormState.value.size = pageSize
  363. loadData()
  364. }
  365. const judge = () => {
  366. return !(props.radioModel && selectedData.value.length > 0)
  367. }
  368. // 添加记录
  369. const addRecord = (record) => {
  370. if (!judge()) {
  371. message.warning('只可选择一条')
  372. return
  373. }
  374. const selectedRecord = selectedData.value.filter((item) => item.id === record.id)
  375. if (selectedRecord.length === 0) {
  376. selectedData.value.push(record)
  377. } else {
  378. message.warning('该记录已存在')
  379. }
  380. }
  381. // 添加全部
  382. const addAllPageRecord = () => {
  383. let newArray = selectedData.value.concat(tableData.value)
  384. let list = []
  385. for (let item1 of newArray) {
  386. let flag = true
  387. for (let item2 of list) {
  388. if (item1.id === item2.id) {
  389. flag = false
  390. }
  391. }
  392. if (flag) {
  393. list.push(item1)
  394. }
  395. }
  396. selectedData.value = list
  397. }
  398. // 删减记录
  399. const delRecord = (record) => {
  400. remove(selectedData.value, (item) => item.id === record.id)
  401. }
  402. // 删减记录
  403. const delAllRecord = () => {
  404. selectedData.value = []
  405. }
  406. // 点击树查询
  407. const treeSelect = (selectedKeys) => {
  408. searchFormState.value.current = 0
  409. if (selectedKeys.length > 0) {
  410. searchFormState.value.orgId = selectedKeys.toString()
  411. } else {
  412. delete searchFormState.value.orgId
  413. }
  414. loadData()
  415. }
  416. const userObj = ref([])
  417. // 确定
  418. const handleOk = () => {
  419. userObj.value = []
  420. const value = []
  421. const showUser = []
  422. selectedData.value.forEach((item) => {
  423. const obj = {
  424. id: item.id,
  425. name: item.name
  426. }
  427. value.push(item.id)
  428. // 拷贝一份obj数据
  429. const objClone = cloneDeep(obj)
  430. objClone.avatar = item.avatar
  431. showUser.push(objClone)
  432. })
  433. userObj.value = showUser
  434. // 判断是否做数据的转换为工作流需要的
  435. const resultData = outDataConverter(value)
  436. emit('update:value', resultData)
  437. emit('onBack', resultData)
  438. handleClose()
  439. }
  440. // 重置
  441. const reset = () => {
  442. delete searchFormState.value.searchKey
  443. loadData()
  444. }
  445. const handleClose = () => {
  446. searchFormState.value = {}
  447. tableRecordNum.value = 0
  448. tableData.value = []
  449. current.value = 0
  450. pageSize.value = 20
  451. total.value = 0
  452. selectedData.value = []
  453. // userObj.value = []
  454. visible.value = false
  455. }
  456. // 数据进入后转换
  457. const goDataConverter = (data) => {
  458. if (props.dataIsConverterFlw) {
  459. const resultData = []
  460. // 处理对象
  461. if (!isEmpty(data.value)) {
  462. const values = data.value.split(',')
  463. if (values.length > 0) {
  464. values.forEach((id) => {
  465. resultData.push(id)
  466. })
  467. } else {
  468. resultData.push(data.value)
  469. }
  470. } else {
  471. // 处理数组
  472. if (!isEmpty(data) && !isEmpty(data[0]) && !isEmpty(data[0].value)) {
  473. const values = data[0].value.split(',')
  474. for (let i = 0; i < values.length; i++) {
  475. resultData.push(values[i])
  476. }
  477. }
  478. }
  479. return resultData
  480. } else {
  481. if (getValueType() !== 'string') {
  482. return data
  483. }
  484. if (data.length > 1) {
  485. const resultData = []
  486. data.split(',').forEach((id) => {
  487. resultData.push(id)
  488. })
  489. return resultData
  490. } else {
  491. return data
  492. }
  493. }
  494. }
  495. // 数据出口转换器
  496. const outDataConverter = (data) => {
  497. if (props.dataIsConverterFlw) {
  498. data = userObj.value
  499. const obj = {}
  500. let label = ''
  501. let value = ''
  502. for (let i = 0; i < data.length; i++) {
  503. if (data.length === i + 1) {
  504. label = label + data[i].name
  505. value = value + data[i].id
  506. } else {
  507. label = label + data[i].name + ','
  508. value = value + data[i].id + ','
  509. }
  510. }
  511. obj.key = 'USER'
  512. obj.label = label
  513. obj.value = value
  514. obj.extJson = ''
  515. return obj
  516. } else {
  517. if (getValueType() !== 'string') {
  518. return data
  519. }
  520. let resultData = ''
  521. data.forEach((id) => {
  522. resultData = resultData + ',' + id
  523. })
  524. resultData = resultData.substring(1, resultData.length)
  525. return resultData
  526. }
  527. }
  528. // 获取数据类型
  529. const getValueType = () => {
  530. if (props.dataType) {
  531. return props.dataType
  532. } else {
  533. if (props.radioModel) {
  534. return 'string'
  535. }
  536. return typeof typeof props.value
  537. }
  538. }
  539. const getUserAvatarById = (ids) => {
  540. if (isEmpty(userObj.value) && !isEmpty(ids)) {
  541. const param = {
  542. idList: recordIds.value
  543. }
  544. // 这里必须转为数组类型的
  545. userListByIdList(param).then((data) => {
  546. userObj.value = data
  547. })
  548. }
  549. }
  550. watch(
  551. () => props.value,
  552. (newValue) => {
  553. if (!isEmpty(props.value)) {
  554. const ids = goDataConverter(newValue)
  555. recordIds.value = ids
  556. getUserAvatarById(ids)
  557. } else {
  558. userObj.value = []
  559. selectedData.value = []
  560. }
  561. },
  562. {
  563. immediate: true // 立即执行
  564. }
  565. )
  566. defineExpose({
  567. showUserPlusModal
  568. })
  569. </script>
  570. <style lang="less" scoped>
  571. .xn-mr-5 {
  572. margin-right: 5px;
  573. }
  574. .xn-mr-10 {
  575. margin-right: 10px;
  576. }
  577. .selectorTreeDiv {
  578. max-height: 500px;
  579. overflow: auto;
  580. }
  581. .ant-form-item {
  582. margin-bottom: 0 !important;
  583. }
  584. .user-table {
  585. overflow: auto;
  586. max-height: 450px;
  587. }
  588. .user-container {
  589. display: flex;
  590. align-items: center; /* 垂直居中 */
  591. flex-direction: column;
  592. margin-right: 10px;
  593. text-align: center;
  594. }
  595. .user-avatar {
  596. width: 30px;
  597. border-radius: 50%; /* 设置为50%以创建圆形头像 */
  598. }
  599. .user-name {
  600. font-size: 12px;
  601. max-width: 50px;
  602. white-space: nowrap;
  603. overflow: hidden;
  604. }
  605. .user-delete {
  606. z-index: 99;
  607. color: rgba(0, 0, 0, 0.25);
  608. position: relative;
  609. display: flex;
  610. flex-direction: column;
  611. }
  612. .delete-icon {
  613. position: absolute;
  614. right: -2px;
  615. z-index: 5;
  616. top: -3px;
  617. cursor: pointer;
  618. visibility: hidden;
  619. }
  620. .show-delete-icon {
  621. visibility: visible;
  622. }
  623. </style>