Files
mes-ui-d2/src/components/page-dialog-form/index.vue
sheng 874cbeaeea
Some checks failed
Release pipeline / publish (push) Has been cancelled
Release pipeline / Always run job (push) Has been cancelled
迁移设备损耗品管理功能
2026-06-26 00:27:03 +08:00

380 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!--
page-dialog-form 增删改查弹框组件
===================================
这是一个带表单验证的弹框组件配合 page-table 使用
负责弹框显隐控制 + 表单渲染 + 表单验证 + 确定/取消按钮
不支持复杂表单联动如有需要通过默认插槽自定义内容
依赖element-ui <el-dialog> <el-form> <el-input> <el-select>
i18n组件内部通过 $t() 翻译传入的 i18n key切换语言时自动响应
@author 前端团队
@since 2026-05
-->
<!--
el-dialog
visible.sync 通过 .sync 修饰符双向绑定父组件的 visible prop
destroy-on-close 每次关闭销毁 DOM 重建避免表单残留上次数据
close-on-click-modal 禁止点击遮罩关闭防止误操作丢失填写数据
-->
<el-dialog
:visible.sync="visibleProxy"
:title="$t(title)"
: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"
:rules="translatedRules"
:label-width="labelWidth || '100px'"
>
<!--
遍历 formCols 中所有行 每行中的每个字段 渲染对应的表单项
当前支持两种字段类型
- type='input' <el-input>支持 textarea密码等
- type='select' <el-select> + <el-option>
由调用方负责翻译传已翻译的文本即可
-->
<el-form-item
v-for="col in flatFormCols"
:key="col.prop"
:label="$t(col.label)"
:prop="col.prop"
>
<!-- ===== 输入框类型 ===== -->
<!--
input
inputType='textarea' 多行文本框
不传 inputType 普通文本输入框
clearable 默认 true false 可关闭
-->
<el-input
v-if="col.type === 'input'"
v-model="formData[col.prop]"
:placeholder="$t(col.placeholder)"
:type="col.inputType || 'text'"
:show-password="!!col.showPassword"
:autosize="col.autosize"
:clearable="col.clearable !== false"
:disabled="!!col.disabled"
:style="col.style"
@focus="handleFieldEvent(col, 'focus', $event)"
@blur="handleFieldEvent(col, 'blur', $event)"
/>
<el-input-number
v-else-if="col.type === 'input-number'"
v-model="formData[col.prop]"
:min="col.min"
:max="col.max"
:step="col.step || 1"
:precision="col.precision"
:controls-position="col.controlsPosition"
:disabled="!!col.disabled"
:style="col.style"
@change="handleFieldEvent(col, 'change', $event)"
@focus="handleFieldEvent(col, 'focus', $event)"
@blur="handleFieldEvent(col, 'blur', $event)"
/>
<el-date-picker
v-else-if="col.type === 'date-picker'"
v-model="formData[col.prop]"
:type="col.dateType || 'datetime'"
:placeholder="$t(col.placeholder)"
:value-format="col.valueFormat || 'yyyy-MM-dd HH:mm:ss'"
:format="col.format"
:clearable="col.clearable !== false"
:disabled="!!col.disabled"
:style="col.style"
@change="handleFieldEvent(col, 'change', $event)"
@focus="handleFieldEvent(col, 'focus', $event)"
/>
<!-- ===== 下拉选择类型 ===== -->
<!--
select
options [{ label: '苹果', value: 1 }]
filterable 默认 true支持搜索过滤
-->
<el-select
v-else-if="col.type === 'select'"
v-model="formData[col.prop]"
:placeholder="$t(col.placeholder)"
:clearable="col.clearable !== false"
:style="col.style"
:filterable="col.filterable !== false"
:disabled="!!col.disabled"
@change="handleFieldEvent(col, 'change', $event)"
@focus="handleFieldEvent(col, 'focus', $event)"
>
<el-option
v-for="opt in col.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<!-- ===== 自定义表单内容插槽 ===== -->
<!--
如果需要超出 input/select 的复杂表单控件
父组件可以用此插槽添加任意内容
-->
<slot />
</el-form>
<!-- ==================== 底部按钮 ==================== -->
<!--
确定type='primary' + submitting loading 状态
取消调用 onClose 重置表单 + 关闭弹框
由调用方负责翻译传已翻译的文本即可
-->
<template #footer>
<el-button @click="onClose">{{ $t(cancelText) }}</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ $t(confirmText) }}</el-button>
</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
* inputinputType'textarea' | 'text'/ autosize / clearable
* selectoptions / 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 },
/**
* 弹框标题,由调用方负责翻译
*/
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 },
/**
* 确定按钮文字,默认 '确定',由调用方负责 i18n 翻译
*/
confirmText: { type: String, default: '确定' },
/**
* 取消按钮文字,默认 '取消',由调用方负责 i18n 翻译
*/
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()
},
translatedRules () {
const rules = this.rules || {}
const translated = {}
for (const [field, validators] of Object.entries(rules)) {
translated[field] = validators.map(v => {
const rule = { ...v }
if (rule.message) {
rule.message = this.$t(rule.message)
}
return rule
})
}
return translated
}
},
methods: {
handleFieldEvent (col, eventName, value) {
if (!col) return
const handlerKey = 'on' + eventName.charAt(0).toUpperCase() + eventName.slice(1)
const handler = col[handlerKey]
if (typeof handler === 'function') {
handler(value, col, this.formData)
}
},
/**
* 点击确定按钮
* 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
this.$message.warning(msg)
}
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>