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