|
|
|
@@ -0,0 +1,86 @@
|
|
|
|
|
<!doctype html>
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
<meta name="color-scheme" content="light">
|
|
|
|
|
<title>Chorus - MVP-2 API Key 原型 v1</title>
|
|
|
|
|
<script defer src="./icons.min.js"></script>
|
|
|
|
|
<style>
|
|
|
|
|
:root{--bg:#f3f6f6;--surface:#fff;--subtle:#f8faf9;--strong:#edf3f2;--text:#172126;--muted:#526169;--soft:#6a777e;--border:#d6dfe1;--border-strong:#b8c6c9;--primary:#006b66;--primary-hover:#005853;--primary-soft:#e4f2f0;--info:#245f87;--info-soft:#e8f1f7;--warning:#81501f;--warning-soft:#fff0df;--danger:#a63630;--danger-soft:#fcebea;--success:#2d6b42;--success-soft:#e7f3eb;--focus:#147bba;--radius:6px;font-family:Inter,"Segoe UI","Microsoft YaHei",Arial,sans-serif;color:var(--text);background:var(--bg)}
|
|
|
|
|
*{box-sizing:border-box;letter-spacing:0}html{min-width:320px}body{min-width:320px;min-height:100dvh;margin:0;background:var(--bg);font-size:15px;line-height:1.5;overflow-x:hidden}button,input,select{font:inherit;letter-spacing:0}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.55}button,a{touch-action:manipulation}svg{width:19px;height:19px;stroke-width:2;pointer-events:none;flex:0 0 19px}:focus-visible{outline:3px solid var(--focus);outline-offset:2px}.hidden{display:none!important}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
|
|
|
.topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:20px;min-height:64px;padding:10px 22px;background:#fffffff5;border-bottom:1px solid var(--border)}.brand{display:flex;align-items:center;gap:10px;min-width:210px;color:var(--text);font-size:20px;font-weight:750;text-decoration:none}.brand-mark{display:grid;place-items:center;width:34px;height:34px;color:#fff;background:var(--primary);border-radius:6px;font-size:17px;font-weight:800}.app-nav{display:flex;align-items:center;gap:4px;min-width:0;overflow-x:auto}.nav-item{display:inline-flex;align-items:center;gap:8px;min-height:44px;padding:0 13px;color:var(--muted);font-weight:650;text-decoration:none;border-bottom:3px solid transparent;white-space:nowrap}.nav-item.active{color:var(--primary);border-bottom-color:var(--primary)}.account{margin-left:auto;color:var(--muted);white-space:nowrap}.prototype-switch{display:flex;gap:4px;padding:4px;background:var(--strong);border-radius:6px}.prototype-switch button{min-height:36px;padding:6px 11px;color:var(--muted);background:transparent;border:0;border-radius:4px}.prototype-switch button[aria-pressed=true]{color:var(--text);background:var(--surface);box-shadow:0 1px 3px #17212618}
|
|
|
|
|
.button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;min-width:44px;padding:9px 15px;font-weight:650;border:1px solid transparent;border-radius:var(--radius)}.button-primary{color:#fff;background:var(--primary);border-color:var(--primary)}.button-primary:hover{background:var(--primary-hover)}.button-secondary{color:var(--text);background:var(--surface);border-color:var(--border-strong)}.button-secondary:hover{background:var(--strong)}.button-danger{color:var(--danger);background:var(--surface);border-color:#dba6a3}.button-danger:hover{background:var(--danger-soft)}.icon-button{display:inline-grid;place-items:center;width:44px;height:44px;padding:0;color:var(--muted);background:transparent;border:1px solid transparent;border-radius:var(--radius)}.icon-button:hover{background:var(--strong);border-color:var(--border)}
|
|
|
|
|
main{width:min(1240px,100%);margin:0 auto;padding:28px clamp(16px,3vw,40px) 56px}.page-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;margin-bottom:20px}.page-heading h1{margin:0;font-size:30px;line-height:1.25}.page-heading p{max-width:720px;margin:7px 0 0;color:var(--muted)}.heading-actions{display:flex;align-items:center;gap:8px}.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius)}.panel-header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:18px 20px;border-bottom:1px solid var(--border)}.panel-header h2{margin:0;font-size:18px}.panel-header p{margin:3px 0 0;color:var(--muted);font-size:14px}.toolbar{display:flex;align-items:flex-end;gap:10px;padding:14px 16px;border-bottom:1px solid var(--border);background:var(--subtle)}.field{display:grid;gap:6px}.field label{font-size:13px;font-weight:650}.input,.select{min-height:44px;padding:9px 11px;color:var(--text);background:var(--surface);border:1px solid var(--border-strong);border-radius:var(--radius)}.input{width:260px}.select{min-width:140px}.toolbar .spacer{flex:1}.result-count{padding-bottom:11px;color:var(--muted);white-space:nowrap}
|
|
|
|
|
.state-bar{display:flex;align-items:center;gap:6px;margin-bottom:14px;padding:8px 10px;background:var(--warning-soft);border:1px solid #ebc99f;border-radius:var(--radius);overflow-x:auto}.state-bar strong{margin-right:4px;white-space:nowrap}.state-button{min-height:34px;padding:5px 9px;color:var(--muted);white-space:nowrap;background:var(--surface);border:1px solid var(--border);border-radius:4px}.state-button.active{color:var(--primary);background:var(--primary-soft);border-color:#91c1bd}.table-wrap{max-width:100%;overflow:auto}.data-table{width:100%;min-width:900px;border-collapse:collapse}.data-table th,.data-table td{padding:13px 15px;text-align:left;border-bottom:1px solid var(--border);vertical-align:middle}.data-table th{color:var(--muted);font-size:13px;font-weight:700;background:var(--subtle);white-space:nowrap}.data-table tbody tr:hover{background:#fbfdfc}.mono{font-family:"Cascadia Code",Consolas,monospace}.key-name{font-weight:700}.secondary{display:block;margin-top:2px;color:var(--soft);font-size:12px}.badge{display:inline-flex;align-items:center;min-height:26px;padding:2px 8px;font-size:12px;font-weight:700;border-radius:999px}.badge.active{color:var(--success);background:var(--success-soft)}.badge.revoked{color:var(--soft);background:var(--strong)}.badge.expiring{color:var(--warning);background:var(--warning-soft)}.row-actions{display:flex;align-items:center;gap:6px}.link-button{min-height:40px;padding:7px 8px;color:var(--primary);font-weight:650;background:transparent;border:0;border-radius:4px}.link-button.danger{color:var(--danger)}.link-button:hover{background:var(--strong)}
|
|
|
|
|
.empty,.message-state{display:grid;place-items:center;min-height:330px;padding:36px;text-align:center}.empty-icon,.message-icon{display:grid;place-items:center;width:52px;height:52px;margin-bottom:12px;color:var(--primary);background:var(--primary-soft);border-radius:6px}.empty h3,.message-state h3{margin:0;font-size:18px}.empty p,.message-state p{max-width:520px;margin:7px 0 18px;color:var(--muted)}.message-state.error .message-icon{color:var(--danger);background:var(--danger-soft)}.skeleton{height:18px;background:linear-gradient(90deg,#edf2f1 20%,#f7f9f8 50%,#edf2f1 80%);background-size:220% 100%;border-radius:4px;animation:shine 1.2s linear infinite}@keyframes shine{to{background-position:-220% 0}}
|
|
|
|
|
.notice{display:flex;align-items:flex-start;gap:11px;margin-bottom:16px;padding:13px 15px;color:var(--info);background:var(--info-soft);border:1px solid #bfd5e5;border-radius:var(--radius)}.notice p{margin:0}.notice strong{display:block;margin-bottom:2px}.admin-shell{display:grid;grid-template-columns:210px minmax(0,1fr);min-height:calc(100dvh - 64px)}.admin-sidebar{padding:18px 12px;background:#263238;color:#dce5e7}.admin-sidebar h2{margin:0 10px 14px;font-size:14px;color:#91a4a9}.admin-link{display:flex;align-items:center;gap:9px;min-height:44px;padding:9px 11px;color:#dce5e7;text-decoration:none;border-radius:4px}.admin-link.active{color:#fff;background:#006b66}.admin-content{min-width:0}.admin-content main{padding-top:22px}.admin-table .data-table{min-width:1060px}.risk-cell{max-width:240px;color:var(--muted);font-size:13px}
|
|
|
|
|
.modal-backdrop{position:fixed;inset:0;z-index:40;display:grid;place-items:center;padding:16px;background:#17212680}.dialog{width:min(570px,100%);max-height:calc(100dvh - 32px);overflow:auto;background:var(--surface);border:1px solid var(--border-strong);border-radius:8px;box-shadow:0 18px 55px #1721263b}.dialog-header{display:flex;align-items:center;justify-content:space-between;padding:18px 20px;border-bottom:1px solid var(--border)}.dialog-header h2{margin:0;font-size:20px}.dialog-body{padding:20px}.dialog-body .field{margin-bottom:15px}.dialog-body .input,.dialog-body .select{width:100%}.help{margin:0;color:var(--muted);font-size:13px}.dialog-footer{display:flex;justify-content:flex-end;gap:8px;padding:14px 20px;border-top:1px solid var(--border);background:var(--subtle)}.secret-box{padding:14px;background:#172126;color:#f2f8f7;border-radius:6px;overflow-wrap:anywhere}.secret-box code{font-size:14px}.one-time{display:flex;gap:10px;margin-bottom:16px;padding:12px;color:var(--warning);background:var(--warning-soft);border:1px solid #ebc99f;border-radius:6px}.confirm-name{padding:9px 11px;background:var(--strong);border-radius:4px;font-weight:700}.toast{position:fixed;right:18px;bottom:18px;z-index:60;display:flex;align-items:center;gap:9px;max-width:min(420px,calc(100vw - 36px));padding:12px 15px;color:#fff;background:#245d38;border-radius:6px;box-shadow:0 8px 28px #17212635;transform:translateY(130%);transition:transform .2s}.toast.show{transform:translateY(0)}.toast.error{background:#8d302b}
|
|
|
|
|
@media(max-width:900px){.topbar{flex-wrap:wrap;gap:8px}.brand{min-width:0}.app-nav{order:3;width:100%}.account{display:none}.admin-shell{grid-template-columns:64px minmax(0,1fr)}.admin-sidebar{padding:12px 8px}.admin-sidebar h2,.admin-link span{display:none}.admin-link{justify-content:center}.page-heading{align-items:stretch;flex-direction:column}.heading-actions{justify-content:flex-start}.toolbar{align-items:stretch;flex-wrap:wrap}.toolbar .field{flex:1 1 180px}.toolbar .input,.toolbar .select{width:100%}.toolbar .spacer{display:none}.result-count{width:100%;padding:0}}
|
|
|
|
|
@media(max-width:560px){.topbar{padding:8px 12px}.brand{font-size:18px}.prototype-switch{margin-left:auto}.prototype-switch button{padding:5px 8px}.nav-item{padding:0 9px}.page-heading h1{font-size:25px}.heading-actions{display:grid;grid-template-columns:1fr}.heading-actions .button{width:100%}.panel-header{align-items:flex-start;flex-direction:column}.toolbar .button{flex:1}.dialog-footer{display:grid;grid-template-columns:1fr}.dialog-footer .button{width:100%}.admin-shell{display:block}.admin-sidebar{display:flex;gap:4px;overflow-x:auto}.admin-link{min-width:44px}.admin-content main{padding-top:18px}}
|
|
|
|
|
@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;animation:none!important;transition:none!important}}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<header class="topbar">
|
|
|
|
|
<a class="brand" href="#"><span class="brand-mark">C</span><span>Chorus</span></a>
|
|
|
|
|
<nav class="app-nav" id="portal-nav" aria-label="用户端导航"><a class="nav-item" href="#"><i data-lucide="sparkles"></i>生成</a><a class="nav-item" href="#"><i data-lucide="history"></i>历史</a><a class="nav-item active" href="#"><i data-lucide="key-round"></i>API Key</a></nav>
|
|
|
|
|
<div class="prototype-switch" role="group" aria-label="原型页面"><button type="button" data-view="portal" aria-pressed="true">用户端</button><button type="button" data-view="admin" aria-pressed="false">管理端</button></div>
|
|
|
|
|
<span class="account">受控用户</span>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div id="portal-view">
|
|
|
|
|
<main id="portal-main" tabindex="-1">
|
|
|
|
|
<div class="state-bar" aria-label="用户端原型状态"><strong>原型状态</strong><button class="state-button active" data-scope="portal" data-state="normal">正常</button><button class="state-button" data-scope="portal" data-state="loading">加载</button><button class="state-button" data-scope="portal" data-state="empty">空记录</button><button class="state-button" data-scope="portal" data-state="error">加载失败</button><button class="state-button" data-scope="portal" data-state="forbidden">无权限</button><button class="state-button" data-scope="portal" data-state="limited">限流</button></div>
|
|
|
|
|
<header class="page-heading"><div><h1>API Key</h1><p>为当前账户创建程序调用凭据。完整 Key 只在创建成功后显示一次,之后只能查看名称、前缀和使用状态。</p></div><div class="heading-actions"><button class="button button-secondary" id="open-api-docs"><i data-lucide="book-open"></i>接口文档</button><button class="button button-primary" id="create-key"><i data-lucide="plus"></i>创建 API Key</button></div></header>
|
|
|
|
|
<div class="notice"><i data-lucide="shield-check"></i><p><strong>每个 Key 都代表当前账户</strong>只通过 Authorization Bearer 请求头发送,不要放进网址、浏览器脚本、工单或日志。</p></div>
|
|
|
|
|
<section class="panel" aria-labelledby="portal-list-title"><div class="panel-header"><div><h2 id="portal-list-title">我的 API Key</h2><p>撤销后立即失效,不能恢复;需要轮换时先创建新 Key。</p></div><span class="result-count" id="portal-count">共 4 个</span></div><div id="portal-content"></div></section>
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="admin-view" class="hidden">
|
|
|
|
|
<div class="admin-shell">
|
|
|
|
|
<aside class="admin-sidebar" aria-label="管理端导航"><h2>运营管理</h2><a class="admin-link" href="#" title="用户管理"><i data-lucide="users"></i><span>用户管理</span></a><a class="admin-link active" href="#" title="API Key 管理"><i data-lucide="key-round"></i><span>API Key 管理</span></a><a class="admin-link" href="#" title="审计记录"><i data-lucide="scroll-text"></i><span>审计记录</span></a></aside>
|
|
|
|
|
<div class="admin-content"><main tabindex="-1">
|
|
|
|
|
<div class="state-bar" aria-label="管理端原型状态"><strong>原型状态</strong><button class="state-button active" data-scope="admin" data-state="normal">正常</button><button class="state-button" data-scope="admin" data-state="loading">加载</button><button class="state-button" data-scope="admin" data-state="empty">空记录</button><button class="state-button" data-scope="admin" data-state="error">加载失败</button><button class="state-button" data-scope="admin" data-state="forbidden">无权限</button></div>
|
|
|
|
|
<header class="page-heading"><div><h1>API Key 管理</h1><p>按用户和状态检索 Key 元数据,并在账户异常或凭据泄露时执行撤销。管理端不显示完整 Key。</p></div><div class="heading-actions"><button class="button button-secondary" id="refresh-admin"><i data-lucide="refresh-cw"></i>刷新</button></div></header>
|
|
|
|
|
<section class="panel admin-table" aria-labelledby="admin-list-title"><div class="panel-header"><div><h2 id="admin-list-title">API Key 列表</h2><p>撤销操作会写入管理员审计与 API 安全审计。</p></div></div><div class="toolbar"><div class="field"><label for="keyword">用户编号 / Key 名称 / 前缀</label><input class="input" id="keyword" type="search" placeholder="输入关键词"></div><div class="field"><label for="key-status">状态</label><select class="select" id="key-status"><option value="">全部状态</option><option value="active">有效</option><option value="expired">已过期</option><option value="revoked">已撤销</option></select></div><button class="button button-secondary" id="reset-filter">重置</button><span class="spacer"></span><span class="result-count" id="admin-count">共 5 条</span></div><div id="admin-content"></div></section>
|
|
|
|
|
</main></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="modal-backdrop hidden" id="create-dialog" role="presentation"><section class="dialog" role="dialog" aria-modal="true" aria-labelledby="create-title"><header class="dialog-header"><h2 id="create-title">创建 API Key</h2><button class="icon-button close-dialog" type="button" aria-label="关闭"><i data-lucide="x"></i></button></header><form id="create-form"><div class="dialog-body"><div class="field"><label for="key-name">名称</label><input class="input" id="key-name" maxlength="80" required value="自动化脚本" placeholder="例如:自动化脚本"><p class="help">使用能区分调用方和用途的名称。</p></div><div class="field"><label for="expires">有效期</label><select class="select" id="expires"><option>30 天</option><option selected>90 天</option><option>180 天</option><option>不设置到期时间</option></select><p class="help">到期后自动拒绝认证;可在到期前创建新 Key。</p></div></div><footer class="dialog-footer"><button class="button button-secondary close-dialog" type="button">取消</button><button class="button button-primary" type="submit"><i data-lucide="key-round"></i>创建</button></footer></form></section></div>
|
|
|
|
|
<div class="modal-backdrop hidden" id="secret-dialog" role="presentation"><section class="dialog" role="dialog" aria-modal="true" aria-labelledby="secret-title"><header class="dialog-header"><h2 id="secret-title">API Key 已创建</h2></header><div class="dialog-body"><div class="one-time"><i data-lucide="triangle-alert"></i><div><strong>这是唯一一次显示完整 Key</strong><div>关闭后无法再次查看;遗失时请创建新 Key 并撤销旧 Key。</div></div></div><div class="secret-box"><code id="secret-value">chorus_demo_THIS_IS_NOT_A_REAL_KEY_7x3K9m</code></div><button class="button button-secondary" id="copy-secret" type="button" style="margin-top:12px"><i data-lucide="copy"></i>复制示例 Key</button></div><footer class="dialog-footer"><button class="button button-primary" id="confirm-secret" type="button">我已妥善保存</button></footer></section></div>
|
|
|
|
|
<div class="modal-backdrop hidden" id="revoke-dialog" role="presentation"><section class="dialog" role="dialog" aria-modal="true" aria-labelledby="revoke-title"><header class="dialog-header"><h2 id="revoke-title">撤销 API Key</h2><button class="icon-button close-revoke" type="button" aria-label="关闭"><i data-lucide="x"></i></button></header><div class="dialog-body"><p>撤销后,使用此 Key 的请求会立即认证失败,且无法恢复。</p><div class="confirm-name" id="revoke-name">自动化脚本 · chorus_7x3K</div></div><footer class="dialog-footer"><button class="button button-secondary close-revoke" type="button">取消</button><button class="button button-danger" id="confirm-revoke" type="button"><i data-lucide="ban"></i>确认撤销</button></footer></section></div>
|
|
|
|
|
<div class="modal-backdrop hidden" id="detail-dialog" role="presentation"><section class="dialog" role="dialog" aria-modal="true" aria-labelledby="detail-title"><header class="dialog-header"><h2 id="detail-title">API Key 详情</h2><button class="icon-button close-detail" type="button" aria-label="关闭"><i data-lucide="x"></i></button></header><div class="dialog-body"><div id="detail-metadata"></div><h3 style="margin:20px 0 8px">最近安全事件</h3><div class="notice" style="margin:0"><i data-lucide="scroll-text"></i><p><strong>程序提交成功</strong>请求编号 req_demo_0188 · 3 分钟前<br><span class="secondary">未保存 Prompt、文件、完整 Key 或原始 IP。</span></p></div></div><footer class="dialog-footer"><button class="button button-primary close-detail" type="button">关闭</button></footer></section></div>
|
|
|
|
|
<div class="toast" id="toast" role="status" aria-live="polite"><i data-lucide="check"></i><span id="toast-text"></span></div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
const $=(s,r=document)=>r.querySelector(s), $$=(s,r=document)=>[...r.querySelectorAll(s)];
|
|
|
|
|
const keys=[
|
|
|
|
|
{name:'自动化脚本',prefix:'chorus_7x3K',created:'2026-08-20 14:32',used:'3 分钟前',expires:'2026-11-18',status:'active',user:'#1024 · 受控用户'},
|
|
|
|
|
{name:'内容工作台',prefix:'chorus_B2pQ',created:'2026-08-12 09:18',used:'昨天 18:05',expires:'不设置',status:'active',user:'#1024 · 受控用户'},
|
|
|
|
|
{name:'旧版接入',prefix:'chorus_M8nR',created:'2026-07-02 11:06',used:'2026-08-01',expires:'-',status:'revoked',user:'#1024 · 受控用户'},
|
|
|
|
|
{name:'临时接入',prefix:'chorus_T4vN',created:'2026-05-20 10:16',used:'2026-06-15',expires:'2026-06-19',status:'expired',user:'#1024 · 受控用户'},
|
|
|
|
|
{name:'批处理工具',prefix:'chorus_K9dL',created:'2026-08-22 16:40',used:'从未使用',expires:'2026-09-21',status:'active',user:'#1041 · 受控用户'}
|
|
|
|
|
];
|
|
|
|
|
let portalState='normal',adminState='normal',revokeTarget=null;
|
|
|
|
|
function icons(){window.renderChorusIcons?.()}
|
|
|
|
|
function statusBadge(row){if(row.status==='active')return '<span class="badge active">有效</span>';if(row.status==='expired')return '<span class="badge expiring">已过期</span>';return '<span class="badge revoked">已撤销</span>'}
|
|
|
|
|
function stateMarkup(state,scope){if(state==='loading')return `<div class="table-wrap"><table class="data-table"><tbody>${[1,2,3].map(()=>'<tr>'+[1,2,3,4,5].map(()=>'<td><div class="skeleton"></div></td>').join('')+'</tr>').join('')}</tbody></table></div>`;if(state==='empty')return '<div class="empty"><div><span class="empty-icon"><i data-lucide="key-round"></i></span><h3>还没有 API Key</h3><p>创建后可用于 OpenAPI 请求;完整 Key 只显示一次。</p><button class="button button-primary" data-create-empty><i data-lucide="plus"></i>创建 API Key</button></div></div>';if(state==='error')return '<div class="message-state error"><div><span class="message-icon"><i data-lucide="circle-alert"></i></span><h3>API Key 加载失败</h3><p>已有数据不会被修改。请检查网络后重试。</p><button class="button button-secondary" data-retry><i data-lucide="refresh-cw"></i>重试</button></div></div>';if(state==='forbidden')return '<div class="message-state error"><div><span class="message-icon"><i data-lucide="shield-x"></i></span><h3>没有访问权限</h3><p>'+(scope==='admin'?'当前管理员角色没有 API Key 查看权限。':'当前账户已被停用或会话已失效。')+'</p></div></div>';if(state==='limited')return '<div class="message-state error"><div><span class="message-icon"><i data-lucide="clock"></i></span><h3>请求过于频繁</h3><p>请在 28 秒后重试。已创建的 Key 和任务不会受到影响。</p><button class="button button-secondary" data-retry>稍后重试</button></div></div>';return ''}
|
|
|
|
|
function portalTable(){if(portalState!=='normal')return stateMarkup(portalState,'portal');return `<div class="table-wrap"><table class="data-table"><thead><tr><th>名称</th><th>Key 前缀</th><th>状态</th><th>最近使用</th><th>到期时间</th><th>创建时间</th><th>操作</th></tr></thead><tbody>${keys.slice(0,4).map((row,i)=>`<tr><td><span class="key-name">${row.name}</span></td><td class="mono">${row.prefix}</td><td>${statusBadge(row)}</td><td>${row.used}</td><td>${row.expires}</td><td>${row.created}</td><td><div class="row-actions">${row.status==='active'?`<button class="link-button danger" data-revoke="${i}">撤销</button>`:'<span class="secondary">不可恢复</span>'}</div></td></tr>`).join('')}</tbody></table></div>`}
|
|
|
|
|
function adminTable(){if(adminState!=='normal')return stateMarkup(adminState,'admin');const keyword=$('#keyword')?.value.trim().toLowerCase()||'',status=$('#key-status')?.value||'';const rows=keys.filter(row=>(!keyword||[row.name,row.prefix,row.user].join(' ').toLowerCase().includes(keyword))&&(!status||row.status===status));$('#admin-count').textContent=`共 ${rows.length} 条`;return `<div class="table-wrap"><table class="data-table"><thead><tr><th>用户</th><th>名称 / 前缀</th><th>状态</th><th>最近使用</th><th>到期时间</th><th>创建时间</th><th>安全提示</th><th>操作</th></tr></thead><tbody>${rows.map(row=>`<tr><td>${row.user}</td><td><span class="key-name">${row.name}</span><span class="secondary mono">${row.prefix}</span></td><td>${statusBadge(row)}</td><td>${row.used}</td><td>${row.expires}</td><td>${row.created}</td><td class="risk-cell">${row.used==='从未使用'?'尚无调用记录':'未发现异常速率'}</td><td><button class="link-button" data-detail="${keys.indexOf(row)}">详情</button>${row.status==='active'?`<button class="link-button danger" data-admin-revoke="${keys.indexOf(row)}">撤销</button>`:''}</td></tr>`).join('')||'<tr><td colspan="8"><div class="empty" style="min-height:220px"><div><h3>没有匹配结果</h3><p>调整关键词或状态后重试。</p></div></div></td></tr>'}</tbody></table></div>`}
|
|
|
|
|
function renderPortal(){$('#portal-content').innerHTML=portalTable();$('#portal-count').textContent=portalState==='normal'?'共 4 个':'';bindActions();icons()}
|
|
|
|
|
function renderAdmin(){$('#admin-content').innerHTML=adminTable();bindActions();icons()}
|
|
|
|
|
function bindActions(){$$('[data-create-empty]').forEach(b=>b.onclick=openCreate);$$('[data-retry]').forEach(b=>b.onclick=()=>{if($('#portal-view').classList.contains('hidden'))setState('admin','normal');else setState('portal','normal')});$$('[data-revoke]').forEach(b=>b.onclick=()=>openRevoke(Number(b.dataset.revoke),false));$$('[data-admin-revoke]').forEach(b=>b.onclick=()=>openRevoke(Number(b.dataset.adminRevoke),true));$$('[data-detail]').forEach(b=>b.onclick=()=>openDetail(Number(b.dataset.detail)))}
|
|
|
|
|
function setState(scope,state){if(scope==='portal'){portalState=state;renderPortal()}else{adminState=state;renderAdmin()}$$(`[data-scope="${scope}"]`).forEach(b=>b.classList.toggle('active',b.dataset.state===state))}
|
|
|
|
|
function switchView(view){const portal=view==='portal';$('#portal-view').classList.toggle('hidden',!portal);$('#admin-view').classList.toggle('hidden',portal);$('#portal-nav').classList.toggle('hidden',!portal);$$('[data-view]').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.view===view)));if(!portal)renderAdmin();document.querySelector(portal?'#portal-main':'#admin-view main').focus()}
|
|
|
|
|
function openCreate(){$('#create-dialog').classList.remove('hidden');$('#key-name').focus()}
|
|
|
|
|
function closeCreate(){$('#create-dialog').classList.add('hidden');$('#create-key').focus()}
|
|
|
|
|
function openRevoke(index,isAdmin){revokeTarget={index,isAdmin};const row=keys[index];$('#revoke-name').textContent=`${row.name} · ${row.prefix}${isAdmin?' · '+row.user:''}`;$('#revoke-dialog').classList.remove('hidden');$('#confirm-revoke').focus()}
|
|
|
|
|
function openDetail(index){const row=keys[index];$('#detail-metadata').innerHTML=`<div class="confirm-name">${row.name} · ${row.prefix}</div><p><strong>所属用户:</strong>${row.user}</p><p><strong>状态:</strong>${statusBadge(row)}</p><p><strong>最近使用:</strong>${row.used}</p><p><strong>到期时间:</strong>${row.expires}</p>`;$('#detail-dialog').classList.remove('hidden');$('.close-detail').focus();icons()}
|
|
|
|
|
function showToast(message,error=false){const toast=$('#toast');$('#toast-text').textContent=message;toast.classList.toggle('error',error);toast.classList.add('show');clearTimeout(toast.timer);toast.timer=setTimeout(()=>toast.classList.remove('show'),3000);icons()}
|
|
|
|
|
document.addEventListener('DOMContentLoaded',()=>{renderPortal();icons();$$('[data-view]').forEach(b=>b.onclick=()=>switchView(b.dataset.view));$$('[data-state]').forEach(b=>b.onclick=()=>setState(b.dataset.scope,b.dataset.state));$('#create-key').onclick=openCreate;$('#open-api-docs').onclick=()=>showToast('将打开 OpenAPI v1 接口文档(原型演示)');$$('.close-dialog').forEach(b=>b.onclick=closeCreate);$('#create-form').onsubmit=e=>{e.preventDefault();closeCreate();$('#secret-dialog').classList.remove('hidden');$('#confirm-secret').focus()};$('#copy-secret').onclick=async()=>{try{await navigator.clipboard.writeText($('#secret-value').textContent);showToast('示例 Key 已复制')}catch{showToast('浏览器未允许复制,请手动选择示例文本',true)}};$('#confirm-secret').onclick=()=>{$('#secret-dialog').classList.add('hidden');showToast('API Key 已创建(原型演示)')};$$('.close-revoke').forEach(b=>b.onclick=()=>$('#revoke-dialog').classList.add('hidden'));$$('.close-detail').forEach(b=>b.onclick=()=>$('#detail-dialog').classList.add('hidden'));$('#confirm-revoke').onclick=()=>{$('#revoke-dialog').classList.add('hidden');showToast(revokeTarget?.isAdmin?'API Key 已由管理员撤销(原型演示)':'API Key 已撤销(原型演示)')};$('#keyword').oninput=renderAdmin;$('#key-status').onchange=renderAdmin;$('#reset-filter').onclick=()=>{$('#keyword').value='';$('#key-status').value='';renderAdmin()};$('#refresh-admin').onclick=()=>{setState('admin','loading');setTimeout(()=>{setState('admin','normal');showToast('列表已刷新')},600)};document.addEventListener('keydown',e=>{if(e.key==='Escape'){$$('.modal-backdrop:not(.hidden)').forEach(m=>m.classList.add('hidden'))}})});
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|