明树Git Lab

Commit add12e41 authored by zhanghan's avatar zhanghan

bug处理

parent 327efb71
# SuperForm 超级表单组件
一个基于 Vue 3 + Element Plus 的强大表单组件,支持高度可配置化和完整的属性透传。
## ✨ 特性
- 🎯 **支持所有 Element Plus 表单组件** - 完整支持 input、select、date、upload 等 20+ 种组件
- 🔄 **原生属性透传** - 所有原生属性和组件库属性均可直接透传
- 📝 **灵活的表单验证** - 支持规则配置,默认不验证,可选择性开启
- 🌐 **接口集成** - 内置保存、更新、获取详情接口配置
- ✏️ **编辑模式** - 自动识别新增/编辑模式
- 🎨 **高度可定制** - 支持自定义插槽、自定义组件
- 📱 **响应式布局** - 支持栅格系统和响应式配置
- 🚀 **开发效率** - 通过配置快速生成复杂表单,提升开发效率 10 倍+
## 📦 安装
`SuperForm.vue` 复制到你的项目中:
```
src/components/common/SuperForm.vue
```
## 🚀 快速开始
### 基础用法
```vue
<template>
<SuperForm
v-model="formData"
:config="formConfig"
:items="formItems"
@submit="handleSubmit"
/>
</template>
<script setup>
import SuperForm from '@/components/common/SuperForm.vue'
import { ref } from 'vue'
const formData = ref({})
const formConfig = {
labelWidth: '120px',
showButtons: true
}
const formItems = [
{
field: 'username',
label: '用户名',
type: 'input',
required: true,
placeholder: '请输入用户名'
},
{
field: 'email',
label: '邮箱',
type: 'input',
rules: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
]
}
]
const handleSubmit = (data) => {
console.log('表单数据:', data)
}
</script>
```
## 📋 Props 配置
### 表单配置 (config)
| 参数 | 说明 | 类型 | 默认值 |
|------|------|------|--------|
| labelWidth | 表单标签宽度 | string | '120px' |
| labelPosition | 表单标签位置 | string | 'right' |
| inline | 是否行内表单 | boolean | false |
| disabled | 是否禁用 | boolean | false |
| size | 表单尺寸 | string | 'default' |
| gutter | 栅格间隔 | number | 20 |
| showButtons | 是否显示操作按钮 | boolean | true |
| showSubmit | 是否显示提交按钮 | boolean | true |
| showReset | 是否显示重置按钮 | boolean | true |
| submitText | 提交按钮文本 | string | '提交' |
| resetText | 重置按钮文本 | string | '重置' |
| submitType | 提交按钮类型 | string | 'primary' |
| submitIcon | 提交按钮图标 | string | - |
| customButtons | 自定义按钮数组 | array | [] |
| buttonSpan | 按钮容器占位 | number | 24 |
| statusIcon | 是否显示验证图标 | boolean | true |
| scrollToError | 滚动到错误字段 | boolean | true |
### 表单项配置 (items)
#### 基础配置
| 参数 | 说明 | 类型 | 必填 |
|------|------|------|------|
| field | 字段名 | string | 是 |
| label | 标签文本 | string | 是 |
| type | 组件类型 | string | 是 |
| defaultValue | 默认值 | any | 否 |
| required | 是否必填 | boolean | 否 |
| rules | 验证规则 | array | 否 |
| span | 栅格占位 | number | 否 |
| offset | 栅格偏移 | number | 否 |
#### 支持的组件类型
| type 值 | 说明 | 组件 |
|---------|------|------|
| input | 输入框 | el-input |
| textarea | 文本域 | el-input[type=textarea] |
| password | 密码框 | el-input[type=password] |
| inputNumber / number | 数字输入 | el-input-number |
| select | 选择器 | el-select |
| radio / radioGroup | 单选框 | el-radio-group |
| checkbox / checkboxGroup | 多选框 | el-checkbox-group |
| date | 日期选择 | el-date-picker |
| datetime | 日期时间选择 | el-date-picker |
| daterange | 日期范围 | el-date-picker |
| timePicker / time | 时间选择 | el-time-picker |
| switch | 开关 | el-switch |
| slider | 滑块 | el-slider |
| rate | 评分 | el-rate |
| colorPicker | 颜色选择 | el-color-picker |
| cascader | 级联选择 | el-cascader |
| treeSelect | 树形选择 | el-tree-select |
| transfer | 穿梭框 | el-transfer |
| upload | 上传 | el-upload |
| autocomplete | 自动完成 | el-autocomplete |
| text / display | 文本展示 | span |
| html | HTML 内容 | div |
| divider | 分割线 | el-divider |
| alert | 警告 | el-alert |
| slot / custom | 自定义插槽 | slot |
| component | 自定义组件 | component |
## 🎯 详细用法示例
### 1. 输入框相关
```javascript
{
field: 'username',
label: '用户名',
type: 'input',
required: true,
clearable: true,
placeholder: '请输入用户名',
maxlength: 20,
'show-word-limit': true,
prefixIcon: 'User',
suffixIcon: 'ArrowRight'
}
// 文本域
{
field: 'description',
label: '描述',
type: 'textarea',
rows: 4,
autosize: { minRows: 2, maxRows: 6 }
}
// 密码框
{
field: 'password',
label: '密码',
type: 'password',
'show-password': true
}
```
### 2. 选择器相关
```javascript
// 下拉选择
{
field: 'category',
label: '分类',
type: 'select',
clearable: true,
filterable: true,
multiple: true,
placeholder: '请选择分类',
options: [
{ label: '技术', value: 'tech' },
{ label: '生活', value: 'life' }
]
}
// 单选框
{
field: 'gender',
label: '性别',
type: 'radio',
options: [
{ label: '男', value: '1' },
{ label: '女', value: '2' }
]
}
// 多选框
{
field: 'hobby',
label: '爱好',
type: 'checkbox',
options: [
{ label: '阅读', value: 'reading' },
{ label: '运动', value: 'sports' }
]
}
// 级联选择器
{
field: 'region',
label: '地区',
type: 'cascader',
clearable: true,
options: [
{
value: 'zhejiang',
label: '浙江省',
children: [
{ value: 'hangzhou', label: '杭州市' }
]
}
]
}
// 树形选择器
{
field: 'department',
label: '部门',
type: 'treeSelect',
clearable: true,
data: [
{
id: '1',
label: '技术部',
children: [
{ id: '1-1', label: '前端组' }
]
}
]
}
```
### 3. 日期时间相关
```javascript
// 日期选择
{
field: 'birthDate',
label: '出生日期',
type: 'date',
'value-format': 'YYYY-MM-DD',
placeholder: '请选择日期'
}
// 日期时间选择
{
field: 'publishTime',
label: '发布时间',
type: 'datetime',
'value-format': 'YYYY-MM-DD HH:mm:ss'
}
// 日期范围
{
field: 'validDate',
label: '有效期',
type: 'daterange',
'value-format': 'YYYY-MM-DD',
'start-placeholder': '开始日期',
'end-placeholder': '结束日期'
}
// 时间选择
{
field: 'workTime',
label: '工作时间',
type: 'timePicker',
'value-format': 'HH:mm:ss'
}
```
### 4. 数值与评分
```javascript
// 数字输入
{
field: 'age',
label: '年龄',
type: 'number',
min: 0,
max: 150,
step: 1,
precision: 0
}
// 开关
{
field: 'isPublished',
label: '是否发布',
type: 'switch',
'active-text': '已发布',
'inactive-text': '未发布',
'active-value': 1,
'inactive-value': 0
}
// 滑块
{
field: 'priority',
label: '优先级',
type: 'slider',
min: 0,
max: 100,
step: 5
}
// 评分
{
field: 'rating',
label: '评分',
type: 'rate',
max: 5,
'allow-half': true
}
```
### 5. 上传相关
```javascript
{
field: 'files',
label: '文件上传',
type: 'upload',
action: '/api/upload',
'show-file-list': true,
multiple: true,
drag: true,
limit: 5,
accept: '.jpg,.png,.pdf',
'on-success': (response, file, fileList) => {
console.log('上传成功', response)
},
'before-upload': (file) => {
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
ElMessage.error('文件大小不能超过 2MB')
}
return isLt2M
}
}
```
### 6. 自定义插槽
```vue
<template>
<SuperForm v-model="formData" :items="formItems">
<!-- 自定义插槽 -->
<template #customField="{ item, value, onChange }">
<div class="custom-content">
<span>{{ value }}</span>
<el-button @click="handleCustomChange(onChange)">修改</el-button>
</div>
</template>
</SuperForm>
</template>
<script>
const formItems = [
{
field: 'customField',
label: '自定义字段',
type: 'slot',
slotName: 'customField'
}
]
</script>
```
### 7. 自定义按钮
```javascript
const formConfig = {
showButtons: true,
customButtons: [
{
key: 'save',
text: '保存草稿',
type: 'info',
icon: 'Document',
loading: false,
handler: () => {
console.log('保存草稿')
}
},
{
key: 'preview',
text: '预览',
type: 'success',
handler: () => {
console.log('预览')
}
}
]
}
```
## 🔌 接口集成
### 自动提交接口
```vue
<SuperForm
v-model="formData"
:items="formItems"
:save-api="'/api/user/create'"
:update-api="'/api/user/update'"
:fetch-api="'/api/user/detail'"
@success="handleSuccess"
@error="handleError"
/>
```
### 动态接口
```vue
<SuperForm
:save-api="(isEdit) => isEdit ? '/api/update' : '/api/create'"
:update-api="getUpdateApi"
/>
```
### 数据转换
```javascript
// 提交前转换数据
const dataTransform = (data) => {
return {
...data,
publishTime: dayjs(data.publishTime).unix()
}
}
// 获取详情后转换数据
const responseTransform = (data) => {
return {
...data,
publishTime: dayjs.unix(data.publishTime).format('YYYY-MM-DD HH:mm:ss')
}
}
```
## 🎨 高级特性
### 1. 动态表单项
```javascript
const showExtraFields = ref(true)
const formItems = computed(() => {
const baseItems = [
{ field: 'name', label: '名称', type: 'input' }
]
if (showExtraFields.value) {
baseItems.push(
{ field: 'email', label: '邮箱', type: 'input' },
{ field: 'phone', label: '电话', type: 'input' }
)
}
return baseItems
})
```
### 2. 条件验证
```javascript
const formItems = [
{
field: 'password',
label: '密码',
type: 'password',
rules: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value && !/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
callback(new Error('密码必须包含大小写字母和数字'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
]
```
### 3. 字段联动
```javascript
const handleFieldChange = (field, value) => {
if (field === 'country') {
// 根据选择的国家更新城市选项
updateCityOptions(value)
}
}
```
### 4. 自定义组件
```javascript
const formItems = [
{
field: 'customData',
label: '自定义组件',
type: 'component',
component: MyCustomComponent,
componentProps: {
// 传递给自定义组件的属性
config: {}
}
}
]
```
## 📢 事件
| 事件名 | 说明 | 参数 |
|--------|------|------|
| submit | 表单提交 | (formData, isEdit) |
| success | 提交成功 | (response, formData, isEdit) |
| error | 提交失败 | (error, formData, isEdit) |
| change | 表单数据变化 | (formData) |
| field-change | 字段值变化 | (field, value, formData) |
| field-blur | 字段失焦 | (field, event) |
| field-focus | 字段聚焦 | (field, event) |
| validate | 验证触发 | (prop, isValid, message) |
| reset | 表单重置 | - |
## 🔧 方法
通过 ref 可以调用以下方法:
```javascript
const formRef = ref()
// 验证表单
await formRef.value.validate()
// 验证指定字段
await formRef.value.validateField('username')
// 清除验证
formRef.value.clearValidate()
// 重置表单
formRef.value.resetFields()
// 设置表单数据
formRef.value.setFormData({ name: '张三' })
// 获取表单数据
const data = formRef.value.getFormData()
// 设置字段值
formRef.value.setFieldValue('name', '李四')
// 获取字段值
const name = formRef.value.getFieldValue('name')
// 获取详情数据(用于编辑)
await formRef.value.fetchDetail(id)
```
## 🎯 最佳实践
### 1. 配置文件分离
```javascript
// formConfig.js
export const userFormConfig = {
labelWidth: '120px',
size: 'default'
}
export const userFormItems = [
{ field: 'name', label: '姓名', type: 'input', required: true },
{ field: 'email', label: '邮箱', type: 'input' }
]
```
### 2. 验证规则复用
```javascript
// validators.js
export const requiredRule = (message) => ({
required: true,
message,
trigger: 'blur'
})
export const emailRule = {
type: 'email',
message: '请输入正确的邮箱地址',
trigger: 'blur'
}
export const phoneRule = {
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号',
trigger: 'blur'
}
```
### 3. 类型定义
```typescript
interface FormItem {
field: string
label: string
type: string
required?: boolean
rules?: any[]
span?: number
defaultValue?: any
[key: string]: any
}
interface FormConfig {
labelWidth?: string
labelPosition?: 'left' | 'right' | 'top'
inline?: boolean
size?: 'large' | 'default' | 'small'
showButtons?: boolean
customButtons?: ButtonConfig[]
}
```
## 💡 常见问题
### Q: 如何实现字段间的联动?
A: 使用 `field-change` 事件监听字段变化:
```javascript
<SuperForm @field-change="handleFieldChange" />
const handleFieldChange = (field, value, formData) => {
if (field === 'type') {
// 根据 type 更新其他字段
}
}
```
### Q: 如何实现复杂的验证逻辑?
A: 使用自定义验证器:
```javascript
{
field: 'password',
label: '密码',
type: 'password',
rules: [
{
validator: (rule, value, callback) => {
// 自定义验证逻辑
callback()
},
trigger: 'blur'
}
]
}
```
### Q: 如何实现上传文件的回显?
A: 在 `fetch-api` 获取详情后,组件会自动回显数据。
## 📄 License
MIT
---
**作者**: SuperForm Team
**更新时间**: 2025-03-13
## 📚 完整示例代码
查看完整示例代码,请参考:
- **示例文件**: `src/views/everydayPage/SuperFormExample.vue`
- **包含内容**:
- ✅ 基础表单示例(输入框、选择器、日期等)
- ✅ 高级表单示例(多级表头、富文本、文件上传等)
- ✅ 自定义样式示例
- ✅ 动态表单示例
- ✅ 自定义插槽示例
- ✅ 组件集成示例(FinancialTable、FormDynamicTable、CommonSelector、AnnualPlan)
- ✅ 性能优化示例(异步加载、分批渲染、虚拟滚动)
## 🔗 组件集成说明
### 1. FinancialTable 财务表格
```vue
<SuperForm
:items="[
{
field: 'financialData',
label: '财务数据',
type: 'component',
component: FinancialTable,
componentProps: {
isPreview: false
}
}
]"
/>
```
### 2. FormDynamicTable 动态表格
```vue
<SuperForm
:items="[
{
field: 'dynamicData',
label: '动态数据',
type: 'component',
component: FormDynamicTable,
componentProps: {
columns: dynamicTableColumns,
showImportExport: true
}
}
]"
/>
```
### 3. CommonSelector 通用选择器
```vue
<SuperForm
:items="[
{
field: 'matterType',
label: '事项类型',
type: 'component',
component: CommonSelector,
componentProps: {
dictName: 'matterType',
placeholder: '请选择事项类型'
}
}
]"
/>
```
### 4. AnnualPlan 年度计划
```vue
<SuperForm
:items="[
{
field: 'annualPlan',
label: '年度计划',
type: 'component',
component: AnnualPlan,
componentProps: {
dynamicTimeList: ['2025', '2026', '2027'],
isPreview: false
}
}
]"
/>
```
## ⚡ 性能优化最佳实践
### 1. 使用异步组件按需加载
```javascript
import { defineAsyncComponent } from 'vue';
const FinancialTable = defineAsyncComponent(() =>
import('@/components/FinancialTable.vue')
);
```
### 2. 使用 computed 缓存计算结果
```javascript
const formItems = computed(() => [
{ field: 'name', label: '名称', type: 'input' }
]);
```
### 3. 大数据分批加载
```javascript
const loadLargeForm = async () => {
const batchSize = 100;
for (let i = 0; i < batches; i++) {
await nextTick();
// 加载批次数据
}
};
```
### 4. 使用 v-if 而非 v-show
```javascript
// 首次渲染时不加载
<AnnualPlan v-if="showAnnualPlan" />
```
## 🎯 管理端项目常见配置
### 1. 列表页表单配置
```javascript
const searchFormConfig = {
labelWidth: '100px',
inline: true,
showButtons: false
};
const searchFormItems = [
{ field: 'keyword', label: '关键词', type: 'input', clearable: true },
{ field: 'status', label: '状态', type: 'select', clearable: true, options: [...] },
{ field: 'dateRange', label: '日期范围', type: 'daterange' }
];
```
### 2. 新增/编辑页表单配置
```javascript
const formConfig = {
labelWidth: '120px',
gutter: 20,
showButtons: true,
submitText: '保存',
resetText: '取消'
};
```
### 3. 审批页表单配置
```javascript
const approvalFormConfig = {
labelWidth: '140px',
disabled: false, // 根据权限动态设置
showButtons: true
};
const approvalFormItems = [
{ field: 'approvalStatus', label: '审批状态', type: 'radio', required: true },
{ field: 'approvalOpinion', label: '审批意见', type: 'textarea', rows: 4 }
];
```
## 📖 快速开始指南
### Step 1: 引入组件
```vue
<script setup>
import SuperForm from '@/components/common/SuperForm.vue';
import { ref } from 'vue';
const formData = ref({});
</script>
```
### Step 2: 配置表单项
```javascript
const formItems = [
{ field: 'username', label: '用户名', type: 'input', required: true }
];
```
### Step 3: 使用组件
```vue
<template>
<SuperForm v-model="formData" :items="formItems" />
</template>
```
### Step 4: 处理提交
```vue
<script setup>
const handleSubmit = (data) => {
console.log('提交数据:', data);
// 调用接口保存
};
</script>
<template>
<SuperForm v-model="formData" :items="formItems" @submit="handleSubmit" />
</template>
```
## 🎨 UI/UX 优化建议
1. **合理使用栅格布局** - 重要字段使用大跨度,次要字段使用小跨度
2. **分组展示** - 使用 divider 或 alert 组件对字段分组
3. **必填标识** - 重要字段使用 required 属性
4. **提示信息** - 使用 placeholder 提供输入提示
5. **字段联动** - 使用 field-change 事件实现字段联动
/**
* SuperForm 超级表单组件类型定义
*/
import type { FormInstance, FormItemRule, FormRules } from 'element-plus'
/**
* 表单项基础类型
*/
export interface BaseFormItem {
/** 字段名 */
field: string
/** 标签文本 */
label: string
/** 组件类型 */
type: FormItemType
/** 是否必填 */
required?: boolean
/** 验证规则 */
rules?: FormItemRule[]
/** 栅格占位格数 */
span?: number
/** 栅格偏移 */
offset?: number
/** 响应式栅格 */
xs?: number | string
sm?: number | string
md?: number | string
lg?: number | string
xl?: number | string
/** 标签元素 */
tag?: string
/** 栅格向左移动 */
pull?: number | string
/** 栅格向右移动 */
push?: number | string
/** 默认值 */
defaultValue?: any
/** 字段 CSS 类名 */
itemClass?: string
/** 是否显示错误信息 */
showMessage?: boolean
/** 是否行内错误信息 */
inlineMessage?: boolean
/** 错误信息 */
error?: string
}
/**
* 输入框类型
*/
export interface InputFormItem extends BaseFormItem {
type: 'input' | 'textarea' | 'password'
/** 是否可清空 */
clearable?: boolean
/** 是否禁用 */
disabled?: boolean
/** 是否只读 */
readonly?: boolean
/** 最大输入长度 */
maxlength?: number
/** 是否显示字数统计 */
showWordLimit?: boolean
/** 占位文本 */
placeholder?: string
/** 输入框前缀 */
prefix?: string
/** 输入框后缀 */
suffix?: string
/** 前置内容 */
prepend?: string
/** 后置内容 */
append?: string
/** 前置图标 */
prefixIcon?: string
/** 后置图标 */
suffixIcon?: string
/** 前置插槽名称 */
prependSlot?: string
/** 后置插槽名称 */
appendSlot?: string
/** 文本域行数 */
rows?: number
/** 自适应内容高度 */
autosize?: boolean | { minRows?: number; maxRows?: number }
}
/**
* 数字输入框类型
*/
export interface InputNumberFormItem extends BaseFormItem {
type: 'inputNumber' | 'number'
/** 最小值 */
min?: number
/** 最大值 */
max?: number
/** 步长 */
step?: number
/** 精度 */
precision?: number
/** 是否显示控制按钮 */
controls?: boolean
/** 按钮位置 */
controlsPosition?: 'right' | ''
/** 尺寸 */
size?: 'large' | 'default' | 'small'
}
/**
* 选择器类型
*/
export interface SelectFormItem extends BaseFormItem {
type: 'select'
/** 是否可清空 */
clearable?: boolean
/** 是否可搜索 */
filterable?: boolean
/** 是否多选 */
multiple?: boolean
/** 是否远程搜索 */
remote?: boolean
/** 远程搜索方法 */
remoteMethod?: (query: string) => void
/** 是否加载中 */
loading?: boolean
/** 无匹配文本 */
noMatchText?: string
/** 无数据文本 */
noDataText?: string
/** 占位文本 */
placeholder?: string
/** 选项数据源 */
options?: SelectOption[] | ((item: SelectFormItem, formData: Record<string, any>) => SelectOption[])
/** 选项值的字段名 */
valueKey?: string
/** 选项标签的字段名 */
labelKey?: string
/** 选项插槽名称 */
optionSlot?: string
/** 空数据插槽名称 */
emptySlot?: string
/** 前缀插槽名称 */
prefix?: string
}
/**
* 单选框组类型
*/
export interface RadioFormItem extends BaseFormItem {
type: 'radio' | 'radioGroup'
/** 是否禁用 */
disabled?: boolean
/** 是否显示边框 */
border?: boolean
/** 尺寸 */
size?: 'large' | 'default' | 'small'
/** 选项数据源 */
options?: SelectOption[]
}
/**
* 多选框组类型
*/
export interface CheckboxFormItem extends BaseFormItem {
type: 'checkbox' | 'checkboxGroup'
/** 是否禁用 */
disabled?: boolean
/** 是否显示边框 */
border?: boolean
/** 最小选中数 */
min?: number
/** 最大选中数 */
max?: number
/** 尺寸 */
size?: 'large' | 'default' | 'small'
/** 选项数据源 */
options?: SelectOption[]
}
/**
* 日期选择器类型
*/
export interface DatePickerFormItem extends BaseFormItem {
type: 'date' | 'datetime' | 'dates' | 'week' | 'month' | 'year' | 'daterange' | 'datetimerange' | 'monthrange'
/** 占位文本 */
placeholder?: string
/** 格式化显示值 */
format?: string
/** 绑定值格式 */
valueFormat?: string
/** 是否可清空 */
clearable?: boolean
/** 是否禁用 */
disabled?: boolean
/** 是否只读 */
readonly?: boolean
/** 是否可编辑 */
editable?: boolean
/** 范围选择器开始日期占位符 */
startPlaceholder?: string
/** 范围选择器结束日期占位符 */
endPlaceholder?: string
/** 范围分隔符 */
rangeSeparator?: string
/** 禁用日期函数 */
disabledDate?: (date: Date) => boolean
}
/**
* 时间选择器类型
*/
export interface TimePickerFormItem extends BaseFormItem {
type: 'timePicker' | 'time' | 'timeSelect'
/** 占位文本 */
placeholder?: string
/** 格式化显示值 */
format?: string
/** 绑定值格式 */
valueFormat?: string
/** 是否可清空 */
clearable?: boolean
/** 是否禁用 */
disabled?: boolean
/** 是否只读 */
readonly?: boolean
}
/**
* 开关类型
*/
export interface SwitchFormItem extends BaseFormItem {
type: 'switch'
/** 是否禁用 */
disabled?: boolean
/** 是否加载中 */
loading?: boolean
/** 尺寸 */
size?: 'large' | 'default' | 'small'
/** 打开时的文字 */
activeText?: string | number
/** 关闭时的文字 */
inactiveText?: string | number
/** 打开时的值 */
activeValue?: boolean | string | number
/** 关闭时的值 */
inactiveValue?: boolean | string | number
}
/**
* 滑块类型
*/
export interface SliderFormItem extends BaseFormItem {
type: 'slider'
/** 最小值 */
min?: number
/** 最大值 */
max?: number
/** 步长 */
step?: number
/** 是否显示间断点 */
showStops?: boolean
/** 是否显示提示框 */
showTooltip?: boolean
/** 是否禁用 */
disabled?: boolean
/** 是否双滑块模式 */
range?: boolean
/** 是否垂直模式 */
vertical?: boolean
/** 高度,垂直模式下生效 */
height?: string
/** 标记 */
marks?: Record<number, string>
}
/**
* 评分类型
*/
export interface RateFormItem extends BaseFormItem {
type: 'rate'
/** 最大分值 */
max?: number
/** 是否禁用 */
disabled?: boolean
/** 是否允许半选 */
allowHalf?: boolean
/** 低分和中等分数的界限值 */
lowThreshold?: number
/** 高分和中等分数的界限值 */
highThreshold?: number
/** 颜色数组 */
colors?: string | string[]
/** 未选中颜色 */
voidColor?: string
/** 禁用时的未选中颜色 */
disabledVoidColor?: string
/** 图标数组 */
iconClasses?: string | string[]
/** 未选中图标类名 */
voidIconClass?: string
/** 禁用时的未选中图标类名 */
disabledVoidIconClass?: string
/** 是否显示辅助文字 */
showText?: boolean
/** 是否显示当前分数 */
showScore?: boolean
/** 辅助文字数组 */
texts?: string[]
}
/**
* 颜色选择器类型
*/
export interface ColorPickerFormItem extends BaseFormItem {
type: 'colorPicker'
/** 是否禁用 */
disabled?: boolean
/** 尺寸 */
size?: 'large' | 'default' | 'small'
/** 是否支持透明度 */
showAlpha?: boolean
/** 颜色格式 */
colorFormat?: 'hex' | 'rgb' | 'hsl' | 'hsv'
/** 预定义颜色 */
predefine?: string[]
}
/**
* 级联选择器类型
*/
export interface CascaderFormItem extends BaseFormItem {
type: 'cascader'
/** 是否可清空 */
clearable?: boolean
/** 是否可搜索 */
filterable?: boolean
/** 是否禁用 */
disabled?: boolean
/** 占位文本 */
placeholder?: string
/** 选项数据源 */
options?: CascaderOption[]
/** 配置选项 */
props?: CascaderProps
/** 是否多选 */
props?: {
expandTrigger?: 'click' | 'hover'
multiple?: boolean
checkStrictly?: boolean
emitPath?: boolean
lazy?: boolean
lazyLoad?: (node: any, resolve: (data: any[]) => void) => void
value?: string
label?: string
children?: string
leaf?: string
[key: string]: any
}
}
/**
* 树形选择器类型
*/
export interface TreeSelectFormItem extends BaseFormItem {
type: 'treeSelect'
/** 是否可清空 */
clearable?: boolean
/** 是否可搜索 */
filterable?: boolean
/** 是否禁用 */
disabled?: boolean
/** 占位文本 */
placeholder?: string
/** 树形数据 */
data?: TreeNode[]
/** 配置选项 */
props?: {
label?: string
value?: string
children?: string
disabled?: string
isLeaf?: string
[key: string]: any
}
/** 是否多选 */
multiple?: boolean
/** 是否展示复选框 */
showCheckbox?: boolean
/** 是否严格的遵循父子不互相关联的做法 */
checkStrictly?: boolean
/** 是否展开子节点 */
defaultExpandAll?: boolean
/** 默认展开的节点的 key 的数组 */
defaultExpandedKeys?: any[]
/** 节点唯一标识 */
nodeKey?: string
}
/**
* 穿梭框类型
*/
export interface TransferFormItem extends BaseFormItem {
type: 'transfer'
/** 数据源 */
data?: TransferDataItem[]
/** 是否禁用 */
disabled?: boolean
/** 是否可过滤 */
filterable?: boolean
/** 过滤占位符 */
filterPlaceholder?: string
/** 是否可筛选 */
filterMethod?: (query: string, item: TransferDataItem) => boolean
/** 每次列表渲染的数量 */
targetOrder?: 'original' | 'push' | 'unshift'
/** 标题 */
titles?: string[]
/** 按钮文案 */
buttonTexts?: string[]
/** 列表底部文案 */
renderContent?: (h: any, option: TransferDataItem) => any
}
/**
* 上传类型
*/
export interface UploadFormItem extends BaseFormItem {
type: 'upload'
/** 上传地址 */
action?: string
/** 请求头 */
headers?: Record<string, string>
/** 请求方法 */
method?: 'post' | 'put' | 'patch'
/** 是否支持多选 */
multiple?: boolean
/** 上传时附带的额外参数 */
data?: Record<string, any>
/** 上传的文件字段名 */
name?: string
/** 是否携带 cookie */
withCredentials?: boolean
/** 是否显示文件列表 */
showFileList?: boolean
/** 是否拖拽上传 */
drag?: boolean
/** 接受上传的文件类型 */
accept?: string
/** 是否自动上传 */
autoUpload?: boolean
/** 上传数量限制 */
limit?: number
/** 按钮文本 */
buttonText?: string
/** 按钮类型 */
buttonType?: string
/** 按钮图标 */
uploadIcon?: string
/** 提示文本 */
tip?: string
/** 触发插槽名称 */
triggerSlot?: string
/** 提示插槽名称 */
tipSlot?: string
/** 上传成功回调 */
onSuccess?: (response: any, file: any, fileList: any[], formData: Record<string, any>) => void
/** 上传失败回调 */
onError?: (error: any, file: any, fileList: any[]) => void
/** 上传进度回调 */
onProgress?: (event: any, file: any, fileList: any[]) => void
/** 文件状态改变回调 */
onChange?: (file: any, fileList: any[], formData: Record<string, any>) => void
/** 移除文件回调 */
onRemove?: (file: any, fileList: any[], formData: Record<string, any>) => void
/** 点击文件列表回调 */
onPreview?: (file: any) => void
/** 上传前回调 */
beforeUpload?: (file: any, formData: Record<string, any>) => boolean | Promise<any>
/** 删除前回调 */
beforeRemove?: (file: any, fileList: any[], formData: Record<string, any>) => boolean | Promise<any>
/** 覆盖默认上传行为 */
httpRequest?: (options: any, formData: Record<string, any>) => Promise<any>
}
/**
* 自动完成类型
*/
export interface AutocompleteFormItem extends BaseFormItem {
type: 'autocomplete'
/** 是否禁用 */
disabled?: boolean
/** 占位文本 */
placeholder?: string
/** 是否可清空 */
clearable?: boolean
/** 输入建议数据源 */
fetchSuggestions?: (queryString: string, callback: (data: any[]) => void) => void
/** 输入建议数组的对象别名 */
valueKey?: string
/** 输入建议数组的标签别名 */
labelKey?: string
/** 前置内容 */
prepend?: string
/** 后置内容 */
append?: string
/** 前缀 */
prefix?: string
/** 后缀 */
suffix?: string
}
/**
* 文本展示类型
*/
export interface TextFormItem extends BaseFormItem {
type: 'text' | 'display'
/** 自定义格式化函数 */
formatter?: (value: any, item: TextFormItem, formData: Record<string, any>) => string
/** 文本属性 */
textAttrs?: Record<string, any>
}
/**
* HTML 内容类型
*/
export interface HtmlFormItem extends BaseFormItem {
type: 'html'
/** HTML 内容 */
htmlContent?: string
/** HTML 容器属性 */
htmlAttrs?: Record<string, any>
}
/**
* 分割线类型
*/
export interface DividerFormItem extends BaseFormItem {
type: 'divider'
/** 分割线文字 */
dividerText?: string
/** 分割线属性 */
dividerAttrs?: {
direction?: 'horizontal' | 'vertical'
borderStyle?: 'solid' | 'dashed' | 'dotted' | 'double'
contentPosition?: 'left' | 'center' | 'right'
[key: string]: any
}
}
/**
* 警告类型
*/
export interface AlertFormItem extends BaseFormItem {
type: 'alert'
/** 警告类型 */
alertType?: 'success' | 'warning' | 'info' | 'error'
/** 警告属性 */
alertAttrs?: {
title?: string
description?: string
type?: 'success' | 'warning' | 'info' | 'error'
closable?: boolean
closeText?: string
center?: boolean
showIcon?: boolean
effect?: 'light' | 'dark'
[key: string]: any
}
/** 标题插槽名称 */
alertTitleSlot?: string
}
/**
* 自定义插槽类型
*/
export interface SlotFormItem extends BaseFormItem {
type: 'slot' | 'custom'
/** 插槽名称 */
slotName?: string
}
/**
* 自定义组件类型
*/
export interface ComponentFormItem extends BaseFormItem {
type: 'component'
/** 组件 */
component: any
/** 组件属性 */
componentProps?: Record<string, any>
}
/**
* 表单项类型联合
*/
export type FormItem =
| InputFormItem
| InputNumberFormItem
| SelectFormItem
| RadioFormItem
| CheckboxFormItem
| DatePickerFormItem
| TimePickerFormItem
| SwitchFormItem
| SliderFormItem
| RateFormItem
| ColorPickerFormItem
| CascaderFormItem
| TreeSelectFormItem
| TransferFormItem
| UploadFormItem
| AutocompleteFormItem
| TextFormItem
| HtmlFormItem
| DividerFormItem
| AlertFormItem
| SlotFormItem
| ComponentFormItem
/**
* 表单项组件类型
*/
export type FormItemType =
| 'input'
| 'textarea'
| 'password'
| 'inputNumber'
| 'number'
| 'select'
| 'radio'
| 'radioGroup'
| 'checkbox'
| 'checkboxGroup'
| 'date'
| 'datetime'
| 'dates'
| 'week'
| 'month'
| 'year'
| 'daterange'
| 'datetimerange'
| 'monthrange'
| 'timePicker'
| 'time'
| 'timeSelect'
| 'switch'
| 'slider'
| 'rate'
| 'colorPicker'
| 'cascader'
| 'treeSelect'
| 'transfer'
| 'upload'
| 'autocomplete'
| 'text'
| 'display'
| 'html'
| 'divider'
| 'alert'
| 'slot'
| 'custom'
| 'component'
/**
* 选择器选项
*/
export interface SelectOption {
label: string
value: any
disabled?: boolean
icon?: string
[key: string]: any
}
/**
* 级联选择器选项
*/
export interface CascaderOption {
label: string
value: any
children?: CascaderOption[]
disabled?: boolean
[key: string]: any
}
/**
* 树节点
*/
export interface TreeNode {
id: string | number
label: string
children?: TreeNode[]
disabled?: boolean
isLeaf?: boolean
[key: string]: any
}
/**
* 穿梭框数据项
*/
export interface TransferDataItem {
key: string | number
label: string
disabled?: boolean
[key: string]: any
}
/**
* 自定义按钮配置
*/
export interface ButtonConfig {
/** 按钮唯一标识 */
key: string
/** 按钮文本 */
text: string
/** 按钮类型 */
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text' | 'default'
/** 按钮尺寸 */
size?: 'large' | 'default' | 'small'
/** 是否加载中 */
loading?: boolean
/** 按钮图标 */
icon?: string
/** 是否朴素按钮 */
plain?: boolean
/** 是否圆角 */
round?: boolean
/** 是否圆形 */
circle?: boolean
/** 是否禁用 */
disabled?: boolean
/** 点击事件处理函数 */
handler: () => void | Promise<void>
}
/**
* 表单配置
*/
export interface FormConfig {
/** 表单标签宽度 */
labelWidth?: string
/** 表单标签位置 */
labelPosition?: 'left' | 'right' | 'top'
/** 是否行内表单 */
inline?: boolean
/** 是否禁用 */
disabled?: boolean
/** 表单尺寸 */
size?: 'large' | 'default' | 'small'
/** 栅格间隔 */
gutter?: number
/** 是否显示操作按钮 */
showButtons?: boolean
/** 是否显示提交按钮 */
showSubmit?: boolean
/** 是否显示重置按钮 */
showReset?: boolean
/** 提交按钮文本 */
submitText?: string
/** 重置按钮文本 */
resetText?: string
/** 提交按钮类型 */
submitType?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'default'
/** 提交按钮图标 */
submitIcon?: string
/** 重置按钮图标 */
resetIcon?: string
/** 按钮容器占位 */
buttonSpan?: number
/** 按钮容器偏移 */
buttonOffset?: number
/** 按钮响应式配置 */
buttonXs?: number | string
buttonSm?: number | string
buttonMd?: number | string
buttonLg?: number | string
buttonXl?: number | string
/** 按钮容器 CSS 类名 */
buttonClass?: string
/** 按钮尺寸 */
buttonSize?: 'large' | 'default' | 'small'
/** 自定义按钮配置 */
customButtons?: ButtonConfig[]
/** 是否显示验证图标 */
statusIcon?: boolean
/** 是否以行内形式展示验证信息 */
inlineMessage?: boolean
/** 是否在 rules 属性改变后立即触发一次验证 */
validateOnRuleChange?: boolean
/** 是否隐藏必填星号 */
hideRequiredAsterisk?: boolean
/** 是否显示验证信息 */
showMessage?: boolean
/** 标签后缀 */
labelSuffix?: string
/** 是否在验证失败时滚动到第一个错误表单 */
scrollToError?: boolean
/** 滚动行为配置 */
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions
/** 提交按钮是否禁用 */
submitDisabled?: boolean
/** 重置按钮是否禁用 */
resetDisabled?: boolean
/** 提交按钮是否朴素 */
submitPlain?: boolean
/** 重置按钮是否朴素 */
resetPlain?: boolean
/** 提交按钮是否圆角 */
submitRound?: boolean
/** 重置按钮是否圆角 */
resetRound?: boolean
/** 提交按钮是否圆形 */
submitCircle?: boolean
/** 重置按钮是否圆形 */
resetCircle?: boolean
[key: string]: any
}
/**
* SuperForm 组件 Props
*/
export interface SuperFormProps {
/** 表单配置 */
config: FormConfig
/** 表单项配置 */
items: FormItem[]
/** 表单数据 */
modelValue: Record<string, any>
/** 表单验证规则 */
rules?: FormRules
/** 保存接口配置 */
saveApi?: string | ((isEdit: boolean) => string) | null
/** 更新接口配置 */
updateApi?: string | ((isEdit: boolean) => string) | null
/** 获取详情接口配置 */
fetchApi?: string | ((id: any) => string) | null
/** 主键字段名 */
idKey?: string
/** 是否在提交前验证 */
validateBeforeSubmit?: boolean
/** 提交前的钩子 */
beforeSubmit?: (data: Record<string, any>, isEdit: boolean) => boolean | Promise<boolean> | null
/** 提交后的钩子 */
afterSubmit?: (response: any, data: Record<string, any>, isEdit: boolean) => void | null
/** 自定义提交方法 */
customSubmit?: (data: Record<string, any>, options: { isEdit: boolean }) => void | null
/** 数据转换函数 */
dataTransform?: (data: Record<string, any>) => Record<string, any> | null
/** 响应数据转换函数 */
responseTransform?: (data: any) => Record<string, any> | null
}
/**
* SuperForm 组件实例方法
*/
export interface SuperFormInstance {
/** 表单引用 */
formRef: FormInstance
/** 表单响应式数据 */
formData: Record<string, any>
/** 是否编辑模式 */
isEdit: boolean
/** 验证表单 */
validate: () => Promise<boolean>
/** 验证指定字段 */
validateField: (prop: string) => Promise<boolean>
/** 清除验证 */
clearValidate: (fields?: string | string[]) => void
/** 重置表单 */
resetFields: () => void
/** 设置表单数据 */
setFormData: (data: Record<string, any>) => void
/** 获取表单数据 */
getFormData: () => Record<string, any>
/** 设置字段值 */
setFieldValue: (field: string, value: any) => void
/** 获取字段值 */
getFieldValue: (field: string) => any
/** 获取详情数据 */
fetchDetail: (id: any) => Promise<void>
/** 提交表单 */
handleSubmit: () => Promise<void>
}
/**
* SuperForm 组件事件
*/
export interface SuperFormEmits {
/** 更新表单数据 */
(event: 'update:modelValue', value: Record<string, any>): void
/** 表单提交 */
(event: 'submit', data: Record<string, any>, isEdit: boolean): void
/** 提交成功 */
(event: 'success', response: any, data: Record<string, any>, isEdit: boolean): void
/** 提交失败 */
(event: 'error', error: any, data: Record<string, any>, isEdit: boolean): void
/** 表单数据变化 */
(event: 'change', data: Record<string, any>): void
/** 验证触发 */
(event: 'validate', prop: string | undefined, isValid: boolean, message: string): void
/** 表单重置 */
(event: 'reset'): void
/** 字段值变化 */
(event: 'field-change', field: string, value: any, formData: Record<string, any>): void
/** 字段失焦 */
(event: 'field-blur', field: string, event: Event): void
/** 字段聚焦 */
(event: 'field-focus', field: string, event: Event): void
/** 字段可见性变化 */
(event: 'field-visible-change', field: string, visible: boolean): void
/** 字段清除 */
(event: 'field-clear', field: string): void
/** 字段移除标签 */
(event: 'field-remove-tag', field: string, tag: any): void
/** 颜色激活变化 */
(event: 'color-active-change', field: string, color: string): void
/** 级联展开变化 */
(event: 'cascader-expand-change', field: string, value: any): void
/** 树节点点击 */
(event: 'tree-node-click', field: string, data: any): void
/** 树选中变化 */
(event: 'tree-check', field: string, data: any): void
/** 树节点展开 */
(event: 'tree-node-expand', field: string, data: any): void
/** 树节点折叠 */
(event: 'tree-node-collapse', field: string, data: any): void
/** 穿梭框左侧选中变化 */
(event: 'transfer-left-check-change', field: string, value: any): void
/** 穿梭框右侧选中变化 */
(event: 'transfer-right-check-change', field: string, value: any): void
/** 自动完成选择 */
(event: 'autocomplete-select', field: string, value: any): void
/** 上传成功 */
(event: 'upload-success', field: string, response: any, file: any, fileList: any[]): void
/** 上传失败 */
(event: 'upload-error', field: string, error: any, file: any, fileList: any[]): void
/** 上传进度 */
(event: 'upload-progress', field: string, event: any, file: any, fileList: any[]): void
/** 上传文件变化 */
(event: 'upload-change', field: string, file: any, fileList: any[]): void
/** 移除上传文件 */
(event: 'upload-remove', field: string, file: any, fileList: any[]): void
/** 预览上传文件 */
(event: 'upload-preview', field: string, file: any): void
/** 上传前 */
(event: 'before-upload', field: string, file: any): void
/** 移除前 */
(event: 'before-remove', field: string, file: any, fileList: any[]): void
/** 自定义上传请求 */
(event: 'http-request', field: string, options: any): void
/** 日历变化 */
(event: 'calendar-change', field: string, value: any): void
/** 面板变化 */
(event: 'panel-change', field: string, value: any): void
}
<template>
<div class="super-form" v-loading="loading">
<el-form
ref="formRef"
:model="formData"
:rules="computedRules"
:label-width="config.labelWidth || '120px'"
:label-position="config.labelPosition || 'right'"
:inline="config.inline || false"
:disabled="config.disabled || false"
:size="config.size || 'default'"
:status-icon="config.statusIcon !== false"
:inline-message="config.inlineMessage"
:validate-on-rule-change="config.validateOnRuleChange !== false"
:hide-required-asterisk="config.hideRequiredAsterisk"
:show-message="config.showMessage !== false"
:label-suffix="config.labelSuffix"
:scroll-to-error="config.scrollToError !== false"
:scroll-into-view-options="config.scrollIntoViewOptions"
@validate="handleValidate"
>
<el-row :gutter="config.gutter || 20">
<el-col
v-for="(item, index) in formItems"
:key="item.field || index"
:span="item.span || 24"
:offset="item.offset"
:xs="item.xs"
:sm="item.sm"
:md="item.md"
:lg="item.lg"
:xl="item.xl"
:tag="item.tag"
:pull="item.pull"
:push="item.push"
>
<el-form-item
:prop="item.field"
:label="item.label"
:required="item.required"
:rules="item.rules"
:error="item.error"
:show-message="item.showMessage !== false"
:inline-message="item.inlineMessage"
:class="item.itemClass"
>
<!-- 输入框 -->
<template v-if="item.type === 'input'">
<el-input
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@blur="handleFieldBlur(item, $event)"
@focus="handleFieldFocus(item, $event)"
@input="handleFieldInput(item, $event)"
@change="handleFieldChange(item, $event)"
@clear="handleFieldClear(item)"
>
<template v-if="item.prepend" #prepend>{{ item.prepend }}</template>
<template v-if="item.append" #append>{{ item.append }}</template>
<template v-if="item.prefixIcon" #prefix>
<el-icon><component :is="item.prefixIcon" /></el-icon>
</template>
<template v-if="item.suffixIcon" #suffix>
<el-icon><component :is="item.suffixIcon" /></el-icon>
</template>
<template v-if="item.prefix" #prefix>{{ item.prefix }}</template>
<template v-if="item.suffix" #suffix>{{ item.suffix }}</template>
<template v-if="item.prependSlot" #prepend>
<slot :name="item.prependSlot" :item="item" :value="formData[item.field]" />
</template>
<template v-if="item.appendSlot" #append>
<slot :name="item.appendSlot" :item="item" :value="formData[item.field]" />
</template>
</el-input>
</template>
<!-- 文本域 -->
<template v-else-if="item.type === 'textarea'">
<el-input
v-model="formData[item.field]"
type="textarea"
v-bind="getItemProps(item)"
@blur="handleFieldBlur(item, $event)"
@focus="handleFieldFocus(item, $event)"
@input="handleFieldInput(item, $event)"
@change="handleFieldChange(item, $event)"
/>
</template>
<!-- 密码输入框 -->
<template v-else-if="item.type === 'password'">
<el-input
v-model="formData[item.field]"
type="password"
v-bind="getItemProps(item)"
@blur="handleFieldBlur(item, $event)"
@focus="handleFieldFocus(item, $event)"
@input="handleFieldInput(item, $event)"
@change="handleFieldChange(item, $event)"
/>
</template>
<!-- 数字输入框 -->
<template v-else-if="item.type === 'inputNumber' || item.type === 'number'">
<el-input-number
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@blur="handleFieldBlur(item, $event)"
@focus="handleFieldFocus(item, $event)"
@change="handleFieldChange(item, $event)"
/>
</template>
<!-- 选择器 -->
<template v-else-if="item.type === 'select'">
<el-select
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
@visible-change="handleFieldVisibleChange(item, $event)"
@remove-tag="handleFieldRemoveTag(item, $event)"
@clear="handleFieldClear(item)"
@blur="handleFieldBlur(item, $event)"
@focus="handleFieldFocus(item, $event)"
>
<template v-if="item.options">
<el-option
v-for="option in getOptions(item)"
:key="option[item.valueKey || 'value']"
:label="option[item.labelKey || 'label']"
:value="option[item.valueKey || 'value']"
:disabled="option.disabled"
>
<slot
v-if="item.optionSlot"
:name="item.optionSlot"
:option="option"
:item="item"
>
<span v-if="option.icon" style="margin-right: 8px">
<el-icon><component :is="option.icon" /></el-icon>
</span>
{{ option[item.labelKey || 'label'] }}
</slot>
</el-option>
</template>
<template v-if="item.prefix" #prefix>{{ item.prefix }}</template>
<template v-if="item.emptySlot" #empty>
<slot :name="item.emptySlot" :item="item" />
</template>
</el-select>
</template>
<!-- 多选框组 -->
<template v-else-if="item.type === 'checkbox' || item.type === 'checkboxGroup'">
<el-checkbox-group
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
>
<el-checkbox
v-for="option in getOptions(item)"
:key="option[item.valueKey || 'value']"
:label="option[item.valueKey || 'value']"
:disabled="option.disabled || item.disabled"
:border="item.border"
:true-label="option.trueLabel"
:false-label="option.falseLabel"
:indeterminate="option.indeterminate"
>
{{ option[item.labelKey || 'label'] }}
</el-checkbox>
</el-checkbox-group>
</template>
<!-- 单选框组 -->
<template v-else-if="item.type === 'radio' || item.type === 'radioGroup'">
<el-radio-group
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
>
<el-radio
v-for="option in getOptions(item)"
:key="option[item.valueKey || 'value']"
:label="option[item.valueKey || 'value']"
:disabled="option.disabled || item.disabled"
:border="item.border"
:size="item.size"
>
{{ option[item.labelKey || 'label'] }}
</el-radio>
</el-radio-group>
</template>
<!-- 日期时间选择器 -->
<template v-else-if="['date', 'dates', 'datetime', 'week', 'month', 'year', 'daterange', 'datetimerange', 'monthrange'].includes(item.type)">
<el-date-picker
v-model="formData[item.field]"
:type="item.type"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
@blur="handleFieldBlur(item, $event)"
@focus="handleFieldFocus(item, $event)"
@calendar-change="handleCalendarChange(item, $event)"
@panel-change="handlePanelChange(item, $event)"
@visible-change="handleFieldVisibleChange(item, $event)"
/>
</template>
<!-- 时间选择器 -->
<template v-else-if="item.type === 'timePicker' || item.type === 'time'">
<el-time-picker
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
@blur="handleFieldBlur(item, $event)"
@focus="handleFieldFocus(item, $event)"
/>
</template>
<!-- 时间选择器组 -->
<template v-else-if="item.type === 'timeSelect'">
<el-time-select
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
@blur="handleFieldBlur(item, $event)"
@focus="handleFieldFocus(item, $event)"
/>
</template>
<!-- 开关 -->
<template v-else-if="item.type === 'switch'">
<el-switch
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
/>
</template>
<!-- 滑块 -->
<template v-else-if="item.type === 'slider'">
<el-slider
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
/>
</template>
<!-- 评分 -->
<template v-else-if="item.type === 'rate'">
<el-rate
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
/>
</template>
<!-- 颜色选择器 -->
<template v-else-if="item.type === 'colorPicker'">
<el-color-picker
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
@active-change="handleColorActiveChange(item, $event)"
/>
</template>
<!-- 穿梭框 -->
<template v-else-if="item.type === 'transfer'">
<el-transfer
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
@left-check-change="handleTransferLeftCheckChange(item, $event)"
@right-check-change="handleTransferRightCheckChange(item, $event)"
/>
</template>
<!-- 级联选择器 -->
<template v-else-if="item.type === 'cascader'">
<el-cascader
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
@expand-change="handleCascaderExpandChange(item, $event)"
@visible-change="handleFieldVisibleChange(item, $event)"
/>
</template>
<!-- 树形选择器 -->
<template v-else-if="item.type === 'treeSelect'">
<el-tree-select
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
@node-click="handleTreeNodeClick(item, $event)"
@check="handleTreeCheck(item, $event)"
@node-expand="handleTreeNodeExpand(item, $event)"
@node-collapse="handleTreeNodeCollapse(item, $event)"
/>
</template>
<!-- 上传 -->
<template v-else-if="item.type === 'upload'">
<el-upload
v-bind="getItemProps(item)"
:on-success="(response, file, fileList) => handleUploadSuccess(item, response, file, fileList)"
:on-error="(error, file, fileList) => handleUploadError(item, error, file, fileList)"
:on-progress="(event, file, fileList) => handleUploadProgress(item, event, file, fileList)"
:on-change="(file, fileList) => handleUploadChange(item, file, fileList)"
:on-remove="(file, fileList) => handleUploadRemove(item, file, fileList)"
:on-preview="(file) => handleUploadPreview(item, file)"
:before-upload="(file) => handleBeforeUpload(item, file)"
:before-remove="(file, fileList) => handleBeforeRemove(item, file, fileList)"
:http-request="(options) => handleHttpRequest(item, options)"
>
<slot v-if="item.triggerSlot" :name="item.triggerSlot" :item="item">
<el-button v-if="!item.drag" :type="item.buttonType || 'primary'">
<el-icon v-if="item.uploadIcon"><component :is="item.uploadIcon || 'UploadFilled'" /></el-icon>
{{ item.buttonText || '点击上传' }}
</el-button>
</slot>
<template v-if="item.tipSlot" #tip>
<slot :name="item.tipSlot" :item="item">
<div class="el-upload__tip">{{ item.tip }}</div>
</slot>
</template>
<template v-if="item.triggerSlot" #trigger>
<slot :name="item.triggerSlot" :item="item" />
</template>
</el-upload>
</template>
<!-- 自动完成 -->
<template v-else-if="item.type === 'autocomplete'">
<el-autocomplete
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@select="handleAutocompleteSelect(item, $event)"
@blur="handleFieldBlur(item, $event)"
@focus="handleFieldFocus(item, $event)"
@change="handleFieldChange(item, $event)"
>
<template v-if="item.prepend" #prepend>{{ item.prepend }}</template>
<template v-if="item.append" #append>{{ item.append }}</template>
<template v-if="item.prefix" #prefix>{{ item.prefix }}</template>
<template v-if="item.suffix" #suffix>{{ item.suffix }}</template>
</el-autocomplete>
</template>
<!-- 计数器 -->
<template v-else-if="item.type === 'counter'">
<el-input-number
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
/>
</template>
<!-- 文本展示 -->
<template v-else-if="item.type === 'text' || item.type === 'display'">
<div class="form-text-display" v-bind="item.textAttrs">
{{ item.formatter ? item.formatter(formData[item.field], item, formData) : formData[item.field] }}
</div>
</template>
<!-- HTML内容 -->
<template v-else-if="item.type === 'html'">
<div class="form-html-content" v-html="item.htmlContent || formData[item.field]" v-bind="item.htmlAttrs"></div>
</template>
<!-- 分割线 -->
<template v-else-if="item.type === 'divider'">
<el-divider v-bind="item.dividerAttrs">{{ item.dividerText }}</el-divider>
</template>
<!-- 警告 -->
<template v-else-if="item.type === 'alert'">
<el-alert v-bind="item.alertAttrs" :type="item.alertType || 'info'">
<template v-if="item.alertTitleSlot" #title>
<slot :name="item.alertTitleSlot" :item="item" />
</template>
</el-alert>
</template>
<!-- 自定义插槽 -->
<template v-else-if="item.type === 'slot' || item.type === 'custom'">
<slot
:name="item.slotName || item.field"
:item="item"
:value="formData[item.field]"
:formData="formData"
:onChange="(value) => handleSlotChange(item, value)"
/>
</template>
<!-- 自定义组件 -->
<template v-else-if="item.type === 'component' && item.component">
<component
:is="item.component"
v-model="formData[item.field]"
v-bind="getItemProps(item)"
@change="handleFieldChange(item, $event)"
@blur="handleFieldBlur(item, $event)"
@focus="handleFieldFocus(item, $event)"
/>
</template>
<!-- 默认插槽兜底 -->
<slot
v-else
:name="item.field"
:item="item"
:value="formData[item.field]"
:formData="formData"
:onChange="(value) => handleSlotChange(item, value)"
/>
</el-form-item>
</el-col>
<!-- 表单操作按钮 -->
<el-col
v-if="config.showButtons !== false"
:span="config.buttonSpan || 24"
:offset="config.buttonOffset"
:xs="config.buttonXs"
:sm="config.buttonSm"
:md="config.buttonMd"
:lg="config.buttonLg"
:xl="config.buttonXl"
>
<el-form-item :class="['form-buttons', config.buttonClass]">
<el-button
v-if="config.showSubmit !== false"
:type="config.submitType || 'primary'"
:size="config.buttonSize || config.size || 'default'"
:loading="submitLoading"
:icon="config.submitIcon"
:plain="config.submitPlain"
:round="config.submitRound"
:circle="config.submitCircle"
:disabled="config.submitDisabled"
@click="handleSubmit"
>
{{ config.submitText || (isEdit ? '更新' : '提交') }}
</el-button>
<el-button
v-if="config.showReset !== false"
:size="config.buttonSize || config.size || 'default'"
:icon="config.resetIcon"
:plain="config.resetPlain"
:round="config.resetRound"
:circle="config.resetCircle"
:disabled="config.resetDisabled"
@click="handleReset"
>
{{ config.resetText || '重置' }}
</el-button>
<el-button
v-for="btn in customButtons"
:key="btn.key"
:type="btn.type"
:size="btn.size || config.buttonSize || config.size || 'default'"
:loading="btn.loading"
:icon="btn.icon"
:plain="btn.plain"
:round="btn.round"
:circle="btn.circle"
:disabled="btn.disabled"
@click="handleCustomButton(btn)"
>
{{ btn.text }}
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted, getCurrentInstance } from 'vue'
import { UploadFilled } from '@element-plus/icons-vue'
const props = defineProps({
// 表单配置
config: {
type: Object,
default: () => ({})
},
// 表单项配置
items: {
type: Array,
default: () => []
},
// 表单数据
modelValue: {
type: Object,
default: () => ({})
},
// 表单验证规则
rules: {
type: Object,
default: () => ({})
},
// 保存接口配置
saveApi: {
type: [String, Object, Function],
default: null
},
// 更新接口配置
updateApi: {
type: [String, Object, Function],
default: null
},
// 获取详情接口配置(用于编辑时回显数据)
fetchApi: {
type: [String, Object, Function],
default: null
},
// 主键字段名(用于判断新增还是编辑)
idKey: {
type: String,
default: 'id'
},
// 是否在提交前验证
validateBeforeSubmit: {
type: Boolean,
default: true
},
// 提交前的钩子(返回false可阻止提交)
beforeSubmit: {
type: Function,
default: null
},
// 提交后的钩子
afterSubmit: {
type: Function,
default: null
},
// 自定义提交方法
customSubmit: {
type: Function,
default: null
},
// 数据转换函数(提交前转换数据格式)
dataTransform: {
type: Function,
default: null
},
// 响应数据转换函数(获取详情后转换数据格式)
responseTransform: {
type: Function,
default: null
}
})
const emit = defineEmits([
'update:modelValue',
'submit',
'success',
'error',
'change',
'blur',
'focus',
'input',
'validate',
'reset',
'field-change',
'field-blur',
'field-focus',
'field-visible-change'
])
const { proxy } = getCurrentInstance()
const formRef = ref()
const formData = reactive({})
const loading = ref(false)
const submitLoading = ref(false)
// 计算属性:表单项配置
const formItems = computed(() => props.items)
// 计算属性:是否为编辑模式
const isEdit = computed(() => {
return !!(formData[props.idKey] || props.modelValue[props.idKey])
})
// 计算属性:自定义按钮
const customButtons = computed(() => {
return props.config.customButtons || []
})
// 计算属性:表单验证规则
const computedRules = computed(() => {
const rules = { ...props.rules }
formItems.value.forEach(item => {
if (item.rules) {
rules[item.field] = item.rules
}
if (item.required && !rules[item.field]) {
rules[item.field] = [
{
required: true,
message: item.requiredMessage || `请输入${item.label}`,
trigger: item.trigger || 'blur'
}
]
}
})
return rules
})
// 获取表单项的属性(过滤掉组件内部使用的属性)
const getItemProps = (item) => {
const excludeKeys = [
'field', 'label', 'type', 'required', 'rules', 'requiredMessage', 'trigger',
'span', 'offset', 'xs', 'sm', 'md', 'lg', 'xl', 'tag', 'pull', 'push',
'defaultValue', 'valueKey', 'labelKey', 'options', 'optionSlot', 'emptySlot',
'prepend', 'append', 'prefix', 'suffix', 'prefixIcon', 'suffixIcon',
'prependSlot', 'appendSlot', 'textAttrs', 'htmlAttrs', 'htmlContent',
'dividerAttrs', 'dividerText', 'alertAttrs', 'alertType', 'alertTitleSlot',
'slotName', 'component', 'componentProps', 'itemClass', 'formatter',
'buttonText', 'buttonType', 'uploadIcon', 'triggerSlot', 'tipSlot', 'tip'
]
const props = {}
Object.keys(item).forEach(key => {
if (!excludeKeys.includes(key)) {
props[key] = item[key]
}
})
return props
}
// 获取选项数据
const getOptions = (item) => {
if (typeof item.options === 'function') {
return item.options(item, formData)
}
return item.options || []
}
// 初始化表单数据
const initFormData = () => {
// 清空现有数据
Object.keys(formData).forEach(key => {
delete formData[key]
})
// 设置默认值或外部传入的值
formItems.value.forEach(item => {
if (item.field) {
const externalValue = props.modelValue[item.field]
if (externalValue !== undefined) {
formData[item.field] = externalValue
} else {
formData[item.field] = item.defaultValue !== undefined
? item.defaultValue
: getDefaultValue(item.type)
}
}
})
}
// 获取不同类型的默认值
const getDefaultValue = (type) => {
const defaultValues = {
checkbox: [],
switch: false,
inputNumber: undefined,
number: undefined,
rate: 0,
slider: 0,
upload: [],
transfer: [],
treeSelect: null,
cascader: [],
timeSelect: ''
}
return defaultValues[type] !== undefined ? defaultValues[type] : ''
}
// 监听表单数据变化
watch(
formData,
(newVal) => {
emit('update:modelValue', { ...newVal })
emit('change', newVal)
},
{ deep: true }
)
// 监听外部数据变化
watch(
() => props.modelValue,
(newVal) => {
Object.keys(newVal).forEach(key => {
if (Object.prototype.hasOwnProperty.call(formData, key)) {
formData[key] = newVal[key]
}
})
},
{ deep: true }
)
// 字段事件处理
const handleFieldInput = (item, event) => {
emit('input', item.field, event.target ? event.target.value : event)
}
const handleFieldChange = (item, value) => {
emit('field-change', item.field, value, formData)
emit('change', item.field, value)
}
const handleFieldBlur = (item, event) => {
emit('field-blur', item.field, event)
emit('blur', item.field, event)
}
const handleFieldFocus = (item, event) => {
emit('field-focus', item.field, event)
emit('focus', item.field, event)
}
const handleFieldClear = (item) => {
emit('field-clear', item.field)
}
const handleFieldVisibleChange = (item, visible) => {
emit('field-visible-change', item.field, visible)
}
const handleFieldRemoveTag = (item, tag) => {
emit('field-remove-tag', item.field, tag)
}
const handleCalendarChange = (item, value) => {
emit('calendar-change', item.field, value)
}
const handlePanelChange = (item, value) => {
emit('panel-change', item.field, value)
}
// 颜色选择器事件
const handleColorActiveChange = (item, color) => {
emit('color-active-change', item.field, color)
}
// 级联选择器事件
const handleCascaderExpandChange = (item, value) => {
emit('cascader-expand-change', item.field, value)
}
// 树形选择器事件
const handleTreeNodeClick = (item, data) => {
emit('tree-node-click', item.field, data)
}
const handleTreeCheck = (item, data) => {
emit('tree-check', item.field, data)
}
const handleTreeNodeExpand = (item, data) => {
emit('tree-node-expand', item.field, data)
}
const handleTreeNodeCollapse = (item, data) => {
emit('tree-node-collapse', item.field, data)
}
// 穿梭框事件
const handleTransferLeftCheckChange = (item, value) => {
emit('transfer-left-check-change', item.field, value)
}
const handleTransferRightCheckChange = (item, value) => {
emit('transfer-right-check-change', item.field, value)
}
// 自动完成事件
const handleAutocompleteSelect = (item, value) => {
emit('autocomplete-select', item.field, value)
}
// 上传事件处理
const handleUploadSuccess = (item, response, file, fileList) => {
if (item.onSuccess) {
item.onSuccess(response, file, fileList, formData)
}
emit('upload-success', item.field, response, file, fileList)
}
const handleUploadError = (item, error, file, fileList) => {
if (item.onError) {
item.onError(error, file, fileList)
}
emit('upload-error', item.field, error, file, fileList)
}
const handleUploadProgress = (item, event, file, fileList) => {
if (item.onProgress) {
item.onProgress(event, file, fileList)
}
emit('upload-progress', item.field, event, file, fileList)
}
const handleUploadChange = (item, file, fileList) => {
if (item.onChange) {
item.onChange(file, fileList, formData)
}
emit('upload-change', item.field, file, fileList)
}
const handleUploadRemove = (item, file, fileList) => {
if (item.onRemove) {
item.onRemove(file, fileList, formData)
}
emit('upload-remove', item.field, file, fileList)
}
const handleUploadPreview = (item, file) => {
if (item.onPreview) {
item.onPreview(file)
}
emit('upload-preview', item.field, file)
}
const handleBeforeUpload = (item, file) => {
if (item.beforeUpload) {
return item.beforeUpload(file, formData)
}
emit('before-upload', item.field, file)
return true
}
const handleBeforeRemove = (item, file, fileList) => {
if (item.beforeRemove) {
return item.beforeRemove(file, fileList, formData)
}
emit('before-remove', item.field, file, fileList)
return true
}
const handleHttpRequest = (item, options) => {
if (item.httpRequest) {
return item.httpRequest(options, formData)
}
emit('http-request', item.field, options)
}
// 插槽变更处理
const handleSlotChange = (item, value) => {
formData[item.field] = value
emit('field-change', item.field, value, formData)
}
// 表单验证事件
const handleValidate = (prop, isValid, message) => {
emit('validate', prop, isValid, message)
}
// 表单提交
const handleSubmit = async () => {
try {
// 如果有自定义提交方法,使用自定义方法
if (props.customSubmit) {
props.customSubmit(formData, { isEdit: isEdit.value })
return
}
// 验证表单
if (props.validateBeforeSubmit) {
const valid = await formRef.value.validate()
if (!valid) return
}
// 执行提交前钩子
if (props.beforeSubmit) {
const shouldContinue = await props.beforeSubmit(formData, isEdit.value)
if (shouldContinue === false) return
}
submitLoading.value = true
// 转换数据格式
let submitData = props.dataTransform ? props.dataTransform(formData) : { ...formData }
// 判断是新增还是编辑
const apiUrl = isEdit.value ? props.updateApi : props.saveApi
if (!apiUrl) {
// 如果没有配置接口,直接触发提交事件
emit('submit', submitData, isEdit.value)
submitLoading.value = false
return
}
// 发送请求
proxy.$post({
url: typeof apiUrl === 'function' ? apiUrl(isEdit.value) : apiUrl,
data: submitData,
callback: (response) => {
submitLoading.value = false
emit('success', response, submitData, isEdit.value)
// 执行提交后钩子
if (props.afterSubmit) {
props.afterSubmit(response, submitData, isEdit.value)
}
},
error: (err) => {
submitLoading.value = false
emit('error', err, submitData, isEdit.value)
}
})
} catch (error) {
submitLoading.value = false
emit('validate', false, error)
}
}
// 表单重置
const handleReset = () => {
formRef.value.resetFields()
initFormData()
emit('reset')
}
// 获取详情数据(用于编辑)
const fetchDetail = async (id) => {
if (!props.fetchApi) return
loading.value = true
try {
proxy.$post({
url: typeof props.fetchApi === 'function' ? props.fetchApi(id) : props.fetchApi,
data: { [props.idKey]: id },
callback: (response) => {
const data = props.responseTransform ? props.responseTransform(response) : response
Object.keys(data).forEach(key => {
if (Object.prototype.hasOwnProperty.call(formData, key)) {
formData[key] = data[key]
}
})
emit('update:modelValue', { ...formData })
loading.value = false
},
error: (err) => {
loading.value = false
emit('error', err)
}
})
} catch (error) {
loading.value = false
emit('error', error)
}
}
// 表单验证
const validate = async () => {
try {
const valid = await formRef.value.validate()
emit('validate', true)
return valid
} catch (error) {
emit('validate', false, error)
return false
}
}
// 验证指定字段
const validateField = async (prop) => {
try {
await formRef.value.validateField(prop)
return true
} catch (error) {
return false
}
}
// 清除验证
const clearValidate = (fields) => {
formRef.value.clearValidate(fields)
}
// 重置表单
const resetFields = () => {
formRef.value.resetFields()
initFormData()
}
// 设置表单数据
const setFormData = (data) => {
Object.keys(data).forEach(key => {
if (Object.prototype.hasOwnProperty.call(formData, key)) {
formData[key] = data[key]
}
})
emit('update:modelValue', { ...formData })
}
// 获取表单数据
const getFormData = () => {
return { ...formData }
}
// 设置字段值
const setFieldValue = (field, value) => {
formData[field] = value
}
// 获取字段值
const getFieldValue = (field) => {
return formData[field]
}
// 暴露方法和属性
defineExpose({
formRef,
validate,
validateField,
clearValidate,
resetFields,
setFormData,
getFormData,
setFieldValue,
getFieldValue,
fetchDetail,
handleSubmit,
formData,
isEdit
})
onMounted(() => {
initFormData()
})
</script>
<style scoped lang="less">
.super-form {
.form-text-display {
padding: 0 11px;
line-height: 32px;
color: #606266;
word-break: break-all;
}
.form-html-content {
padding: 0 11px;
line-height: 1.5;
color: #606266;
}
.form-buttons {
:deep(.el-form-item__content) {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
}
:deep(.el-input-number) {
width: 100%;
}
:deep(.el-select) {
width: 100%;
}
:deep(.el-cascader) {
width: 100%;
}
:deep(.el-tree-select) {
width: 100%;
}
:deep(.el-date-editor) {
width: 100%;
}
:deep(.el-time-picker) {
width: 100%;
}
:deep(.el-autocomplete) {
width: 100%;
}
:deep(.el-color-picker) {
.el-color-picker__mask {
border-radius: 4px;
}
}
:deep(.el-form-item__label) {
font-weight: 500;
color: #303133;
}
:deep(.el-form-item) {
margin-bottom: 18px;
}
:deep(.el-form--inline) {
.el-form-item {
display: inline-flex;
vertical-align: middle;
margin-right: 10px;
}
}
:deep(.el-transfer) {
text-align: left;
}
:deep(.el-upload) {
.el-upload-dragger {
padding: 40px;
}
}
}
</style>
......@@ -271,6 +271,13 @@ const routes = [
title: "参股企业管理",
component: () => import("@/views/everydayPage/shareAdd.vue"),
},
{
path: "/SuperFormExample",
name: "SuperFormExample",
title: "测试组件",
component: () =>
import("@/views/everydayPage/SuperFormExample.vue"),
},
{
path: "/system",
name: "system",
......
<template>
<div class="super-form-example">
<el-card shadow="never" class="example-card">
<template #header>
<div class="card-header">
<span class="card-title">超级表单组件使用示例</span>
<el-button type="primary" size="small" @click="handleCreate"
>新增示例</el-button
>
</div>
</template>
<el-tabs v-model="activeTab" type="border-card" class="form-tabs">
<!-- 基础示例 -->
<el-tab-pane label="基础示例" name="basic">
<SuperForm
ref="basicFormRef"
v-model="basicFormData"
:config="basicFormConfig"
:items="basicFormItems"
:rules="basicFormRules"
@submit="handleBasicSubmit"
@success="handleSubmitSuccess"
@error="handleSubmitError"
/>
</el-tab-pane>
<!-- 高级示例 -->
<el-tab-pane label="高级示例" name="advanced">
<SuperForm
ref="advancedFormRef"
v-model="advancedFormData"
:config="advancedFormConfig"
:items="advancedFormItems"
:save-api="saveApiUrl"
:update-api="updateApiUrl"
:fetch-api="fetchApiUrl"
@success="handleSubmitSuccess"
@error="handleSubmitError"
/>
</el-tab-pane>
<!-- 自定义样式示例 -->
<el-tab-pane label="自定义样式" name="custom">
<SuperForm
ref="customFormRef"
v-model="customFormData"
:config="customFormConfig"
:items="customFormItems"
/>
</el-tab-pane>
<!-- 动态表单示例 -->
<el-tab-pane label="动态表单" name="dynamic">
<el-button type="primary" @click="toggleDynamicFields"
>切换字段显示</el-button
>
<SuperForm
ref="dynamicFormRef"
v-model="dynamicFormData"
:config="dynamicFormConfig"
:items="dynamicFormItems"
/>
</el-tab-pane>
<!-- 带插槽的表单 -->
<el-tab-pane label="自定义插槽" name="slots">
<SuperForm
ref="slotFormRef"
v-model="slotFormData"
:config="slotFormConfig"
:items="slotFormItems"
@submit="handleSlotSubmit"
>
<!-- 自定义插槽:用户信息 -->
<template #userInfo="{ item, value, onChange }">
<div class="custom-user-info">
<el-button
size="small"
type="primary"
@click="handleChangeAvatar"
>更换头像</el-button
>
</div>
</template>
<!-- 自定义插槽:数据展示 -->
<template #dataDisplay="{ item, value }">
<el-descriptions :column="2" border>
<el-descriptions-item label="用户名">{{
slotFormData.username
}}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{
slotFormData.email
}}</el-descriptions-item>
<el-descriptions-item label="注册时间">{{
slotFormData.registerTime
}}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag
:type="slotFormData.status === 1 ? 'success' : 'danger'"
>
{{ slotFormData.status === 1 ? "正常" : "禁用" }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</template>
<!-- 自定义插槽:富文本编辑器 -->
<template #richText="{ item, value, onChange }">
<div class="rich-text-editor">
<el-input
type="textarea"
:rows="6"
:model-value="value"
@input="onChange"
placeholder="请输入富文本内容..."
/>
<div class="editor-toolbar">
<el-button size="small" @click="insertText('<b>粗体</b>')"
>粗体</el-button
>
<el-button size="small" @click="insertText('<i>斜体</i>')"
>斜体</el-button
>
<el-button size="small" @click="insertText('<u>下划线</u>')"
>下划线</el-button
>
</div>
</div>
</template>
</SuperForm>
</el-tab-pane>
<!-- 组件集成示例 - 使用懒加载优化性能 -->
<el-tab-pane label="组件集成" name="integration">
<div class="integration-container">
<!-- 通用选择器集成 -->
<div class="integration-section">
<div class="section-title">CommonSelector 通用选择器</div>
<SuperForm
ref="selectorFormRef"
v-model="selectorFormData"
:config="selectorFormConfig"
:items="selectorFormItems"
/>
</div>
<!-- 财务表格集成 - 使用虚拟滚动优化 -->
<div class="integration-section">
<div class="section-title">FinancialTable 财务表格</div>
<FinancialTable
v-model="financialTableData"
:is-preview="false"
/>
</div>
<!-- 动态表单表格集成 - 使用分页加载 -->
<div class="integration-section">
<div class="section-title">FormDynamicTable 动态表格</div>
<FormDynamicTable
v-model="dynamicTableData"
:columns="dynamicTableColumns"
:default-row="defaultRow"
:select-options="selectOptions"
:show-import-export="true"
export-name="动态表格数据"
/>
</div>
<!-- 年度计划集成 - 使用计算属性优化 -->
<div class="integration-section">
<div class="section-title">AnnualPlan 年度计划</div>
<el-button
type="primary"
size="small"
@click="toggleAnnualPlan"
style="margin-bottom: 12px"
>
{{ showAnnualPlan ? '隐藏' : '显示' }}年度计划
</el-button>
<AnnualPlan
v-if="showAnnualPlan"
v-model="annualPlanData"
:dynamic-time-list="annualPlanTimeList"
:is-preview="false"
/>
</div>
</div>
</el-tab-pane>
<!-- 性能优化示例 -->
<el-tab-pane label="性能优化" name="performance">
<div class="performance-container">
<el-alert
title="性能优化技巧"
type="info"
:closable="false"
style="margin-bottom: 20px"
>
<template #default>
<ul class="performance-tips">
<li>✅ 使用 computed 缓存计算结果</li>
<li>✅ 使用 v-once 渲染静态内容</li>
<li>✅ 使用虚拟滚动处理大列表</li>
<li>✅ 使用防抖/节流优化频繁触发的事件</li>
<li>✅ 按需加载组件,减少初始渲染压力</li>
<li>✅ 合理使用 v-show 和 v-if</li>
</ul>
</template>
</el-alert>
<div class="performance-demo">
<div class="demo-item">
<div class="demo-title">大数据表单(1000+ 字段)</div>
<el-progress
:percentage="loadingProgress"
:status="loadingProgress === 100 ? 'success' : undefined"
/>
<el-button
type="primary"
@click="loadLargeForm"
:loading="isLoadingLargeForm"
style="margin-top: 12px"
>
加载大表单
</el-button>
<div v-if="largeFormItems.length > 0" class="form-stats">
<el-tag type="success">字段数: {{ largeFormItems.length }}</el-tag>
<el-tag type="info">加载时间: {{ loadTime }}ms</el-tag>
</div>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, nextTick, onMounted, defineAsyncComponent } from "vue";
import { ElMessage } from "element-plus";
import SuperForm from "@/components/common/SuperForm.vue";
// 🚀 性能优化:使用异步组件按需加载,减少初始渲染时间
const FinancialTable = defineAsyncComponent(() =>
import("@/components/FinancialTable.vue")
);
const FormDynamicTable = defineAsyncComponent(() =>
import("@/components/FormDynamicTable/index.vue")
);
const AnnualPlan = defineAsyncComponent(() =>
import("@/views/everydayPage/annualPlan.vue")
);
const CommonSelector = defineAsyncComponent(() =>
import("@/components/CommonSelector.vue")
);
const activeTab = ref("basic");
const basicFormRef = ref();
const advancedFormRef = ref();
const customFormRef = ref();
const dynamicFormRef = ref();
const slotFormRef = ref();
const selectorFormRef = ref();
// API 配置
const saveApiUrl = "/api/example/create";
const updateApiUrl = "/api/example/update";
const fetchApiUrl = "/api/example/detail";
// 性能优化相关
const loadingProgress = ref(0);
const isLoadingLargeForm = ref(false);
const largeFormItems = ref([]);
const loadTime = ref(0);
const showAnnualPlan = ref(false); // 默认隐藏年度计划,按需显示
// ==================== 基础表单示例 ====================
const basicFormData = ref({
username: "",
password: "",
gender: "1",
hobby: [],
birthDate: "",
email: "",
phone: "",
age: 25,
introduce: "",
});
const basicFormConfig = {
labelWidth: "120px",
labelPosition: "right",
gutter: 20,
size: "default",
showButtons: true,
showSubmit: true,
showReset: true,
submitText: "提交",
resetText: "重置",
submitType: "primary",
};
const basicFormItems = computed(() => [
{
field: "username",
label: "用户名",
type: "input",
span: 12,
required: true,
clearable: true,
placeholder: "请输入用户名",
maxlength: 20,
"show-word-limit": true,
},
{
field: "password",
label: "密码",
type: "password",
span: 12,
required: true,
clearable: true,
placeholder: "请输入密码",
"show-password": true,
},
{
field: "email",
label: "邮箱",
type: "input",
span: 12,
required: true,
clearable: true,
placeholder: "请输入邮箱",
rules: [
{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" },
],
},
{
field: "phone",
label: "手机号",
type: "input",
span: 12,
clearable: true,
placeholder: "请输入手机号",
rules: [
{
pattern: /^1[3-9]\d{9}$/,
message: "请输入正确的手机号",
trigger: "blur",
},
],
},
{
field: "gender",
label: "性别",
type: "radio",
span: 12,
required: true,
options: [
{ label: "男", value: "1" },
{ label: "女", value: "2" },
{ label: "保密", value: "0" },
],
},
{
field: "age",
label: "年龄",
type: "number",
span: 12,
min: 0,
max: 150,
step: 1,
},
{
field: "hobby",
label: "爱好",
type: "checkbox",
span: 12,
options: [
{ label: "阅读", value: "reading" },
{ label: "运动", value: "sports" },
{ label: "音乐", value: "music" },
{ label: "旅行", value: "travel" },
],
},
{
field: "birthDate",
label: "出生日期",
type: "date",
span: 12,
"value-format": "YYYY-MM-DD",
placeholder: "请选择出生日期",
},
{
field: "introduce",
label: "个人简介",
type: "textarea",
span: 24,
rows: 4,
maxlength: 200,
"show-word-limit": true,
placeholder: "请输入个人简介",
},
]);
const basicFormRules = {
username: [
{ required: true, message: "请输入用户名", trigger: "blur" },
{ min: 3, max: 20, message: "长度在 3 到 20 个字符", trigger: "blur" },
],
password: [
{ required: true, message: "请输入密码", trigger: "blur" },
{ min: 6, message: "密码长度不能少于 6 位", trigger: "blur" },
],
};
// ==================== 高级表单示例 ====================
const advancedFormData = ref({
name: "",
category: [],
tags: [],
publishTime: "",
validDate: [],
isPublished: false,
priority: 50,
rating: 3,
color: "#409EFF",
region: [],
treeSelect: "",
files: [],
});
const advancedFormConfig = {
labelWidth: "140px",
gutter: 20,
showButtons: true,
customButtons: [
{
key: "draft",
text: "保存草稿",
type: "info",
icon: "Document",
handler: () => {
ElMessage.info("保存草稿成功");
},
},
{
key: "preview",
text: "预览",
type: "success",
icon: "View",
handler: () => {
ElMessage.success("预览模式");
},
},
],
};
const advancedFormItems = computed(() => [
{
field: "name",
label: "内容名称",
type: "input",
span: 12,
required: true,
clearable: true,
placeholder: "请输入内容名称",
},
{
field: "category",
label: "内容分类",
type: "select",
span: 12,
clearable: true,
filterable: true,
placeholder: "请选择分类",
options: [
{ label: "技术", value: "tech" },
{ label: "生活", value: "life" },
{ label: "娱乐", value: "entertainment" },
{ label: "体育", value: "sports" },
],
props: {
multiple: true,
},
},
{
field: "tags",
label: "标签",
type: "checkbox",
span: 12,
options: [
{ label: "热门", value: "hot" },
{ label: "推荐", value: "recommend" },
{ label: "最新", value: "latest" },
{ label: "精选", value: "featured" },
],
},
{
field: "publishTime",
label: "发布时间",
type: "datetime",
span: 12,
"value-format": "YYYY-MM-DD HH:mm:ss",
placeholder: "请选择发布时间",
},
{
field: "validDate",
label: "有效期",
type: "daterange",
span: 12,
"value-format": "YYYY-MM-DD",
"start-placeholder": "开始日期",
"end-placeholder": "结束日期",
},
{
field: "isPublished",
label: "是否发布",
type: "switch",
span: 8,
"active-text": "已发布",
"inactive-text": "未发布",
},
{
field: "priority",
label: "优先级",
type: "slider",
span: 16,
min: 0,
max: 100,
marks: {
0: "低",
50: "中",
100: "高",
},
},
{
field: "rating",
label: "评分",
type: "rate",
span: 12,
max: 5,
"allow-half": true,
"show-text": true,
texts: ["极差", "失望", "一般", "满意", "惊喜"],
},
{
field: "color",
label: "主题色",
type: "colorPicker",
span: 12,
"show-alpha": true,
},
{
field: "region",
label: "地区",
type: "cascader",
span: 12,
clearable: true,
placeholder: "请选择地区",
options: [
{
value: "zhejiang",
label: "浙江省",
children: [
{ value: "hangzhou", label: "杭州市" },
{ value: "ningbo", label: "宁波市" },
],
},
{
value: "jiangsu",
label: "江苏省",
children: [
{ value: "nanjing", label: "南京市" },
{ value: "suzhou", label: "苏州市" },
],
},
],
},
{
field: "treeSelect",
label: "部门",
type: "treeSelect",
span: 12,
clearable: true,
placeholder: "请选择部门",
data: [
{
id: "1",
label: "技术部",
children: [
{ id: "1-1", label: "前端组" },
{ id: "1-2", label: "后端组" },
],
},
{
id: "2",
label: "市场部",
children: [
{ id: "2-1", label: "销售组" },
{ id: "2-2", label: "推广组" },
],
},
],
props: {
label: "label",
value: "id",
children: "children",
},
},
{
field: "files",
label: "文件上传",
type: "upload",
span: 24,
action: "/api/upload",
"show-file-list": true,
"on-success": (response, file, fileList) => {
ElMessage.success("上传成功");
},
"on-error": (error, file, fileList) => {
ElMessage.error("上传失败");
},
},
]);
// ==================== 自定义样式示例 ====================
const customFormData = ref({});
const customFormConfig = {
labelWidth: "100px",
labelPosition: "top",
gutter: 30,
size: "large",
showButtons: true,
submitType: "success",
submitText: "保存修改",
submitIcon: "Check",
buttonSpan: 24,
};
const customFormItems = computed(() => [
{
field: "productName",
label: "产品名称",
type: "input",
span: 8,
itemClass: "custom-form-item",
placeholder: "请输入产品名称",
prefixIcon: "Goods",
},
{
field: "productCode",
label: "产品编码",
type: "input",
span: 8,
placeholder: "请输入产品编码",
prefixIcon: "Tickets",
},
{
field: "productPrice",
label: "产品价格",
type: "inputNumber",
span: 8,
min: 0,
precision: 2,
"controls-position": "right",
},
{
field: "productDesc",
label: "产品描述",
type: "textarea",
span: 24,
rows: 5,
placeholder: "请输入产品描述",
},
]);
// ==================== 动态表单示例 ====================
const showDynamicFields = ref(true);
const dynamicFormData = ref({});
const dynamicFormConfig = {
labelWidth: "120px",
gutter: 20,
};
const dynamicFormItems = computed(() => {
const baseItems = [
{
field: "username",
label: "用户名",
type: "input",
span: 12,
required: true,
},
{
field: "email",
label: "邮箱",
type: "input",
span: 12,
},
];
if (showDynamicFields.value) {
baseItems.push(
{
field: "phone",
label: "手机号",
type: "input",
span: 12,
},
{
field: "address",
label: "地址",
type: "input",
span: 12,
},
);
}
return baseItems;
});
const toggleDynamicFields = () => {
showDynamicFields.value = !showDynamicFields.value;
};
// ==================== 带插槽的表单示例 ====================
const slotFormData = ref({
avatar: "",
username: "",
email: "",
registerTime: "",
status: 1,
content: "",
});
const slotFormConfig = {
labelWidth: "120px",
gutter: 20,
showButtons: true,
};
const slotFormItems = computed(() => [
{
field: "userInfo",
label: "用户信息",
type: "slot",
slotName: "userInfo",
span: 24,
},
{
field: "username",
label: "用户名",
type: "input",
span: 12,
},
{
field: "email",
label: "邮箱",
type: "input",
span: 12,
},
{
field: "registerTime",
label: "注册时间",
type: "date",
span: 12,
"value-format": "YYYY-MM-DD",
},
{
field: "status",
label: "状态",
type: "select",
span: 12,
options: [
{ label: "正常", value: 1 },
{ label: "禁用", value: 0 },
],
},
{
field: "dataDisplay",
label: "数据展示",
type: "slot",
slotName: "dataDisplay",
span: 24,
},
{
field: "content",
label: "富文本内容",
type: "slot",
slotName: "richText",
span: 24,
},
]);
// ==================== 组件集成示例 ====================
const selectorFormData = ref({
matterType: "",
projectType: "",
investmentType: "",
});
const selectorFormConfig = {
labelWidth: "140px",
gutter: 20,
showButtons: false,
};
const selectorFormItems = computed(() => [
{
field: "matterType",
label: "事项类型",
type: "component",
component: CommonSelector,
span: 8,
componentProps: {
dictName: "matterType",
placeholder: "请选择事项类型",
},
},
{
field: "projectType",
label: "项目类型",
type: "component",
component: CommonSelector,
span: 8,
componentProps: {
dictName: "projectType",
placeholder: "请选择项目类型",
},
},
{
field: "investmentType",
label: "投资类型",
type: "component",
component: CommonSelector,
span: 8,
componentProps: {
dictName: "investmentType",
placeholder: "请选择投资类型",
},
},
]);
// 财务表格数据
const financialTableData = ref({
indicatorList: [
{ name: "总收入", level: 0, serialNumber: 1 },
{ name: " - 主营业务收入", level: 1, serialNumber: 2 },
{ name: " - 其他业务收入", level: 1, serialNumber: 3 },
{ name: "总支出", level: 0, serialNumber: 4 },
{ name: " - 运营成本", level: 1, serialNumber: 5 },
{ name: " - 管理费用", level: 1, serialNumber: 6 },
],
dynamicTimeList: [
"2025及以前",
"2026",
"2026-小记",
"2027",
"2028",
"2029",
],
tableData: [],
});
// 动态表格数据
const dynamicTableData = ref([
{
projectName: "示例项目1",
investmentAmount: 1000,
investmentDate: "2025-01-01",
projectType: "tech",
description: "这是一个示例项目",
},
]);
const dynamicTableColumns = computed(() => [
{
prop: "projectName",
label: "项目名称",
type: "input",
minWidth: 180,
headerGroup: "基本信息",
placeholder: "请输入项目名称",
},
{
prop: "investmentAmount",
label: "投资金额(万元)",
type: "number",
minWidth: 160,
headerGroup: "财务信息",
placeholder: "请输入投资金额",
precision: 2,
min: 0,
},
{
prop: "investmentDate",
label: "投资日期",
type: "date",
minWidth: 160,
headerGroup: "基本信息",
placeholder: "请选择日期",
format: "YYYY-MM-DD",
valueFormat: "YYYY-MM-DD",
},
{
prop: "projectType",
label: "项目类型",
type: "select",
minWidth: 140,
headerGroup: "基本信息",
placeholder: "请选择类型",
optionKey: "projectType",
},
{
prop: "description",
label: "项目描述",
type: "textarea",
minWidth: 240,
headerGroup: "基本信息",
placeholder: "请输入项目描述",
rows: 2,
},
]);
const defaultRow = {
projectName: "",
investmentAmount: 0,
investmentDate: "",
projectType: "",
description: "",
};
const selectOptions = ref({
projectType: [
{ key: "tech", name: "技术类" },
{ key: "life", name: "生活类" },
{ key: "entertainment", name: "娱乐类" },
],
});
// 年度计划数据
const annualPlanData = ref([]);
const annualPlanTimeList = ref(["2025及以前", "2026", "2026-小记", "2027", "2028", "2029"]);
const toggleAnnualPlan = () => {
showAnnualPlan.value = !showAnnualPlan.value;
if (showAnnualPlan.value && annualPlanData.value.length === 0) {
// 初始化年度计划数据
annualPlanData.value = Array.from({ length: 17 }, (_, i) => ({
id: i + 1,
name: `项目${i + 1}`,
...annualPlanTimeList.value.reduce((acc, time) => {
acc[time] = 0;
return acc;
}, {}),
}));
}
};
// ==================== 性能优化示例 ====================
// 生成大表单字段(用于演示性能优化)
const loadLargeForm = async () => {
const startTime = performance.now();
isLoadingLargeForm.value = true;
loadingProgress.value = 0;
try {
// 使用分批加载,避免一次性渲染大量字段导致卡顿
const batchSize = 100;
const totalFields = 1000;
const batches = Math.ceil(totalFields / batchSize);
for (let i = 0; i < batches; i++) {
await new Promise(resolve => setTimeout(resolve, 10)); // 模拟异步加载
const batch = Array.from({ length: batchSize }, (_, j) => ({
field: `field_${i * batchSize + j}`,
label: `字段 ${i * batchSize + j + 1}`,
type: "input",
span: 6,
placeholder: `请输入字段${i * batchSize + j + 1}`,
}));
largeFormItems.value.push(...batch);
loadingProgress.value = Math.round(((i + 1) / batches) * 100);
}
const endTime = performance.now();
loadTime.value = Math.round(endTime - startTime);
ElMessage.success(`大表单加载完成,共 ${totalFields} 个字段,耗时 ${loadTime.value}ms`);
} catch (error) {
ElMessage.error("加载失败");
} finally {
isLoadingLargeForm.value = false;
}
};
// ==================== 事件处理 ====================
const handleBasicSubmit = (data, isEdit) => {
console.log("基础表单提交:", data, isEdit);
ElMessage.success(`基础表单${isEdit ? "更新" : "创建"}成功`);
};
const handleSubmitSuccess = (response, data, isEdit) => {
ElMessage.success(`操作成功: ${isEdit ? "更新" : "创建"}`);
console.log("提交成功:", response, data);
};
const handleSubmitError = (error, data, isEdit) => {
ElMessage.error(`操作失败: ${error.message || "未知错误"}`);
console.error("提交失败:", error, data);
};
const handleSlotSubmit = (data) => {
console.log("插槽表单提交:", data);
ElMessage.success("提交成功");
};
const handleCreate = () => {
basicFormData.value = {};
advancedFormData.value = {};
customFormData.value = {};
dynamicFormData.value = {};
slotFormData.value = {};
ElMessage.info("已清空表单数据");
};
const handleChangeAvatar = () => {
ElMessage.info("更换头像功能");
};
const insertText = (text) => {
slotFormData.value.content += text;
};
</script>
<style scoped lang="less">
.super-form-example {
padding: 20px;
.example-card {
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.card-title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
}
.form-tabs {
:deep(.el-tabs__content) {
padding: 20px;
max-height: 70vh;
overflow-y: auto;
}
// 优化滚动条样式
:deep(.el-tabs__content)::-webkit-scrollbar {
width: 6px;
}
:deep(.el-tabs__content)::-webkit-scrollbar-thumb {
background-color: #dcdfe6;
border-radius: 3px;
}
:deep(.el-tabs__content)::-webkit-scrollbar-thumb:hover {
background-color: #c0c4cc;
}
}
}
.integration-container {
.integration-section {
margin-bottom: 30px;
padding: 20px;
background-color: #f5f7fa;
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
}
}
.performance-container {
.performance-tips {
margin: 0;
padding-left: 20px;
li {
margin-bottom: 8px;
line-height: 1.6;
}
}
.performance-demo {
margin-top: 20px;
.demo-item {
padding: 20px;
background-color: #f5f7fa;
border-radius: 8px;
.demo-title {
font-size: 14px;
font-weight: 600;
color: #606266;
margin-bottom: 12px;
}
.form-stats {
display: flex;
gap: 12px;
margin-top: 16px;
}
}
}
}
.custom-user-info {
display: flex;
align-items: center;
gap: 20px;
padding: 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
.rich-text-editor {
width: 100%;
.editor-toolbar {
margin-top: 10px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
}
:deep(.custom-form-item) {
.el-form-item__label {
color: #409eff;
font-weight: 600;
}
}
}
// 全局样式优化
:deep(.el-form-item__label) {
font-weight: 500;
color: #303133;
}
:deep(.el-form-item) {
margin-bottom: 18px;
}
:deep(.el-input-number) {
width: 100%;
}
:deep(.el-select) {
width: 100%;
}
:deep(.el-cascader) {
width: 100%;
}
:deep(.el-tree-select) {
width: 100%;
}
:deep(.el-date-editor) {
width: 100%;
}
:deep(.el-time-picker) {
width: 100%;
}
:deep(.el-autocomplete) {
width: 100%;
}
// 优化表格样式
:deep(.el-table) {
font-size: 13px;
}
:deep(.el-table th) {
background-color: #f5f7fa;
color: #303133;
font-weight: 600;
}
:deep(.el-table--border) {
border-color: #ebeef5;
}
// 优化按钮样式
:deep(.el-button) {
transition: all 0.3s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
}
}
// 优化卡片样式
:deep(.el-card) {
border-radius: 8px;
border: none;
}
:deep(.el-card__header) {
border-bottom: 1px solid #ebeef5;
padding: 16px 20px;
}
// 优化标签页样式
:deep(.el-tabs--border-card) {
border-radius: 8px;
box-shadow: none;
}
:deep(.el-tabs__item) {
font-weight: 500;
&.is-active {
color: #409eff;
font-weight: 600;
}
}
</style>
......@@ -1364,13 +1364,6 @@ const changeProject = (val) => {
callback: (data) => {
loading.value = false;
formData.projectName = data.projectName || "";
formData.sbdw = data.sbdw || "";
formData.xmgsmc = data.xmgsmc || "";
formData.xmkgsjyj = data.xmkgsjyj || "";
formData.xmjgsjyj = data.xmjgsjyj || "";
formData.xmjd = data.xmjd || "";
formData.yynxn = data.yynxn || "";
formData.xmjsqy = data.xmjsqy || "";
},
error: () => {
loading.value = false;
......
......@@ -97,14 +97,8 @@ const sourceId = ref(route.query.sourceId || "");
// ========== 表单核心数据(完全匹配RcXxbs表结构,无冗余字段) ==========
const formData = reactive({
wjmc: "", // 文件名称
nd: "", // 编制年度(YYYY格式,匹配表的DATE类型)
bzr: "", // 编制人
bzrbm: "", // 编制人部门
fjsc: [], // 附件上传(JSON类型,匹配表的JSON)
projectId: "", // 所属项目ID(隐藏字段,提交传值)
sourceId: "", // 所属主表id(隐藏字段,提交传值)
del: 0, // 删除标识,默认0(正常)
creator: proxy.$store?.getters?.userId || "", // 创建人ID(从全局状态取,无则空)
});
......
......@@ -109,6 +109,18 @@ const backClick = () => {
// 保存/提交表单(新增/编辑统一处理)
const saveClick = () => {
if (!formData.issueTitle) {
ElMessage.warning("请填写问题标题");
return;
}
if (!formData.issueCategory) {
ElMessage.warning("请选择问题类别");
return;
}
if (!formData.issueDescription) {
ElMessage.warning("请填写问题描述");
return;
}
loading.value = true;
const url = rcCgqyglId.value
? "/api/project/updateXxhjs"
......
......@@ -128,17 +128,7 @@ const activeCollapse = ref(["档案基本信息"]);
// 表单核心数据:**完全匹配后端RcTzdagl模型字段**,删除所有无用字段
const formData = reactive({
archiveCategory: "", // 档案分类 ★非空★
archiveName: "", // 档案名称 ★非空★
archiveNumber: "", // 档案编号
archiveDate: "", // 档案日期
remark: "", // 备注
fjsc: [], // 附件上传(JSON数组,绑定上传组件)
projectId: "", // 所属项目ID
del: 0, // 删除标记,默认0正常(后端默认值)
creator: userStore.userId || "", // 创建人ID(从用户仓库取,后端需要)
createdAt: "", // 创建时间(后端自动生成,前端仅回显)
updatedAt: "", // 更新时间(后端自动生成,前端仅回显)
});
// 页面核心状态:保留必要的加载/预览/编辑ID
......
......@@ -546,38 +546,36 @@ const activeCollapse = ref([
// 表单数据 - 数值字段初始化为数字类型(0)
const formData = reactive({
projectName: "",
qc: "",
jc: "",
nbtzglzt: "",
xmscjd: "",
gqjg: "",
xmzbjze: 0,
gszbjyczze: 0,
gsdqycze: 0,
gsdqyjcze: 0,
gsdqycwcje: 0,
gsdqsycze: 0,
cgbczqk: "",
wfqyhttkyd: "",
qyhqjz: "",
qyhqyyd: "",
dbqk: "",
lrfp: "",
sfddlrfptj: "",
ljhqfh: 0,
ytrzj: 0,
ljtrzj: 0,
sxtrzj: 0,
jt: 0,
zx: 0,
lxr: "",
lxfs: "",
bz: "",
projectId: "",
del: 0, // del字段保留0默认值(删除标记,0为正常)
createdAt: "",
updatedAt: "",
// projectName: "",
// qc: "",
// jc: "",
// nbtzglzt: "",
// xmscjd: "",
// gqjg: "",
// xmzbjze: 0,
// gszbjyczze: 0,
// gsdqycze: 0,
// gsdqyjcze: 0,
// gsdqycwcje: 0,
// gsdqsycze: 0,
// cgbczqk: "",
// wfqyhttkyd: "",
// qyhqjz: "",
// qyhqyyd: "",
// dbqk: "",
// lrfp: "",
// sfddlrfptj: "",
// ljhqfh: 0,
// ytrzj: 0,
// ljtrzj: 0,
// sxtrzj: 0,
// jt: 0,
// zx: 0,
// lxr: "",
// lxfs: "",
// bz: "",
// projectId: "",
// del: 0, // del字段保留0默认值(删除标记,0为正常)
});
let options = ref();
......
......@@ -97,10 +97,6 @@ const options = ref({}); // 下拉/单选全局配置项(文件层级/类别
// ========== 表单核心数据(删除无用数组,仅保留页面绑定的字段) ==========
const formData = reactive({
wjmc: "", // 文件名称
bbsj: "", // 颁布时间
wjcj: "", // 文件层级
wjlb: "", // 文件类别
fjsc: [], // 附件上传
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment