|
@@ -0,0 +1,415 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import type { TableSelectProps } from './props'
|
|
|
+import { tableSelectEmits } from './props'
|
|
|
+import { ElPopover } from 'element-plus'
|
|
|
+import type { VxeTableInstance } from 'vxe-table'
|
|
|
+
|
|
|
+const emits = defineEmits(tableSelectEmits)
|
|
|
+const props = withDefaults(defineProps<TableSelectProps>(), {
|
|
|
+ modelValue: '',
|
|
|
+ size: 'default',
|
|
|
+ clearable: true,
|
|
|
+ valueKey: 'id',
|
|
|
+ labelKey: 'name',
|
|
|
+ placement: 'bottom-start',
|
|
|
+ placeholder: '请选择',
|
|
|
+ popperWidth: 560,
|
|
|
+ tagType: 'info',
|
|
|
+ maxTagCount: 5
|
|
|
+})
|
|
|
+
|
|
|
+/* 格式化提交数据 */
|
|
|
+const transformData = (value: any) => {
|
|
|
+ let result = ''
|
|
|
+ if (value) {
|
|
|
+ if (Array.isArray(value)) {
|
|
|
+ result = value
|
|
|
+ .map(item => {
|
|
|
+ return JSON.stringify({
|
|
|
+ [props.valueKey]: item[props.valueKey],
|
|
|
+ [props.labelKey]: item[props.labelKey]
|
|
|
+ })
|
|
|
+ })
|
|
|
+ .join(',')
|
|
|
+ } else {
|
|
|
+ result = JSON.stringify({ [props.valueKey]: value[props.valueKey], [props.labelKey]: value[props.labelKey] })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+// popover 实例
|
|
|
+const popoverRef = ref<InstanceType<typeof ElPopover>>()
|
|
|
+// 表格实例
|
|
|
+const tableRef = ref<VxeTableInstance<any>>()
|
|
|
+// 是否显示
|
|
|
+const visible = ref(false)
|
|
|
+// 选中展示的数据
|
|
|
+const selectLabel = ref<string | Array<any>>('')
|
|
|
+// 加载状态
|
|
|
+const loading = ref(false)
|
|
|
+// 表格数据
|
|
|
+const tableData = ref<any[]>([])
|
|
|
+// 页码
|
|
|
+const pageNo = ref<number>(1)
|
|
|
+// 总数
|
|
|
+const tableTotal = ref<number>(0)
|
|
|
+
|
|
|
+// 是否未选中
|
|
|
+const isEmpty = computed<boolean>(() => {
|
|
|
+ return props.modelValue == null || props.modelValue === ''
|
|
|
+})
|
|
|
+
|
|
|
+// 是否需要清空图标
|
|
|
+const closeEnable = computed<boolean>(() => {
|
|
|
+ return props.clearable && !props.disabled && !isEmpty.value
|
|
|
+})
|
|
|
+
|
|
|
+// 多选显示的标签
|
|
|
+const currentValues = computed(() => {
|
|
|
+ if (selectLabel.value == null) {
|
|
|
+ return selectLabel.value ?? []
|
|
|
+ }
|
|
|
+ return selectLabel.value.slice(0, props.maxTagCount)
|
|
|
+})
|
|
|
+// 多选折叠的标签
|
|
|
+const omittedValues = computed(() => {
|
|
|
+ if (selectLabel.value == null) {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+ return selectLabel.value.slice(props.maxTagCount)
|
|
|
+})
|
|
|
+
|
|
|
+/* 打开弹窗 */
|
|
|
+const onFocus = (e: FocusEvent) => {
|
|
|
+ emits('focus', e)
|
|
|
+}
|
|
|
+
|
|
|
+/* 关闭弹窗 */
|
|
|
+const onBlur = (e: FocusEvent) => {
|
|
|
+ emits('blur', e)
|
|
|
+}
|
|
|
+
|
|
|
+/* 清除事件 */
|
|
|
+const onClear = () => {
|
|
|
+ updateModelValue('')
|
|
|
+ selectLabel.value = ''
|
|
|
+ // 取消表格全部选中
|
|
|
+ tableRef.value?.clearCheckboxRow()
|
|
|
+ emits('clear')
|
|
|
+}
|
|
|
+
|
|
|
+/* 单个清除事件 */
|
|
|
+const onItemClear = (item: any) => {
|
|
|
+ const list = [...(selectLabel.value as Array<any>)]
|
|
|
+ const index = list.findIndex(x => x[props.labelKey] === item[props.labelKey])
|
|
|
+ list.splice(index, 1)
|
|
|
+ selectLabel.value = list
|
|
|
+
|
|
|
+ // 取消表格选中数据
|
|
|
+ tableRef.value?.toggleCheckboxRow(item)
|
|
|
+ updateModelValue(list)
|
|
|
+ emits('item-clear', { item, list })
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格单选事件 */
|
|
|
+const tableRadioChange = (data: any) => {
|
|
|
+ selectLabel.value = data.row[props.labelKey]
|
|
|
+ visible.value = false
|
|
|
+ // 发出选择事件
|
|
|
+ updateModelValue(data.row)
|
|
|
+ emits('change', data.row)
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格多选择事件 */
|
|
|
+const tableCheckboxChange = (data: any) => {
|
|
|
+ let result: Array<any> = []
|
|
|
+ if (data.checked) {
|
|
|
+ // 使用 Set 去重
|
|
|
+ const uniqueArray = Array.from(
|
|
|
+ new Set([...selectLabel.value, ...data.records].map((x: any) => JSON.stringify(x)))
|
|
|
+ ).map((str: any) => JSON.parse(str))
|
|
|
+ selectLabel.value = uniqueArray
|
|
|
+ result = uniqueArray
|
|
|
+ } else {
|
|
|
+ const selects = selectLabel.value as Array<any>
|
|
|
+ const index = selects.findIndex(x => x[props.valueKey] === data.row[props.valueKey])
|
|
|
+ selects?.splice(index, 1)
|
|
|
+ result = selects
|
|
|
+ }
|
|
|
+ // 发出选择事件
|
|
|
+ updateModelValue(result)
|
|
|
+ emits('change', selectLabel.value)
|
|
|
+}
|
|
|
+
|
|
|
+/* initValue 改变 */
|
|
|
+const initValueChange = (value: any) => {
|
|
|
+ // 如果是字符串,则解析成对象
|
|
|
+ value = typeof value === 'string' ? JSON.parse(value) : value
|
|
|
+ if (props.initValue) {
|
|
|
+ // 处理回显数据
|
|
|
+ if (props.multiple) {
|
|
|
+ selectLabel.value = value
|
|
|
+ } else {
|
|
|
+ selectLabel.value = value[props.labelKey] as string
|
|
|
+ }
|
|
|
+ updateModelValue(value)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 分页改变事件 */
|
|
|
+const paginationChange = (data: number) => {
|
|
|
+ pageNo.value = data
|
|
|
+ request()
|
|
|
+}
|
|
|
+
|
|
|
+/* 表格请求完成 */
|
|
|
+const tableDone = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ const newTableData = tableRef.value?.getTableData().tableData
|
|
|
+ newTableData?.forEach(item => {
|
|
|
+ if (props.multiple) {
|
|
|
+ const temp =
|
|
|
+ Array.isArray(selectLabel.value) && selectLabel.value.find(x => x[props.valueKey] === item[props.valueKey])
|
|
|
+ temp && tableRef.value?.setCheckboxRow(item, true)
|
|
|
+ } else {
|
|
|
+ const temp = item[props.valueKey] === (props.modelValue && JSON.parse(props.modelValue)[props.valueKey])
|
|
|
+ temp && tableRef.value?.setRadioRow(item)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/* 请求数据 */
|
|
|
+const request = (where?: any) => {
|
|
|
+ if (typeof props.tableConfig?.datasource === 'function') {
|
|
|
+ loading.value = true
|
|
|
+ if (where && where.pageNo) {
|
|
|
+ pageNo.value = where.pageNo
|
|
|
+ }
|
|
|
+ props.tableConfig
|
|
|
+ .datasource({
|
|
|
+ pageNo: pageNo.value,
|
|
|
+ pageSize: props.tableConfig.pageSize,
|
|
|
+ ...where
|
|
|
+ })
|
|
|
+ .then((res: any) => {
|
|
|
+ tableTotal.value = res.total || res.totalCount
|
|
|
+ tableData.value = res.infos || res.list || res.records || res.data || res.info
|
|
|
+ tableDone()
|
|
|
+ })
|
|
|
+ .catch((e: any) => {
|
|
|
+ console.warn(e)
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ loading.value = false
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ console.warn('tableConfig.datasource 必须为 Promise')
|
|
|
+ }
|
|
|
+}
|
|
|
+request()
|
|
|
+
|
|
|
+/* 更新选中值 */
|
|
|
+const updateModelValue = (value: any) => {
|
|
|
+ if (value) {
|
|
|
+ emits('update:modelValue', props.transformData ? props.transformData(value) : transformData(value))
|
|
|
+ } else {
|
|
|
+ emits('update:modelValue', '')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 更新气泡位置 */
|
|
|
+watch(currentValues, () => {
|
|
|
+ if (
|
|
|
+ popoverRef.value &&
|
|
|
+ popoverRef.value.popperRef &&
|
|
|
+ popoverRef.value.popperRef.popperInstanceRef &&
|
|
|
+ popoverRef.value.popperRef.popperInstanceRef.update
|
|
|
+ ) {
|
|
|
+ popoverRef.value.popperRef.popperInstanceRef.update()
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.initValue,
|
|
|
+ () => {
|
|
|
+ initValueChange(props.initValue)
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+)
|
|
|
+
|
|
|
+defineExpose({
|
|
|
+ request
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <el-popover
|
|
|
+ ref="popoverRef"
|
|
|
+ v-model:visible="visible"
|
|
|
+ :placement="placement"
|
|
|
+ :width="popperWidth"
|
|
|
+ :popper-class="popperClass"
|
|
|
+ :popper-options="popperOptions"
|
|
|
+ :disabled="disabled"
|
|
|
+ trigger="click"
|
|
|
+ transition="el-zoom-in-top"
|
|
|
+ teleported
|
|
|
+ >
|
|
|
+ <template #reference>
|
|
|
+ <div class="table-select-container" :class="{ 'is-multiple': multiple, 'is-visible': visible }">
|
|
|
+ <el-input
|
|
|
+ :size="size"
|
|
|
+ :disabled="disabled"
|
|
|
+ :placeholder="multiple && !isEmpty ? '' : placeholder"
|
|
|
+ :readonly="true"
|
|
|
+ :validateEvent="false"
|
|
|
+ :modelValue="multiple ? '' : selectLabel"
|
|
|
+ @focus="onFocus"
|
|
|
+ @blur="onBlur"
|
|
|
+ >
|
|
|
+ <template v-if="$slots.prefix" #prefix>
|
|
|
+ <slot name="prefix"></slot>
|
|
|
+ </template>
|
|
|
+ <template #suffix>
|
|
|
+ <div class="select-suffix">
|
|
|
+ <ElIcon v-if="closeEnable" class="select-clear" @click.stop="onClear">
|
|
|
+ <slot name="clearIcon">
|
|
|
+ <CircleClose />
|
|
|
+ </slot>
|
|
|
+ </ElIcon>
|
|
|
+ <ElIcon class="select-down" :class="{ 'select-down-rotate': visible }">
|
|
|
+ <slot name="suffixIcon">
|
|
|
+ <ArrowDown />
|
|
|
+ </slot>
|
|
|
+ </ElIcon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ <div class="table-multiple-tag" :class="{ 'is-disabled': disabled }" v-if="multiple">
|
|
|
+ <el-tag
|
|
|
+ :type="tagType"
|
|
|
+ size="small"
|
|
|
+ disable-transitions
|
|
|
+ :closable="!disabled"
|
|
|
+ style="margin-right: 5px"
|
|
|
+ v-for="item in currentValues"
|
|
|
+ :key="item[valueKey]"
|
|
|
+ @close="onItemClear(item)"
|
|
|
+ >
|
|
|
+ {{ item[labelKey] }}
|
|
|
+ </el-tag>
|
|
|
+ <el-tag v-if="omittedValues && omittedValues.length" size="small" disable-transitions :type="tagType">
|
|
|
+ + {{ omittedValues.length }} ...
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <!-- 上方插槽 -->
|
|
|
+ <slot name="top-extra"></slot>
|
|
|
+ <vxe-table
|
|
|
+ :data="tableData"
|
|
|
+ :row-config="{ isCurrent: true, isHover: true }"
|
|
|
+ :radio-config="{ trigger: multiple ? '' : 'row' }"
|
|
|
+ :checkbox-config="{ trigger: multiple ? 'row' : '' }"
|
|
|
+ ref="tableRef"
|
|
|
+ @radio-change="tableRadioChange"
|
|
|
+ @checkbox-all="tableCheckboxChange"
|
|
|
+ @checkbox-change="tableCheckboxChange"
|
|
|
+ v-loading="loading"
|
|
|
+ >
|
|
|
+ <vxe-column type="checkbox" width="60" v-if="multiple"></vxe-column>
|
|
|
+ <vxe-column type="radio" width="40" v-else> </vxe-column>
|
|
|
+ <vxe-column v-for="(item, index) in tableConfig?.column" :key="index" v-bind="item">
|
|
|
+ <template #default="slotProps" v-if="item.slot">
|
|
|
+ <slot :name="item.slot" v-bind="slotProps || {}"></slot>
|
|
|
+ </template>
|
|
|
+ </vxe-column>
|
|
|
+ </vxe-table>
|
|
|
+ <div class="table-select-pagination" v-if="tableTotal">
|
|
|
+ <el-pagination
|
|
|
+ background
|
|
|
+ layout="total, prev, pager, next, jumper"
|
|
|
+ size="small"
|
|
|
+ :pager-count="5"
|
|
|
+ :page-size="tableConfig && tableConfig.pageSize"
|
|
|
+ :total="tableTotal"
|
|
|
+ v-model:current-page="pageNo"
|
|
|
+ @current-change="paginationChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <!-- 下方插槽 -->
|
|
|
+ <slot name="bottom-extra"></slot>
|
|
|
+ </el-popover>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.table-select-container {
|
|
|
+ width: 100%;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ .select-clear {
|
|
|
+ position: absolute;
|
|
|
+ right: 10px;
|
|
|
+ top: 49%;
|
|
|
+ transform: translateY(-50%);
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 50%;
|
|
|
+ overflow: hidden;
|
|
|
+ z-index: 2;
|
|
|
+ color: #fff;
|
|
|
+ background-color: var(--el-color-info-light-3);
|
|
|
+ opacity: 0;
|
|
|
+ &:hover {
|
|
|
+ background-color: var(--el-color-info);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .select-down {
|
|
|
+ transition: transform 0.2s;
|
|
|
+ }
|
|
|
+ .select-down-rotate {
|
|
|
+ transform: rotate(180deg);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-multiple {
|
|
|
+ :deep(.el-input) {
|
|
|
+ position: absolute;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &.is-visible {
|
|
|
+ :deep(.el-input:not(.is-disabled) .el-input__wrapper) {
|
|
|
+ box-shadow: 0 0 0 1px var(--el-color-primary) inset;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ .select-clear {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.table-multiple-tag {
|
|
|
+ position: relative;
|
|
|
+ top: 0px;
|
|
|
+ width: calc(100% - 24px);
|
|
|
+ height: 100%;
|
|
|
+ min-height: 34px;
|
|
|
+ z-index: 2;
|
|
|
+ padding: 0px 10px 2px 10px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ &.is-disabled {
|
|
|
+ cursor: not-allowed;
|
|
|
+ }
|
|
|
+}
|
|
|
+.table-select-pagination {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ margin-top: 12px;
|
|
|
+}
|
|
|
+</style>
|