Files
shanxia2.0/web-admin/src/components/DictSelect/index.vue
T
2026-09-26 11:59:39 +08:00

183 lines
5.0 KiB
Vue

<template>
<el-select
:model-value="modelValue"
:placeholder="placeholder"
:clearable="clearable"
:filterable="filterable"
:allow-create="allowCreate"
:default-first-option="allowCreate"
:disabled="disabled || loading"
:size="size"
:style="style"
:multiple="multiple"
:collapse-tags="multiple"
:collapse-tags-tooltip="multiple"
@update:model-value="onSelect"
>
<el-option
v-for="opt in options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
>
<div class="dict-select-option">
<span>{{ opt.label }}</span>
<el-button
v-if="dictStore.isSessionCreated(dictType, opt.value)"
link
type="danger"
size="small"
class="dict-select-option__remove"
@click.stop="onRemove(opt.value)"
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
</el-option>
</el-select>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Delete } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useDictStore } from '@/store/modules/dict'
import { normalizePackageSpecs } from '@/utils/crm-package-spec'
const props = withDefaults(
defineProps<{
dictType: string
modelValue?: string | string[] | null
allowCreate?: boolean
clearable?: boolean
filterable?: boolean
disabled?: boolean
multiple?: boolean
placeholder?: string
size?: '' | 'default' | 'small' | 'large'
style?: string | Record<string, string>
}>(),
{
modelValue: '',
allowCreate: true,
clearable: true,
filterable: true,
disabled: false,
multiple: false,
placeholder: '请选择',
size: 'default'
}
)
const emit = defineEmits<{
'update:modelValue': [value: string | string[]]
change: [value: string | string[]]
}>()
const dictStore = useDictStore()
const loading = ref(false)
const options = computed(() => dictStore.getOptions(props.dictType))
function isKnown(value: string) {
return options.value.some((o) => o.value === value || o.label === value)
}
async function resolveKnownValue(next: string): Promise<string | null> {
const trimmed = String(next ?? '').trim()
if (!trimmed) return null
if (!props.allowCreate || isKnown(trimmed)) {
const known = options.value.find((o) => o.value === trimmed || o.label === trimmed)
return known?.value ?? trimmed
}
try {
await ElMessageBox.confirm(`将「${trimmed}」新增到字典,是否继续?`, '新增字典项', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
})
} catch {
return null
}
loading.value = true
try {
const data = await dictStore.ensure(props.dictType, trimmed)
return data.value
} catch (e: any) {
ElMessage.error(e?.message || '写入字典失败')
return null
} finally {
loading.value = false
}
}
async function onSelect(raw: string | string[] | null | undefined) {
if (props.multiple) {
const incoming = Array.isArray(raw) ? raw : raw == null || raw === '' ? [] : [String(raw)]
const result: string[] = []
for (const item of incoming) {
const resolved = await resolveKnownValue(String(item))
if (resolved) result.push(resolved)
}
const deduped = normalizePackageSpecs(result)
emit('update:modelValue', deduped)
emit('change', deduped)
return
}
const next = raw == null ? '' : String(raw).trim()
if (!next) {
emit('update:modelValue', '')
emit('change', '')
return
}
const resolved = await resolveKnownValue(next)
if (resolved == null) return
emit('update:modelValue', resolved)
emit('change', resolved)
}
async function onRemove(value: string) {
try {
await ElMessageBox.confirm(`确定从字典中删除「${value}」吗?`, '删除字典项', {
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消'
})
} catch {
return
}
loading.value = true
try {
await dictStore.ensureRemove(props.dictType, value)
if (props.multiple) {
const current = Array.isArray(props.modelValue) ? props.modelValue : []
const next = current.filter((v) => v !== value)
emit('update:modelValue', next)
emit('change', next)
} else if (props.modelValue === value) {
emit('update:modelValue', '')
emit('change', '')
}
ElMessage.success('已删除')
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
} finally {
loading.value = false
}
}
</script>
<style scoped>
.dict-select-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.dict-select-option__remove {
flex-shrink: 0;
}
</style>