Files
guoju0808/ry-vue3/src/components/DoctorDeptSelect.vue
T

161 lines
4.7 KiB
Vue

<template>
<!--
公共科室下拉控件 ("其他"逃生口)
行为契约 (前端控件自管理, 父组件无感知):
- 标准字典项: 用户从下拉选 v-model = 字典 label/value
- "其他" (控件自己虚拟追加, 不来自后端字典): 下方出现 el-input v-model = input 内容
- 回填: modelValue 不在字典里 自动按 "其他 + input" 显示, input 预填原值
用法 (与之前一样, 父组件不变):
<doctor-dept-select v-model="form.department" value-field="label" filterable placeholder="搜索科室" />
-->
<div class="doctor-other-select">
<el-select
v-model="selectedKey"
:placeholder="placeholder"
:disabled="disabled"
:clearable="clearable"
:filterable="filterable"
:loading="loading"
style="width: 100%"
@change="handleSelectChange"
@clear="handleClear"
>
<el-option
v-for="o in displayOptions"
:key="o.key"
:label="o.label"
:value="o.key"
/>
</el-select>
<el-input
v-if="showOtherInput"
v-model="otherText"
:placeholder="otherPlaceholder"
class="other-input"
maxlength="50"
@input="emitValue"
/>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { getActiveDict, DICT_TYPE } from '@/api/dict'
const props = defineProps({
modelValue: { type: [String, Number], default: null },
/** 'value' (ID) 或 'label' (科室名称字符串) */
valueField: { type: String, default: 'label' },
placeholder: { type: String, default: '请选择科室' },
otherPlaceholder: { type: String, default: '请输入其他科室' },
disabled: { type: Boolean, default: false },
clearable: { type: Boolean, default: true },
filterable: { type: Boolean, default: false }
})
const emit = defineEmits(['update:modelValue', 'change'])
// "其他" 虚拟项的内部 key — 不暴露给父组件, 仅用于区分 selectedKey 状态
const OTHER_KEY = '__other__'
// 原始后端字典项 (label/value/sort/...)
const rawOptions = ref([])
const loading = ref(false)
// el-select 内部绑定的 key (可能 = 字典值, 可能 = OTHER_KEY)
const selectedKey = ref(props.modelValue ?? null)
// "其他" 输入框独立内容
const otherText = ref(props.modelValue ?? '')
// 是否显示下方 input
const showOtherInput = computed(() => selectedKey.value === OTHER_KEY)
// 给 el-select 渲染的 options (raw + 末尾追加 "其他" 虚拟项)
const displayOptions = computed(() => {
const raw = rawOptions.value.map(o => ({
key: props.valueField === 'label' ? o.label : o.value,
label: o.label
}))
return [...raw, { key: OTHER_KEY, label: '其他' }]
})
// 检测 modelValue 是否在 rawOptions 里
function isValueInOptions(v) {
if (v == null || v === '') return false
return rawOptions.value.some(o => (props.valueField === 'label' ? o.label : o.value) === v)
}
// 从外部 modelValue 同步 → 内部 selectedKey + otherText
function syncFromModelValue(v) {
if (v == null || v === '') {
selectedKey.value = null
otherText.value = ''
return
}
if (isValueInOptions(v)) {
selectedKey.value = v
otherText.value = ''
} else {
// 不在字典里 → 走 "其他" 路径
selectedKey.value = OTHER_KEY
otherText.value = v
}
}
// 内部状态 → emit 给父组件
function emitValue() {
let v
if (selectedKey.value === OTHER_KEY) {
v = otherText.value || ''
} else if (selectedKey.value == null) {
v = ''
} else {
v = selectedKey.value
}
emit('update:modelValue', v)
emit('change', v)
}
function handleSelectChange(key) {
// 切换到 "其他" 时, 若原 selectedKey 不是 OTHER_KEY (即之前不是 "其他"), 清空 otherText 让用户从零输入
if (key === OTHER_KEY && selectedKey.value !== OTHER_KEY) {
otherText.value = ''
}
selectedKey.value = key
emitValue()
}
function handleClear() {
selectedKey.value = null
otherText.value = ''
emitValue()
}
async function load() {
loading.value = true
try {
rawOptions.value = await getActiveDict(DICT_TYPE.DEPARTMENT)
} catch (e) {
console.warn('[DoctorDeptSelect] load failed', e)
} finally {
loading.value = false
}
}
onMounted(async () => {
await load()
// 字典加载完后再做一次同步: 避免初始 modelValue 在字典未加载时被错判为 "其他"
syncFromModelValue(props.modelValue)
})
watch(() => props.modelValue, v => syncFromModelValue(v))
watch(() => rawOptions.value, () => syncFromModelValue(props.modelValue))
</script>
<style scoped>
.doctor-other-select { display: flex; flex-direction: column; gap: 6px; width: 100%; }
.other-input { width: 100%; }
</style>