index.vue 9.2 KB

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