Files
tape-springboot-sysadmin/src/iview/form/IViewFormSelect.vue
iqudoo eb4e8f1a04 init
2026-06-05 17:22:32 +08:00

408 lines
10 KiB
Vue

<template>
<template v-if="supportedRemoteSearch">
<a-select
style="width: 100%"
:value="currentValue"
:mode="multiple ? 'multiple' : '-'"
:id="'form_item_' + formatFieldName(field.name)"
:disabled="readOnly"
:allowClear="field.clearable"
:placeholder="placeholder"
:filter-option="supportedFilterOption"
:show-search="supportedRemoteSearch"
@search="handleRemoteSearchValue"
@change="handleChange"
>
<template v-if="remoteSearchLoading || remoteOptionsLoading" #notFoundContent>
<a-spin size="small" />
</template>
<template
v-else-if="supportedRemoteSearch || supportedRemoteOptions"
#notFoundContent
>
{{
remoteSearchErrorMsg
? remoteSearchErrorMsg
: remoteOptionsErrorMsg
? remoteOptionsErrorMsg
: notFoundContent
}}
<a-button
v-if="remoteSearchErrorMsg || remoteOptionsErrorMsg"
size="small"
type="default"
@click="handleRefresh"
>
刷新
</a-button>
</template>
<a-select-option
v-for="option in parseOptions(field.options)"
:key="option.value"
:value="option.value"
>
{{ readLabel(option.label) }}
<template v-if="showValueTag">
<span style="color: #999; font-size: 12px"> {{ option.value }} </span>
</template>
<template v-else>
<span v-if="!!readTag(option.label)" style="color: #999; font-size: 12px">
{{ readTag(option.label) }}
</span>
</template>
</a-select-option>
</a-select>
</template>
<template v-else>
<a-select
style="width: 100%"
:value="currentValue"
:mode="multiple ? 'multiple' : '-'"
:id="'form_item_' + formatFieldName(field.name)"
:disabled="readOnly"
:allowClear="field.clearable"
:placeholder="placeholder"
:filter-option="supportedFilterOption"
:show-search="false"
@change="handleChange"
>
<template v-if="remoteSearchLoading || remoteOptionsLoading" #notFoundContent>
<a-spin size="small" />
</template>
<template
v-else-if="supportedRemoteSearch || supportedRemoteOptions"
#notFoundContent
>
{{
remoteSearchErrorMsg
? remoteSearchErrorMsg
: remoteOptionsErrorMsg
? remoteOptionsErrorMsg
: notFoundContent
}}
<a-button
v-if="remoteSearchErrorMsg || remoteOptionsErrorMsg"
size="small"
type="default"
@click="handleRefresh"
>
刷新
</a-button>
</template>
<a-select-option
v-for="option in parseOptions(field.options)"
:key="option.value"
:value="option.value"
>
{{ readLabel(option.label) }}
<template v-if="showValueTag">
<span style="color: #999; font-size: 12px"> {{ option.value }} </span>
</template>
<template v-else>
<span v-if="!!readTag(option.label)" style="color: #999; font-size: 12px">
{{ readTag(option.label) }}
</span>
</template>
</a-select-option>
</a-select>
</template>
</template>
<script setup>
import { ref, computed, onMounted, watch } from "vue";
import { useVModel } from "@/iview/utils/useModel";
import { delayCall } from "@/iview/utils/delayCall";
import { callFunc } from "@/iview/utils/func";
import { toAny } from "@/iview/utils/toany";
const props = defineProps({
field: {
type: Object,
required: true,
},
fieldPrefix: {
type: String,
default: "",
},
modelValue: {
type: Object,
required: true,
},
placeholder: {
type: String,
default: "请选择",
},
readOnly: {
type: Boolean,
default: false,
},
rawFormData: {
type: Object,
},
});
const emit = defineEmits(["update:modelValue"]);
const vModel = useVModel(props, "modelValue", emit);
const initValueFlag = ref(false);
const remoteOptionsData = ref([]);
const remoteOptionsLoading = ref(false);
const remoteOptionsErrorMsg = ref(null);
const remoteSearchValue = ref("");
const remoteSearchData = ref([]);
const remoteSearchLoading = ref(false);
const remoteSearchErrorMsg = ref(null);
const remoteSearchId = ref(0);
const handleChange = (value) => {
if (multiple.value && maxSelectCount.value > 0 && value.length > maxSelectCount.value) {
return;
}
vModel.value[formatFieldName(props.field.name)] = value;
};
const maxSelectCount = computed(() => {
return props.field.maxSelectCount || 0;
});
const showValueTag = computed(() => {
return !!props.field.showValueTag;
});
const notFoundContent = computed(() => {
if (!supportedRemoteSearch.value) {
return "暂无可选项";
}
return props.field.notFoundContent || !!remoteSearchValue.value
? "暂无可选项"
: "输入关键字搜索";
});
const defaultValueList = computed(() => {
return props.field.defaultValueList || [];
});
const forceInOptions = computed(() => {
return props.field.forceInOptions === true;
});
const multiple = computed(() => {
return props.field.multiple || false;
});
// 解析选项字符串或数组
const parseOptions = (options) => {
if (remoteSearchData.value && remoteSearchData.value.length > 0) {
return remoteSearchData.value;
}
if (remoteOptionsData.value && remoteOptionsData.value.length > 0) {
return remoteOptionsData.value;
}
if (supportedRemoteSearch.value && currentValue.value) {
return [
{
label: currentValue.value,
value: currentValue.value,
},
];
}
if (!options) return [];
// 如果已经是数组,直接返回
if (Array.isArray(options)) {
return options;
}
// 如果是字符串,按换行符分割,再按|分割键值对
if (typeof options === "string") {
return options
.split("\n")
.filter((line) => line.trim() !== "")
.map((line) => {
const parts = line.split("|");
return {
label: parts[0]?.trim() || "",
value: parts[1]?.trim() || parts[0]?.trim() || "",
};
});
}
return [];
};
const inOption = (value) => {
let optionValues = parseOptions(props.field.options).map((item) => {
return item.value;
});
return optionValues.includes(value);
};
const formatFieldName = (name) => {
if (!props.fieldPrefix) {
return name;
}
return props.fieldPrefix + "-" + name;
};
const readLabel = (value) => {
return `${value}`.split("#")[0];
};
const readTag = (value) => {
return `${value}`.split("#")[1];
};
const currentValue = computed(() => {
let value = vModel.value[formatFieldName(props.field.name)];
if (value === undefined || value === null) {
return undefined;
}
if (!!value || inOption(value)) {
return value;
}
return undefined;
});
const supportedRemoteSearch = computed(() => {
return (
!!props.field.remoteSearchMethod &&
typeof props.field.remoteSearchMethod === "function"
);
});
const supportedRemoteOptions = computed(() => {
return (
!!props.field.remoteOptionsMethod &&
typeof props.field.remoteOptionsMethod === "function"
);
});
const supportedFilterOption = computed(() => {
return !!props.field.supportedFilterOption && !supportedRemoteSearch.value;
});
const handleRefresh = () => {
if (!supportedRemoteSearch.value && !supportedRemoteOptions.value) {
return;
}
if (supportedRemoteSearch.value && !!remoteSearchValue.value) {
handleRemoteSearchValue(remoteSearchValue.value);
}
if (supportedRemoteOptions.value) {
handleRemoteOptions();
}
};
const handleRemoteOptions = () => {
if (!supportedRemoteOptions.value) {
return;
}
remoteOptionsLoading.value = true;
remoteOptionsErrorMsg.value = null;
Promise.resolve()
.then(() => {
let remoteOptionsMethod = props.field.remoteOptionsMethod;
return callFunc([], [], remoteOptionsMethod, []);
})
.then((data) => {
remoteOptionsData.value = data || [];
remoteOptionsLoading.value = false;
})
.catch((err) => {
remoteOptionsErrorMsg.value =
(err && err.msg) || (err && err.message) || "连接服务器失败,请检查网络";
remoteOptionsLoading.value = false;
});
};
const handleRemoteSearchValue = delayCall((value) => {
remoteSearchId.value += 1;
remoteSearchValue.value = value;
const currentRemoteSearchId = remoteSearchId.value;
remoteSearchLoading.value = true;
remoteSearchErrorMsg.value = null;
remoteSearchData.value = [];
Promise.resolve()
.then(() => {
let remoteSearchMethod = props.field.remoteSearchMethod;
return callFunc(["value"], [value], remoteSearchMethod, []);
})
.then((data) => {
if (currentRemoteSearchId !== remoteSearchId.value) {
return;
}
remoteSearchData.value = data || [];
remoteSearchLoading.value = false;
remoteSearchValue.value = null;
})
.catch((err) => {
remoteSearchErrorMsg.value =
(err && err.msg) || (err && err.message) || "连接服务器失败,请检查网络";
remoteSearchLoading.value = false;
});
}, 300);
const readInitValue = () => {
if (initValueFlag.value) {
initValueFlag.value = false;
return;
}
initValueFlag.value = true;
let curValue = [];
if (!!currentValue.value) {
if (currentValue.value instanceof Array) {
curValue = currentValue.value.filter((val) => !!val);
} else if (typeof currentValue.value === "string") {
curValue = [currentValue.value];
}
}
let newValue = [];
if (forceInOptions.value && !supportedRemoteSearch.value) {
let inOptions = [];
let options = parseOptions(props.field.options);
for (let option of options) {
curValue.forEach((val) => {
if (option.value === val) {
inOptions.push(option.value);
}
});
}
newValue = inOptions;
} else {
newValue = curValue;
}
if (newValue.length === 0) {
newValue = defaultValueList.value || [];
}
if (newValue !== undefined && newValue !== null) {
return multiple.value ? newValue : newValue[0];
} else {
return undefined;
}
};
const checkValue = () => {
let newValue = readInitValue();
let curValue = currentValue.value;
if (!!newValue && toAny(newValue, "") !== toAny(curValue, "")) {
vModel.value[formatFieldName(props.field.name)] = newValue;
}
};
onMounted(() => {
checkValue();
handleRemoteOptions();
});
watch(
() => forceInOptions.value,
() => {
checkValue();
}
);
watch(
() => defaultValueList.value,
() => {
checkValue();
}
);
</script>