This commit is contained in:
2026-01-20 09:43:58 +08:00
parent 9312d47430
commit 8c2d943dba
10 changed files with 892 additions and 72 deletions

View File

@@ -0,0 +1,320 @@
<template>
<a-form :model="formData" :rules="rules" :label-col="labelCol" :wrapper-col="wrapperCol" :layout="layout"
@finish="handleFinish" @finish-failed="handleFinishFailed">
<a-form-item v-for="item in formItems" :key="item.field" :label="item.label" :name="item.field"
:required="item.required" :colon="item.colon">
<!-- 输入框 -->
<template v-if="item.type === 'input'">
<a-input v-model:value="formData[item.field]" :placeholder="item.placeholder || `请输入${item.label}`"
:disabled="item.disabled" :allow-clear="item.allowClear !== false" :max-length="item.maxLength"
:type="item.inputType || 'text'" :prefix="item.prefix" :suffix="item.suffix"
@change="item.onChange && item.onChange(formData[item.field])" />
</template>
<!-- 文本域 -->
<template v-else-if="item.type === 'textarea'">
<a-textarea v-model:value="formData[item.field]" :placeholder="item.placeholder || `请输入${item.label}`"
:disabled="item.disabled" :allow-clear="item.allowClear !== false" :rows="item.rows || 4"
:max-length="item.maxLength" :show-count="item.showCount"
@change="item.onChange && item.onChange(formData[item.field])" />
</template>
<!-- 密码输入框 -->
<template v-else-if="item.type === 'password'">
<a-input-password v-model:value="formData[item.field]"
:placeholder="item.placeholder || `请输入${item.label}`" :disabled="item.disabled"
:max-length="item.maxLength" @change="item.onChange && item.onChange(formData[item.field])" />
</template>
<!-- 数字输入框 -->
<template v-else-if="item.type === 'number'">
<a-input-number v-model:value="formData[item.field]"
:placeholder="item.placeholder || `请输入${item.label}`" :disabled="item.disabled" :min="item.min"
:max="item.max" :step="item.step || 1" :precision="item.precision"
:controls="item.controls !== false" style="width: 100%"
@change="item.onChange && item.onChange(formData[item.field])" />
</template>
<!-- 下拉选择 -->
<template v-else-if="item.type === 'select'">
<a-select v-model:value="formData[item.field]" :placeholder="item.placeholder || `请选择${item.label}`"
:disabled="item.disabled" :allow-clear="item.allowClear !== false" :mode="item.mode"
:options="item.options" :field-names="item.fieldNames" style="width: 100%"
@change="item.onChange && item.onChange(formData[item.field])">
<template v-if="!item.options" #notFoundContent>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" description="暂无数据" />
</template>
</a-select>
</template>
<!-- 单选框 -->
<template v-else-if="item.type === 'radio'">
<a-radio-group v-model:value="formData[item.field]" :disabled="item.disabled"
:button-style="item.buttonStyle" @change="item.onChange && item.onChange(formData[item.field])">
<template v-if="item.options">
<a-radio v-for="opt in item.options" :key="opt.value" :value="opt.value"
:disabled="opt.disabled">
{{ opt.label }}
</a-radio>
</template>
<template v-else-if="item.buttonStyle === 'solid'">
<a-radio-button v-for="opt in item.options" :key="opt.value" :value="opt.value"
:disabled="opt.disabled">
{{ opt.label }}
</a-radio-button>
</template>
</a-radio-group>
</template>
<!-- 多选框 -->
<template v-else-if="item.type === 'checkbox'">
<a-checkbox-group v-model:value="formData[item.field]" :disabled="item.disabled"
@change="item.onChange && item.onChange(formData[item.field])">
<template v-if="item.options">
<a-checkbox v-for="opt in item.options" :key="opt.value" :value="opt.value"
:disabled="opt.disabled">
{{ opt.label }}
</a-checkbox>
</template>
</a-checkbox-group>
</template>
<!-- 开关 -->
<template v-else-if="item.type === 'switch'">
<a-switch v-model:checked="formData[item.field]" :disabled="item.disabled"
:checked-children="item.checkedChildren || ''" :un-checked-children="item.unCheckedChildren || ''"
@change="item.onChange && item.onChange(formData[item.field])" />
</template>
<!-- 日期选择 -->
<template v-else-if="item.type === 'date'">
<a-date-picker v-model:value="formData[item.field]"
:placeholder="item.placeholder || `请选择${item.label}`" :disabled="item.disabled"
:format="item.format || 'YYYY-MM-DD'" :value-format="item.valueFormat || 'YYYY-MM-DD'"
style="width: 100%" @change="item.onChange && item.onChange(formData[item.field])" />
</template>
<!-- 日期范围选择 -->
<template v-else-if="item.type === 'dateRange'">
<a-range-picker v-model:value="formData[item.field]" :placeholder="item.placeholder || ['开始日期', '结束日期']"
:disabled="item.disabled" :format="item.format || 'YYYY-MM-DD'"
:value-format="item.valueFormat || 'YYYY-MM-DD'" style="width: 100%"
@change="item.onChange && item.onChange(formData[item.field])" />
</template>
<!-- 时间选择 -->
<template v-else-if="item.type === 'time'">
<a-time-picker v-model:value="formData[item.field]"
:placeholder="item.placeholder || `请选择${item.label}`" :disabled="item.disabled"
:format="item.format || 'HH:mm:ss'" :value-format="item.valueFormat || 'HH:mm:ss'"
style="width: 100%" @change="item.onChange && item.onChange(formData[item.field])" />
</template>
<!-- 上传 -->
<template v-else-if="item.type === 'upload'">
<a-upload v-model:file-list="formData[item.field]" :list-type="item.listType || 'text'"
:action="item.action" :max-count="item.maxCount" :before-upload="item.beforeUpload"
:custom-request="item.customRequest" :accept="item.accept" :disabled="item.disabled"
@change="(info) => item.onChange && item.onChange(info)">
<a-button v-if="item.listType !== 'picture-card'" type="primary">
<UploadOutlined />
点击上传
</a-button>
<div v-else>
<PlusOutlined />
<div class="ant-upload-text">上传</div>
</div>
</a-upload>
</template>
<!-- 评分 -->
<template v-else-if="item.type === 'rate'">
<a-rate v-model:value="formData[item.field]" :disabled="item.disabled" :count="item.count || 5"
:allow-half="item.allowHalf" @change="item.onChange && item.onChange(formData[item.field])" />
</template>
<!-- 滑块 -->
<template v-else-if="item.type === 'slider'">
<a-slider v-model:value="formData[item.field]" :disabled="item.disabled" :min="item.min || 0"
:max="item.max || 100" :step="item.step || 1" :marks="item.marks" :range="item.range"
@change="item.onChange && item.onChange(formData[item.field])" />
</template>
<!-- 级联选择 -->
<template v-else-if="item.type === 'cascader'">
<a-cascader v-model:value="formData[item.field]" :options="item.options"
:placeholder="item.placeholder || `请选择${item.label}`" :disabled="item.disabled"
:change-on-select="item.changeOnSelect" :field-names="item.fieldNames" style="width: 100%"
@change="item.onChange && item.onChange(formData[item.field])" />
</template>
<!-- 自定义插槽 -->
<template v-else-if="item.type === 'slot'">
<slot :name="item.slotName || item.field" :field="item.field" :value="formData[item.field]"></slot>
</template>
<!-- 提示信息 -->
<template v-if="item.tip">
<div class="form-item-tip">{{ item.tip }}</div>
</template>
</a-form-item>
<!-- 表单操作按钮 -->
<a-form-item v-if="showActions" :wrapper-col="actionWrapperCol">
<a-space>
<a-button type="primary" html-type="submit" :loading="loading" :size="buttonSize">
{{ submitText || '提交' }}
</a-button>
<a-button v-if="showReset" @click="handleReset" :size="buttonSize">
{{ resetText || '重置' }}
</a-button>
<a-button v-if="showCancel" @click="handleCancel" :size="buttonSize">
{{ cancelText || '取消' }}
</a-button>
<slot name="actions"></slot>
</a-space>
</a-form-item>
<!-- 自定义插槽 -->
<slot></slot>
</a-form>
</template>
<script setup>
import { ref, reactive, computed, watch } from 'vue'
import { Empty } from 'ant-design-vue'
import { UploadOutlined, PlusOutlined } from '@ant-design/icons-vue'
const props = defineProps({
// 表单项配置
formItems: {
type: Array,
default: () => [],
required: true,
},
// 表单初始值
initialValues: {
type: Object,
default: () => ({}),
},
// 表单布局
layout: {
type: String,
default: 'horizontal', // horizontal, vertical, inline
},
// 标签宽度
labelCol: {
type: Object,
default: () => ({ span: 6 }),
},
// 内容宽度
wrapperCol: {
type: Object,
default: () => ({ span: 16 }),
},
// 是否显示操作按钮
showActions: {
type: Boolean,
default: true,
},
// 操作按钮布局
actionWrapperCol: {
type: Object,
default: () => ({ offset: 6, span: 16 }),
},
// 是否显示重置按钮
showReset: {
type: Boolean,
default: true,
},
// 是否显示取消按钮
showCancel: {
type: Boolean,
default: false,
},
// 按钮文字
submitText: String,
resetText: String,
cancelText: String,
// 按钮大小
buttonSize: {
type: String,
default: 'middle',
},
// 加载状态
loading: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['finish', 'finish-failed', 'reset', 'cancel'])
// 表单数据
const formData = reactive({ ...props.initialValues })
// 表单验证规则
const rules = computed(() => {
const result = {}
props.formItems.forEach((item) => {
if (item.rules && item.rules.length > 0) {
result[item.field] = item.rules
}
})
return result
})
// 监听初始值变化
watch(
() => props.initialValues,
(newVal) => {
Object.assign(formData, newVal)
},
{ deep: true },
)
// 表单提交
const handleFinish = (values) => {
emit('finish', values)
}
// 表单验证失败
const handleFinishFailed = (errorInfo) => {
emit('finish-failed', errorInfo)
}
// 重置表单
const handleReset = () => {
Object.assign(formData, props.initialValues)
emit('reset', formData)
}
// 取消操作
const handleCancel = () => {
emit('cancel')
}
// 暴露方法给父组件
defineExpose({
formData,
resetForm: handleReset,
setFieldValue: (field, value) => {
formData[field] = value
},
getFieldValue: (field) => {
return formData[field]
},
setFieldsValue: (values) => {
Object.assign(formData, values)
},
})
</script>
<style scoped lang="scss">
.form-item-tip {
margin-top: 4px;
font-size: 12px;
color: #999;
line-height: 1.5;
}
</style>