Files

298 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
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()
})()