TableSelect.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. <script setup lang="ts">
  2. import { ElPopover } from 'element-plus'
  3. import type { VxeTableInstance } from 'vxe-table'
  4. import type { PopoverProps, TagProps } from 'element-plus'
  5. interface TableColumn {
  6. // 类型
  7. type?: string
  8. // 列名
  9. field?: string
  10. // 表格宽度
  11. width?: number | string
  12. // 对齐方式
  13. align?: string
  14. // 标题
  15. title?: string
  16. // 插槽
  17. slot?: string
  18. }
  19. interface TableConfig {
  20. // 表格列
  21. column?: Array<TableColumn>
  22. // 表格数据
  23. request: (...args: any) => Promise<any>
  24. // 每页显示条数
  25. pageSize?: Number
  26. }
  27. interface TableSelectProps {
  28. // 绑定值
  29. modelValue: any
  30. // 是否多选
  31. multiple?: boolean
  32. // 是否禁用
  33. disabled?: boolean
  34. // 是否支持清除
  35. clearable?: boolean
  36. // value 的属性名
  37. valueKey?: string
  38. // label 的属性名
  39. labelKey?: string
  40. // 回显数据,用于后端分页显示
  41. initValue?: any
  42. // 气泡位置
  43. placement?: PopoverProps['placement']
  44. // 占位符
  45. placeholder?: string
  46. // popover 宽度
  47. popperWidth?: number | string
  48. // 自定义 popper 类名
  49. popperClass?: string
  50. // popper 配置项
  51. popperOptions?: PopoverProps['popperOptions']
  52. // 表格配置
  53. tableConfig?: TableConfig
  54. // tag 类型
  55. tagType?: TagProps['type']
  56. // 最多显示多少个tag
  57. maxTagCount?: number
  58. // 自定义数据转换方法
  59. transformData?: Function
  60. }
  61. const props = withDefaults(defineProps<TableSelectProps>(), {
  62. modelValue: '',
  63. clearable: true,
  64. valueKey: 'id',
  65. labelKey: 'name',
  66. placement: 'bottom-start',
  67. placeholder: '请选择',
  68. popperWidth: 560,
  69. tagType: 'info',
  70. maxTagCount: 5
  71. })
  72. const emits = defineEmits(['update:modelValue', 'change', 'clear', 'item-clear', 'focus', 'blur'])
  73. /* 格式化提交数据 */
  74. const transformData = (value: any) => {
  75. let result = ''
  76. if (value) {
  77. if (Array.isArray(value)) {
  78. result = value
  79. .map(item => {
  80. return JSON.stringify({
  81. [props.valueKey]: item[props.valueKey],
  82. [props.labelKey]: item[props.labelKey]
  83. })
  84. })
  85. .join(',')
  86. } else {
  87. result = JSON.stringify({ [props.valueKey]: value[props.valueKey], [props.labelKey]: value[props.labelKey] })
  88. }
  89. }
  90. return result
  91. }
  92. // popover 实例
  93. const popoverRef = ref<InstanceType<typeof ElPopover>>()
  94. // 表格实例
  95. const tableRef = ref<VxeTableInstance<any>>()
  96. // 是否显示
  97. const visible = ref(false)
  98. // 选中展示的数据
  99. const selectLabel = ref<string | Array<any>>('')
  100. // 加载状态
  101. const loading = ref(false)
  102. // 表格数据
  103. const tableData = ref<any[]>([])
  104. // 页码
  105. const pageNo = ref<number>(1)
  106. // 总数
  107. const tableTotal = ref<number>(0)
  108. // 是否未选中
  109. const isEmpty = computed<boolean>(() => {
  110. return props.modelValue == null || props.modelValue === ''
  111. })
  112. // 是否需要清空图标
  113. const closeEnable = computed<boolean>(() => {
  114. return props.clearable && !props.disabled && !isEmpty.value
  115. })
  116. // 多选显示的标签
  117. const currentValues = computed(() => {
  118. if (selectLabel.value == null) {
  119. return selectLabel.value ?? []
  120. }
  121. return selectLabel.value.slice(0, props.maxTagCount)
  122. })
  123. // 多选折叠的标签
  124. const omittedValues = computed(() => {
  125. if (selectLabel.value == null) {
  126. return []
  127. }
  128. return selectLabel.value.slice(props.maxTagCount)
  129. })
  130. /* 打开弹窗 */
  131. const onFocus = (e: FocusEvent) => {
  132. emits('focus', e)
  133. }
  134. /* 关闭弹窗 */
  135. const onBlur = (e: FocusEvent) => {
  136. emits('blur', e)
  137. }
  138. /* 清除事件 */
  139. const onClear = () => {
  140. updateModelValue('')
  141. selectLabel.value = ''
  142. // 取消表格全部选中
  143. tableRef.value?.clearCheckboxRow()
  144. emits('clear')
  145. }
  146. /* 单个清除事件 */
  147. const onItemClear = (item: any) => {
  148. const list = [...(selectLabel.value as Array<any>)]
  149. const index = list.findIndex(x => x[props.labelKey] === item[props.labelKey])
  150. list.splice(index, 1)
  151. selectLabel.value = list
  152. // 取消表格选中数据
  153. tableRef.value?.toggleCheckboxRow(item)
  154. updateModelValue(list)
  155. emits('item-clear', { item, list })
  156. }
  157. /* 表格单选事件 */
  158. const tableRadioChange = (data: any) => {
  159. selectLabel.value = data.row[props.labelKey]
  160. visible.value = false
  161. // 发出选择事件
  162. updateModelValue(data.row)
  163. emits('change', data.row)
  164. }
  165. /* 表格多选择事件 */
  166. const tableCheckboxChange = (data: any) => {
  167. let result: Array<any> = []
  168. if (data.checked) {
  169. // 使用 Set 去重
  170. const uniqueArray = Array.from(
  171. new Set([...selectLabel.value, ...data.records].map((x: any) => JSON.stringify(x)))
  172. ).map((str: any) => JSON.parse(str))
  173. selectLabel.value = uniqueArray
  174. result = uniqueArray
  175. } else {
  176. const selects = selectLabel.value as Array<any>
  177. const index = selects.findIndex(x => x[props.valueKey] === data.row[props.valueKey])
  178. selects?.splice(index, 1)
  179. result = selects
  180. }
  181. // 发出选择事件
  182. updateModelValue(result)
  183. emits('change', selectLabel.value)
  184. }
  185. /* initValue 改变 */
  186. const initValueChange = (value: any) => {
  187. // 如果是字符串,则解析成对象
  188. value = typeof value === 'string' ? JSON.parse(value) : value
  189. if (props.initValue) {
  190. // 处理回显数据
  191. if (props.multiple) {
  192. selectLabel.value = value
  193. } else {
  194. selectLabel.value = value[props.labelKey] as string
  195. }
  196. updateModelValue(value)
  197. }
  198. }
  199. /* 分页改变事件 */
  200. const paginationChange = (data: number) => {
  201. pageNo.value = data
  202. request()
  203. }
  204. /* 表格请求完成 */
  205. const tableDone = () => {
  206. nextTick(() => {
  207. const newTableData = tableRef.value?.getTableData().tableData
  208. newTableData?.forEach(item => {
  209. if (props.multiple) {
  210. const temp =
  211. Array.isArray(selectLabel.value) && selectLabel.value.find(x => x[props.valueKey] === item[props.valueKey])
  212. temp && tableRef.value?.setCheckboxRow(item, true)
  213. } else {
  214. const temp = item[props.valueKey] === (props.modelValue && JSON.parse(props.modelValue)[props.valueKey])
  215. temp && tableRef.value?.setRadioRow(item)
  216. }
  217. })
  218. })
  219. }
  220. /* 请求数据 */
  221. const request = (where?: any) => {
  222. if (typeof props.tableConfig?.request === 'function') {
  223. loading.value = true
  224. if (where && where.pageNo) {
  225. pageNo.value = where.pageNo
  226. }
  227. props.tableConfig
  228. .request({
  229. pageNo: pageNo.value,
  230. pageSize: props.tableConfig.pageSize,
  231. ...where
  232. })
  233. .then((res: any) => {
  234. tableTotal.value = res.total || res.totalCount
  235. tableData.value = res.infos || res.list || res.records || res.data || res.info
  236. tableDone()
  237. })
  238. .catch((e: any) => {
  239. console.warn(e)
  240. })
  241. .finally(() => {
  242. loading.value = false
  243. })
  244. } else {
  245. console.warn('tableConfig.request 必须为 Promise')
  246. }
  247. }
  248. request()
  249. /* 更新选中值 */
  250. const updateModelValue = (value: any) => {
  251. if (value) {
  252. emits('update:modelValue', props.transformData ? props.transformData(value) : transformData(value))
  253. } else {
  254. emits('update:modelValue', '')
  255. }
  256. }
  257. /* 更新气泡位置 */
  258. watch(currentValues, () => {
  259. if (
  260. popoverRef.value &&
  261. popoverRef.value.popperRef &&
  262. popoverRef.value.popperRef.popperInstanceRef &&
  263. popoverRef.value.popperRef.popperInstanceRef.update
  264. ) {
  265. popoverRef.value.popperRef.popperInstanceRef.update()
  266. }
  267. })
  268. watch(
  269. () => props.initValue,
  270. () => {
  271. initValueChange(props.initValue)
  272. },
  273. { immediate: true }
  274. )
  275. defineExpose({
  276. request
  277. })
  278. </script>
  279. <template>
  280. <el-popover
  281. ref="popoverRef"
  282. v-model:visible="visible"
  283. :placement="placement"
  284. :width="popperWidth"
  285. :popper-class="popperClass"
  286. :popper-options="popperOptions"
  287. :disabled="disabled"
  288. trigger="click"
  289. transition="el-zoom-in-top"
  290. teleported
  291. >
  292. <template #reference>
  293. <div class="table-select-container" :class="{ 'is-multiple': multiple, 'is-visible': visible }">
  294. <el-input
  295. :disabled="disabled"
  296. :placeholder="multiple && !isEmpty ? '' : placeholder"
  297. :readonly="true"
  298. :validateEvent="false"
  299. :modelValue="multiple ? '' : selectLabel"
  300. @focus="onFocus"
  301. @blur="onBlur"
  302. >
  303. <template v-if="$slots.prefix" #prefix>
  304. <slot name="prefix"></slot>
  305. </template>
  306. <template #suffix>
  307. <div class="select-suffix">
  308. <ElIcon v-if="closeEnable" class="select-clear" @click.stop="onClear">
  309. <slot name="clearIcon">
  310. <CircleClose />
  311. </slot>
  312. </ElIcon>
  313. <ElIcon class="select-down" :class="{ 'select-down-rotate': visible }" v-else>
  314. <slot name="suffixIcon">
  315. <ArrowDown />
  316. </slot>
  317. </ElIcon>
  318. </div>
  319. </template>
  320. </el-input>
  321. <div class="table-multiple-tag" :class="{ 'is-disabled': disabled }" v-if="multiple">
  322. <el-tag
  323. :type="tagType"
  324. size="small"
  325. disable-transitions
  326. :closable="!disabled"
  327. style="margin-right: 5px"
  328. v-for="item in currentValues"
  329. :key="item[valueKey]"
  330. @close="onItemClear(item)"
  331. >
  332. {{ item[labelKey] }}
  333. </el-tag>
  334. <el-tag v-if="omittedValues && omittedValues.length" size="small" disable-transitions :type="tagType">
  335. + {{ omittedValues.length }} ...
  336. </el-tag>
  337. </div>
  338. </div>
  339. </template>
  340. <!-- 上方插槽 -->
  341. <slot name="top-extra"></slot>
  342. <vxe-table
  343. :data="tableData"
  344. :row-config="{ isCurrent: true, isHover: true }"
  345. :radio-config="{ trigger: multiple ? '' : 'row' }"
  346. :checkbox-config="{ trigger: multiple ? 'row' : '' }"
  347. ref="tableRef"
  348. @radio-change="tableRadioChange"
  349. @checkbox-all="tableCheckboxChange"
  350. @checkbox-change="tableCheckboxChange"
  351. v-loading="loading"
  352. >
  353. <vxe-column type="checkbox" width="60" v-if="multiple"></vxe-column>
  354. <vxe-column type="radio" width="40" v-else> </vxe-column>
  355. <vxe-column v-for="(item, index) in tableConfig?.column" :key="index" v-bind="item">
  356. <template #default="slotProps" v-if="item.slot">
  357. <slot :name="item.slot" v-bind="slotProps || {}"></slot>
  358. </template>
  359. </vxe-column>
  360. </vxe-table>
  361. <div class="table-select-pagination" v-if="tableTotal">
  362. <el-pagination
  363. background
  364. layout="total, prev, pager, next, jumper"
  365. size="small"
  366. :pager-count="5"
  367. :page-size="tableConfig && tableConfig.pageSize"
  368. :total="tableTotal"
  369. v-model:current-page="pageNo"
  370. @current-change="paginationChange"
  371. />
  372. </div>
  373. <!-- 下方插槽 -->
  374. <slot name="bottom-extra"></slot>
  375. </el-popover>
  376. </template>
  377. <style scoped lang="scss">
  378. .table-select-container {
  379. width: 100%;
  380. position: relative;
  381. .select-clear {
  382. position: absolute;
  383. right: 10px;
  384. top: 49%;
  385. transform: translateY(-50%);
  386. cursor: pointer;
  387. border-radius: 50%;
  388. overflow: hidden;
  389. z-index: 2;
  390. opacity: 0;
  391. &:hover {
  392. opacity: 100;
  393. }
  394. }
  395. .select-down {
  396. transition: transform 0.2s;
  397. }
  398. .select-down-rotate {
  399. transform: rotate(180deg);
  400. }
  401. &.is-multiple {
  402. :deep(.el-input) {
  403. position: absolute;
  404. width: 100%;
  405. height: 100%;
  406. top: 0;
  407. left: 0;
  408. }
  409. }
  410. &.is-visible {
  411. :deep(.el-input:not(.is-disabled) .el-input__wrapper) {
  412. box-shadow: 0 0 0 1px var(--el-color-primary) inset;
  413. }
  414. }
  415. &:hover {
  416. .select-clear {
  417. opacity: 1;
  418. }
  419. }
  420. }
  421. .table-multiple-tag {
  422. position: relative;
  423. top: 0px;
  424. width: calc(100% - 24px);
  425. height: 100%;
  426. min-height: 34px;
  427. z-index: 2;
  428. padding: 0px 10px 2px 10px;
  429. box-sizing: border-box;
  430. &.is-disabled {
  431. cursor: not-allowed;
  432. }
  433. }
  434. .table-select-pagination {
  435. display: flex;
  436. justify-content: center;
  437. margin-top: 12px;
  438. }
  439. </style>