feat: 初始提交(仅核心代码,已排除大文件)
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>语言和行为冷静期</title>
|
||||
<link rel="stylesheet" href="/common/common.css" />
|
||||
<link rel="stylesheet" href="/tools/language_behavior_lock/language_behavior_lock.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<header class="hero">
|
||||
<div class="hero-main">
|
||||
<div>
|
||||
<h1>语言和行为冷静期</h1>
|
||||
<p>把发言、行为和证券先锁住,等情绪过后再决定</p>
|
||||
</div>
|
||||
<div class="hero-auth">
|
||||
<div id="auth-login-box" class="auth-login-inline">
|
||||
<input id="login-username" autocomplete="off" readonly />
|
||||
<input id="login-password" type="password" autocomplete="off" readonly />
|
||||
<button id="btn-login" class="btn btn-primary" type="button">登录</button>
|
||||
</div>
|
||||
<div id="auth-logged-box" style="display:none">
|
||||
<div class="auth-row">
|
||||
<div id="auth-current-user" class="auth-current-user"></div>
|
||||
<button id="btn-logout" class="btn" type="button">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="auth-status" class="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="app-content" style="display:none">
|
||||
<section class="card">
|
||||
<h2>提交冷静期申请</h2>
|
||||
<form id="application-form">
|
||||
<div class="grid two">
|
||||
<label>类型
|
||||
<select id="entry-type" required>
|
||||
<option value="speech">发言</option>
|
||||
<option value="behavior">行为</option>
|
||||
<option value="securities">证券</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>标题
|
||||
<input id="entry-title" required placeholder="例如:今晚要不要去陌生饭局" />
|
||||
</label>
|
||||
</div>
|
||||
<label>内容
|
||||
<textarea id="entry-content" required placeholder="发言:把你想说的原话写下来;行为:把你想做的事写下来"></textarea>
|
||||
</label>
|
||||
|
||||
<h3>核心三问</h3>
|
||||
<div class="grid one">
|
||||
<label>1️⃣ 这是冲动,还是计划?
|
||||
<div class="inline-risk-field">
|
||||
<select id="q1" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="impulse">冲动</option>
|
||||
<option value="mixed">说不清</option>
|
||||
<option value="plan">计划</option>
|
||||
</select>
|
||||
<input id="q1-risk-note" required placeholder="请写下你为什么这样判断" />
|
||||
</div>
|
||||
</label>
|
||||
<label>2️⃣ 不做会不会后悔?
|
||||
<div class="inline-risk-field">
|
||||
<select id="q2" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="no_regret">大概率不会</option>
|
||||
<option value="maybe_regret">可能会</option>
|
||||
<option value="must_do">很可能后悔</option>
|
||||
</select>
|
||||
<input id="q2-risk-note" placeholder="选择后两项时,请用文字写下理由" style="display:none" />
|
||||
</div>
|
||||
</label>
|
||||
<label>3️⃣ 最坏情况我能接受吗?
|
||||
<div class="inline-risk-field">
|
||||
<select id="q3" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="cannot_accept">不能接受</option>
|
||||
<option value="hard_accept">勉强接受</option>
|
||||
<option value="accept">可以接受</option>
|
||||
</select>
|
||||
<input id="q3-risk-note" placeholder="涉及危害时,写下你担心的后果" style="display:none" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h3>风险维度</h3>
|
||||
<div class="grid two">
|
||||
<label>影响范围
|
||||
<select id="impact-scope" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="self">仅自己</option>
|
||||
<option value="single">一对一</option>
|
||||
<option value="group">多人/公开场景</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>可逆性
|
||||
<select id="reversible" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="high">容易撤回</option>
|
||||
<option value="medium">部分可补救</option>
|
||||
<option value="low">几乎不可逆</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>主要后果类型
|
||||
<select id="cost-type" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="relationship">关系</option>
|
||||
<option value="reputation">信誉</option>
|
||||
<option value="money">金钱</option>
|
||||
<option value="safety">安全</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>当前情绪强度(1-5)
|
||||
<select id="emotion-level" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h3>自动评估</h3>
|
||||
<div class="grid one">
|
||||
<div id="risk-box" class="result-box">请选择完整信息后自动评估</div>
|
||||
<label>冷静期时长(分钟)
|
||||
<input id="cooling-minutes" type="number" min="5" step="1" required />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h3>黑名单关键词命中提醒</h3>
|
||||
<div id="keyword-hit-box" class="warn" style="display:none"></div>
|
||||
|
||||
<h3>最终确认</h3>
|
||||
<div class="check-grid" id="commit-final">
|
||||
<label><input type="checkbox" value="1" />我已完整填写,不跳步决策</label>
|
||||
<label><input type="checkbox" value="1" />我接受冷静期内不能执行最终动作</label>
|
||||
<label><input type="checkbox" value="1" />我愿意对最终选择承担责任</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button id="btn-save-draft" class="btn" type="button">保存草稿</button>
|
||||
<button id="btn-load-draft" class="btn" type="button">继续填写草稿</button>
|
||||
<button id="btn-clear-draft" class="btn" type="button">清空草稿</button>
|
||||
<button id="btn-open-keyword-settings" class="btn" type="button">设置</button>
|
||||
</div>
|
||||
<button id="btn-submit" class="btn btn-primary" type="submit">提交并开始冷静期</button>
|
||||
<div id="form-status" class="status"></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>当前冷静期与结果</h2>
|
||||
<div id="decision-panel" class="decision"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>历史记录</h2>
|
||||
<div class="history-toolbar">
|
||||
<label>搜索
|
||||
<input id="history-search" placeholder="标题/内容/结果" />
|
||||
</label>
|
||||
<label>类型
|
||||
<select id="history-type">
|
||||
<option value="all">全部</option>
|
||||
<option value="speech">发言</option>
|
||||
<option value="behavior">行为</option>
|
||||
<option value="securities">证券</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>状态
|
||||
<select id="history-status">
|
||||
<option value="all">全部</option>
|
||||
<option value="cooling">冷静中</option>
|
||||
<option value="sent">已发送</option>
|
||||
<option value="done">已执行</option>
|
||||
<option value="postponed">已延后</option>
|
||||
<option value="abandoned">已放弃</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="btn-history-reset" class="btn" type="button">重置筛选</button>
|
||||
</div>
|
||||
<div id="history-list" class="list"></div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="app-modal" class="modal-mask" style="display:none">
|
||||
<div class="modal-panel">
|
||||
<div id="modal-title" class="modal-title"></div>
|
||||
<div id="modal-message" class="modal-message"></div>
|
||||
<div id="modal-actions" class="modal-actions"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/tools/language_behavior_lock/language_behavior_lock.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,297 @@
|
||||
(() => {
|
||||
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()
|
||||
})()
|
||||
@@ -0,0 +1,465 @@
|
||||
body {
|
||||
margin: 0;
|
||||
background: #f3f6fb;
|
||||
color: #1f2937;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.hero-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
margin: 0;
|
||||
opacity: .92;
|
||||
}
|
||||
|
||||
.hero-auth {
|
||||
min-width: 320px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
justify-items: end;
|
||||
}
|
||||
|
||||
.auth-login-inline {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.auth-login-inline input {
|
||||
width: 110px;
|
||||
height: 34px;
|
||||
border-color: #334155;
|
||||
background: #0f172a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.auth-login-inline input::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.hero-auth .btn-primary {
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.auth-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hero-auth .auth-row .btn {
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
.auth-current-user {
|
||||
font-size: 13px;
|
||||
color: #e2e8f0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hero-auth .status {
|
||||
margin-top: 0;
|
||||
min-height: 16px;
|
||||
font-size: 12px;
|
||||
color: #cbd5e1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 2px 12px rgba(17, 24, 39, 0.06);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 14px 0 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
height: 38px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
padding: 0 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 88px;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
resize: vertical;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.grid.two {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.grid.one {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.inline-risk-field {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.check-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.check-grid label {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.check-grid input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
height: 38px;
|
||||
padding: 0 14px;
|
||||
background: #334155;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: .45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn.btn-soft-disabled {
|
||||
opacity: .6;
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
background: #2563eb;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #1d4ed8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status {
|
||||
min-height: 20px;
|
||||
margin-top: 8px;
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.warn {
|
||||
background: #fee2e2;
|
||||
border: 1px solid #ef4444;
|
||||
color: #991b1b;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 8px;
|
||||
background: #eff6ff;
|
||||
color: #1e3a8a;
|
||||
padding: 10px;
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.decision {
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.decision.pass {
|
||||
border-color: #16a34a;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.decision.wait {
|
||||
border-color: #2563eb;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.decision.stop {
|
||||
border-color: #dc2626;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.row {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.row-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.row .meta {
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.row .actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.history-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr auto;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #f8fafc;
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tag.high {
|
||||
border-color: #ef4444;
|
||||
color: #b91c1c;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.tag.medium {
|
||||
border-color: #f59e0b;
|
||||
color: #92400e;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.tag.low {
|
||||
border-color: #22c55e;
|
||||
color: #166534;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-panel {
|
||||
width: min(92vw, 460px);
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(15, 23, 42, 0.25);
|
||||
padding: 14px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.modal-panel.wide {
|
||||
width: min(92vw, 760px);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.modal-message {
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
max-height: 62vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-message.is-rich {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
margin: 10px 0 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: 130px 1fr;
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #1d4ed8;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal-actions .btn-primary {
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.hero-main {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hero-auth {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
justify-items: stretch;
|
||||
}
|
||||
|
||||
.auth-login-inline {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
}
|
||||
|
||||
.auth-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.hero-auth .status {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.grid.two {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.history-toolbar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.inline-risk-field {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.row .actions .btn {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 19px;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user