Files
group_fqcd_jr/app/static/portal/employee-risk/dashboard/dashboard.js
T
2026-09-13 15:56:54 +08:00

445 lines
27 KiB
JavaScript
Raw 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.
import { apiClient } from '/static/portal/common/api-client.js?v=20260913';
import { getAuthContext, getPermissions, requireRiskStaff, updateAuthPermissions } from '/static/portal/common/auth.js';
import { escapeHtml, formatDateTime } from '/static/portal/common/formatters.js';
import { mountShell } from '/static/portal/common/layout/app-shell.js';
import { showToast } from '/static/portal/common/notifications.js';
import { applyPermissionGuard } from '/static/portal/common/permission-guard.js';
import { PERM_CODES } from '/static/portal/common/permission-codes.js';
import { renderEmpty, renderError, renderLoading } from '/static/portal/common/state-view.js';
const EVIDENCE_COLUMNS = Object.freeze({
customers: [['customer_no', '客户编号'], ['name', '客户姓名'], ['risk_level', '风险等级'], ['behavior_score', '行为分'], ['total_asset', '资产快照'], ['status', '状态']],
products: [['product_code', '产品代码'], ['product_name', '产品名称'], ['risk_level', '产品风险'], ['product_category', '产品类型'], ['status', '状态']],
transactions: [['transaction_no', '成交编号'], ['customer_no', '客户编号'], ['product_code', '产品代码'], ['transaction_type', '方向'], ['amount', '金额'], ['executed_at', '成交时间']],
capital_flows: [['flow_no', '流水编号'], ['customer_no', '客户编号'], ['flow_type', '类型'], ['amount', '金额'], ['source_type', '来源'], ['settled_at', '到账时间']],
holdings: [['customer_no', '客户编号'], ['product_code', '产品代码'], ['product_name', '产品名称'], ['shares', '持有份额'], ['current_value', '当前市值'], ['holding_ratio', '持仓占比']],
login_records: [['customer_no', '客户编号'], ['login_at', '登录时间'], ['login_result', '结果'], ['ip_region', '地区'], ['device_id', '设备'], ['is_common_device', '常用设备']],
alerts: [['alert_no', '预警编号'], ['customer_no', '客户编号'], ['alert_type', '预警类型'], ['risk_level', '风险等级'], ['status', '状态'], ['created_at', '生成时间']],
notifications: [['notification_no', '通知编号'], ['alert_no', '预警编号'], ['channel', '渠道'], ['send_status', '发送状态'], ['title', '标题'], ['send_time', '发送时间']],
});
const ACTIONS = Object.freeze({
acknowledge: { endpoint: 'RK007', title: '确认接收预警', copy: '确认后将记录当前处理人与接收时间。', success: '预警已确认接收' },
investigate: { endpoint: 'RK008', title: '进入调查', copy: '预警将进入人工调查状态。', success: '已进入调查' },
exclude: { endpoint: 'RK009', title: '关闭误报', copy: '请填写可追溯的误报理由。', label: '误报理由', field: 'reason', success: '已关闭误报' },
resolve: { endpoint: 'RK010', title: '完成结案', copy: '结案后将按风险等级更新行为分。', label: '处置结论', field: 'resolution', success: '预警已完成结案' },
escalate: { endpoint: 'RK011', title: '升级处理', copy: '升级是预警标记,不改变当前闭环状态。', label: '升级理由', field: 'reason', success: '预警已升级' },
});
function displayValue(value, key = '') {
if (value === null || value === undefined || value === '') return '--';
if (Array.isArray(value)) return value.length ? value.join('、') : '--';
if (typeof value === 'boolean') return value ? '是' : '否';
if (typeof value === 'object') return JSON.stringify(value);
if (key.endsWith('_at') || key.endsWith('_time') || key === 'created_at' || key === 'updated_at') return formatDateTime(value);
return String(value);
}
function riskTag(level) {
const key = level === '高' || level === '高风险' ? 'high' : level === '中' || level === '中风险' ? 'medium' : 'low';
const label = String(level || '--').endsWith('风险') ? level : `${level || '--'}风险`;
return `<span class="tag status-tag--${key}">${escapeHtml(label)}</span>`;
}
function statusTag(status) {
const failed = status === '发送失败';
return `<span class="tag ${failed ? 'status-tag--failed' : 'tag--neutral'}">${escapeHtml(status || '--')}</span>`;
}
function tableMarkup(items, columns, { action } = {}) {
const headers = columns.map(([, label]) => `<th>${escapeHtml(label)}</th>`).join('');
const rows = items.map((item) => {
const cells = columns.map(([key]) => {
const content = key === 'risk_level' ? riskTag(item[key]) : key === 'send_status' || key === 'status' ? statusTag(item[key]) : escapeHtml(displayValue(item[key], key));
return `<td class="ellipsis-cell" title="${escapeHtml(displayValue(item[key], key))}">${content}</td>`;
}).join('');
const actionCell = action ? `<td><button class="button table-action" type="button" data-row-action="${escapeHtml(action.key(item))}">${escapeHtml(action.label)}</button></td>` : '';
return `<tr>${cells}${actionCell}</tr>`;
}).join('');
return `<div class="data-table-wrap"><table class="data-table"><thead><tr>${headers}${action ? '<th>操作</th>' : ''}</tr></thead><tbody>${rows}</tbody></table></div>`;
}
if (requireRiskStaff()) {
mountShell({ active: 'risk-dashboard', mode: 'risk' });
const context = getAuthContext();
const state = {
alert: { cursors: [null], page: 0, next: null, filters: {} },
evidence: { cursors: [null], page: 0, next: null, source: 'customers', filters: {} },
notification: { cursors: [null], page: 0, next: null, filters: {} },
currentAlert: null,
action: null,
controllers: new Set(),
};
const metrics = document.querySelector('[data-risk-metrics]');
const highPriority = document.querySelector('[data-high-priority]');
const alertTable = document.querySelector('[data-alert-table]');
const evidenceTable = document.querySelector('[data-evidence-table]');
const notificationTable = document.querySelector('[data-notification-table]');
const alertDialog = document.querySelector('[data-alert-dialog]');
const actionDialog = document.querySelector('[data-action-dialog]');
const reportDialog = document.querySelector('[data-report-dialog]');
function controller() {
const item = new AbortController();
state.controllers.add(item);
item.signal.addEventListener('abort', () => state.controllers.delete(item), { once: true });
return item;
}
function hasPermission(code) {
return getPermissions().includes(code);
}
async function hydrateIdentity() {
const response = await apiClient.get('A038', { pathParams: { userId: context.userId } });
const identity = response.data;
updateAuthPermissions(identity.permissions, identity.data_scope);
document.querySelector('[data-staff-name]').textContent = `${identity.username} · ${identity.user_no}`;
document.querySelector('[data-data-scope]').textContent = `数据范围:${identity.data_scope || '--'} · 权限 ${identity.permissions.length} 项`;
applyPermissionGuard();
}
function renderOverview(data) {
const levels = data.levels || {};
metrics.innerHTML = [
['未闭环预警', data.total ?? 0, '待处理与调查中的全部预警', ''],
['高风险', levels['高风险'] ?? 0, `中风险 ${levels['中风险'] ?? 0} · 低风险 ${levels['低风险'] ?? 0}`, 'negative'],
['待处理', data.pending ?? 0, '尚未进入调查', ''],
['已超时', data.overdue ?? 0, '超过处置截止时间', data.overdue ? 'negative' : ''],
].map(([label, value, meta, tone]) => `<article class="metric-card"><p class="metric-card__label">${label}</p><p class="metric-card__value${tone ? ` metric-card__value--${tone}` : ''}">${escapeHtml(value)}</p><p class="metric-card__meta">${escapeHtml(meta)}</p></article>`).join('');
const important = Array.isArray(data.high_priority) ? data.high_priority : [];
if (!important.length) {
renderEmpty(highPriority, '暂无重点预警', '当前没有高风险未闭环事项。');
return;
}
highPriority.innerHTML = `<ul class="operations-list">${important.map((item) => `<li class="operations-list__item"><strong>${escapeHtml(item.alert_no)}</strong><span>${escapeHtml(item.evidence_summary || item.alert_type || '待人工研判')}</span><button class="button table-action" type="button" data-priority-alert="${escapeHtml(item.alert_no)}">查看详情</button></li>`).join('')}</ul>`;
highPriority.querySelectorAll('[data-priority-alert]').forEach((button) => button.addEventListener('click', () => openAlert(button.dataset.priorityAlert)));
}
async function loadOverview() {
renderLoading(metrics, 4);
renderLoading(highPriority, 2);
try {
const response = await apiClient.get('RK001');
renderOverview(response.data || {});
} catch (error) {
apiClient.reportError(error);
renderError(metrics, error, loadOverview);
renderError(highPriority, error, loadOverview);
}
}
function updatePager(kind, meta) {
const current = state[kind];
current.next = meta.next_cursor || null;
document.querySelector(`[data-${kind}-prev]`).disabled = current.page === 0;
document.querySelector(`[data-${kind}-next]`).disabled = !current.next;
}
async function loadAlerts() {
renderLoading(alertTable, 4);
try {
const response = await apiClient.get('RK002', { query: { ...state.alert.filters, cursor: state.alert.cursors[state.alert.page], limit: 5 } });
const items = Array.isArray(response.data) ? response.data : [];
if (!items.length) renderEmpty(alertTable, '暂无预警', '当前筛选条件下没有风险预警。');
else {
alertTable.innerHTML = tableMarkup(items, [
['alert_no', '预警编号'], ['risk_level', '风险等级'], ['customer_no', '客户'], ['product_name', '产品'], ['alert_type', '风险类型'], ['status', '状态'], ['created_at', '生成时间'],
], { action: { label: '详情', key: (item) => item.alert_no } });
alertTable.querySelectorAll('[data-row-action]').forEach((button) => button.addEventListener('click', () => openAlert(button.dataset.rowAction)));
}
updatePager('alert', response.meta || {});
} catch (error) {
apiClient.reportError(error);
renderError(alertTable, error, loadAlerts);
}
}
async function loadEvidence() {
renderLoading(evidenceTable, 4);
try {
const response = await apiClient.get('RK004', { pathParams: { source: state.evidence.source }, query: { ...state.evidence.filters, cursor: state.evidence.cursors[state.evidence.page], limit: 10 } });
const items = Array.isArray(response.data) ? response.data : [];
if (!items.length) renderEmpty(evidenceTable, '暂无证据记录', '当前类型和筛选条件下没有数据。');
else evidenceTable.innerHTML = `<div class="evidence-table">${tableMarkup(items, EVIDENCE_COLUMNS[state.evidence.source])}</div>`;
updatePager('evidence', response.meta || {});
} catch (error) {
apiClient.reportError(error);
renderError(evidenceTable, error, loadEvidence);
}
}
async function loadNotifications() {
renderLoading(notificationTable, 4);
try {
const response = await apiClient.get('RK005', { query: { ...state.notification.filters, cursor: state.notification.cursors[state.notification.page], limit: 10 } });
const items = Array.isArray(response.data) ? response.data : [];
if (!items.length) renderEmpty(notificationTable, '暂无通知记录', '扫描产生的站内与邮件通知将在这里显示。');
else {
const enriched = items.map((item) => ({ ...item, fail_reason: item.fail_reason || '--' }));
notificationTable.innerHTML = tableMarkup(enriched, [['notification_no', '通知编号'], ['alert_no', '预警编号'], ['channel', '渠道'], ['send_status', '发送状态'], ['receiver_email', '收件邮箱'], ['fail_reason', '失败原因'], ['send_time', '发送时间']]);
}
updatePager('notification', response.meta || {});
} catch (error) {
apiClient.reportError(error);
renderError(notificationTable, error, loadNotifications);
}
}
function detailSection(title, value) {
if (value === null || value === undefined || (Array.isArray(value) && !value.length)) return '';
return `<section><h3 class="section-heading__title">${escapeHtml(title)}</h3><pre class="json-view">${escapeHtml(JSON.stringify(value, null, 2))}</pre></section>`;
}
function renderAlertActions(detail) {
const alert = detail.alert || detail;
const open = ['待处理', '调查中'].includes(alert.status);
const acknowledged = alert.ack_status === '已确认';
const buttons = [];
if (hasPermission(PERM_CODES.RISK_ALERT_WRITE)) {
if (alert.status === '待处理' && !acknowledged) buttons.push(['acknowledge', '确认接收']);
if (alert.status === '待处理' && acknowledged) buttons.push(['investigate', '进入调查']);
if (open && acknowledged) buttons.push(['exclude', '关闭误报']);
if (alert.status === '调查中' && acknowledged) buttons.push(['resolve', '完成结案']);
if (open && acknowledged && !alert.is_escalated) buttons.push(['escalate', '升级处理']);
}
document.querySelector('[data-alert-actions]').innerHTML = `${buttons.map(([key, label]) => `<button class="button${key === 'resolve' ? ' button--primary' : ''}${key === 'exclude' ? ' button--danger' : ''}" type="button" data-alert-action="${key}">${label}</button>`).join('')}<button class="button" type="button" data-close-alert>关闭</button>`;
document.querySelectorAll('[data-alert-action]').forEach((button) => button.addEventListener('click', () => openAction(button.dataset.alertAction)));
document.querySelectorAll('[data-close-alert]').forEach((button) => button.addEventListener('click', () => alertDialog.close()));
}
async function openAlert(alertNo) {
if (!alertDialog.open) alertDialog.showModal();
const target = document.querySelector('[data-alert-detail]');
renderLoading(target, 3);
try {
const response = await apiClient.get('RK003', { pathParams: { alertNo } });
state.currentAlert = response.data;
const detail = response.data || {};
const alert = detail.alert || detail;
target.innerHTML = `<dl class="detail-grid"><div><dt>预警编号</dt><dd>${escapeHtml(alert.alert_no)}</dd></div><div><dt>风险等级</dt><dd>${riskTag(alert.risk_level)}</dd></div><div><dt>客户</dt><dd>${escapeHtml(alert.customer_no || '--')} · ${escapeHtml(alert.customer_name || '--')}</dd></div><div><dt>产品</dt><dd>${escapeHtml(alert.product_code || '--')} · ${escapeHtml(alert.product_name || '--')}</dd></div><div><dt>处置状态</dt><dd>${statusTag(alert.status)}</dd></div><div><dt>确认状态</dt><dd>${escapeHtml(alert.ack_status || '--')}</dd></div><div><dt>命中规则</dt><dd>${escapeHtml(displayValue(alert.rule_codes))}</dd></div><div><dt>升级标记</dt><dd>${alert.is_escalated ? '已升级' : '未升级'}</dd></div></dl><section><h3 class="section-heading__title">证据摘要</h3><p>${escapeHtml(alert.evidence_summary || '暂无摘要')}</p></section>${detail.evidence_truncated?.length ? `<p class="form-alert form-alert--visible">部分证据达到返回上限:${escapeHtml(detail.evidence_truncated.join('、'))}</p>` : ''}${detailSection('客户画像', detail.customer)}${detailSection('关联交易', detail.transaction)}${detailSection('关联产品', detail.product)}${detailSection('资金流水', detail.capital_flows)}${detailSection('持仓证据', detail.holdings)}${detailSection('登录证据', detail.login_records)}${detailSection('证据快照', detail.evidence_snapshot)}${hasPermission(PERM_CODES.RISK_ALERT_WRITE) ? `<form class="risk-upload" data-evidence-upload><label class="form-field"><span class="form-field__label">补充证据</span><input class="form-field__input" type="file" name="evidence_file" accept="image/*,.pdf,.doc,.docx" required></label><button class="button" type="submit">上传归档</button></form>` : ''}`;
target.querySelector('[data-evidence-upload]')?.addEventListener('submit', uploadEvidence);
renderAlertActions(detail);
} catch (error) {
apiClient.reportError(error);
renderError(target, error, () => openAlert(alertNo));
}
}
function openAction(key) {
const config = ACTIONS[key];
if (!config || !state.currentAlert) return;
state.action = key;
document.querySelector('[data-action-title]').textContent = config.title;
document.querySelector('[data-action-copy]').textContent = config.copy;
const field = document.querySelector('[data-action-field]');
field.hidden = !config.field;
field.querySelector('[data-action-label]').textContent = config.label || '处理说明';
field.querySelector('textarea').required = Boolean(config.field);
field.querySelector('textarea').value = '';
document.querySelector('[data-action-alert]').classList.remove('form-alert--visible');
actionDialog.showModal();
}
async function submitAction(event) {
event.preventDefault();
const config = ACTIONS[state.action];
const alertNo = state.currentAlert?.alert?.alert_no || state.currentAlert?.alert_no;
if (!config || !alertNo) return;
const form = event.currentTarget;
const value = String(new FormData(form).get('reason') || '').trim();
const body = config.field ? { [config.field]: value } : {};
const alert = form.querySelector('[data-action-alert]');
const submit = form.querySelector('[type="submit"]');
if (config.field && !value) {
alert.textContent = `请填写${config.label}。`;
alert.classList.add('form-alert--visible');
return;
}
submit.disabled = true;
try {
await apiClient.post(config.endpoint, body, { pathParams: { alertNo } });
showToast(config.success);
actionDialog.close();
await Promise.all([loadOverview(), loadAlerts(), openAlert(alertNo)]);
} catch (error) {
apiClient.reportError(error);
alert.textContent = error.status === 409 ? '预警可能已处理,请核对当前状态。' : error.message;
alert.classList.add('form-alert--visible');
} finally { submit.disabled = false; }
}
async function uploadEvidence(event) {
event.preventDefault();
const form = event.currentTarget;
const alertNo = state.currentAlert?.alert?.alert_no || state.currentAlert?.alert_no;
const submit = form.querySelector('[type="submit"]');
submit.disabled = true;
try {
await apiClient.upload('RK012', new FormData(form), { pathParams: { alertNo } });
showToast('证据上传成功');
await openAlert(alertNo);
} catch (error) {
apiClient.reportError(error);
showToast(error.message || '证据上传失败', 'error');
} finally { submit.disabled = false; }
}
async function scan() {
const button = document.querySelector('[data-scan]');
button.disabled = true;
button.textContent = '扫描中';
try {
const response = await apiClient.post('RK006', {});
showToast(`预警扫描完成,新建 ${response.data?.created_count ?? 0} 条`);
await Promise.all([loadOverview(), loadAlerts(), loadNotifications()]);
} catch (error) {
apiClient.reportError(error);
showToast(error.message || '预警扫描失败', 'error');
} finally {
button.textContent = '手动扫描';
applyPermissionGuard();
}
}
async function generateReport() {
const form = document.querySelector('[data-report-form]');
const content = form.elements.content;
const status = document.querySelector('[data-report-status]');
const button = document.querySelector('[data-generate-report]');
const reportDate = form.elements.report_date.value || null;
const item = controller();
button.disabled = true;
content.value = '';
status.textContent = '正在生成日报';
try {
await apiClient.stream('RK014', { report_date: reportDate }, {
signal: item.signal,
onEvent: ({ type, data }) => {
if (type === 'progress') status.textContent = data.message || '正在生成日报';
if (type === 'replace') content.value = data.content || '';
if (type === 'done') {
content.value = data.report?.content || content.value;
status.textContent = data.report?.data_truncated ? '日报生成完成,数据已达到查询上限' : '日报生成完成';
}
},
});
showToast('日报生成完成');
} catch (error) {
apiClient.reportError(error);
status.textContent = error.message || '日报生成失败';
showToast(status.textContent, 'error');
} finally { button.disabled = false; state.controllers.delete(item); }
}
async function sendReport(event) {
event.preventDefault();
const form = event.currentTarget;
const alert = form.querySelector('[data-report-alert]');
const submit = form.querySelector('[type="submit"]');
const recipients = form.elements.recipients.value.split(/[,,;;]/).map((item) => item.trim()).filter(Boolean);
alert.classList.remove('form-alert--visible');
submit.disabled = true;
try {
await apiClient.post('RK015', { recipients, subject: form.elements.subject.value.trim(), content: form.elements.content.value.trim() });
showToast('日报邮件发送成功');
} catch (error) {
apiClient.reportError(error);
alert.textContent = error.message || '日报邮件发送失败';
alert.classList.add('form-alert--visible');
} finally { submit.disabled = false; }
}
function appendMessage(kind, text) {
const log = document.querySelector('[data-chat-log]');
const message = document.createElement('div');
message.className = `risk-message risk-message--${kind}`;
message.textContent = text;
log.append(message);
log.scrollTop = log.scrollHeight;
return message;
}
async function sendAgentMessage(message) {
const form = document.querySelector('[data-agent-form]');
const submit = form.querySelector('[type="submit"]');
appendMessage('user', message);
const reply = appendMessage('pending', '正在检索风险数据并形成研判草案');
submit.disabled = true;
const item = controller();
const timer = window.setTimeout(() => item.abort(), 45000);
try {
const accepted = await apiClient.post('R001', { agent_type: 'risk', message, session_id: crypto.randomUUID(), idempotency_key: crypto.randomUUID().replaceAll('-', '') });
const runId = accepted.data.run_id;
let content = '';
await apiClient.stream('R003', null, {
pathParams: { runId },
signal: item.signal,
onEvent: ({ type, data }) => {
if (type === 'delta') content += data.content || '';
if (type === 'replace') content = data.content || '';
if (type === 'tools') reply.dataset.tools = JSON.stringify(data.tool_calls || {});
if (type === 'error') throw new Error(data.error_code || '风险助手运行失败');
if (content) {
reply.className = 'risk-message risk-message--assistant';
reply.textContent = content;
if (reply.dataset.tools) {
const tools = document.createElement('div');
tools.className = 'risk-message__tools';
tools.textContent = '已调用受控只读工具';
reply.append(tools);
}
}
},
});
} catch (error) {
reply.className = 'risk-message risk-message--error';
reply.textContent = error.name === 'AbortError' ? '请求仍在处理中,请确认 Agent Worker 已启动后重试。' : (error.message || '风险助手运行失败');
} finally {
window.clearTimeout(timer);
state.controllers.delete(item);
submit.disabled = false;
}
}
function resetPager(kind) {
state[kind].cursors = [null];
state[kind].page = 0;
state[kind].next = null;
}
function bindPager(kind, loader) {
document.querySelector(`[data-${kind}-prev]`).addEventListener('click', () => { if (state[kind].page > 0) { state[kind].page -= 1; loader(); } });
document.querySelector(`[data-${kind}-next]`).addEventListener('click', () => { if (state[kind].next) { state[kind].cursors[state[kind].page + 1] = state[kind].next; state[kind].page += 1; loader(); } });
}
document.querySelectorAll('[data-tab]').forEach((tab) => tab.addEventListener('click', () => {
document.querySelectorAll('[data-tab]').forEach((item) => item.setAttribute('aria-selected', String(item === tab)));
document.querySelectorAll('[data-view]').forEach((view) => { view.hidden = view.dataset.view !== tab.dataset.tab; });
if (tab.dataset.tab === 'evidence' && !evidenceTable.dataset.loaded) { evidenceTable.dataset.loaded = 'true'; loadEvidence(); }
if (tab.dataset.tab === 'notifications' && !notificationTable.dataset.loaded) { notificationTable.dataset.loaded = 'true'; loadNotifications(); }
}));
document.querySelector('[data-alert-filter]').addEventListener('submit', (event) => { event.preventDefault(); state.alert.filters = Object.fromEntries(new FormData(event.currentTarget)); resetPager('alert'); loadAlerts(); });
document.querySelector('[data-evidence-filter]').addEventListener('submit', (event) => { event.preventDefault(); const values = Object.fromEntries(new FormData(event.currentTarget)); state.evidence.source = values.source; delete values.source; state.evidence.filters = values; resetPager('evidence'); loadEvidence(); });
document.querySelector('[data-notification-filter]').addEventListener('submit', (event) => { event.preventDefault(); state.notification.filters = Object.fromEntries(new FormData(event.currentTarget)); resetPager('notification'); loadNotifications(); });
bindPager('alert', loadAlerts);
bindPager('evidence', loadEvidence);
bindPager('notification', loadNotifications);
document.querySelector('[data-scan]').addEventListener('click', scan);
document.querySelectorAll('[data-close-alert]').forEach((button) => button.addEventListener('click', () => alertDialog.close()));
document.querySelectorAll('[data-close-action]').forEach((button) => button.addEventListener('click', () => actionDialog.close()));
document.querySelectorAll('[data-close-report]').forEach((button) => button.addEventListener('click', () => reportDialog.close()));
document.querySelector('[data-action-form]').addEventListener('submit', submitAction);
document.querySelector('[data-open-report]').addEventListener('click', () => { reportDialog.showModal(); applyPermissionGuard(reportDialog); });
document.querySelector('[data-generate-report]').addEventListener('click', generateReport);
document.querySelector('[data-report-form]').addEventListener('submit', sendReport);
document.querySelector('[data-agent-form]').addEventListener('submit', (event) => { event.preventDefault(); const input = event.currentTarget.elements.message; const message = input.value.trim(); if (message) { input.value = ''; sendAgentMessage(message); } });
document.querySelectorAll('[data-prompt]').forEach((button) => button.addEventListener('click', () => sendAgentMessage(button.dataset.prompt)));
window.addEventListener('pagehide', () => state.controllers.forEach((item) => item.abort()));
apiClient.track('page_view', { page: 'risk-dashboard', portal: 'employee-risk' });
hydrateIdentity().catch((error) => { apiClient.reportError(error); showToast(error.message || '权限加载失败', 'error'); });
loadOverview();
loadAlerts();
}