refactor: 全局重构原生弹窗为自定义 Modal 并同步更新项目文档

- 替换所有不可用的 `alert`, `confirm`, `prompt` 调用为异步的自定义 `Modal` 组件以适配 Tauri WebView 的 API 限制。
- 优化与重构核心服务组件接口,增加模型有效性测试 (`test_model`) 以及依赖更新支持。
- 同步补齐 `README.md` 与 `CHANGELOG.md` 新增的系统特性说明(含仪表盘、日记、存储、重构页面调整)。
This commit is contained in:
晴天
2026-02-28 03:42:19 +08:00
parent 75e94a7560
commit 84a6ab4d45
24 changed files with 1591 additions and 488 deletions

View File

@@ -1,21 +1,90 @@
/**
* Modal 弹窗组件
*/
// 转义 HTML 属性值,防止双引号等字符破坏 HTML 结构
function escapeAttr(str) {
if (!str) return ''
return String(str)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
}
/**
* 自定义确认弹窗,替代原生 confirm()
* Tauri WebView 不支持原生 confirm/alert必须用自定义弹窗
* @param {string} message 确认消息
* @returns {Promise<boolean>} 用户选择确认返回 true取消返回 false
*/
export function showConfirm(message) {
return new Promise((resolve) => {
const overlay = document.createElement('div')
overlay.className = 'modal-overlay'
overlay.innerHTML = `
<div class="modal" style="max-width:400px">
<div class="modal-title">确认操作</div>
<div style="font-size:var(--font-size-sm);color:var(--text-secondary);white-space:pre-wrap;line-height:1.6">${escapeAttr(message)}</div>
<div class="modal-actions">
<button class="btn btn-secondary btn-sm" data-action="cancel">取消</button>
<button class="btn btn-danger btn-sm" data-action="confirm">确定</button>
</div>
</div>
`
document.body.appendChild(overlay)
const close = (result) => {
overlay.remove()
resolve(result)
}
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close(false)
})
overlay.querySelector('[data-action="cancel"]').onclick = () => close(false)
overlay.querySelector('[data-action="confirm"]').onclick = () => close(true)
overlay.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); close(true) }
else if (e.key === 'Escape') close(false)
})
// 聚焦确认按钮以接收键盘事件
overlay.querySelector('[data-action="confirm"]').focus()
})
}
export function showModal({ title, fields, onConfirm }) {
const overlay = document.createElement('div')
overlay.className = 'modal-overlay'
const fieldHtml = fields.map(f => `
<div class="form-group">
<label class="form-label">${f.label}</label>
${f.type === 'select'
? `<select class="form-input" data-name="${f.name}">
const fieldHtml = fields.map(f => {
if (f.type === 'checkbox') {
return `
<div class="form-group">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" data-name="${f.name}" ${f.value ? 'checked' : ''}>
<span class="form-label" style="margin:0">${f.label}</span>
</label>
${f.hint ? `<div class="form-hint">${f.hint}</div>` : ''}
</div>`
}
if (f.type === 'select') {
return `
<div class="form-group">
<label class="form-label">${f.label}</label>
<select class="form-input" data-name="${f.name}">
${f.options.map(o => `<option value="${o.value}" ${o.value === f.value ? 'selected' : ''}>${o.label}</option>`).join('')}
</select>`
: `<input class="form-input" data-name="${f.name}" value="${f.value || ''}" placeholder="${f.placeholder || ''}">`
}
</div>
`).join('')
</select>
${f.hint ? `<div class="form-hint">${f.hint}</div>` : ''}
</div>`
}
return `
<div class="form-group">
<label class="form-label">${f.label}</label>
<input class="form-input" data-name="${f.name}" value="${escapeAttr(f.value)}" placeholder="${escapeAttr(f.placeholder)}">
${f.hint ? `<div class="form-hint">${f.hint}</div>` : ''}
</div>`
}).join('')
overlay.innerHTML = `
<div class="modal">
@@ -40,7 +109,11 @@ export function showModal({ title, fields, onConfirm }) {
overlay.querySelector('[data-action="confirm"]').onclick = () => {
const result = {}
overlay.querySelectorAll('[data-name]').forEach(el => {
result[el.dataset.name] = el.value
if (el.type === 'checkbox') {
result[el.dataset.name] = el.checked
} else {
result[el.dataset.name] = el.value
}
})
overlay.remove()
onConfirm(result)