vue3【组件封装】超级表单 S-form.vue
最终效果
代码实现
components/SUI/S-form.vue
<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-formref="formRef":inline="true":model="localFomrData":disabled="props.disabled"><el-collapsev-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"><templatev-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-formRowv-else:formItemConfigList="formItemConfigList":colNum="props.colNum":action="props.action":disabled="props.disabled"v-model="localFomrData"><templatev-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
<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-colv-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-pickerv-if="formItemConfig.type === 'date'"v-model="localFomrData[formItemConfig.prop as string]"type="date"placeholder="选择日期"v-bind="formItemConfig"/><el-input-numberv-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-switchv-else-if="formItemConfig.type === 'switch'"v-model="localFomrData[formItemConfig.prop as string]"v-bind="formItemConfig"class="w-220px!"/><el-selectv-else-if="formItemConfig.type === 'select'"v-model="localFomrData[formItemConfig.prop as string]"filterableclearable:multiple="formItemConfig.multSelect"class="w-220px!"placeholder=""><el-optionv-for="item in formItemConfig.options || []":key="item.value":label="item.label":value="item.value"/></el-select><el-tree-selectv-else-if="formItemConfig.type === 'treeSelect'"v-model="localFomrData[formItemConfig.prop as string]":data="formItemConfig.treeData":render-after-expand="false"class="w-220px!"filterableclearable:node-key="formItemConfig.key"default-expand-all/><AvatarCropperv-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-inputv-elsev-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