vue3 + elementPlus 封装hook,检测form表单数据修改变更;示例用 script setup 语法使用
vue3 + elementPlus 封装hook,检测form表单数据修改变更;示例 script setup 语法
- 原文:https://mp.weixin.qq.com/s/gCuqKskp-KBxdClxcpwFqw
- 原文:https://mp.weixin.qq.com/s/gCuqKskp-KBxdClxcpwFqw
- 原文:https://mp.weixin.qq.com/s/gCuqKskp-KBxdClxcpwFqw
原文:https://mp.weixin.qq.com/s/gCuqKskp-KBxdClxcpwFqw
运行系统:windows 10
运行终端:Goodle Chrome(谷歌) 浏览器
开发框架:vue3 + elementPlus
开发环境:nodeJS v20.17.0
nvm 版本:1.1.12
前言:
目前要实现这样一个功能,修改了form表单数据后,检测form表单数据修改前,修改后的变化,记录下来,传递给后端,后端偷懒让前端来做,花了一天时间,缝缝补补优化后,没有什么问题,记录一下
封装hooks组件,组件路径 hooks/formChangeTracker.ts
// 引入 Vue 的响应式 API
import { ref, reactive, watch } from "vue";
/*** 表单修改追踪 Hook(支持动态模块)* @param {Object} initialFormData - 初始表单数据结构,示例: { module1: { field1: value1 }, module2: { field2: value2 } }* @param {Object} [options={}] - 配置选项* @param {Boolean} [options.autoDetect=false] - 是否自动检测变化,设置为true时会自动监听表单变化* @param {Object} [options.fieldLabels={}] - 字段标签映射,结构: {模块名: {字段名: 显示标签}}* @param {Object} [options.moduleLabels={}] - 模块标签映射,结构: {模块名: 显示标签}* @returns {Object} 包含表单数据和操作方法的对象*/
export function useFormChangeTracker(initialFormData, options = {}) {// 解构配置选项,设置默认值const { autoDetect = false, fieldLabels = {}, moduleLabels = {} } = options;// 存储后端原始数据(使用 ref 保持响应式,深拷贝初始数据避免引用问题)// 这里使用ref而不是reactive是因为我们可能需要完全替换整个后端数据const backendData = ref(JSON.parse(JSON.stringify(initialFormData)));// 响应式表单数据(使用 reactive 创建响应式对象,用于绑定表单)// 使用reactive是因为表单数据通常是嵌套对象,reactive对嵌套响应式更友好const formContent = reactive(JSON.parse(JSON.stringify(initialFormData)) // 深拷贝初始数据);// 变化记录(使用 ref 保持响应式)// 使用ref是因为我们可能会完全替换整个changes对象const changes = ref({oldValue: {}, // 模块级别的旧值 {模块名: {字段名: 值}}newValue: {}, // 模块级别的新值 {模块名: {字段名: 值}}changedFields: [], // 变化的字段列表,每个元素包含字段详情和单独的新旧值changeType: {} // 记录哪些模块发生了变化 {模块名: boolean}});// 是否有变化的标记(使用 ref 保持响应式)const hasChanges = ref(false);/*** 从后端加载数据并重置表单* @param {Object} data - 后端返回的数据,结构应与initialFormData一致* @example * loadBackendData({* userInfo: { name: 'John', age: 30 },* contactInfo: { email: 'john@example.com' }* })*/const loadBackendData = (data) => {// 更新后端原始数据(深拷贝避免引用问题)backendData.value = JSON.parse(JSON.stringify(data));// 重置表单数据(将后端数据同步到表单)Object.keys(data).forEach(module => {if (formContent[module]) {// 只更新已存在的模块// 使用Object.assign而不是直接赋值,保留表单的响应性Object.assign(formContent[module], data[module]);}});// 重置变化记录resetForm();};/*** 检查模块是否有效(关键字段一致)* @param {String} module - 要检查的模块名* @returns {Boolean} 是否有效模块* @description * 有效的模块必须同时存在于:* 1. 后端数据(backendData)* 2. 字段标签配置(fieldLabels)* 3. 模块标签配置(moduleLabels)*/const isValidModule = (module) => {return (backendData.value.hasOwnProperty(module) && // 有后端数据Object.keys(fieldLabels).includes(module) && // 有字段标签配置Object.keys(moduleLabels).includes(module) // 有模块标签配置);};/*** 检测表单变化并更新 changes 数据* @returns {Object} 包含所有变化的字段信息,结构:* {* oldValue: {模块: {字段: 旧值}},* newValue: {模块: {字段: 新值}},* changedFields: [{* module: 模块名,* field: 字段名,* label: 字段显示标签,* moduleLabel: 模块显示标签,* oldValue: 旧值,* newValue: 新值* }],* changeType: {模块名: boolean}* }*/const checkChanges = () => {const changedFields = []; // 存储所有变化的字段详情const oldValue = {}; // 存储模块级别的旧值const newValue = {}; // 存储模块级别的新值const changeType = {}; // 记录哪些模块发生了变化// 遍历表单中的所有模块Object.keys(formContent).forEach(module => {// 跳过无效模块(配置不完整的模块)if (!isValidModule(module)) {console.warn(`模块 ${module} 缺少必要的配置,跳过检测`);return; // 继续下一个模块}let moduleChanged = false; // 标记当前模块是否有变化const moduleOldValues = {}; // 存储当前模块的旧值const moduleNewValues = {}; // 存储当前模块的新值// 遍历模块中的所有字段Object.keys(formContent[module]).forEach(field => {// 使用严格不等于比较当前表单值和后端原始值if (formContent[module][field] !== backendData.value[module][field]) {moduleChanged = true; // 标记模块有变化// 记录字段级别的旧值和新值moduleOldValues[field] = backendData.value[module][field];moduleNewValues[field] = formContent[module][field];// 记录变化的字段详情(包含字段级新旧值)changedFields.push({module, // 模块名field, // 字段名label: fieldLabels[module]?.[field] || field, // 显示标签(优先使用配置的标签)moduleLabel: moduleLabels[module] || module, // 模块显示标签oldValue: backendData.value[module][field], // 字段旧值newValue: formContent[module][field] // 字段新值});}});// 如果当前模块有变化,记录模块级别的变化if (moduleChanged) {oldValue[module] = moduleOldValues;newValue[module] = moduleNewValues;changeType[module] = true;}});// 更新响应式数据changes.value = {oldValue,newValue,changedFields,changeType};// 更新是否有变化的标记(根据变化字段数量判断)hasChanges.value = changedFields.length > 0;return changes.value;};/*** 重置表单到初始状态(后端数据状态)* @description* 1. 将表单数据恢复为后端原始数据* 2. 清空所有变化记录* 3. 重置hasChanges标志*/const resetForm = () => {// 遍历所有后端数据模块Object.keys(backendData.value).forEach(module => {// 只重置表单中存在的模块if (formContent[module]) {// 将表单数据重置为后端原始数据// 使用Object.assign保留响应性Object.assign(formContent[module], backendData.value[module]);}});// 清空变化记录changes.value = {oldValue: {},newValue: {},changedFields: [],changeType: {}};// 重置变化标记hasChanges.value = false;};// 如果启用自动检测,设置深度监听if (autoDetect) {watch(// 监听表单数据(使用深拷贝确保能检测到嵌套变化)() => JSON.parse(JSON.stringify(formContent)),// 变化时执行检测() => checkChanges(),// 深度监听选项{ deep: true });}// 返回供组件使用的方法和数据return {formContent, // 响应式表单数据(用于表单绑定)changes, // 变化记录(包含新旧值)hasChanges, // 是否有变化的标记checkChanges, // 手动检测变化的方法resetForm, // 重置表单的方法loadBackendData, // 加载后端数据的方法isValidModule // 检查模块是否有效的方法};
}
功能说明
这个 useFormChangeTracker Hook 主要提供以下功能:
表单数据管理:维护表单的当前状态和原始状态
变化追踪:检测表单字段的修改并记录新旧值
模块化支持:支持按模块组织表单数据
自动/手动检测:可配置自动检测变化或手动触发检测
重置功能:可以将表单重置为原始状态
数据加载:可以从后端加载新数据
核心实现
使用 Vue 的 ref 和 reactive 创建响应式数据
通过深拷贝确保数据独立性(避免引用问题)
提供 checkChanges 方法比较当前表单值与原始值的差异
支持自动监听表单变化(通过 watch 实现)
记录详细的变更信息,包括字段级和模块级的变化
使用示例 :script setup 语法
<template><!-- 表单容器 --><div class="form-container"><!-- 用户信息模块 --><div class="form-section"><h3>用户信息</h3><!-- 姓名字段 --><div class="form-field"><label>姓名:</label><!-- 双向绑定到 formContent.userInfo.name --><input v-model="formContent.userInfo.name" /><!-- 如果姓名有修改,显示修改指示器 --><span v-if="changes.changeType.userInfo?.name" class="change-indicator">(已修改)</span></div><!-- 年龄字段 --><div class="form-field"><label>年龄:</label><!-- 使用.number修饰符确保绑定为数字类型 --><input v-model.number="formContent.userInfo.age" type="number" /><span v-if="changes.changeType.userInfo?.age" class="change-indicator">(已修改)</span></div></div><!-- 联系信息模块 --><div class="form-section"><h3>联系信息</h3><!-- 邮箱字段 --><div class="form-field"><label>电子邮箱:</label><input v-model="formContent.contactInfo.email" /><span v-if="changes.changeType.contactInfo?.email" class="change-indicator">(已修改)</span></div><!-- 电话字段 --><div class="form-field"><label>联系电话:</label><input v-model="formContent.contactInfo.phone" /><span v-if="changes.changeType.contactInfo?.phone" class="change-indicator">(已修改)</span></div></div><!-- 操作按钮区域 --><div class="action-buttons"><!-- 加载数据按钮 --><button @click="loadDataFromBackend" class="btn btn-load">加载数据</button><!-- 检查变更按钮(autoDetect为true时通常不需要手动检查) --><button @click="checkChanges" :disabled="!hasChanges" class="btn btn-check">检查变更</button><!-- 提交变更按钮 --><button @click="submitChanges" :disabled="!hasChanges" class="btn btn-submit">提交变更</button><!-- 重置表单按钮 --><button @click="resetForm" :disabled="!hasChanges" class="btn btn-reset">重置表单</button></div><!-- 变更信息展示区域(只有有变更时才显示) --><div v-if="hasChanges" class="changes-panel"><h3>变更记录</h3><ul class="changes-list"><!-- 遍历所有变更的字段 --><li v-for="(change, index) in changes.changedFields" :key="index" class="change-item"><!-- 显示模块标签 - 字段标签 --><strong>{{ change.moduleLabel }} - {{ change.label }}:</strong><!-- 显示旧值(带删除线) -->从 "<span class="old-value">{{ change.oldValue }}</span>" <!-- 显示新值(加粗) -->修改为 "<span class="new-value">{{ change.newValue }}</span>"</li></ul><!-- 变更统计 --><div class="changes-summary">共 {{ changes.changedFields.length }} 处变更</div></div></div>
</template><script setup>
// 引入需要的Vue API和自定义Hook
import { useFormChangeTracker } from '@/hooks/useFormChangeTracker';/*** 初始表单数据结构* 采用模块化设计,每个模块包含多个字段*/
const initialFormData = {// 用户信息模块userInfo: {name: '', // 姓名字段,初始为空字符串age: 0 // 年龄字段,初始为0},// 联系信息模块contactInfo: {email: '', // 邮箱字段,初始为空phone: '' // 电话字段,初始为空}
};/*** Hook配置选项* 控制表单追踪行为和显示标签*/
const options = {// 自动检测变化(设为true时表单值变化会自动更新changes)autoDetect: true,// 字段标签映射(将字段名映射为更友好的显示名称)fieldLabels: {userInfo: {name: '姓名', // userInfo模块的name字段显示为"姓名"age: '年龄' // age字段显示为"年龄"},contactInfo: {email: '电子邮箱', // email字段显示为"电子邮箱"phone: '联系电话' // phone字段显示为"联系电话"}},// 模块标签映射(将模块名映射为更友好的显示名称)moduleLabels: {userInfo: '用户信息', // userInfo模块显示为"用户信息"contactInfo: '联系信息' // contactInfo模块显示为"联系信息"}
};/*** 使用表单追踪Hook* 获取表单状态和管理方法*/
const {// 响应式表单数据(用于v-model绑定)formContent,// 变化记录对象,包含:// - oldValue: 旧值// - newValue: 新值// - changedFields: 变化的字段列表// - changeType: 各模块变化状态changes,// 是否有未提交的更改(布尔值)hasChanges,// 手动检查变化的函数checkChanges,// 重置表单的函数resetForm,// 加载后端数据的函数loadBackendData
} = useFormChangeTracker(initialFormData, options);/*** 模拟从后端加载数据* 在实际应用中,这里应该是API调用*/
const loadDataFromBackend = () => {// 模拟API返回的数据const backendData = {userInfo: {name: '张三', // 模拟的用户名age: 25 // 模拟的年龄},contactInfo: {email: 'zhangsan@example.com', // 模拟的邮箱phone: '13800138000' // 模拟的电话}};// 调用Hook提供的方法加载数据// 这会更新backendData和formContent,并重置changesloadBackendData(backendData);console.log('数据已从后端加载并填充到表单');
};/*** 提交变更* 在实际应用中,这里应该调用API提交changes.value*/
const submitChanges = () => {// 首先检查当前变化(确保changes是最新的)const currentChanges = checkChanges();console.log('准备提交的变更:', currentChanges);// 这里可以添加实际提交到后端的逻辑// 例如:// 1. 调用API提交变更// 2. 提交成功后调用loadBackendData更新本地数据// 示例:显示提交成功的提示alert(`成功提交 ${currentChanges.changedFields.length} 处变更`);// 实际项目中可能需要:// try {// await api.submitChanges(currentChanges);// loadBackendData(await api.getLatestData());// } catch (error) {// console.error('提交失败:', error);// }
};
</script><style scoped>
/* 表单容器样式 */
.form-container {max-width: 800px; /* 限制最大宽度 */margin: 0 auto; /* 居中显示 */padding: 20px; /* 内边距 */
}/* 表单模块样式 */
.form-section {margin-bottom: 30px; /* 模块间距 */padding: 15px; /* 内边距 */border: 1px solid #eee; /* 边框 */border-radius: 5px; /* 圆角 */
}/* 表单字段样式 */
.form-field {margin: 10px 0; /* 字段间距 */display: flex; /* 弹性布局 */align-items: center; /* 垂直居中 */
}/* 字段标签样式 */
.form-field label {width: 100px; /* 固定宽度 */font-weight: bold; /* 加粗 */
}/* 修改指示器样式 */
.change-indicator {color: #f56c6c; /* 红色 */margin-left: 10px; /* 左边距 */
}/* 操作按钮容器 */
.action-buttons {margin: 20px 0; /* 上下边距 */display: flex; /* 弹性布局 */gap: 10px; /* 按钮间距 */
}/* 基础按钮样式 */
.btn {padding: 8px 15px; /* 内边距 */border: none; /* 无边框 */border-radius: 4px; /* 圆角 */cursor: pointer; /* 手型指针 */
}/* 禁用按钮样式 */
.btn:disabled {opacity: 0.6; /* 半透明 */cursor: not-allowed; /* 禁用指针 */
}/* 加载按钮样式 */
.btn-load {background-color: #409eff; /* 蓝色 */color: white; /* 白色文字 */
}/* 检查按钮样式 */
.btn-check {background-color: #909399; /* 灰色 */color: white;
}/* 提交按钮样式 */
.btn-submit {background-color: #67c23a; /* 绿色 */color: white;
}/* 重置按钮样式 */
.btn-reset {background-color: #f56c6c; /* 红色 */color: white;
}/* 变更面板样式 */
.changes-panel {margin-top: 30px; /* 上边距 */padding: 15px; /* 内边距 */background-color: #f8f8f8; /* 浅灰背景 */border-radius: 5px; /* 圆角 */
}/* 变更列表样式 */
.changes-list {list-style-type: none; /* 无列表标记 */padding: 0; /* 无内边距 */
}/* 单个变更项样式 */
.change-item {padding: 8px 0; /* 内边距 */border-bottom: 1px dashed #ddd; /* 虚线边框 */
}/* 旧值样式 */
.old-value {color: #f56c6c; /* 红色 */text-decoration: line-through; /* 删除线 */
}/* 新值样式 */
.new-value {color: #67c23a; /* 绿色 */font-weight: bold; /* 加粗 */
}/* 变更统计样式 */
.changes-summary {margin-top: 10px; /* 上边距 */font-weight: bold; /* 加粗 */text-align: right; /* 右对齐 */
}
</style>
原文:https://mp.weixin.qq.com/s/gCuqKskp-KBxdClxcpwFqw
注意:
-
JS数据与template内的form绑定数据,要采用hooks 封装的 formContent,否则无法检测到form数据的变化
-
initialFormData ,fieldLabels, moduleLabels 三个对象里面的字段名称key必须都是 userInfo,contactInfo, 字段名称key必须保持统一, 否则无法检测到form数据的变化