(() => { const API_BASE = '/api/rational_lock' const DEFAULT_USERNAME = 'admin' const $ = id => document.getElementById(id) const now = () => Date.now() const uid = () => `${Date.now()}_${Math.random().toString(36).slice(2, 8)}` const emptyDB = () => ({ items: [], applications: [] }) const state = { db: emptyDB(), auth: { currentUser: '' }, ui: { editingId: '' } } const normalizeDB = value => { const base = value && typeof value === 'object' ? value : {} return { items: Array.isArray(base.items) ? base.items : [], applications: Array.isArray(base.applications) ? base.applications : [] } } const setAuthStatus = text => { $('auth-status').textContent = text || '' } const setFormStatus = text => { $('form-status').textContent = text || '' } const openModal = ({ title, message, okText = '知道了', cancelText = '', tone = 'normal' }) => { return new Promise(resolve => { const mask = $('app-modal') const titleBox = $('modal-title') const msgBox = $('modal-message') const actions = $('modal-actions') actions.innerHTML = '' titleBox.textContent = title || '' msgBox.textContent = message || '' const close = val => { mask.style.display = 'none' resolve(val) } if (cancelText) { const cancelBtn = document.createElement('button') cancelBtn.className = 'btn' cancelBtn.type = 'button' cancelBtn.textContent = cancelText cancelBtn.addEventListener('click', () => close(false)) actions.appendChild(cancelBtn) } const okBtn = document.createElement('button') okBtn.className = tone === 'primary' ? 'btn btn-primary' : 'btn' okBtn.type = 'button' okBtn.textContent = okText okBtn.addEventListener('click', () => close(true)) actions.appendChild(okBtn) mask.style.display = '' }) } const modalAlert = (title, message) => openModal({ title, message, okText: '我知道了' }) const modalConfirm = (title, message, okText = '继续', cancelText = '取消') => openModal({ title, message, okText, cancelText, tone: 'primary' }) const apiRequest = async (path, options = {}) => { const res = await fetch(`${API_BASE}${path}`, { method: options.method || 'GET', headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }, body: options.body === undefined ? undefined : JSON.stringify(options.body), credentials: 'include' }) let payload = null try { payload = await res.json() } catch { payload = null } if (!res.ok) { const msg = payload && payload.error ? String(payload.error) : `http_${res.status}` throw new Error(msg) } return payload || {} } const renderAuth = () => { const user = state.auth.currentUser $('auth-login-box').style.display = user ? 'none' : '' $('auth-logged-box').style.display = user ? '' : 'none' $('app-content').style.display = user ? '' : 'none' $('auth-current-user').textContent = user ? `当前账号:${user}` : '' } const loadDB = async () => { if (!state.auth.currentUser) { state.db = emptyDB() return } const payload = await apiRequest('/data') state.db = normalizeDB(payload.db) } const saveDB = async () => { if (!state.auth.currentUser) return await apiRequest('/data', { method: 'PUT', body: { db: state.db } }) } const upsertItem = (name, category, note) => { const normalizedName = String(name || '').trim() if (!normalizedName) return null const normalizedCategory = String(category || '其他').trim() || '其他' const idx = state.db.items.findIndex(it => String(it.name || '').trim().toLowerCase() === normalizedName.toLowerCase() && String(it.category || '').trim() === normalizedCategory) if (idx >= 0) { const next = { ...state.db.items[idx], name: normalizedName, category: normalizedCategory, note: String(note || '').trim(), updatedAt: now() } state.db.items[idx] = next return next } const row = { id: uid(), name: normalizedName, category: normalizedCategory, note: String(note || '').trim(), createdAt: now() } state.db.items.unshift(row) return row } const renderLibrary = () => { $('btn-lib-add').textContent = state.ui.editingId ? '保存修改' : '加入物品库' $('btn-lib-cancel-edit').style.display = state.ui.editingId ? '' : 'none' const box = $('lib-list') box.innerHTML = '' if (!state.db.items.length) { box.textContent = '暂无物品库记录' return } state.db.items.forEach(it => { const row = document.createElement('div') row.className = 'row' row.innerHTML = `
${it.name} · ${it.category}
${it.note || '无备注'}
` box.appendChild(row) }) } const renderAll = () => { renderAuth() renderLibrary() } const bindLibrary = () => { $('btn-lib-add').addEventListener('click', async () => { if (!state.auth.currentUser) { await modalAlert('请先登录', '请先输入账号和密码登录后再操作。') return } const name = $('lib-name').value.trim() const category = $('lib-category').value const note = $('lib-note').value.trim() if (!name) { await modalAlert('无法保存', '请先填写物品名称。') return } if (state.ui.editingId) { const idx = state.db.items.findIndex(it => it.id === state.ui.editingId) if (idx >= 0) { state.db.items[idx] = { ...state.db.items[idx], name, category, note, updatedAt: now() } } state.ui.editingId = '' await modalAlert('✅ 保存成功!', `你改的【${name}】信息已经存好啦,不会丢的~`) } else { upsertItem(name, category, note) await modalAlert('🎉 新增成功!', `你添加的【${name}】已经入库啦,后续会用于同类提醒。`) } await saveDB() $('lib-name').value = '' $('lib-note').value = '' renderLibrary() setFormStatus('物品库已更新') }) $('btn-lib-cancel-edit').addEventListener('click', () => { state.ui.editingId = '' $('lib-name').value = '' $('lib-note').value = '' renderLibrary() }) $('lib-list').addEventListener('click', async e => { if (!state.auth.currentUser) { await modalAlert('请先登录', '请先输入账号和密码登录后再操作。') return } const target = e.target const editId = target && target.getAttribute('data-lib-edit') if (editId) { const it = state.db.items.find(x => x.id === editId) if (!it) return state.ui.editingId = editId $('lib-name').value = it.name || '' $('lib-category').value = it.category || '其他' $('lib-note').value = it.note || '' renderLibrary() setFormStatus(`已加载物品库【${it.name}】到编辑区,修改后点击“保存修改”`) return } const id = target && target.getAttribute('data-lib-del') if (!id) return const it = state.db.items.find(x => x.id === id) const ok = await modalConfirm('确认删除', `确认删除物品库中的【${it ? it.name : '该条'}】吗?这不会改动历史申请记录。`, '确定删除', '再想想') if (!ok) return state.db.items = state.db.items.filter(x => x.id !== id) if (state.ui.editingId === id) state.ui.editingId = '' await saveDB() renderLibrary() setFormStatus('物品库已更新') await modalAlert('✅ 删除成功!', `已删除【${it ? it.name : '该物品'}】。历史申请快照不会被改写。`) }) } const bindAuth = () => { const tryLogin = async () => { const username = String($('login-username').value || '').trim() const password = String($('login-password').value || '') if (!username || !password) { setAuthStatus('请输入账号和密码') return } try { await apiRequest('/auth/login', { method: 'POST', body: { username, password } }) } catch { setAuthStatus('账号或密码不正确,请重试') await modalAlert('登录失败', '账号或密码不正确,请重新输入。') return } state.auth.currentUser = username $('login-password').value = '' await loadDB() renderAll() setAuthStatus(`登录成功,欢迎你:${username}`) setFormStatus('可以开始维护物品库') } $('btn-login').addEventListener('click', tryLogin) $('login-password').addEventListener('keydown', async e => { if (e.key !== 'Enter') return e.preventDefault() await tryLogin() }) $('btn-logout').addEventListener('click', async () => { try { await apiRequest('/auth/logout', { method: 'POST' }) } catch {} state.auth.currentUser = '' state.db = emptyDB() state.ui.editingId = '' $('lib-name').value = '' $('lib-note').value = '' setFormStatus('') setAuthStatus('已退出登录') renderAll() }) } const bindNav = () => { $('btn-back-main').addEventListener('click', () => { window.location.href = '/tools/rational_lock/index.html' }) } const init = async () => { bindAuth() bindLibrary() bindNav() try { const payload = await apiRequest('/auth/me') state.auth.currentUser = String(payload.username || DEFAULT_USERNAME) await loadDB() setAuthStatus(`已登录:${state.auth.currentUser}`) setFormStatus('可以开始维护物品库') } catch { state.auth.currentUser = '' state.db = emptyDB() setAuthStatus('请先登录') } renderAll() } init() })()