| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469 |
- <script setup lang="ts">
- import { ElPopover } from 'element-plus'
- import type { VxeTableInstance } from 'vxe-table'
- import type { PopoverProps, TagProps } from 'element-plus'
- interface TableColumn {
- // 类型
- type?: string
- // 列名
- field?: string
- // 表格宽度
- width?: number | string
- // 对齐方式
- align?: string
- // 标题
- title?: string
- // 插槽
- slot?: string
- }
- interface TableConfig {
- // 表格列
- column?: Array<TableColumn>
- // 表格数据
- request: (...args: any) => Promise<any>
- // 每页显示条数
- pageSize?: Number
- }
- interface TableSelectProps {
- // 绑定值
- modelValue: any
- // 是否多选
- multiple?: boolean
- // 是否禁用
- disabled?: boolean
- // 是否支持清除
- clearable?: boolean
- // value 的属性名
- valueKey?: string
- // label 的属性名
- labelKey?: string
- // 回显数据,用于后端分页显示
- initValue?: any
- // 气泡位置
- placement?: PopoverProps['placement']
- // 占位符
- placeholder?: string
- // popover 宽度
- popperWidth?: number | string
- // 自定义 popper 类名
- popperClass?: string
- // popper 配置项
- popperOptions?: PopoverProps['popperOptions']
- // 表格配置
- tableConfig?: TableConfig
- // tag 类型
- tagType?: TagProps['type']
- // 最多显示多少个tag
- maxTagCount?: number
- // 自定义数据转换方法
- transformData?: Function
- }
- const props = withDefaults(defineProps<TableSelectProps>(), {
- modelValue: '',
- clearable: true,
- valueKey: 'id',
- labelKey: 'name',
- placement: 'bottom-start',
- placeholder: '请选择',
- popperWidth: 560,
- tagType: 'info',
- maxTagCount: 5
- })
- const emits = defineEmits(['update:modelValue', 'change', 'clear', 'item-clear', 'focus', 'blur'])
- /* 格式化提交数据 */
- 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?.request === 'function') {
- loading.value = true
- if (where && where.pageNo) {
- pageNo.value = where.pageNo
- }
- props.tableConfig
- .request({
- 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.request 必须为 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
- :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 }" v-else>
- <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;
- opacity: 0;
- &:hover {
- opacity: 100;
- }
- }
- .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>
|