298 lines
11 KiB
JavaScript
298 lines
11 KiB
JavaScript
(() => {
|
||
const API_BASE = '/api/language_behavior_lock'
|
||
const DEFAULT_USERNAME = 'admin'
|
||
const $ = id => document.getElementById(id)
|
||
const now = () => Date.now()
|
||
const genId = () => `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||
|
||
const state = {
|
||
auth: { currentUser: '' },
|
||
db: { records: [], keywords: [] }
|
||
}
|
||
|
||
const defaultKeywords = () => ([
|
||
{ id: genId(), text: '你怎么这么', type: 'speech', level: 'high', extraMinutes: 20, tip: '你触发了高风险表达,建议延长冷静期。', enabled: true, createdAt: now() },
|
||
{ id: genId(), text: '早就说了', type: 'speech', level: 'medium', extraMinutes: 15, tip: '这类表达容易升级冲突,建议继续冷静。', enabled: true, createdAt: now() },
|
||
{ id: genId(), text: '你一直都', type: 'speech', level: 'high', extraMinutes: 20, tip: '绝对化表达风险较高,建议先改写再决定。', enabled: true, createdAt: now() },
|
||
{ id: genId(), text: '拿回家', type: 'behavior', level: 'high', extraMinutes: 25, tip: '涉及边界风险行为,建议延长冷静并复核后果。', enabled: true, createdAt: now() }
|
||
])
|
||
|
||
const normalizeKeyword = row => {
|
||
const levelRaw = String(row && row.level || 'medium')
|
||
const level = ['low', 'medium', 'high'].includes(levelRaw) ? levelRaw : 'medium'
|
||
const typeRaw = String(row && row.type || 'all')
|
||
const type = ['all', 'speech', 'behavior', 'securities'].includes(typeRaw) ? typeRaw : 'all'
|
||
return {
|
||
id: String(row && row.id || genId()),
|
||
text: String(row && row.text || '').trim(),
|
||
type,
|
||
level,
|
||
extraMinutes: Math.max(0, Number(row && row.extraMinutes) || 0),
|
||
tip: String(row && row.tip || '').trim(),
|
||
enabled: row && row.enabled !== false,
|
||
createdAt: Number(row && row.createdAt) || now()
|
||
}
|
||
}
|
||
|
||
const normalizeDB = raw => {
|
||
const db = raw && typeof raw === 'object' ? raw : {}
|
||
const records = Array.isArray(db.records) ? db.records : []
|
||
const keywordsRaw = Array.isArray(db.keywords) ? db.keywords : []
|
||
const keywords = keywordsRaw.map(normalizeKeyword).filter(k => k.text)
|
||
return {
|
||
records,
|
||
keywords: keywords.length ? keywords : defaultKeywords()
|
||
}
|
||
}
|
||
|
||
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 loadDB = async () => {
|
||
const payload = await apiRequest('/data')
|
||
state.db = normalizeDB(payload.db)
|
||
}
|
||
|
||
const saveDB = async () => {
|
||
await apiRequest('/data', { method: 'PUT', body: { db: state.db, draft: null } })
|
||
}
|
||
|
||
const levelText = level => ({ low: '低风险', medium: '中风险', high: '高风险' })[level] || '中风险'
|
||
|
||
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 renderKeywordRules = () => {
|
||
const box = $('keyword-list')
|
||
box.innerHTML = ''
|
||
if (!state.db.keywords.length) {
|
||
box.textContent = '暂无关键词规则'
|
||
return
|
||
}
|
||
state.db.keywords.slice().sort((a, b) => b.createdAt - a.createdAt).forEach(rule => {
|
||
const row = document.createElement('div')
|
||
row.className = 'row'
|
||
const typeLabel = rule.type === 'speech' ? '发言' : rule.type === 'behavior' ? '行为' : rule.type === 'securities' ? '证券' : '全部'
|
||
row.innerHTML = `
|
||
<div><strong>${rule.text}</strong> <span class="tag ${rule.level}">${levelText(rule.level)}</span></div>
|
||
<div class="meta">适用:${typeLabel} | 建议加时:${rule.extraMinutes} 分钟 | 状态:${rule.enabled ? '启用' : '停用'}</div>
|
||
<div class="meta">${rule.tip || ''}</div>
|
||
<div class="actions">
|
||
<button class="btn" data-act="toggle-rule" data-id="${rule.id}" type="button">${rule.enabled ? '停用' : '启用'}</button>
|
||
<button class="btn" data-act="delete-rule" data-id="${rule.id}" type="button">删除</button>
|
||
</div>
|
||
`
|
||
box.appendChild(row)
|
||
})
|
||
}
|
||
|
||
const renderAll = () => {
|
||
renderAuth()
|
||
renderKeywordRules()
|
||
}
|
||
|
||
const bindKeywordAdd = () => {
|
||
$('btn-keyword-add').addEventListener('click', async () => {
|
||
if (!state.auth.currentUser) {
|
||
await modalAlert('请先登录', '请先输入账号和密码登录后再操作。')
|
||
return
|
||
}
|
||
const text = $('kw-text').value.trim()
|
||
if (!text) {
|
||
await modalAlert('无法新增', '请先输入关键词')
|
||
return
|
||
}
|
||
const exists = state.db.keywords.some(k => k.text.toLowerCase() === text.toLowerCase())
|
||
if (exists) {
|
||
await modalAlert('无法新增', '该关键词已存在')
|
||
return
|
||
}
|
||
const rule = normalizeKeyword({
|
||
id: genId(),
|
||
text,
|
||
type: $('kw-type').value || 'all',
|
||
level: $('kw-level').value || 'medium',
|
||
extraMinutes: Number($('kw-extra-minutes').value || 0),
|
||
tip: $('kw-tip').value.trim(),
|
||
enabled: true,
|
||
createdAt: now()
|
||
})
|
||
state.db.keywords.unshift(rule)
|
||
await saveDB()
|
||
$('kw-text').value = ''
|
||
$('kw-tip').value = ''
|
||
renderKeywordRules()
|
||
setFormStatus('关键词规则已新增')
|
||
await modalAlert('规则已新增', `关键词“${rule.text}”已启用,后续命中会自动提醒。`)
|
||
})
|
||
}
|
||
|
||
const bindActions = () => {
|
||
document.addEventListener('click', async e => {
|
||
const btn = e.target.closest('button[data-act]')
|
||
if (!btn) return
|
||
if (!state.auth.currentUser) {
|
||
await modalAlert('请先登录', '请先输入账号和密码登录后再操作。')
|
||
return
|
||
}
|
||
const id = btn.getAttribute('data-id')
|
||
const act = btn.getAttribute('data-act')
|
||
if (!id || !act) return
|
||
if (act === 'toggle-rule') {
|
||
const idx = state.db.keywords.findIndex(k => k.id === id)
|
||
if (idx < 0) return
|
||
state.db.keywords[idx].enabled = !state.db.keywords[idx].enabled
|
||
await saveDB()
|
||
renderKeywordRules()
|
||
setFormStatus(`关键词已${state.db.keywords[idx].enabled ? '启用' : '停用'}`)
|
||
await modalAlert('状态已更新', `关键词已${state.db.keywords[idx].enabled ? '启用' : '停用'}。`)
|
||
return
|
||
}
|
||
if (act === 'delete-rule') {
|
||
const rule = state.db.keywords.find(k => k.id === id)
|
||
if (!rule) return
|
||
const ok = await modalConfirm('确认删除', `确认删除关键词“${rule.text}”吗?`, '确认删除', '取消')
|
||
if (!ok) return
|
||
state.db.keywords = state.db.keywords.filter(k => k.id !== id)
|
||
await saveDB()
|
||
renderKeywordRules()
|
||
setFormStatus('关键词已删除')
|
||
await modalAlert('删除成功', `关键词“${rule.text}”已删除。`)
|
||
}
|
||
})
|
||
}
|
||
|
||
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 = { records: [], keywords: defaultKeywords() }
|
||
setFormStatus('')
|
||
setAuthStatus('已退出登录')
|
||
renderAll()
|
||
})
|
||
}
|
||
|
||
const bindNav = () => {
|
||
$('btn-back-main').addEventListener('click', () => {
|
||
window.location.href = '/tools/language_behavior_lock/index.html'
|
||
})
|
||
}
|
||
|
||
const init = async () => {
|
||
bindAuth()
|
||
bindKeywordAdd()
|
||
bindActions()
|
||
bindNav()
|
||
try {
|
||
const me = await apiRequest('/auth/me')
|
||
state.auth.currentUser = String(me.username || DEFAULT_USERNAME)
|
||
await loadDB()
|
||
setAuthStatus(`已登录:${state.auth.currentUser}`)
|
||
setFormStatus('可以开始维护关键词规则')
|
||
} catch {
|
||
state.auth.currentUser = ''
|
||
state.db = { records: [], keywords: defaultKeywords() }
|
||
setAuthStatus('请先登录')
|
||
}
|
||
renderAll()
|
||
}
|
||
|
||
init()
|
||
})()
|