2025-12-12 18:05:49 +08:00

441 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="dynamic-input-container">
<!-- 1. 开关组件(替代原单选框 type=0:是/否 → 开启/关闭) -->
<el-switch
class="custom-switch"
v-if="type === '0'"
v-model="innerValue"
active-color="#13ce66"
inactive-color="#ff4949"
active-value="1"
inactive-value="0"
@change="handleSwitchChange"
/>
<!-- 2. 下拉框(1):直接绑定value -->
<el-select
v-else-if="type === '1'"
v-model="innerValue"
size="small"
clearable
@change="handleValueChange"
>
<el-option
v-for="item in dictOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<!-- 3. 字典单选(2):绑定zddh(value),显示zdmc(label) -->
<el-select
v-else-if="type === '2'"
v-model="innerValue"
size="small"
clearable
@change="handleValueChange"
v-loading="dictLoading"
>
<el-option
v-for="item in dictOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<!-- 4. 字典多选(3):数组↔逗号分隔字符串 → 修复核心:仅绑定multipleValue,移除冗余逻辑 -->
<el-select
v-else-if="type === '3'"
v-model="multipleValue"
size="small"
multiple
@change="handleMultipleChange"
v-loading="dictLoading"
clearable
>
<el-option
v-for="item in dictOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<!-- 5. 特殊(4) -->
<el-input
v-else-if="type === '4'"
v-model="innerValue"
size="small"
disabled
@change="handleValueChange"
/>
<!-- 6. 文本框(5) -->
<el-input
v-else-if="type === '5'"
v-model="innerValue"
size="small"
:placeholder="placeholder"
@change="handleValueChange"
/>
<!-- 7. 数字框(6):支持dictData定义小数位数,空值初始化 -->
<el-input-number
v-else-if="type === '6'"
v-model="innerValue"
size="small"
:min="minNum"
:precision="finalPrecision"
controls-position="right"
@change="handleValueChange"
:placeholder="placeholder || '请输入数字'"
/>
<!-- 8. 日期框(7):适配yyyy/mm/dd -->
<el-date-picker
v-else-if="type === '7'"
v-model="innerValue"
size="small"
type="date"
format="YYYY/MM/DD"
value-format="YYYY-MM-DD"
:placeholder="placeholder"
@change="handleValueChange"
/>
<!-- 9. 时间框(8):适配hh:mm(修复标签闭合问题) -->
<el-time-picker
v-else-if="type === '8'"
v-model="innerValue"
size="small"
format="HH:mm"
value-format="HH:mm:ss"
:placeholder="placeholder"
@change="handleValueChange"
:picker-options="{ selectableRange: '00:00:00 - 23:59:59' }"
/>
<!-- 兜底:默认文本框 -->
<el-input
v-else
v-model="innerValue"
size="small"
:placeholder="placeholder"
@change="handleValueChange"
/>
</div>
</template>
<script setup name="DynamicInput">
import { ref, watch, defineProps, defineEmits, onMounted, computed } from "vue";
import { ElMessage } from "element-plus";
import { getoptionselect } from "@/api/liswork/xtwh/localconfigApi.ts";
// 字典缓存:key=字典类型,value=映射后的{label,value}列表
const dictCache = ref({});
// Props定义
const props = defineProps({
modelValue: { // Vue3 规范:v-model 绑定 modelValue
type: [String, Number, Boolean, null, undefined], // 新增null/undefined支持空值
required: false, // 允许空值
default: ""
},
type: { type: String, required: true, default: "5" },
dictData: { type: [String, Number, Object, Array], default: "" }, // 支持Number类型(小数位数)
placeholder: { type: String, default: "请输入值" },
minNum: { type: Number, default: -Infinity }, // 数字框最小值改为无限制(空值场景)
precisionNum: { type: Number, default: 0 } // 默认整数
});
// Emits定义(统一触发 update:modelValue)
const emit = defineEmits(["update:modelValue", "change"]);
// 内部状态
const innerValue = ref(null); // 初始值改为null,适配空值
const multipleValue = ref([]); // 多选值(仅type=3用)→ 核心:初始化为空数组,杜绝默认0
const dictOptions = ref([]); // 下拉/字典选项列表
const dictLoading = ref(false); // 字典加载状态
// ========== 核心:数字框小数位数计算 ==========
const finalPrecision = computed(() => {
if (props.type !== "6") return props.precisionNum;
// dictData为小数保留位数(优先级最高)
if (props.dictData !== "" && props.dictData !== null && props.dictData !== undefined) {
const precision = Number(props.dictData);
return isNaN(precision) ? 0 : Math.max(0, precision); // 确保非负整数
}
// dictData为空,默认整数(precisionNum=0)
return 0;
});
// ========== 核心修复:值初始化 + 格式适配 → 重点优化多选逻辑 ==========
watch(() => props.modelValue, (newVal) => {
const val = newVal; // 移除兜底空值,保留原始值
switch (props.type) {
// 开关组件(替代原单选框):后端1/0 → 前端"1"/"0"
case "0":
innerValue.value = (val === 1 || val === "1" || val === true) ? "1" : "0";
break;
// 下拉框/字典单选:直接绑定value(zddh)
case "1":
case "2":
innerValue.value = val === null || val === undefined ? "" : String(val);
break;
// 字典多选:核心修复 → 严格过滤空值/无效值,杜绝默认0
case "3":
// 1. 空值/0/空字符串 → 空数组
if (val === null || val === undefined || val === "" || val === 0) {
multipleValue.value = [];
}
// 2. 有效字符串 → 分割为数组(过滤空项)
else if (typeof val === "string") {
multipleValue.value = val.split(",").filter(item => item.trim() !== "").map(item => String(item));
}
// 3. 数组类型(特殊场景)→ 直接赋值
else if (Array.isArray(val)) {
multipleValue.value = val.filter(item => item !== null && item !== undefined && item !== "").map(item => String(item));
}
// 4. 其他类型 → 空数组
else {
multipleValue.value = [];
}
// 移除innerValue同步(避免干扰多选)
break;
// 数字框:空值处理,支持小数位数
case "6":
// 空值/非数字 → 空(null),否则转为数字
if (val === null || val === undefined || val === "" || isNaN(Number(val))) {
innerValue.value = null;
} else {
innerValue.value = Number(val);
}
break;
// 日期框:后端 yyyy/mm/dd → 前端 yyyy-MM-dd
case "7":
innerValue.value = typeof val === "string" && val.includes("/")
? val.replace(/\//g, "-")
: val || null;
break;
// 时间框:后端 hh:mm → 前端 hh:mm:ss
case "8":
innerValue.value = typeof val === "string" && val.includes(":") && val.split(":").length === 2
? `${val}:00`
: val || null;
break;
// 其他类型:保留原始值(空值则为null)
default:
innerValue.value = val === null || val === undefined ? "" : val;
break;
}
}, { immediate: true, deep: true });
// ========== 修复:移除innerValue对多选的干扰 → 仅监听非多选类型 ==========
watch(innerValue, (newVal) => {
// 多选类型跳过innerValue监听
if (props.type === "3") return;
let finalVal = newVal;
// 数字框:空值提交空字符串,非空则保留数字类型
if (props.type === "6") {
finalVal = newVal === null || newVal === undefined ? "" : Number(newVal);
}
// 日期框:前端 yyyy-MM-dd → 后端 yyyy/mm/dd
else if (props.type === "7" && typeof newVal === "string" && newVal.includes("-")) {
finalVal = newVal.replace(/-/g, "/");
}
// 时间框:前端 hh:mm:ss → 后端 hh:mm
else if (props.type === "8" && typeof newVal === "string" && newVal.includes(":")) {
finalVal = newVal.split(":").slice(0, 2).join(":");
}
emit("update:modelValue", finalVal);
emit("change", finalVal);
}, { deep: true });
// ========== 字典加载逻辑 → 新增:字典加载后重新同步多选值 ==========
watch([() => props.type, () => props.dictData], async () => {
dictOptions.value = [];
// 数字框(type=6)不需要加载字典
if (props.type === "1") {
await parseJsonDict();
} else if (["2", "3"].includes(props.type)) {
await loadApiDict();
// 字典加载完成后,重新同步多选值(修复选不到值的问题)
if (props.type === "3") {
const val = props.modelValue;
if (val === null || val === undefined || val === "" || val === 0) {
multipleValue.value = [];
} else if (typeof val === "string") {
multipleValue.value = val.split(",").filter(item => item.trim() !== "").map(item => String(item));
}
}
}
}, { immediate: true, deep: true });
// 解析下拉框JSON数组(type=1)
async function parseJsonDict() {
if (!props.dictData) return;
try {
let jsonStr = typeof props.dictData === "string" ? props.dictData : JSON.stringify(props.dictData);
jsonStr = jsonStr.replace(/\s+/g, " ").replace(/[\n\t]/g, "");
let jsonData = JSON.parse(jsonStr);
if (!Array.isArray(jsonData)) jsonData = [jsonData];
dictOptions.value = jsonData.map(item => ({
label: item.label || item.zdmc || item.name || "",
value: item.value || item.zddh || item.code || ""
})).filter(item => item.label && item.value);
} catch (error) {
ElMessage.error(`下拉框数据解析失败:${error.message}`);
console.error("JSON解析失败:", { raw: props.dictData, error });
}
}
// 加载字典API(type=2/3):zddh→value,zdmc→label
async function loadApiDict() {
const dictType = props.dictData;
if (!dictType) return;
if (dictCache.value[dictType]) {
dictOptions.value = dictCache.value[dictType];
return;
}
try {
dictLoading.value = true;
const res = await getoptionselect({ zdlb: dictType });
if (res.code === 200 && Array.isArray(res.data)) {
dictOptions.value = res.data.map(item => ({
label: item.zdmc || item.label || "",
value: item.zddh || item.value || ""
})).filter(item => item.label && item.value);
dictCache.value[dictType] = dictOptions.value;
} else {
ElMessage.warning(`字典${dictType}无有效数据`);
}
} catch (error) {
ElMessage.error("字典请求失败");
console.error("字典API异常:", { dictType, error });
} finally {
dictLoading.value = false;
}
}
// ========== 开关组件专属变更处理(直接返回数字1/0) ==========
function handleSwitchChange(val) {
// val是字符串1/0,转为数字提交给后端
const finalVal = Number(val);
emit("update:modelValue", finalVal);
emit("change", finalVal);
}
// ========== 普通值变更处理(除开关/多选外) ==========
function handleValueChange(val) {
let finalVal = val;
emit("update:modelValue", finalVal);
emit("change", finalVal);
}
// ========== 核心修复:多选值变更处理 → 简化逻辑,杜绝值覆盖 ==========
function handleMultipleChange(val) {
// 1. 过滤空值,避免提交无效数据
const validVal = val.filter(item => item !== null && item !== undefined && item !== "");
// 2. 转为逗号分隔字符串(后端格式)
const strVal = validVal.join(",");
// 3. 更新多选值(确保同步)
multipleValue.value = validVal;
// 4. 触发父组件更新
emit("update:modelValue", strVal);
emit("change", strVal);
}
// ========== 初始化优化 → 移除多选冗余逻辑,杜绝默认0 ==========
onMounted(() => {
// 开关组件初始值兜底
if (props.type === "0" && innerValue.value === null) {
innerValue.value = "0"; // 默认关闭
emit("update:modelValue", 0); // 后端默认值:0
}
// 多选初始化:仅处理有效值,杜绝空值/0转为["0"]
if (props.type === "3") {
const val = props.modelValue;
if (val === null || val === undefined || val === "" || val === 0) {
multipleValue.value = [];
} else if (typeof val === "string") {
multipleValue.value = val.split(",").filter(item => item.trim() !== "").map(item => String(item));
}
}
});
</script>
<style scoped>
.dynamic-input-container {
width: 100%;
height: 100%; /* 关键:占满表格单元格高度 */
display: flex;
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 */
padding: 0 2px; /* 轻微内边距,避免贴边 */
}
/* 开关组件样式优化 */
:deep(.el-switch) {
margin: 0 auto; /* 开关水平居中 */
}
/* 深度选择器强制覆盖激活颜色 */
:deep(.custom-switch .el-switch__core) {
/* 开启状态背景色 */
--el-switch-on-color: #13ce66 !important;
/* 关闭状态背景色(可选) */
--el-switch-off-color: #C3C3C3 !important;
}
/* 兼容旧版Element Plus(可选) */
:deep(.custom-switch .el-switch__core.is-checked) {
background-color: #13ce66 !important;
border-color: #13ce66 !important;
}
:deep(.el-radio-group) {
display: flex;
gap: 10px;
align-items: center;
}
:deep(.el-select--small),
:deep(.el-input--small),
:deep(.el-input-number--small),
:deep(.el-date-picker--small),
:deep(.el-time-picker--small) {
width: 100%;
}
/* 数字输入框空值样式优化 */
:deep(.el-input-number--small) {
.el-input__inner {
text-align: center;
}
/* 空值时占位符样式 */
.el-input__placeholder {
color: #999;
font-size: 12px;
}
}
/* 多选下拉样式优化:避免高度溢出 */
:deep(.el-select--multiple .el-select__tags) {
flex-wrap: wrap;
min-height: 32px; /* 匹配small尺寸高度 */
}
</style>