408 lines
10 KiB
Vue
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>
|