清空 relation/select 时 el-* 常 emit undefined,进 submitData 后 JSON.stringify 丢键 → 后端收不到、看似'提交无效'。修: - handleSave 编辑 diff 把值归一成 null(??null),清空 → null → 提交 → 后端置空 - FieldRelation 清空归一成 null(单)/[](多),避免 ObjectId 存 '' cast 报错
140 lines
5.6 KiB
Vue
140 lines
5.6 KiB
Vue
<template>
|
||
<template v-if="mode === 'display'">
|
||
<template v-if="field.multiple">
|
||
<span v-if="!displayItems.length">-</span>
|
||
<template v-for="(it, i) in displayItems" :key="i"><a v-if="jumpable" class="rel-link" @click.stop="goto(it.id)">{{ it.name }}</a><span v-else>{{ it.name }}</span><span v-if="i < displayItems.length - 1">、</span></template>
|
||
</template>
|
||
<template v-else>
|
||
<a v-if="jumpable && modelValue != null" class="rel-link" @click.stop="goto(modelValue)">{{ displayName }}</a>
|
||
<span v-else>{{ displayName || '-' }}</span>
|
||
</template>
|
||
</template>
|
||
<el-select
|
||
v-else-if="mode === 'filter'"
|
||
:model-value="stringValue"
|
||
@update:model-value="$emit('update:modelValue', $event)"
|
||
multiple clearable filterable remote :remote-method="onSearch" :loading="loading"
|
||
style="width: 100%"
|
||
>
|
||
<el-option v-for="opt in options" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
<el-select v-else :model-value="stringValue" @update:model-value="onEdit" :disabled="disabled" :multiple="field.multiple" :clearable="!field.required" filterable remote :remote-method="onSearch" :loading="loading" style="width: 100%">
|
||
<el-option v-for="opt in options" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, inject, onMounted, computed, type Ref } from 'vue'
|
||
import { vueAdmin } from '../../VueAdmin'
|
||
import { ITableCrudProto } from '../../IShrimpType'
|
||
|
||
const props = defineProps<{
|
||
field: any
|
||
modelValue: any
|
||
mode: 'display' | 'edit' | 'filter'
|
||
disabled?: boolean
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
'update:modelValue': [value: any]
|
||
}>()
|
||
|
||
// 编辑态清空:el-select 单选清空发 '' 或 undefined,relation 是 ObjectId、且 undefined 会被
|
||
// JSON.stringify 丢键 → 都归一成 null(取消关联)。多选清空是 [],原样。
|
||
function onEdit(v: any) {
|
||
emit('update:modelValue', v === '' || v == null ? (props.field.multiple ? [] : null) : v)
|
||
}
|
||
|
||
const options = ref<{ label: string; value: string }[]>([])
|
||
const loading = ref(false)
|
||
|
||
// 显示用:从 ShrimpTable provide 的 refs 字典按 id 查名(替代旧的 _label)
|
||
const refs = inject<Ref<Record<string, Record<string, any>>>>('shrimpRefs', ref<Record<string, Record<string, any>>>({}))
|
||
const dict = computed(() => refs.value?.[props.field.relation?.moduleKey] ?? {})
|
||
const displayName = computed(() => {
|
||
const id = props.modelValue
|
||
if (id == null) return ''
|
||
return dict.value[String(id)] ?? String(id)
|
||
})
|
||
const displayItems = computed(() =>
|
||
(Array.isArray(props.modelValue) ? props.modelValue : [])
|
||
.filter((id: any) => id != null)
|
||
.map((id: any) => ({ id, name: dict.value[String(id)] ?? String(id) }))
|
||
)
|
||
|
||
// 单项查看跳转:目标模块可达(已注册路由)且非子模块时,名字可点 → 跳目标模块并过滤到该 _id(单行)
|
||
const jumpable = computed(() =>
|
||
props.mode === 'display' && !!props.field.relation && !!vueAdmin.routeForModule(props.field.relation.moduleKey)
|
||
)
|
||
function goto(id: any) {
|
||
const r = props.field.relation && vueAdmin.routeForModule(props.field.relation.moduleKey)
|
||
if (!r || id == null) return
|
||
vueAdmin.router.push({ name: r.name, query: { filter: JSON.stringify({ _id: String(id) }) } })
|
||
}
|
||
|
||
/** 统一转为字符串值,解决 ObjectId 与 el-select 的匹配问题 */
|
||
const stringValue = computed(() => {
|
||
// filter 模式强制 multiple,始终返回数组
|
||
if (props.mode === 'filter' || props.field.multiple) {
|
||
return Array.isArray(props.modelValue) ? props.modelValue.map(String) : []
|
||
}
|
||
return props.modelValue != null ? String(props.modelValue) : undefined
|
||
})
|
||
|
||
async function onSearch(query: string) {
|
||
if (!props.field.relation) return
|
||
loading.value = true
|
||
try {
|
||
const api = vueAdmin.getModule<ITableCrudProto<any>>(props.field.relation.moduleKey)
|
||
const listQuery: any = { keyword: query, pageSize: 20 }
|
||
if (props.field.filter) {
|
||
listQuery.filter = props.field.filter
|
||
}
|
||
const res = await api.list(listQuery)
|
||
const list = res.list || res as any
|
||
options.value = list.map((item: any) => ({
|
||
label: item[props.field.relation.labelKey],
|
||
value: String(item._id),
|
||
}))
|
||
} catch (e) {
|
||
console.warn('[FieldRelation] search failed', e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
if ((props.mode === 'edit' || props.mode === 'filter') && props.field.relation) {
|
||
await onSearch('')
|
||
// 确保当前值在选项中
|
||
const currentValues = props.field.multiple
|
||
? (Array.isArray(props.modelValue) ? props.modelValue.map(String) : [])
|
||
: (props.modelValue ? [String(props.modelValue)] : [])
|
||
for (const val of currentValues) {
|
||
if (val && !options.value.some(o => o.value === val)) {
|
||
try {
|
||
const api = vueAdmin.getModule<ITableCrudProto<any>>(props.field.relation.moduleKey)
|
||
const current = await api.get(val)
|
||
if (current) {
|
||
options.value.unshift({
|
||
label: (current as any)[props.field.relation.labelKey],
|
||
value: String(val),
|
||
})
|
||
}
|
||
} catch { }
|
||
}
|
||
}
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.rel-link {
|
||
color: var(--el-color-primary);
|
||
cursor: pointer;
|
||
}
|
||
.rel-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
</style>
|