最终效果
代码实现
components/SUI/S-form.vue
c
<script lang="ts" setup>
import type { FormInstance } from "element-plus";
// 使用索引签名定义对象类型
type GenericObject = {
[key: string]: any;
};
const props = defineProps<{
Model?: GenericObject;
disabled?: boolean;
hideHandle?: boolean;
saveAPI?: string;
saveOK?: () => void;
local_save?: (formData: GenericObject) => void;
cancel?: () => void;
colNum?: number;
action?: string;
PageConfig?: GenericObject;
}>();
const formData = defineModel<GenericObject>({});
const formItemConfigList = computed(() => {
let result: any = [];
if (props.Model) {
for (const [key, value] of Object.entries(props.Model)) {
let temp_value = JSON.parse(JSON.stringify(value));
// 解析 -- 必填
if ("require" in temp_value && temp_value.require) {
if (
"formRules" in temp_value &&
temp_value.formRules &&
Array.isArray(temp_value.formRules)
) {
temp_value.formRules.push({
required: true,
message: "请输入" + temp_value.label,
});
} else {
temp_value.formRules = [
{
required: true,
message: "请输入" + temp_value.label,
},
];
}
}
result.push({
prop: key,
...(temp_value as object),
});
}
}
return result;
});
const group_formItemConfigList_Obj = computed(() => {
let result: any = {};
if (props.PageConfig && props.PageConfig.formGrouped) {
let final_formItemConfigList: any[] = [];
formItemConfigList.value.forEach((formItemConfig: any) => {
if (
!(
formItemConfig.formHide &&
(formItemConfig.formHide === "all" ||
(Array.isArray(formItemConfig.formHide) &&
formItemConfig.formHide.includes(props.action)))
)
) {
final_formItemConfigList.push(formItemConfig);
}
});
result = groupBy(
final_formItemConfigList,
"group",
props.PageConfig.groupName_default
);
}
return result;
});
const activeGroups: string[] = Object.keys(group_formItemConfigList_Obj.value);
const pageData = reactive<{
localFomrData: GenericObject;
}>({
localFomrData: formData.value || {},
});
const { localFomrData } = toRefs(pageData);
const formRef = ref<FormInstance>();
const callbackMessage = ref({
show: false,
valid: true,
content: "",
});
// 按钮 -- 保存
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (valid) {
if (props.local_save) {
props.local_save(pageData.localFomrData);
return;
}
try {
await $fetch(`/api${props.saveAPI}`, {
body: pageData.localFomrData,
method: "POST",
});
callbackMessage.value = {
show: true,
valid: true,
content: "操作成功",
};
if (props.saveOK) {
props.saveOK();
}
} catch (e: any) {
callbackMessage.value = {
show: true,
valid: false,
content: e.data.message,
};
}
} else {
console.log("提交报错!");
}
});
};
// 将方法暴露给父组件
defineExpose({
submitForm,
localFomrData,
formRef,
});
</script>
<template>
<div class="relative mt-10">
<el-scrollbar max-height="460px" class="px10">
<el-form
ref="formRef"
:inline="true"
:model="localFomrData"
:disabled="props.disabled"
>
<el-collapse
v-if="props.PageConfig && props.PageConfig.formGrouped"
v-model="activeGroups"
>
<el-collapse-item
:name="group"
v-for="(formItemConfigList, group) in group_formItemConfigList_Obj"
:key="group"
>
<template #title>
<div class="font-bold text-14px">
{{ group }}
</div>
</template>
<S-formRow
:formItemConfigList="formItemConfigList"
:colNum="props.colNum"
:action="props.action"
v-model="localFomrData"
:disabled="props.disabled"
>
<template
v-for="formItemConfig in formItemConfigList.filter(
(item:any) => item.type === 'custom'
)"
:key="formItemConfig.prop"
#[formItemConfig.prop]
>
<slot :name="formItemConfig.prop" />
</template>
</S-formRow>
</el-collapse-item>
</el-collapse>
<S-formRow
v-else
:formItemConfigList="formItemConfigList"
:colNum="props.colNum"
:action="props.action"
:disabled="props.disabled"
v-model="localFomrData"
>
<template
v-for="formItemConfig in formItemConfigList.filter(
(item:any) => item.type === 'custom'
)"
:key="formItemConfig.prop"
#[formItemConfig.prop]
>
<slot :name="formItemConfig.prop" />
</template>
</S-formRow>
</el-form>
</el-scrollbar>
<div class="flex justify-center p4" v-if="!props.disabled && !hideHandle">
<el-button @click="props.cancel">取消</el-button>
<el-button type="primary" @click="submitForm(formRef)">保存</el-button>
</div>
<S-msgWin :msg="callbackMessage" />
</div>
</template>
components/SUI/S-formRow.vue
c
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { defineAsyncComponent } from "vue";
const props = defineProps<{
formItemConfigList: any;
colNum?: number;
action?: string;
disabled?: boolean;
}>();
const localFomrData = defineModel<any>({});
// 标记客户端环境
const isClient = ref(false);
// 动态导入组件,禁用SSR
const AvatarCropper = defineAsyncComponent({
loader: () => import("~/components/SUI/S-avatar.vue"),
suspensible: false, // 关键:禁止在服务端渲染该组件,使用 suspensible 替代 ssr
});
onMounted(() => {
isClient.value = true; // 确保在客户端挂载后才显示组件
});
</script>
<template>
<el-row :sapn="24">
<template v-for="formItemConfig in formItemConfigList">
<el-col
v-if="
!(
formItemConfig.formHide &&
(formItemConfig.formHide === 'all' ||
(Array.isArray(formItemConfig.formHide) &&
formItemConfig.formHide.includes(props.action)))
)
"
:span="formItemConfig.span || (props.colNum && 24 / props.colNum) || 12"
:key="formItemConfig.prop"
>
<el-form-item
:label="formItemConfig.label"
:label-width="160"
:rules="formItemConfig.formRules"
:prop="formItemConfig.prop"
>
<el-date-picker
v-if="formItemConfig.type === 'date'"
v-model="localFomrData[formItemConfig.prop as string]"
type="date"
placeholder="选择日期"
v-bind="formItemConfig"
/>
<el-input-number
v-else-if="formItemConfig.type === 'number'"
v-model="localFomrData[formItemConfig.prop as string]"
v-bind="formItemConfig"
controls-position="right"
class="w-220px!"
>
<template #suffix>
<span>{{ formItemConfig.unit }}</span>
</template>
</el-input-number>
<el-switch
v-else-if="formItemConfig.type === 'switch'"
v-model="localFomrData[formItemConfig.prop as string]"
v-bind="formItemConfig"
class="w-220px!"
/>
<el-select
v-else-if="formItemConfig.type === 'select'"
v-model="localFomrData[formItemConfig.prop as string]"
filterable
clearable
:multiple="formItemConfig.multSelect"
class="w-220px!"
placeholder=""
>
<el-option
v-for="item in formItemConfig.options || []"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-tree-select
v-else-if="formItemConfig.type === 'treeSelect'"
v-model="localFomrData[formItemConfig.prop as string]"
:data="formItemConfig.treeData"
:render-after-expand="false"
class="w-220px!"
filterable
clearable
:node-key="formItemConfig.key"
default-expand-all
/>
<AvatarCropper
v-else-if="isClient && formItemConfig.type === 'avatar'"
:disabled="
(formItemConfig.formDisable &&
formItemConfig.formDisable.includes(props.action)) ||
props.disabled
"
v-model="localFomrData[formItemConfig.prop as string]"
/>
<template v-else-if="formItemConfig.type === 'custom'">
<slot :name="formItemConfig.prop" />
</template>
<el-input
v-else
v-model="localFomrData[formItemConfig.prop as string]"
v-bind="formItemConfig"
class="w-220px!"
:type="formItemConfig.type || 'text'"
:disabled="
formItemConfig.formDisable &&
formItemConfig.formDisable.includes(props.action)
"
:autosize="formItemConfig.autosize || { minRows: 2, maxRows: 4 }"
show-word-limit
/>
</el-form-item>
</el-col>
</template>
</el-row>
</template>
相关组件
头像 S-avatar.vue
https://blog.csdn.net/weixin_41192489/article/details/149716009
消息弹窗 S-msgWin.vue
https://blog.csdn.net/weixin_41192489/article/details/149717948