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數據的變化