143 lines
3.9 KiB
Vue
143 lines
3.9 KiB
Vue
<template>
|
||
<!--
|
||
v-bind="$attrs" 透传父组件传递的 width, class, style 以及 change, focus 等事件
|
||
-->
|
||
<el-select
|
||
v-model="modelValue"
|
||
v-bind="$attrs"
|
||
:placeholder="placeholder"
|
||
:disabled="disabled"
|
||
:clearable="clearable"
|
||
:filterable="filterable"
|
||
:multiple="multiple"
|
||
:collapse-tags="collapseTags"
|
||
:collapse-tags-tooltip="collapseTagsTooltip"
|
||
>
|
||
<!-- 遍历生成选项 -->
|
||
<el-option
|
||
v-for="(item, index) in options"
|
||
:key="index"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
:disabled="item.disabled"
|
||
>
|
||
<!-- 支持自定义 option 模板 (可选),不传则只显示 label -->
|
||
<slot name="option" :item="item">
|
||
<span>{{ item.label }}</span>
|
||
</slot>
|
||
</el-option>
|
||
|
||
<!-- 透传 el-select 的其他插槽 (如 prefix, empty) -->
|
||
<template v-for="(_, name) in $slots" #[name]="slotData">
|
||
<slot v-if="name !== 'option'" :name="name" v-bind="slotData || {}"></slot>
|
||
</template>
|
||
</el-select>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
import { useDictStore } from '@/store/modules/dict'
|
||
import { useI18n } from 'vue-i18n'
|
||
|
||
defineOptions({ name: 'SaSelect', inheritAttrs: false })
|
||
|
||
interface Props {
|
||
/** 字典编码 (必填) */
|
||
dict: string
|
||
|
||
/**
|
||
* 强制转换字典值的类型
|
||
* 解决后端返回字符串但前端表单需要数字的问题
|
||
*/
|
||
valueType?: 'number' | 'string'
|
||
|
||
// --- 以下为常用属性显式定义,为了 IDE 提示友好 ---
|
||
placeholder?: string
|
||
disabled?: boolean
|
||
clearable?: boolean
|
||
filterable?: boolean
|
||
multiple?: boolean
|
||
collapseTags?: boolean
|
||
collapseTagsTooltip?: boolean
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
placeholder: '请选择',
|
||
disabled: false,
|
||
clearable: true, // 下拉框默认开启清除,体验更好
|
||
filterable: false, // 下拉框默认关闭搜索
|
||
multiple: false,
|
||
collapseTags: false,
|
||
collapseTagsTooltip: false,
|
||
valueType: 'number'
|
||
})
|
||
|
||
// 支持单选(string/number) 或 多选(Array)
|
||
const modelValue = defineModel<string | number | Array<string | number>>()
|
||
|
||
const dictStore = useDictStore()
|
||
const { t, te, locale } = useI18n()
|
||
|
||
// 判断能否转成数字
|
||
const canConvertToNumberStrict = (value: any) => {
|
||
// 严格模式:排除 null、undefined、布尔值、空数组等
|
||
if (value == null) return false
|
||
if (typeof value === 'boolean') return false
|
||
if (Array.isArray(value) && value.length !== 1) return false
|
||
if (typeof value === 'object' && !Array.isArray(value)) return false
|
||
|
||
const num = Number(value)
|
||
return !isNaN(num)
|
||
}
|
||
|
||
// 计算属性:获取字典数据并处理类型转换
|
||
const options = computed(() => {
|
||
// 让字典选项在切换语言时可响应更新
|
||
locale.value
|
||
|
||
const list = dictStore.getByCode(props.dict) || []
|
||
|
||
// 1. 如果没有指定 valueType,直接返回
|
||
if (!props.valueType) {
|
||
return list.map((item) => {
|
||
const key = `dict.${props.dict}.${item.value}`
|
||
return {
|
||
...item,
|
||
label: te(key) ? t(key) : item.label
|
||
}
|
||
})
|
||
}
|
||
|
||
// 2. 如果指定了类型,进行映射转换
|
||
return list.map((item) => {
|
||
let newValue = item.value
|
||
switch (props.valueType) {
|
||
case 'number':
|
||
if (canConvertToNumberStrict(item.value)) {
|
||
newValue = Number(item.value)
|
||
}
|
||
break
|
||
case 'string':
|
||
newValue = String(item.value)
|
||
break
|
||
}
|
||
|
||
return {
|
||
...item,
|
||
value: newValue,
|
||
label: (() => {
|
||
const key = `dict.${props.dict}.${newValue}`
|
||
return te(key) ? t(key) : item.label
|
||
})()
|
||
}
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 让 Select 默认宽度占满父容器,通常在表单中体验更好,可视情况删除 */
|
||
.el-select {
|
||
width: 100%;
|
||
}
|
||
</style>
|