303 lines
6.0 KiB
Vue
303 lines
6.0 KiB
Vue
<template>
|
|
<a-select v-bind="$attrs" :loading="loading" :options="options" :field-names="fieldNames" @focus="handleFocus">
|
|
<template v-for="(_, slot) of $slots" #[slot]="scope">
|
|
<slot :name="slot" v-bind="scope || {}" />
|
|
</template>
|
|
</a-select>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch } from 'vue'
|
|
import { useDictionaryStore } from '@/stores/modules/dictionary'
|
|
import request from '@/utils/request'
|
|
|
|
defineOptions({
|
|
name: 'ScSelect',
|
|
inheritAttrs: false,
|
|
})
|
|
|
|
const props = defineProps({
|
|
// 数据源类型:data(直接数据)、api(接口数据)、dictionary(字典数据)
|
|
sourceType: {
|
|
type: String,
|
|
default: 'data',
|
|
validator: (value) => ['data', 'api', 'dictionary'].includes(value),
|
|
},
|
|
// 直接数据(当 sourceType 为 data 时使用)
|
|
data: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
// API 接口地址(当 sourceType 为 api 时使用)
|
|
api: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
// API 请求参数(当 sourceType 为 api 时使用)
|
|
apiParams: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
// 字典编码(当 sourceType 为 dictionary 时使用)
|
|
dictionaryCode: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
// 是否启用 API 数据缓存(当 sourceType 为 api 时使用)
|
|
enableApiCache: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
// API 缓存时间(毫秒,当 sourceType 为 api 时使用)
|
|
apiCacheTime: {
|
|
type: Number,
|
|
default: 5 * 60 * 1000, // 默认 5 分钟
|
|
},
|
|
// 字段映射配置
|
|
fieldNames: {
|
|
type: Object,
|
|
default: () => ({
|
|
label: 'label',
|
|
value: 'value',
|
|
}),
|
|
},
|
|
// 是否在组件挂载时立即加载数据
|
|
immediate: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
// 数据处理函数,用于自定义数据格式转换
|
|
dataProcessor: {
|
|
type: Function,
|
|
default: null,
|
|
},
|
|
})
|
|
|
|
const dictionaryStore = useDictionaryStore()
|
|
const loading = ref(false)
|
|
const apiData = ref(null)
|
|
const apiCacheTime = ref(null)
|
|
const options = computed(() => {
|
|
switch (props.sourceType) {
|
|
case 'data':
|
|
return processData(props.data)
|
|
case 'api':
|
|
return processData(apiData.value || [])
|
|
case 'dictionary':
|
|
return processData(getDictionaryData())
|
|
default:
|
|
return []
|
|
}
|
|
})
|
|
|
|
// API 数据缓存
|
|
const apiCache = new Map()
|
|
|
|
/**
|
|
* 处理数据格式
|
|
*/
|
|
function processData(data) {
|
|
if (!data || !Array.isArray(data)) {
|
|
return []
|
|
}
|
|
|
|
// 如果有自定义处理函数,使用自定义处理
|
|
if (props.dataProcessor && typeof props.dataProcessor === 'function') {
|
|
return props.dataProcessor(data)
|
|
}
|
|
|
|
// 默认处理:确保数据有 label 和 value 字段
|
|
return data.map((item) => {
|
|
if (typeof item === 'string' || typeof item === 'number') {
|
|
return {
|
|
label: item,
|
|
value: item,
|
|
}
|
|
}
|
|
// 如果已经有 label 和 value 字段,直接返回
|
|
if (item.label !== undefined && item.value !== undefined) {
|
|
return item
|
|
}
|
|
// 尝试使用 fieldNames 映射
|
|
const labelKey = props.fieldNames.label || 'label'
|
|
const valueKey = props.fieldNames.value || 'value'
|
|
return {
|
|
label: item[labelKey] || item.name || item.title || item.id,
|
|
value: item[valueKey] !== undefined ? item[valueKey] : item.id,
|
|
...item,
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 获取字典数据
|
|
*/
|
|
function getDictionaryData() {
|
|
if (!props.dictionaryCode) {
|
|
return []
|
|
}
|
|
return dictionaryStore.dictionaries[props.dictionaryCode] || []
|
|
}
|
|
|
|
/**
|
|
* 加载 API 数据
|
|
*/
|
|
async function loadApiData() {
|
|
if (!props.api) {
|
|
return
|
|
}
|
|
|
|
// 检查缓存
|
|
if (props.enableApiCache) {
|
|
const cacheKey = props.api + JSON.stringify(props.apiParams)
|
|
const cached = apiCache.get(cacheKey)
|
|
|
|
if (cached && Date.now() - cached.time < props.apiCacheTime) {
|
|
apiData.value = cached.data
|
|
return
|
|
}
|
|
}
|
|
|
|
loading.value = true
|
|
try {
|
|
const res = await request.get(props.api, { params: props.apiParams })
|
|
|
|
if (res.code === 200) {
|
|
const data = res.data || res.list || []
|
|
apiData.value = data
|
|
|
|
// 缓存数据
|
|
if (props.enableApiCache) {
|
|
const cacheKey = props.api + JSON.stringify(props.apiParams)
|
|
apiCache.set(cacheKey, {
|
|
data,
|
|
time: Date.now(),
|
|
})
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('加载 API 数据失败:', error)
|
|
apiData.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 加载字典数据
|
|
*/
|
|
async function loadDictionaryData() {
|
|
if (!props.dictionaryCode) {
|
|
return
|
|
}
|
|
|
|
// 检查缓存
|
|
if (dictionaryStore.dictionaries[props.dictionaryCode]) {
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
try {
|
|
await dictionaryStore.getDictionary(props.dictionaryCode)
|
|
} catch (error) {
|
|
console.error('加载字典数据失败:', error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 处理 focus 事件,按需加载数据
|
|
*/
|
|
async function handleFocus() {
|
|
switch (props.sourceType) {
|
|
case 'api':
|
|
if (!apiData.value || apiData.value.length === 0) {
|
|
await loadApiData()
|
|
}
|
|
break
|
|
case 'dictionary':
|
|
if (!dictionaryStore.dictionaries[props.dictionaryCode]) {
|
|
await loadDictionaryData()
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 强制刷新数据
|
|
*/
|
|
async function refresh() {
|
|
switch (props.sourceType) {
|
|
case 'api':
|
|
await loadApiData()
|
|
break
|
|
case 'dictionary':
|
|
await dictionaryStore.getDictionary(props.dictionaryCode, true)
|
|
break
|
|
}
|
|
}
|
|
|
|
// 监听数据源变化
|
|
watch(
|
|
() => props.data,
|
|
(newVal) => {
|
|
if (props.sourceType === 'data') {
|
|
// data 类型不需要特殊处理,计算属性会自动更新
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
watch(
|
|
() => props.api,
|
|
() => {
|
|
if (props.sourceType === 'api') {
|
|
apiData.value = null
|
|
}
|
|
},
|
|
)
|
|
|
|
watch(
|
|
() => props.apiParams,
|
|
() => {
|
|
if (props.sourceType === 'api') {
|
|
apiData.value = null
|
|
}
|
|
},
|
|
{ deep: true },
|
|
)
|
|
|
|
watch(
|
|
() => props.dictionaryCode,
|
|
() => {
|
|
if (props.sourceType === 'dictionary') {
|
|
// dictionary 变化时,数据会自动从 store 获取
|
|
}
|
|
},
|
|
)
|
|
|
|
// 组件挂载时立即加载数据
|
|
if (props.immediate) {
|
|
switch (props.sourceType) {
|
|
case 'api':
|
|
loadApiData()
|
|
break
|
|
case 'dictionary':
|
|
loadDictionaryData()
|
|
break
|
|
}
|
|
}
|
|
|
|
// 暴露方法供外部调用
|
|
defineExpose({
|
|
refresh,
|
|
loadApiData,
|
|
loadDictionaryData,
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
// 组件样式继承自 a-select
|
|
</style>
|