Files
TalkingQ_Dashboard/scripts/privacy.js
“yuanmengy” a225272d1e data&device ready
2026-06-05 17:11:56 +08:00

314 lines
14 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.

/**
* TalkingQ Platform - Privacy (PII) Page
*/
registerPage('privacy', function(container) {
container.innerHTML = `
<div class="page-header">
<div>
<div class="page-title">隐私保护 (PII)</div>
<div class="page-desc">个人信息脱敏、COPPA/GDPR 合规配置与数据保留策略</div>
</div>
</div>
<div class="tabs" id="privacyTabs">
<div class="tab-item active" data-tab="pii">PII 脱敏规则</div>
<div class="tab-item" data-tab="gdpr">GDPR/COPPA</div>
<div class="tab-item" data-tab="retention">数据保留</div>
</div>
<!-- PII Tab -->
<div class="tab-content active" id="tab-pii">
<div class="card" style="margin-bottom:16px">
<div class="card-header">
<div>
<div class="card-title">PII 自动脱敏配置</div>
<div class="card-subtitle">自动识别并脱敏对话中的个人敏感信息</div>
</div>
<div style="display:flex;align-items:center;gap:8px">
<span style="font-size:13px;color:var(--text-muted)">全局启用</span>
<label class="toggle-switch">
<input type="checkbox" checked>
<span class="toggle-track"></span>
</label>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:12px">
${[
{ name: '姓名', example: '张三 → ***', enabled: true },
{ name: '手机号', example: '138****8888', enabled: true },
{ name: 'IP 地址', example: '192.168.x.x', enabled: true },
{ name: '身份证号', example: '330102****1234', enabled: true },
{ name: '家庭住址', example: '北京市朝阳区*** → [地址已脱敏]', enabled: true },
{ name: '银行卡号', example: '6222****1234', enabled: false },
].map(r => `
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border:1px solid var(--border);border-radius:var(--radius-sm)">
<div>
<div style="font-weight:500;font-size:13px">${r.name}</div>
<div style="font-size:12px;color:var(--text-muted);font-family:monospace">${r.example}</div>
</div>
<label class="toggle-switch">
<input type="checkbox" ${r.enabled ? 'checked' : ''}>
<span class="toggle-track"></span>
</label>
</div>
`).join('')}
</div>
</div>
<div class="api-note">
<i data-lucide="info"></i>
<div>PII 规则接口:<code>GET /api/v1/privacy/pii-rules</code> · <code>PUT /api/v1/privacy/pii-rules/{id}</code>。
脱敏事件通过 <code>pii_detected</code> / <code>pii_masked</code> 事件上报。</div>
</div>
</div>
<!-- GDPR Tab -->
<div class="tab-content" id="tab-gdpr">
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:16px">
<div class="card">
<div class="card-title" style="margin-bottom:12px">COPPA 合规(儿童隐私保护)</div>
${[
{ label: '用户年龄验证', enabled: true },
{ label: '家长知情同意收集', enabled: true },
{ label: '禁止收集13岁以下精确位置', enabled: true },
{ label: '行为追踪限制', enabled: true },
{ label: '数据最小化原则', enabled: true },
].map(i => `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<span style="font-size:13px">${i.label}</span>
<label class="toggle-switch"><input type="checkbox" ${i.enabled ? 'checked' : ''}><span class="toggle-track"></span></label>
</div>
`).join('')}
<div style="margin-top:12px;padding:10px;background:var(--primary-bg);border-radius:var(--radius-sm);font-size:12px;color:var(--primary)">
✅ COPPA 合规状态:已合规
</div>
</div>
<div class="card">
<div class="card-title" style="margin-bottom:12px">GDPR 数据权利</div>
${[
{ label: '知情权(提供隐私政策)', enabled: true },
{ label: '访问权(数据导出)', enabled: true },
{ label: '更正权', enabled: true },
{ label: '删除权(被遗忘权)', enabled: true },
{ label: '数据可携带权', enabled: false },
].map(i => `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<span style="font-size:13px">${i.label}</span>
<label class="toggle-switch"><input type="checkbox" ${i.enabled ? 'checked' : ''}><span class="toggle-track"></span></label>
</div>
`).join('')}
<div style="margin-top:12px;padding:10px;background:#fffbe6;border-radius:var(--radius-sm);font-size:12px;color:var(--warning)">
⚠️ 数据可携带权尚未完整实现
</div>
</div>
</div>
</div>
<!-- Retention Tab -->
<div class="tab-content" id="tab-retention">
<div class="card">
<div class="card-title" style="margin-bottom:16px">数据保留策略</div>
${[
{ type: '对话记录 (Sessions)', period: '180', unit: '天' },
{ type: '设备日志 (Device Logs)', period: '90', unit: '天' },
{ type: 'Bad Case 数据', period: '365', unit: '天' },
{ type: 'OTA 升级记录', period: '365', unit: '天' },
{ type: '审计日志 (Audit Logs)', period: '730', unit: '天' },
{ type: 'PII 脱敏日志', period: '30', unit: '天' },
].map(p => `
<div style="display:flex;align-items:center;gap:16px;padding:10px 0;border-bottom:1px solid var(--border)">
<span style="flex:1;font-size:13px">${p.type}</span>
<input type="number" class="form-control" value="${p.period}" style="width:80px">
<span style="font-size:13px;color:var(--text-muted)">${p.unit}</span>
</div>
`).join('')}
<button class="btn btn-primary" style="margin-top:16px" onclick="Toast.success('数据保留策略已保存')">
<i data-lucide="save"></i> 保存策略
</button>
</div>
</div>
`;
lucide.createIcons({ elements: [container] });
container.querySelectorAll('#privacyTabs .tab-item').forEach(tab => {
tab.addEventListener('click', () => {
container.querySelectorAll('#privacyTabs .tab-item').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
container.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
container.querySelector(`#tab-${tab.dataset.tab}`)?.classList.add('active');
});
});
});
/**
* TalkingQ Platform - Settings Page
*/
registerPage('settings', function(container) {
container.innerHTML = `
<div class="page-header">
<div class="page-title">系统设置</div>
<div class="page-desc">租户信息、API Key 管理、通知配置与集成</div>
</div>
<div class="tabs" id="settingsTabs">
<div class="tab-item active" data-tab="tenant">租户信息</div>
<div class="tab-item" data-tab="apikeys">API Keys</div>
<div class="tab-item" data-tab="notify">通知配置</div>
<div class="tab-item" data-tab="integrations">集成</div>
</div>
<div class="tab-content active" id="stab-tenant">
<div class="card" style="max-width:600px">
<div class="card-title" style="margin-bottom:20px">租户基本信息</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">租户名称</label>
<input type="text" class="form-control" value="TalkingQ 默认租户">
</div>
<div class="form-group">
<label class="form-label">租户 ID</label>
<input type="text" class="form-control" value="tenant-20240501" readonly style="background:#fafafa;color:var(--text-muted)">
</div>
</div>
<div class="form-group">
<label class="form-label">联系邮箱</label>
<input type="email" class="form-control" value="admin@talkingq.com">
</div>
<div class="form-group">
<label class="form-label">时区</label>
<select class="form-control">
<option selected>Asia/Shanghai (UTC+8)</option>
<option>UTC</option>
</select>
</div>
<button class="btn btn-primary" onclick="Toast.success('租户信息已保存')">
<i data-lucide="save"></i> 保存
</button>
</div>
</div>
<div class="tab-content" id="stab-apikeys">
<div class="card">
<div style="display:flex;justify-content:space-between;margin-bottom:16px">
<div class="card-title">API Keys</div>
<button class="btn btn-primary btn-sm" id="createKeyBtn">
<i data-lucide="plus"></i> 创建 API Key
</button>
</div>
<div class="table-wrapper">
<table>
<thead><tr><th>名称</th><th>Key部分</th><th>权限</th><th>创建时间</th><th>最后使用</th><th>操作</th></tr></thead>
<tbody>
${[
{ name: 'Backend Service Key', key: 'tq_live_****a3f2', scope: '全部权限', created: '2026-01-01', used: '2分钟前' },
{ name: 'Analytics Read-only', key: 'tq_ro_****c8e1', scope: '只读', created: '2026-03-15', used: '今天' },
].map(k => `
<tr>
<td style="font-weight:500">${k.name}</td>
<td style="font-family:monospace;font-size:12px">${k.key}</td>
<td><span class="badge badge-info">${k.scope}</span></td>
<td style="font-size:12px;color:var(--text-muted)">${k.created}</td>
<td style="font-size:12px;color:var(--text-muted)">${k.used}</td>
<td>
<button class="btn btn-ghost btn-sm" data-tooltip="复制 Key" onclick="Toast.info('Key 已复制到剪贴板')"><i data-lucide="copy"></i></button>
<button class="btn btn-ghost btn-sm" style="color:var(--danger)" data-tooltip="撤销 Key" onclick="Toast.warning('Key 已撤销')"><i data-lucide="trash-2"></i></button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<div class="api-note" style="margin-top:12px">
<i data-lucide="info"></i>
<span>API Key 接口:<code>GET /api/v1/settings/api-keys</code> · <code>POST /api/v1/settings/api-keys</code></span>
</div>
</div>
</div>
<div class="tab-content" id="stab-notify">
<div class="card" style="max-width:600px">
<div class="card-title" style="margin-bottom:16px">通知配置</div>
${[
{ label: '设备离线告警', enabled: true },
{ label: 'OTA 升级失败告警', enabled: true },
{ label: 'Bad Case 严重问题告警', enabled: true },
{ label: 'API 超时告警', enabled: false },
{ label: '每日数据报告', enabled: true },
].map(n => `
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid var(--border)">
<span style="font-size:13px">${n.label}</span>
<label class="toggle-switch"><input type="checkbox" ${n.enabled ? 'checked' : ''}><span class="toggle-track"></span></label>
</div>
`).join('')}
<div class="form-group" style="margin-top:16px">
<label class="form-label">Webhook URL</label>
<input type="text" class="form-control" placeholder="https://your-webhook.com/alerts">
</div>
<button class="btn btn-outline btn-sm" onclick="Toast.info('Webhook 测试请求已发送')">
<i data-lucide="send"></i> 测试 Webhook
</button>
</div>
</div>
<div class="tab-content" id="stab-integrations">
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px">
${[
{ name: '腾讯云 ASR', icon: '🔵', status: 'connected' },
{ name: '腾讯云 TTS', icon: '🔵', status: 'connected' },
{ name: 'OpenAI', icon: '⚫', status: 'disconnected' },
{ name: '阿里云 OSS', icon: '🟠', status: 'connected' },
{ name: 'Kafka', icon: '🟡', status: 'connected' },
{ name: 'ClickHouse', icon: '🔴', status: 'disconnected' },
].map(i => `
<div class="card" style="display:flex;align-items:center;gap:12px">
<span style="font-size:24px">${i.icon}</span>
<div style="flex:1">
<div style="font-weight:600;font-size:13.5px">${i.name}</div>
<span class="status-dot ${i.status === 'connected' ? 'online' : 'offline'}" style="font-size:12px">${i.status === 'connected' ? '已连接' : '未连接'}</span>
</div>
<button class="btn btn-outline btn-sm" onclick="Toast.info('配置 ${i.name} 集成')">配置</button>
</div>
`).join('')}
</div>
</div>
`;
lucide.createIcons({ elements: [container] });
container.querySelectorAll('#settingsTabs .tab-item').forEach(tab => {
tab.addEventListener('click', () => {
container.querySelectorAll('#settingsTabs .tab-item').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
container.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
container.querySelector(`#stab-${tab.dataset.tab}`)?.classList.add('active');
});
});
container.querySelector('#createKeyBtn')?.addEventListener('click', () => {
createModal({
title: '创建 API Key',
content: `
<div class="form-group">
<label class="form-label">Key 名称<span class="required">*</span></label>
<input type="text" class="form-control" placeholder="例Backend Service Key">
</div>
<div class="form-group">
<label class="form-label">权限范围</label>
<select class="form-control">
<option>全部权限</option>
<option>只读</option>
<option>设备管理</option>
<option>OTA 管理</option>
</select>
</div>`,
onConfirm: async () => {
await API.settings.createApiKey({ name: '新 API Key' });
Toast.success('API Key 创建成功,请妥善保存');
},
confirmText: '创建',
});
});
});