index.vue 11 KB

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