304 lines
10 KiB
JavaScript
304 lines
10 KiB
JavaScript
(() => {
|
|
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 = `
|
|
<div><strong>${it.name}</strong> · ${it.category}</div>
|
|
<div class="meta">${it.note || '无备注'}</div>
|
|
<div class="actions">
|
|
<button class="btn" data-lib-edit="${it.id}" type="button">修改</button>
|
|
<button class="btn" data-lib-del="${it.id}" type="button">删除</button>
|
|
</div>
|
|
`
|
|
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()
|
|
})()
|