| @ -0,0 +1,261 @@ | |||||
| <template> | |||||
| <div style="padding: 20px; max-width: 600px;"> | |||||
| <h2>用户信息表单(带修改记录)</h2> | |||||
| <el-form ref="formRef" :model="form" label-width="120px"> | |||||
| <!-- 姓名 --> | |||||
| <el-form-item label="姓名"> | |||||
| <el-tooltip | |||||
| :content="currentTooltipContent" | |||||
| placement="top" | |||||
| effect="light" | |||||
| :disabled="!isTooltipVisible" | |||||
| raw-content | |||||
| popper-class="audit-tooltip" | |||||
| > | |||||
| <el-input | |||||
| v-model="form.basicInfo.name" | |||||
| placeholder="请输入姓名" | |||||
| @blur="onFieldChange('basicInfo.name', $event)" | |||||
| @focus="loadTooltip('basicInfo.name')" | |||||
| @mouseleave="hideTooltip" | |||||
| /> | |||||
| </el-tooltip> | |||||
| </el-form-item> | |||||
| <!-- 邮箱 --> | |||||
| <el-form-item label="邮箱"> | |||||
| <el-tooltip | |||||
| :content="currentTooltipContent" | |||||
| placement="top" | |||||
| effect="light" | |||||
| :disabled="!isTooltipVisible" | |||||
| raw-content | |||||
| popper-class="audit-tooltip" | |||||
| > | |||||
| <el-input | |||||
| v-model="form.contact.email" | |||||
| placeholder="请输入邮箱" | |||||
| @blur="onFieldChange('contact.email', $event)" | |||||
| @focus="loadTooltip('contact.email')" | |||||
| @mouseleave="hideTooltip" | |||||
| /> | |||||
| </el-tooltip> | |||||
| </el-form-item> | |||||
| <!-- 年龄 --> | |||||
| <el-form-item label="年龄"> | |||||
| <el-tooltip | |||||
| :content="currentTooltipContent" | |||||
| placement="top" | |||||
| effect="light" | |||||
| :disabled="!isTooltipVisible" | |||||
| raw-content | |||||
| popper-class="audit-tooltip" | |||||
| > | |||||
| <el-input | |||||
| v-model.number="form.basicInfo.age" | |||||
| placeholder="请输入年龄" | |||||
| type="number" | |||||
| @blur="onFieldChange('basicInfo.age', $event)" | |||||
| @focus="loadTooltip('basicInfo.age')" | |||||
| @mouseleave="hideTooltip" | |||||
| /> | |||||
| </el-tooltip> | |||||
| </el-form-item> | |||||
| <!-- 调试:显示所有日志(可选) --> | |||||
| <el-button size="small" @click="showAllLogs">查看所有本地日志(控制台)</el-button> | |||||
| </el-form> | |||||
| </div> | |||||
| </template> | |||||
| <script> | |||||
| export default { | |||||
| name: 'AuditFormDemo', | |||||
| data() { | |||||
| return { | |||||
| form: { | |||||
| basicInfo: { | |||||
| name: '', | |||||
| age: null | |||||
| }, | |||||
| contact: { | |||||
| email: '' | |||||
| } | |||||
| }, | |||||
| commonContent: '', | |||||
| // 缓存初始值,用于对比旧值 | |||||
| initialValues: { | |||||
| 'basicInfo.name': '', | |||||
| 'contact.email': '', | |||||
| 'basicInfo.age': null | |||||
| }, | |||||
| // 本地缓存日志(提升 hover 性能,避免频繁读 DB) | |||||
| cachedLogs: {}, | |||||
| cachedLogs: {}, | |||||
| currentTooltipContent: '', // 当前 tooltip 显示的内容(字符串!) | |||||
| isTooltipVisible: false, // 控制 tooltip 是否启用 | |||||
| db: null | |||||
| }; | |||||
| }, | |||||
| async created() { | |||||
| await this.initDB(); | |||||
| // 初始化表单值(可从 DB 恢复草稿,此处略) | |||||
| }, | |||||
| methods: { | |||||
| // ========== IndexedDB 操作 ========== | |||||
| initDB() { | |||||
| return new Promise((resolve, reject) => { | |||||
| const request = indexedDB.open('AuditLogDB', 1); | |||||
| request.onerror = () => reject(request.error); | |||||
| request.onsuccess = () => { | |||||
| this.db = request.result; | |||||
| resolve(); | |||||
| }; | |||||
| request.onupgradeneeded = (event) => { | |||||
| const db = event.target.result; | |||||
| if (!db.objectStoreNames.contains('fieldLogs')) { | |||||
| const store = db.createObjectStore('fieldLogs', { keyPath: 'fieldPath' }); | |||||
| store.createIndex('byField', 'fieldPath', { unique: false }); | |||||
| } | |||||
| }; | |||||
| }); | |||||
| }, | |||||
| // 保存一条日志到 IndexedDB | |||||
| async saveLog(fieldPath, oldValue, newValue) { | |||||
| if (oldValue === newValue) return; | |||||
| const logEntry = { | |||||
| fieldPath, | |||||
| logs: [ | |||||
| { | |||||
| oldValue, | |||||
| newValue, | |||||
| timestamp: new Date().toISOString(), | |||||
| operator: '当前用户' // 可替换为真实用户名 | |||||
| }, | |||||
| ...(this.cachedLogs[fieldPath] || []) | |||||
| ].slice(0, 10) // 最多保留10条 | |||||
| }; | |||||
| const transaction = this.db.transaction(['fieldLogs'], 'readwrite'); | |||||
| const store = transaction.objectStore('fieldLogs'); | |||||
| store.put(logEntry); | |||||
| // 更新缓存 | |||||
| this.$set(this.cachedLogs, fieldPath, logEntry.logs); | |||||
| }, | |||||
| // 从 IndexedDB 读取某字段日志(优先用缓存) | |||||
| async loadLogs(fieldPath) { | |||||
| if (this.cachedLogs[fieldPath]) { | |||||
| return this.cachedLogs[fieldPath]; | |||||
| } | |||||
| return new Promise((resolve) => { | |||||
| const transaction = this.db.transaction(['fieldLogs'], 'readonly'); | |||||
| const store = transaction.objectStore('fieldLogs'); | |||||
| const request = store.get(fieldPath); | |||||
| request.onsuccess = () => { | |||||
| const result = request.result?.logs || []; | |||||
| this.$set(this.cachedLogs, fieldPath, result); | |||||
| resolve(result); | |||||
| }; | |||||
| request.onerror = () => { | |||||
| console.error('读取日志失败:', request.error); | |||||
| resolve([]); | |||||
| }; | |||||
| }); | |||||
| }, | |||||
| // ========== 表单交互 ========== | |||||
| onFieldChange(fieldPath, e) { | |||||
| console.log(newValue,fieldPath) | |||||
| const newValue = e.target.value | |||||
| // 获取旧值:先看是否已修改过(cachedLogs 最新记录的 oldValue),否则用 initialValues | |||||
| let oldValue; | |||||
| if (this.cachedLogs[fieldPath]?.length > 0) { | |||||
| oldValue = this.cachedLogs[fieldPath][0].oldValue; | |||||
| } else { | |||||
| oldValue = this.initialValues[fieldPath]; | |||||
| } | |||||
| // 异步保存日志 | |||||
| this.saveLog(fieldPath, oldValue, newValue); | |||||
| }, | |||||
| // ========== Tooltip 渲染 ========== | |||||
| hasLogs(fieldPath) { | |||||
| return (this.cachedLogs[fieldPath]?.length || 0) > 0; | |||||
| }, | |||||
| async loadTooltip(fieldPath) { | |||||
| this.isTooltipVisible = false; // 先禁用,避免闪烁 | |||||
| console.log("lofo") | |||||
| const logs = await this.loadLogs(fieldPath); | |||||
| if (logs.length === 0) { | |||||
| this.currentTooltipContent = '<div class="empty">暂无修改记录</div>'; | |||||
| } else { | |||||
| this.currentTooltipContent = logs | |||||
| .map( | |||||
| (log) => | |||||
| `<div class="log-item"><strong>${this.formatTime( | |||||
| log.timestamp | |||||
| )}</strong><br/>${log.operator} 将值从 "<code>${this.escapeHtml( | |||||
| String(log.oldValue) | |||||
| )}</code>" 改为 "<code>${this.escapeHtml(String(log.newValue))}</code>"</div>` | |||||
| ) | |||||
| .join(''); | |||||
| } | |||||
| this.isTooltipVisible = true; // 加载完成后再启用 | |||||
| }, | |||||
| hideTooltip() { | |||||
| this.isTooltipVisible = false; | |||||
| }, | |||||
| formatTime(isoString) { | |||||
| return new Date(isoString).toLocaleString('zh-CN'); | |||||
| }, | |||||
| escapeHtml(text) { | |||||
| const div = document.createElement('div'); | |||||
| div.textContent = text; | |||||
| return div.innerHTML; | |||||
| }, | |||||
| // 调试用:打印所有日志 | |||||
| async showAllLogs() { | |||||
| const transaction = this.db.transaction(['fieldLogs'], 'readonly'); | |||||
| const store = transaction.objectStore('fieldLogs'); | |||||
| const request = store.getAll(); | |||||
| request.onsuccess = () => { | |||||
| console.log('所有字段修改记录:', request.result); | |||||
| }; | |||||
| } | |||||
| } | |||||
| }; | |||||
| </script> | |||||
| <style scoped> | |||||
| .empty { | |||||
| color: #999; | |||||
| font-style: italic; | |||||
| } | |||||
| .log-item { | |||||
| margin: 6px 0; | |||||
| line-height: 1.4; | |||||
| } | |||||
| .log-item code { | |||||
| background: #f5f5f5; | |||||
| padding: 2px 4px; | |||||
| border-radius: 3px; | |||||
| } | |||||
| </style> | |||||