2026-05-26 18:32:57 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<!--
|
|
|
|
|
|
page-dialog-form — 增删改查弹框组件
|
|
|
|
|
|
===================================
|
|
|
|
|
|
这是一个「带表单验证的弹框」组件,配合 page-table 使用。
|
|
|
|
|
|
负责:弹框显隐控制 + 表单渲染 + 表单验证 + 确定/取消按钮。
|
|
|
|
|
|
|
|
|
|
|
|
不支持复杂表单联动——如有需要,通过默认插槽自定义内容。
|
|
|
|
|
|
|
|
|
|
|
|
依赖:element-ui 的 <el-dialog> <el-form> <el-input> <el-select>
|
2026-05-28 11:30:08 +08:00
|
|
|
|
i18n:组件内部通过 $t() 翻译传入的 i18n key,切换语言时自动响应
|
2026-05-26 18:32:57 +08:00
|
|
|
|
|
|
|
|
|
|
@author 前端团队
|
|
|
|
|
|
@since 2026-05
|
|
|
|
|
|
-->
|
|
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
|
el-dialog:
|
|
|
|
|
|
visible.sync → 通过 .sync 修饰符双向绑定父组件的 visible prop
|
|
|
|
|
|
destroy-on-close → 每次关闭销毁 DOM 重建,避免表单残留上次数据
|
|
|
|
|
|
close-on-click-modal → 禁止点击遮罩关闭,防止误操作丢失填写数据
|
|
|
|
|
|
-->
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
:visible.sync="visibleProxy"
|
2026-05-28 11:30:08 +08:00
|
|
|
|
:title="$t(title)"
|
2026-05-26 18:32:57 +08:00
|
|
|
|
:width="width"
|
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
|
:destroy-on-close="true"
|
|
|
|
|
|
@close="onClose"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- ==================== 表单区 ==================== -->
|
|
|
|
|
|
<!--
|
|
|
|
|
|
el-form:
|
|
|
|
|
|
rules 由父组件传入,字段名与 formData 的 key 一一对应
|
|
|
|
|
|
label-width 默认 100px,可自定义
|
|
|
|
|
|
-->
|
|
|
|
|
|
<el-form
|
|
|
|
|
|
ref="form"
|
|
|
|
|
|
:model="formData"
|
2026-05-28 11:30:08 +08:00
|
|
|
|
:rules="translatedRules"
|
2026-05-26 18:32:57 +08:00
|
|
|
|
:label-width="labelWidth || '100px'"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!--
|
|
|
|
|
|
遍历 formCols 中所有行 → 每行中的每个字段 → 渲染对应的表单项
|
|
|
|
|
|
当前支持两种字段类型:
|
|
|
|
|
|
- type='input' → <el-input>(支持 textarea、密码等)
|
|
|
|
|
|
- type='select' → <el-select> + <el-option>
|
|
|
|
|
|
|
2026-05-27 18:37:37 +08:00
|
|
|
|
由调用方负责翻译,传已翻译的文本即可
|
2026-05-26 18:32:57 +08:00
|
|
|
|
-->
|
|
|
|
|
|
<el-form-item
|
|
|
|
|
|
v-for="col in flatFormCols"
|
|
|
|
|
|
:key="col.prop"
|
2026-05-28 11:30:08 +08:00
|
|
|
|
:label="$t(col.label)"
|
2026-05-26 18:32:57 +08:00
|
|
|
|
:prop="col.prop"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- ===== 输入框类型 ===== -->
|
|
|
|
|
|
<!--
|
|
|
|
|
|
input:
|
|
|
|
|
|
inputType='textarea' → 多行文本框
|
|
|
|
|
|
不传 inputType → 普通文本输入框
|
|
|
|
|
|
clearable → 默认 true,传 false 可关闭
|
|
|
|
|
|
-->
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-if="col.type === 'input'"
|
|
|
|
|
|
v-model="formData[col.prop]"
|
2026-05-28 11:30:08 +08:00
|
|
|
|
:placeholder="$t(col.placeholder)"
|
2026-05-26 18:32:57 +08:00
|
|
|
|
:type="col.inputType || 'text'"
|
|
|
|
|
|
:autosize="col.autosize"
|
|
|
|
|
|
:clearable="col.clearable !== false"
|
|
|
|
|
|
:style="col.style"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<!-- ===== 下拉选择类型 ===== -->
|
|
|
|
|
|
<!--
|
|
|
|
|
|
select:
|
|
|
|
|
|
options → [{ label: '苹果', value: 1 }]
|
|
|
|
|
|
filterable → 默认 true,支持搜索过滤
|
|
|
|
|
|
-->
|
|
|
|
|
|
<el-select
|
|
|
|
|
|
v-else-if="col.type === 'select'"
|
|
|
|
|
|
v-model="formData[col.prop]"
|
2026-05-28 11:30:08 +08:00
|
|
|
|
:placeholder="$t(col.placeholder)"
|
2026-05-26 18:32:57 +08:00
|
|
|
|
:clearable="col.clearable !== false"
|
|
|
|
|
|
:style="col.style"
|
|
|
|
|
|
:filterable="col.filterable !== false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="opt in col.options"
|
|
|
|
|
|
:key="opt.value"
|
2026-05-27 18:37:37 +08:00
|
|
|
|
:label="opt.label"
|
2026-05-26 18:32:57 +08:00
|
|
|
|
:value="opt.value"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<!-- ===== 自定义表单内容插槽 ===== -->
|
|
|
|
|
|
<!--
|
|
|
|
|
|
如果需要超出 input/select 的复杂表单控件
|
|
|
|
|
|
父组件可以用此插槽添加任意内容
|
|
|
|
|
|
-->
|
|
|
|
|
|
<slot />
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ==================== 底部按钮 ==================== -->
|
|
|
|
|
|
<!--
|
|
|
|
|
|
确定:type='primary' + submitting loading 状态
|
|
|
|
|
|
取消:调用 onClose → 重置表单 + 关闭弹框
|
2026-05-27 18:37:37 +08:00
|
|
|
|
由调用方负责翻译,传已翻译的文本即可
|
2026-05-26 18:32:57 +08:00
|
|
|
|
-->
|
|
|
|
|
|
<template #footer>
|
2026-05-28 11:30:08 +08:00
|
|
|
|
<el-button @click="onClose">{{ $t(cancelText) }}</el-button>
|
|
|
|
|
|
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ $t(confirmText) }}</el-button>
|
2026-05-26 18:32:57 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
/**
|
|
|
|
|
|
* PageDialogForm — 增删改查弹框组件
|
|
|
|
|
|
*
|
|
|
|
|
|
* 【核心职责】
|
|
|
|
|
|
* 1. 管理弹框显隐
|
|
|
|
|
|
* 2. 根据 formCols 声明式渲染表单
|
|
|
|
|
|
* 3. 表单验证 + 验证失败提示
|
|
|
|
|
|
* 4. 提交/取消事件通知父组件
|
|
|
|
|
|
*
|
|
|
|
|
|
* 【典型用法】
|
|
|
|
|
|
* <page-dialog-form
|
|
|
|
|
|
* ref="dialogForm"
|
|
|
|
|
|
* :visible.sync="dialogVisible"
|
|
|
|
|
|
* :title="dialogTitle"
|
|
|
|
|
|
* width="35%"
|
|
|
|
|
|
* :form-cols="formCols"
|
|
|
|
|
|
* :form-data="formData"
|
|
|
|
|
|
* :rules="rules"
|
|
|
|
|
|
* :submitting="submitting"
|
|
|
|
|
|
* @submit="onDialogSubmit"
|
|
|
|
|
|
* @close="onDialogClose"
|
|
|
|
|
|
* />
|
|
|
|
|
|
*
|
|
|
|
|
|
* 【父组件调用的方法(通过 ref)】
|
|
|
|
|
|
* this.$refs.dialogForm.reset() — 重置表单
|
|
|
|
|
|
* this.$refs.dialogForm.validate() — 手动验证,返回 Promise<boolean>
|
|
|
|
|
|
*
|
|
|
|
|
|
* 【formCols 数据结构说明】
|
|
|
|
|
|
* 二维数组:外层是行,里层是该行的字段。大多数场景每行一个字段即可。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 例:
|
|
|
|
|
|
* formCols: [
|
|
|
|
|
|
* [ { type: 'input', prop: 'code', label: T+'.code', placeholder: T+'.enter_code' } ],
|
|
|
|
|
|
* [ { type: 'input', prop: 'remark', label: T+'.remark', inputType: 'textarea', autosize: { minRows: 2 } } ],
|
|
|
|
|
|
* [ { type: 'select', prop: 'area_id', label: T+'.area', options: [{ label: 'A区', value: 1 }] } ]
|
|
|
|
|
|
* ]
|
|
|
|
|
|
*
|
|
|
|
|
|
* 【表单字段支持的属性】
|
|
|
|
|
|
* 基础:type / prop / label / placeholder
|
|
|
|
|
|
* input:inputType('textarea' | 'text')/ autosize / clearable
|
|
|
|
|
|
* select:options / filterable
|
|
|
|
|
|
* 通用:style(如 { width: '90%' })
|
|
|
|
|
|
*
|
|
|
|
|
|
* 【表单验证(rules)】
|
|
|
|
|
|
* rules 与 el-form 的 rules 完全一致:
|
|
|
|
|
|
* rules: {
|
|
|
|
|
|
* code: [{ required: true, message: '请输入编码', trigger: 'blur' }]
|
|
|
|
|
|
* }
|
|
|
|
|
|
* 验证失败时,组件会用 $message.warning 显示第一条错误信息
|
|
|
|
|
|
* message 字段支持 i18n key,父组件用 this.$t() 传值即可
|
|
|
|
|
|
*/
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'PageDialogForm',
|
|
|
|
|
|
props: {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 弹框显隐状态,父组件用 .sync 绑定
|
|
|
|
|
|
* 例::visible.sync="dialogVisible"
|
|
|
|
|
|
*/
|
|
|
|
|
|
visible: { type: Boolean, default: false },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-27 18:37:37 +08:00
|
|
|
|
* 弹框标题,由调用方负责翻译
|
2026-05-26 18:32:57 +08:00
|
|
|
|
*/
|
|
|
|
|
|
title: { type: String, default: '' },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 弹框宽度,可以是百分比或像素
|
|
|
|
|
|
* 例:'35%' / '600px'
|
|
|
|
|
|
*/
|
|
|
|
|
|
width: { type: String, default: '35%' },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 表单字段结构,二维数组
|
|
|
|
|
|
* 外层数组每一个元素代表表单的一行
|
|
|
|
|
|
* 内层数组每个元素代表该行中的一个字段
|
|
|
|
|
|
*
|
|
|
|
|
|
* 每个字段对象支持的属性见组件顶部的注释
|
|
|
|
|
|
*/
|
|
|
|
|
|
formCols: { type: Array, default: () => [] },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 表单数据对象,key 与 formCols 中的 prop 一一对应
|
|
|
|
|
|
* 编辑时父组件将 row 数据赋值给 formData
|
|
|
|
|
|
*/
|
|
|
|
|
|
formData: { type: Object, default: () => ({}) },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 表单验证规则,与 el-form 的 rules 完全一致
|
|
|
|
|
|
* 例:{ code: [{ required: true, message: '请输入编码', trigger: 'blur' }] }
|
|
|
|
|
|
*/
|
|
|
|
|
|
rules: { type: Object, default: () => ({}) },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 表单 label 宽度,默认 '100px'
|
|
|
|
|
|
*/
|
|
|
|
|
|
labelWidth: { type: String, default: '100px' },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 提交 loading 状态,提交期间显示转圈防止重复点击
|
|
|
|
|
|
*/
|
|
|
|
|
|
submitting: { type: Boolean, default: false },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-27 18:37:37 +08:00
|
|
|
|
* 确定按钮文字,默认 '确定',由调用方负责 i18n 翻译
|
2026-05-26 18:32:57 +08:00
|
|
|
|
*/
|
|
|
|
|
|
confirmText: { type: String, default: '确定' },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-27 18:37:37 +08:00
|
|
|
|
* 取消按钮文字,默认 '取消',由调用方负责 i18n 翻译
|
2026-05-26 18:32:57 +08:00
|
|
|
|
*/
|
|
|
|
|
|
cancelText: { type: String, default: '取消' }
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* visible 代理:用于 .sync 双向绑定
|
|
|
|
|
|
* get → 返回父组件传入的 visible
|
|
|
|
|
|
* set → emit update:visible 通知父组件
|
|
|
|
|
|
*/
|
|
|
|
|
|
visibleProxy: {
|
|
|
|
|
|
get () { return this.visible },
|
|
|
|
|
|
set (val) { this.$emit('update:visible', val) }
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将二维 formCols 打平为一维数组,方便 v-for 遍历
|
|
|
|
|
|
* 二维数组 → 一维数组:[ [{a}], [{b},{c}] ] → [a, b, c]
|
|
|
|
|
|
*/
|
|
|
|
|
|
flatFormCols () {
|
|
|
|
|
|
return this.formCols.flat()
|
2026-05-28 11:30:08 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
translatedRules () {
|
|
|
|
|
|
const rules = this.rules || {}
|
|
|
|
|
|
const translated = {}
|
|
|
|
|
|
for (const [field, validators] of Object.entries(rules)) {
|
|
|
|
|
|
translated[field] = validators.map(v => ({ ...v, message: this.$t(v.message) }))
|
|
|
|
|
|
}
|
|
|
|
|
|
return translated
|
2026-05-26 18:32:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 点击确定按钮
|
|
|
|
|
|
* 1. 调用 el-form 的 validate 方法验证所有字段
|
|
|
|
|
|
* 2. 验证通过 → emit('submit') 通知父组件执行提交逻辑
|
|
|
|
|
|
* 3. 验证失败 → 用 $message.warning 显示第一条错误信息
|
|
|
|
|
|
*
|
|
|
|
|
|
* 注意:rules 中的 message 由父组件传入时已用 $t() 翻译
|
|
|
|
|
|
* 这里只负责显示,翻译在父组件或 i18n 语言包中完成
|
|
|
|
|
|
*/
|
|
|
|
|
|
onSubmit () {
|
|
|
|
|
|
this.$refs.form.validate((valid, invalidFields) => {
|
|
|
|
|
|
if (!valid) {
|
|
|
|
|
|
const firstKey = Object.keys(invalidFields)[0]
|
|
|
|
|
|
if (firstKey && invalidFields[firstKey].length) {
|
|
|
|
|
|
const msg = invalidFields[firstKey][0].message
|
2026-05-27 18:37:37 +08:00
|
|
|
|
this.$message.warning(msg)
|
2026-05-26 18:32:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.$emit('submit')
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 关闭弹框
|
|
|
|
|
|
* 1. 重置表单清除验证状态和输入内容
|
|
|
|
|
|
* 2. emit update:visible 关闭弹框(.sync 机制)
|
|
|
|
|
|
* 3. emit close 通知父组件执行清理逻辑
|
|
|
|
|
|
*/
|
|
|
|
|
|
onClose () {
|
|
|
|
|
|
this.$refs.form && this.$refs.form.resetFields()
|
|
|
|
|
|
this.$emit('update:visible', false)
|
|
|
|
|
|
this.$emit('close')
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 手动验证表单,返回 Promise<boolean>
|
|
|
|
|
|
* 主要用于父组件需要自行控制验证时机的场景
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用法:const ok = await this.$refs.dialogForm.validate()
|
|
|
|
|
|
*/
|
|
|
|
|
|
validate () {
|
|
|
|
|
|
return new Promise(resolve => {
|
|
|
|
|
|
this.$refs.form.validate(valid => resolve(valid))
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重置表单(清除验证状态和输入值)
|
|
|
|
|
|
* 通常在打开新增弹框时调用
|
|
|
|
|
|
*/
|
|
|
|
|
|
reset () {
|
|
|
|
|
|
this.$refs.form && this.$refs.form.resetFields()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
/* 表单项间距固定 22px,不受 Element UI 状态覆盖 */
|
|
|
|
|
|
/deep/ .el-form-item {
|
|
|
|
|
|
margin-bottom: 22px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 错误文字下沉到 margin 间隙中,不挤占表单项自身高度 */
|
|
|
|
|
|
/deep/ .el-form-item__error {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: auto;
|
|
|
|
|
|
bottom: -18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* textarea 对齐顶部 */
|
|
|
|
|
|
/deep/ .el-textarea {
|
|
|
|
|
|
vertical-align: top;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|