| @ -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> | |||