Files
2026-06-20 10:08:44 +08:00

1688 lines
74 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理后台</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
color: #fff;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
header {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 {
font-size: 24px;
background: linear-gradient(90deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header-actions {
display: flex;
gap: 10px;
align-items: center;
}
.admin-info {
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 12px;
text-align: center;
}
.stat-card .number {
font-size: 32px;
font-weight: bold;
color: #667eea;
}
.stat-card .label {
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
margin-top: 5px;
}
.card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 12px;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.9);
}
th {
color: rgba(255, 255, 255, 0.95);
font-weight: 700;
font-size: 13px;
}
tr:hover {
background: rgba(255, 255, 255, 0.05);
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 700;
color: #fff;
white-space: nowrap;
}
.owner-cell {
color: rgba(255, 255, 255, 0.86);
font-size: 12px;
line-height: 1.45;
min-width: 110px;
}
.owner-cell strong {
color: #fff;
font-size: 13px;
}
.owner-cell .muted {
color: rgba(255, 255, 255, 0.62);
}
.row-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
min-width: 220px;
}
.badge-admin {
background: #667eea;
}
.badge-user {
background: #4a5568;
}
.badge-active {
background: #48bb78;
}
.badge-inactive {
background: #f56565;
}
.badge-suspended {
background: #f56565;
}
.badge-expired {
background: #ed8936;
}
.badge-valid {
background: #38b2ac;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
margin-right: 5px;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: #fff;
}
.btn-danger {
background: #f56565;
color: #fff;
}
.btn-warning {
background: #ed8936;
color: #fff;
}
.btn-success {
background: #48bb78;
color: #fff;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background: #1a1a2e;
padding: 30px;
border-radius: 12px;
max-width: 400px;
width: 90%;
}
.modal-content h3 {
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: rgba(255, 255, 255, 0.7);
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.modal-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.modal-actions .btn {
flex: 1;
}
.refresh-btn {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.3);
color: #fff;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
}
.refresh-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
/* 登录页面样式 */
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.login-box {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 40px;
border-radius: 16px;
width: 100%;
max-width: 400px;
}
.login-box h1 {
text-align: center;
margin-bottom: 30px;
font-size: 28px;
background: linear-gradient(90deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.login-box .form-group {
margin-bottom: 20px;
}
.login-box .form-group input {
padding: 14px;
font-size: 16px;
}
.login-box .btn-login {
width: 100%;
padding: 14px;
font-size: 16px;
margin-top: 10px;
}
.login-error {
color: #f56565;
text-align: center;
margin-bottom: 15px;
display: none;
}
.hidden {
display: none !important;
}
.tabs {
display: flex;
gap: 0;
margin-bottom: 20px;
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
}
.tab-btn {
padding: 10px 24px;
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.5);
font-size: 15px;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: all 0.3s;
}
.tab-btn:hover {
color: rgba(255, 255, 255, 0.8);
}
.tab-btn.active {
color: #667eea;
border-bottom-color: #667eea;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.config-section {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
}
.config-section h4 {
color: #667eea;
margin-bottom: 15px;
font-size: 16px;
}
.config-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.config-grid .form-group.full-width {
grid-column: 1 / -1;
}
.btn-cloud-on {
background: #48bb78;
color: #fff;
}
.btn-cloud-off {
background: #f56565;
color: #fff;
}
</style>
</head>
<body>
<!-- 登录页面 -->
<div id="loginPage" class="login-container">
<div class="login-box">
<h1>🔐 管理员登录</h1>
<div id="loginError" class="login-error"></div>
<div class="form-group">
<label>手机号</label>
<input type="text" id="loginEmail" placeholder="请输入管理员手机号">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="loginPassword" placeholder="请输入密码">
</div>
<button class="btn btn-primary btn-login" onclick="adminLogin()">登录</button>
</div>
</div>
<!-- 管理后台主页面 -->
<div id="adminPage" class="container hidden">
<header>
<h1>🔐 用户管理后台</h1>
<div class="header-actions">
<span class="admin-info" id="adminInfo"></span>
<button class="btn btn-success" id="createAdminBtn" onclick="showCreateAdmin()">+ 创建子管理员</button>
<button class="btn btn-primary" onclick="showAdminLogs()">📝 操作日志</button>
<button class="btn btn-warning" onclick="showGenerationStats()">📊 生成统计</button>
<button class="refresh-btn" onclick="loadAll()">🔄 刷新数据</button>
<button class="btn btn-danger" onclick="adminLogout()">退出登录</button>
</div>
</header>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('users')">👥 用户管理</button>
<button class="tab-btn" id="apiConfigTabBtn" onclick="switchTab('apiConfig')">🔑 API 配置</button>
</div>
<div id="tab-users" class="tab-content active">
<div class="stats-grid" id="statsGrid">
<div class="stat-card">
<div class="number" id="totalUsers">-</div>
<div class="label">总用户数</div>
</div>
<div class="stat-card">
<div class="number" id="activeUsers">-</div>
<div class="label">活跃用户</div>
</div>
<div class="stat-card">
<div class="number" id="validSubs">-</div>
<div class="label">有效订阅</div>
</div>
<div class="stat-card">
<div class="number" id="expiredSubs">-</div>
<div class="label">已过期</div>
</div>
<div class="stat-card">
<div class="number" id="totalDevices">-</div>
<div class="label">绑定设备</div>
</div>
</div>
<div class="card">
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px">
<span>👥 用户列表</span>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="text" id="userSearchInput" placeholder="搜索用户名/手机号/ID..."
style="padding:6px 12px;border:1px solid rgba(255,255,255,0.3);border-radius:6px;background:rgba(255,255,255,0.1);color:#fff;width:220px;font-size:13px"
oninput="debounceSearch()">
<select id="userStatusFilter" onchange="searchUsers()"
style="padding:6px 10px;border:1px solid rgba(255,255,255,0.3);border-radius:6px;background:#1a2332;color:#fff;font-size:13px">
<option value="">全部状态</option>
<option value="active">活跃</option>
<option value="suspended">已禁用</option>
</select>
<select id="userSubFilter" onchange="searchUsers()"
style="padding:6px 10px;border:1px solid rgba(255,255,255,0.3);border-radius:6px;background:#1a2332;color:#fff;font-size:13px">
<option value="">全部订阅</option>
<option value="valid">有效订阅</option>
<option value="expired">已过期</option>
</select>
<button class="btn btn-primary" onclick="searchUsers()" style="font-size:12px">🔍 搜索</button>
</div>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>手机号</th>
<th>角色</th>
<th>归属/邀请码</th>
<th>状态</th>
<th>积分</th>
<th>订阅</th>
<th>到期时间</th>
<th>云端数字人</th>
<th>最后登录</th>
<th>操作</th>
</tr>
</thead>
<tbody id="usersTable">
<tr>
<td colspan="12" style="text-align:center">加载中...</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="tab-apiConfig" class="tab-content">
<div class="card">
<div class="card-title">🔑 系统 API 配置</div>
<p style="color:rgba(255,255,255,0.6);margin-bottom:20px;font-size:13px">配置客户端使用的 API Key 和账号信息,保存后客户端将实时读取最新配置(无需重新打包)。</p>
<div class="config-section">
<h4>☁️ 阿里云百炼 DashScope</h4>
<div class="config-grid">
<div class="form-group">
<label>API Key</label>
<input type="text" id="cfg_aliyun_bailian_api_key" placeholder="sk-xxx">
</div>
<div class="form-group">
<label>API Base URL</label>
<input type="text" id="cfg_aliyun_bailian_base_url" placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1">
</div>
</div>
</div>
<div class="config-section">
<h4>📦 阿里云 OSS</h4>
<div class="config-grid">
<div class="form-group">
<label>AccessKey ID</label>
<input type="text" id="cfg_aliyun_oss_access_key_id" placeholder="LTAI5xxx">
</div>
<div class="form-group">
<label>AccessKey Secret</label>
<input type="password" id="cfg_aliyun_oss_access_key_secret" placeholder="输入AccessKey Secret">
</div>
<div class="form-group">
<label>Bucket</label>
<input type="text" id="cfg_aliyun_oss_bucket" placeholder="your-bucket">
</div>
<div class="form-group">
<label>Region</label>
<input type="text" id="cfg_aliyun_oss_region" placeholder="oss-cn-beijing">
</div>
</div>
</div>
<div class="config-section">
<h4>🌋 火山引擎(豆包)</h4>
<div class="config-grid">
<div class="form-group">
<label>API Key</label>
<input type="text" id="cfg_volcengine_api_key" placeholder="输入火山引擎API Key">
</div>
<div class="form-group">
<label>默认模型</label>
<input type="text" id="cfg_volcengine_model" placeholder="doubao-seed-2-0-mini-260215">
</div>
</div>
</div>
<div class="config-section">
<h4>🎬 RunningHub</h4>
<div class="config-grid">
<div class="form-group full-width">
<label>API Key</label>
<input type="text" id="cfg_runninghub_api_key" placeholder="输入RunningHub API Key">
</div>
<div class="form-group">
<label>云端模式1 工作流ID</label>
<input type="text" id="cfg_runninghub_workflow_id_1" placeholder="默认: 2009491298805354497">
</div>
<div class="form-group">
<label>云端模式Plus 工作流ID</label>
<input type="text" id="cfg_runninghub_workflow_id_2" placeholder="默认: 2013514129943826433">
</div>
<div class="form-group">
<label>TTS V2 应用ID</label>
<input type="text" id="cfg_runninghub_tts_webapp_id_v2" placeholder="默认: 1965614643077070850">
</div>
<div class="form-group">
<label>TTS V3 应用ID</label>
<input type="text" id="cfg_runninghub_tts_webapp_id_v3" placeholder="默认: 2028779949334728706">
</div>
<div class="form-group">
<label>TTS V2 音频节点ID</label>
<input type="text" id="cfg_runninghub_tts_v2_audio_node" placeholder="默认: 13">
</div>
<div class="form-group">
<label>TTS V2 文本节点ID</label>
<input type="text" id="cfg_runninghub_tts_v2_text_node" placeholder="默认: 14">
</div>
<div class="form-group">
<label>TTS V2 情感音频节点ID</label>
<input type="text" id="cfg_runninghub_tts_v2_emotion_node" placeholder="默认: 15">
</div>
</div>
</div>
<div class="config-section">
<h4>🔗 自定义模型通道</h4>
<p style="color:rgba(255,255,255,0.5);font-size:12px;margin-bottom:12px">第三方模型,支持多种模式</p>
<div class="config-grid">
<div class="form-group full-width" style="display:flex;align-items:center;gap:12px;padding:8px 0;">
<label style="margin:0;white-space:nowrap;">是否开启</label>
<div style="position:relative;display:inline-block;width:44px;height:24px;cursor:pointer;">
<input type="checkbox" id="cfg_cloud_beauty_enabled" style="opacity:0;width:0;height:0;position:absolute;">
<span style="position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#2d3748;border-radius:24px;transition:.3s;" onclick="var cb=this.previousElementSibling;cb.checked=!cb.checked;this.style.backgroundColor=cb.checked?'#22c55e':'#2d3748';this.querySelector('span').style.transform=cb.checked?'translateX(20px)':'translateX(0)'"><span style="position:absolute;height:18px;width:18px;left:3px;bottom:3px;background-color:white;border-radius:50%;transition:.3s;"></span></span>
</div>
<span style="color:rgba(255,255,255,0.4);font-size:12px;">开启后用户端将显示此模型选项</span>
</div>
<div class="form-group full-width">
<label>Base URL</label>
<input type="text" id="cfg_cloud_beauty_base_url" placeholder="https://xxx.vicp.fun">
</div>
<div class="form-group full-width">
<label>API Key (秘钥)</label>
<input type="text" id="cfg_cloud_beauty_api_key" placeholder="sryd_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
</div>
<div class="form-group full-width">
<label>默认模式(用户未选择时使用)</label>
<select id="cfg_cloud_beauty_default_mode" style="width:100%;padding:8px 12px;background:#1a2332;color:#fff;border:1px solid #2d3748;border-radius:6px;">
<option value="256m">极速美化(默认推荐)</option>
<option value="256o">极速真实</option>
<option value="384m">高清版(稍慢)</option>
</select>
</div>
</div>
</div>
<div class="config-section">
<h4>⏱️ 试用时长</h4>
<p style="color:rgba(255,255,255,0.5);font-size:12px;margin-bottom:12px">新用户注册后的默认试用时长</p>
<div class="config-grid">
<div class="form-group full-width">
<label>试用时长(小时)</label>
<input type="number" id="cfg_trial_duration_hours" value="24" min="1" placeholder="24">
</div>
</div>
</div>
<div style="margin-top:20px;display:flex;gap:10px">
<button class="btn btn-primary" onclick="saveSystemConfig()" style="padding:10px 30px;font-size:14px">💾 保存配置</button>
<button class="btn btn-success" onclick="loadSystemConfig()" style="padding:10px 20px;font-size:14px">🔄 重新加载</button>
</div>
<div id="configSaveMsg" style="margin-top:10px;display:none"></div>
</div>
</div>
</div>
<!-- 创建子管理员弹窗 -->
<div class="modal" id="createAdminModal">
<div class="modal-content">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0">👑 创建子管理员</h3>
<button onclick="closeModal('createAdminModal')" style="background:none;border:none;color:rgba(255,255,255,0.6);font-size:24px;cursor:pointer;padding:0 4px;line-height:1;"></button>
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" id="newAdminUsername" placeholder="请输入用户名">
</div>
<div class="form-group">
<label>手机号</label>
<input type="text" id="newAdminEmail" placeholder="请输入手机号">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="newAdminPassword" placeholder="至少6位">
</div>
<div class="form-group">
<label>邀请码</label>
<input type="text" id="newAdminInviteCode" placeholder="例如 AAA">
</div>
<div class="modal-actions">
<button class="btn btn-success" onclick="confirmCreateAdmin()">创建</button>
<button class="btn" onclick="closeModal('createAdminModal')">取消</button>
</div>
</div>
</div>
<!-- 积分管理弹窗 -->
<div class="modal" id="assignOwnerModal">
<div class="modal-content">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0">分配用户归属</h3>
<button onclick="closeModal('assignOwnerModal')" style="background:none;border:none;color:rgba(255,255,255,0.6);font-size:24px;cursor:pointer;padding:0 4px;line-height:1;">x</button>
</div>
<input type="hidden" id="assignOwnerUserId">
<div class="form-group">
<label>用户</label>
<input type="text" id="assignOwnerUsername" readonly>
</div>
<div class="form-group">
<label>归属管理员</label>
<select id="assignOwnerAdminId" style="width:100%;padding:8px 12px;background:#1a2332;color:#fff;border:1px solid #2d3748;border-radius:6px;">
<option value="">总后台</option>
</select>
</div>
<div class="modal-actions">
<button class="btn btn-primary" onclick="confirmAssignOwner()">确认分配</button>
<button class="btn" onclick="closeModal('assignOwnerModal')">取消</button>
</div>
</div>
</div>
<div class="modal" id="pointsModal">
<div class="modal-content">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0">💰 修改积分</h3>
<button onclick="closeModal('pointsModal')" style="background:none;border:none;color:rgba(255,255,255,0.6);font-size:24px;cursor:pointer;padding:0 4px;line-height:1;"></button>
</div>
<input type="hidden" id="pointsUserId">
<div class="form-group">
<label>用户</label>
<input type="text" id="pointsUsername" readonly>
</div>
<div class="form-group">
<label>当前积分</label>
<input type="text" id="currentPoints" readonly>
</div>
<div class="form-group">
<label>积分变动(正数增加,负数扣减)</label>
<input type="number" id="pointsChange" value="100" placeholder="如 100 或 -50">
</div>
<div class="form-group">
<label>原因/备注</label>
<input type="text" id="pointsReason" placeholder="可选">
</div>
<div class="modal-actions">
<button class="btn btn-success" onclick="confirmModifyPoints()">确认修改</button>
<button class="btn" onclick="closeModal('pointsModal')">取消</button>
</div>
</div>
</div>
<!-- 添加时长弹窗 -->
<div class="modal" id="addTimeModal">
<div class="modal-content">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0"> 添加使用时长</h3>
<button onclick="closeModal('addTimeModal')" style="background:none;border:none;color:rgba(255,255,255,0.6);font-size:24px;cursor:pointer;padding:0 4px;line-height:1;"></button>
</div>
<input type="hidden" id="addTimeUserId">
<div class="form-group">
<label>用户</label>
<input type="text" id="addTimeUsername" readonly>
</div>
<div class="form-group">
<label>添加时长(小时)</label>
<input type="number" id="addTimeHours" value="720" min="1" placeholder="720">
<span style="color:rgba(255,255,255,0.4);font-size:12px;margin-top:4px;display:block">1小时 = 0.04天,24小时 = 1天,720小时 = 30天</span>
</div>
<div class="form-group">
<label>原因/备注</label>
<input type="text" id="addTimeReason" placeholder="可选">
</div>
<div class="modal-actions">
<button class="btn btn-primary" onclick="confirmAddTime()">确认添加</button>
<button class="btn" onclick="closeModal('addTimeModal')">取消</button>
</div>
</div>
</div>
<!-- 重置密码弹窗 -->
<div class="modal" id="resetPasswordModal">
<div class="modal-content">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0">🔑 重置密码</h3>
<button onclick="closeModal('resetPasswordModal')" style="background:none;border:none;color:rgba(255,255,255,0.6);font-size:24px;cursor:pointer;padding:0 4px;line-height:1;"></button>
</div>
<input type="hidden" id="resetPasswordUserId">
<div class="form-group">
<label>用户</label>
<input type="text" id="resetPasswordUsername" readonly>
</div>
<div class="form-group">
<label>新密码</label>
<input type="password" id="newPassword" placeholder="至少6位">
</div>
<div class="form-group">
<label>确认密码</label>
<input type="password" id="confirmPassword" placeholder="再次输入">
</div>
<div class="modal-actions">
<button class="btn btn-primary" onclick="confirmResetPassword()">确认重置</button>
<button class="btn" onclick="closeModal('resetPasswordModal')">取消</button>
</div>
</div>
</div>
<!-- 用户日志弹窗 -->
<div class="modal" id="userLogsModal">
<div class="modal-content" style="max-width:800px;max-height:80vh;overflow-y:auto">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0">📋 用户日志 - <span id="userLogsUsername"></span></h3>
<button onclick="closeModal('userLogsModal')" style="background:none;border:none;color:rgba(255,255,255,0.6);font-size:24px;cursor:pointer;padding:0 4px;line-height:1;"></button>
</div>
<div id="userLogsContent" style="margin-top:15px">
<p style="text-align:center">加载中...</p>
</div>
<div class="modal-actions">
<button class="btn" onclick="closeModal('userLogsModal')">关闭</button>
</div>
</div>
</div>
<!-- 管理员操作日志弹窗 -->
<div class="modal" id="adminLogsModal">
<div class="modal-content" style="max-width:900px;max-height:80vh;overflow-y:auto">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0">📝 管理员操作日志</h3>
<button onclick="closeModal('adminLogsModal')" style="background:none;border:none;color:rgba(255,255,255,0.6);font-size:24px;cursor:pointer;padding:0 4px;line-height:1;"></button>
</div>
<div id="adminLogsContent" style="margin-top:15px">
<p style="text-align:center">加载中...</p>
</div>
<div class="modal-actions">
<button class="btn" onclick="closeModal('adminLogsModal')">关闭</button>
</div>
</div>
</div>
<!-- 生成统计弹窗 -->
<div class="modal" id="generationStatsModal">
<div class="modal-content" style="max-width:950px;max-height:85vh;overflow-y:auto">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0">📊 生成记录统计</h3>
<button onclick="closeModal('generationStatsModal')" style="background:none;border:none;color:rgba(255,255,255,0.6);font-size:24px;cursor:pointer;padding:0 4px;line-height:1;"></button>
</div>
<div id="generationStatsContent" style="margin-top:15px">
<p style="text-align:center">加载中...</p>
</div>
<div id="generationLogsContent" style="margin-top:20px">
</div>
</div>
</div>
<script>
const apiParam = new URLSearchParams(window.location.search).get('api')
const storedApiBase = localStorage.getItem('adminApiBase') || ''
const resolvedApiBase = apiParam === null ? storedApiBase : apiParam
const API_BASE = String(resolvedApiBase || '').trim().replace(/\/+$/, '')
if (apiParam !== null) {
localStorage.setItem('adminApiBase', API_BASE)
}
let adminToken = localStorage.getItem('adminToken')
let adminUser = JSON.parse(localStorage.getItem('adminUser') || 'null')
let adminList = []
function isSuperAdmin() {
return Number(adminUser?.is_super_admin || 0) === 1
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, (ch) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[ch]))
}
function escapeJs(value) {
return String(value ?? '').replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '')
}
// 页面加载时检查登录状态
document.addEventListener('DOMContentLoaded', () => {
if (adminToken && adminUser) {
verifyAdminToken()
} else {
showLoginPage()
}
// 回车登录
document.getElementById('loginPassword').addEventListener('keypress', (e) => {
if (e.key === 'Enter') adminLogin()
})
document.getElementById('loginEmail').addEventListener('keypress', (e) => {
if (e.key === 'Enter') document.getElementById('loginPassword').focus()
})
})
function showLoginPage() {
document.getElementById('loginPage').classList.remove('hidden')
document.getElementById('adminPage').classList.add('hidden')
}
function showAdminPage() {
document.getElementById('loginPage').classList.add('hidden')
document.getElementById('adminPage').classList.remove('hidden')
document.getElementById('adminInfo').textContent = `${isSuperAdmin() ? '总管理员' : '子管理员'}: ${adminUser.username}${adminUser.invite_code ? ' / 邀请码: ' + adminUser.invite_code : ''}`
document.getElementById('createAdminBtn').style.display = isSuperAdmin() ? '' : 'none'
document.getElementById('apiConfigTabBtn').style.display = isSuperAdmin() ? '' : 'none'
loadAll()
}
async function adminLogin() {
const email = document.getElementById('loginEmail').value.trim()
const password = document.getElementById('loginPassword').value
const errorDiv = document.getElementById('loginError')
if (!email || !password) {
errorDiv.textContent = '请输入手机号和密码'
errorDiv.style.display = 'block'
return
}
try {
const res = await fetch(`${API_BASE}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
const data = await res.json()
if (data.success) {
if (data.user.role !== 'admin') {
errorDiv.textContent = '您不是管理员,无权访问后台'
errorDiv.style.display = 'block'
return
}
adminToken = data.token
adminUser = data.user
localStorage.setItem('adminToken', adminToken)
localStorage.setItem('adminUser', JSON.stringify(adminUser))
errorDiv.style.display = 'none'
showAdminPage()
} else {
errorDiv.textContent = data.message || '登录失败'
errorDiv.style.display = 'block'
}
} catch (err) {
errorDiv.textContent = '网络错误,请检查连接'
errorDiv.style.display = 'block'
}
}
async function verifyAdminToken() {
try {
const res = await fetch(`${API_BASE}/api/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: adminToken })
})
const data = await res.json()
if (data.valid && data.user.role === 'admin') {
adminUser = data.user
localStorage.setItem('adminUser', JSON.stringify(adminUser))
showAdminPage()
} else {
adminLogout()
}
} catch (err) {
adminLogout()
}
}
function adminLogout() {
adminToken = null
adminUser = null
localStorage.removeItem('adminToken')
localStorage.removeItem('adminUser')
showLoginPage()
}
// 带认证的请求
async function authFetch(url, options = {}) {
if (!adminToken) {
adminLogout()
throw new Error('未登录')
}
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${adminToken}`,
...options.headers
}
const res = await fetch(url, { ...options, headers })
const data = await res.json()
if (res.status === 401 || res.status === 403) {
adminLogout()
throw new Error('认证失败')
}
return data
}
async function loadStats() {
try {
const data = await authFetch(`${API_BASE}/api/admin/stats`)
if (data.success) {
document.getElementById('totalUsers').textContent = data.stats.total_users || 0
document.getElementById('activeUsers').textContent = data.stats.active_users || 0
document.getElementById('validSubs').textContent = data.stats.valid_subscriptions || 0
document.getElementById('expiredSubs').textContent = data.stats.expired_subscriptions || 0
document.getElementById('totalDevices').textContent = data.stats.total_devices || 0
}
} catch (err) {
console.error('加载统计失败:', err)
}
}
let _searchTimer = null
function debounceSearch() {
clearTimeout(_searchTimer)
_searchTimer = setTimeout(searchUsers, 300)
}
async function searchUsers() {
const search = document.getElementById('userSearchInput').value.trim()
const status = document.getElementById('userStatusFilter').value
const subscription = document.getElementById('userSubFilter').value
const params = new URLSearchParams()
if (search) params.set('search', search)
if (status) params.set('status', status)
if (subscription) params.set('subscription', subscription)
const qs = params.toString() ? '?' + params.toString() : ''
try {
const data = await authFetch(`${API_BASE}/api/admin/users${qs}`)
if (data.success) {
renderUsersTable(data.users)
}
} catch (err) {
console.error('搜索用户失败:', err)
}
}
function renderUsersTable(users) {
const tbody = document.getElementById('usersTable')
if (!users || users.length === 0) {
tbody.innerHTML = '<tr><td colspan="12" style="text-align:center">暂无用户</td></tr>'
return
}
tbody.innerHTML = users.map(u => {
const now = new Date()
const expiresAt = u.expires_at ? new Date(u.expires_at) : null
const isExpired = expiresAt ? now > expiresAt : true
const remainingMs = expiresAt ? expiresAt - now : 0
const remainingHours = expiresAt ? Math.ceil(remainingMs / (1000 * 60 * 60)) : 0
const remainingDisplay = remainingHours >= 24 ? `${Math.floor(remainingHours / 24)}${remainingHours % 24}小时` : `${remainingHours}小时`
const points = u.points || 0
const username = escapeHtml(u.username)
const usernameArg = escapeJs(u.username)
const ownerName = u.owner_admin_name ? escapeHtml(u.owner_admin_name) : '总后台'
const ownerInfo = u.role === 'admin'
? `<div class="owner-cell"><strong>代理邀请码</strong><br><span class="muted">${escapeHtml(u.invite_code || '未设置')}</span></div>`
: `<div class="owner-cell"><strong>${ownerName}</strong><br><span class="muted">${u.owner_admin_id ? '子管理员ID: ' + u.owner_admin_id : '默认归总管理员'}</span></div>`
const superAdminTools = isSuperAdmin()
? `${u.role === 'user'
? `<button class="btn btn-success" onclick="promoteToSubAdmin(${u.id}, '${usernameArg}', '${escapeJs(u.invite_code || '')}')">升子管理</button>
<button class="btn btn-warning" onclick="showAssignOwner(${u.id}, '${usernameArg}', ${u.owner_admin_id || 'null'})">归属</button>`
: `<button class="btn btn-warning" onclick="updateAdminInviteCode(${u.id}, '${escapeJs(u.invite_code || '')}')">邀请码</button>`}`
: ''
return `<tr>
<td>${u.id}</td>
<td>${username}</td>
<td>${escapeHtml(u.email)}</td>
<td><span class="badge badge-${u.role}">${u.role}</span></td>
<td>${ownerInfo}</td>
<td><span class="badge badge-${u.status}">${u.status}</span></td>
<td><span class="badge" style="background:#f6ad55;cursor:pointer" onclick="showModifyPoints(${u.id}, '${usernameArg}', ${points})">${points} 💰</span></td>
<td><span class="badge badge-${isExpired ? 'expired' : 'valid'}">${isExpired ? '已过期' : remainingDisplay}</span></td>
<td>${expiresAt ? expiresAt.toLocaleString() : '-'}</td>
<td>
${u.cloud_digital_human_disabled
? '<span class="badge" style="background:#f56565">已关闭</span>'
: '<span class="badge" style="background:#48bb78">已开启</span>'
}
<button class="btn ${u.cloud_digital_human_disabled ? 'btn-cloud-on' : 'btn-cloud-off'}"
onclick="toggleCloudDigitalHuman(${u.id}, ${u.cloud_digital_human_disabled || 0})"
style="margin-top:4px;font-size:11px">
${u.cloud_digital_human_disabled ? '开启' : '关闭'}
</button>
</td>
<td>${u.last_login_at ? new Date(u.last_login_at).toLocaleString() : '-'}</td>
<td><div class="row-actions">
${superAdminTools}
<button class="btn btn-primary" onclick="showAddTime(${u.id}, '${usernameArg}')">加时</button>
<button class="btn btn-primary" onclick="showUserLogs(${u.id}, '${usernameArg}')">日志</button>
<button class="btn btn-warning" onclick="showGenerationStats(${u.id})">📊 统计</button>
<button class="btn btn-warning" onclick="showResetPassword(${u.id}, '${usernameArg}')">改密</button>
${u.status === 'active'
? `<button class="btn btn-danger" onclick="setStatus(${u.id}, 'suspended')">禁用</button>`
: `<button class="btn btn-success" onclick="setStatus(${u.id}, 'active')">启用</button>`
}
</div></td>
</tr>`
}).join('')
}
async function loadUsers() {
try {
const data = await authFetch(`${API_BASE}/api/admin/users`)
if (data.success) {
renderUsersTable(data.users)
}
} catch (err) {
console.error('加载用户失败:', err)
}
}
function showAddTime(userId, username) {
document.getElementById('addTimeUserId').value = userId
document.getElementById('addTimeUsername').value = username
document.getElementById('addTimeModal').style.display = 'flex'
}
function closeModal(id) {
document.getElementById(id).style.display = 'none'
}
async function confirmAddTime() {
const userId = document.getElementById('addTimeUserId').value
const hours = parseInt(document.getElementById('addTimeHours').value)
const reason = document.getElementById('addTimeReason').value
try {
const data = await authFetch(`${API_BASE}/api/admin/add-time`, {
method: 'POST',
body: JSON.stringify({ user_id: userId, hours, reason })
})
if (data.success) {
alert(`成功添加 ${hours} 小时!新到期时间: ${new Date(data.newExpiresAt).toLocaleString()}`)
closeModal('addTimeModal')
loadAll()
} else {
alert('添加失败: ' + data.error)
}
} catch (err) {
alert('操作失败: ' + err.message)
}
}
async function setStatus(userId, status) {
if (!confirm(`确定要${status === 'active' ? '启用' : '禁用'}该用户吗?`)) return
try {
const data = await authFetch(`${API_BASE}/api/admin/user/status`, {
method: 'POST',
body: JSON.stringify({ user_id: userId, status })
})
if (data.success) {
loadAll()
} else {
alert('操作失败: ' + data.error)
}
} catch (err) {
alert('操作失败: ' + err.message)
}
}
async function unbindDevices(userId) {
if (!confirm('确定要解绑该用户的所有设备吗?')) return
try {
const data = await authFetch(`${API_BASE}/api/admin/device/unbind`, {
method: 'POST',
body: JSON.stringify({ user_id: userId })
})
if (data.success) {
alert('设备已全部解绑')
loadAll()
} else {
alert('解绑失败: ' + data.error)
}
} catch (err) {
alert('操作失败: ' + err.message)
}
}
function showResetPassword(userId, username) {
document.getElementById('resetPasswordUserId').value = userId
document.getElementById('resetPasswordUsername').value = username
document.getElementById('newPassword').value = ''
document.getElementById('confirmPassword').value = ''
document.getElementById('resetPasswordModal').style.display = 'flex'
}
async function confirmResetPassword() {
const userId = document.getElementById('resetPasswordUserId').value
const newPassword = document.getElementById('newPassword').value
const confirmPassword = document.getElementById('confirmPassword').value
if (!newPassword || newPassword.length < 6) {
alert('密码长度至少6位')
return
}
if (newPassword !== confirmPassword) {
alert('两次输入的密码不一致')
return
}
try {
const data = await authFetch(`${API_BASE}/api/admin/user/reset-password`, {
method: 'POST',
body: JSON.stringify({ user_id: userId, new_password: newPassword })
})
if (data.success) {
alert('密码重置成功!')
closeModal('resetPasswordModal')
} else {
alert('重置失败: ' + (data.message || data.error))
}
} catch (err) {
alert('操作失败: ' + err.message)
}
}
// ==================== 创建子管理员 ====================
function showCreateAdmin() {
document.getElementById('newAdminUsername').value = ''
document.getElementById('newAdminEmail').value = ''
document.getElementById('newAdminPassword').value = ''
document.getElementById('newAdminInviteCode').value = ''
document.getElementById('createAdminModal').style.display = 'flex'
}
async function confirmCreateAdmin() {
const username = document.getElementById('newAdminUsername').value.trim()
const email = document.getElementById('newAdminEmail').value.trim()
const password = document.getElementById('newAdminPassword').value
const invite_code = document.getElementById('newAdminInviteCode').value.trim()
if (!username || !email || !password) {
alert('请填写所有字段')
return
}
if (password.length < 6) {
alert('密码长度至少6位')
return
}
try {
const data = await authFetch(`${API_BASE}/api/admin/create-admin`, {
method: 'POST',
body: JSON.stringify({ username, email, password, invite_code })
})
if (data.success) {
alert(`子管理员 ${username} 创建成功!`)
closeModal('createAdminModal')
loadAll()
} else {
alert('创建失败: ' + data.message)
}
} catch (err) {
alert('操作失败: ' + err.message)
}
}
// ==================== 积分管理 ====================
function showModifyPoints(userId, username, currentPoints) {
document.getElementById('pointsUserId').value = userId
document.getElementById('pointsUsername').value = username
document.getElementById('currentPoints').value = currentPoints
document.getElementById('pointsChange').value = 100
document.getElementById('pointsReason').value = ''
document.getElementById('pointsModal').style.display = 'flex'
}
async function confirmModifyPoints() {
const userId = document.getElementById('pointsUserId').value
const pointsChange = parseInt(document.getElementById('pointsChange').value)
const reason = document.getElementById('pointsReason').value
if (!pointsChange || pointsChange === 0) {
alert('积分变动不能为0')
return
}
try {
const data = await authFetch(`${API_BASE}/api/admin/points/modify`, {
method: 'POST',
body: JSON.stringify({ user_id: userId, points_change: pointsChange, reason })
})
if (data.success) {
alert(`积分修改成功!变动: ${pointsChange > 0 ? '+' : ''}${pointsChange},当前积分: ${data.newPoints}`)
closeModal('pointsModal')
loadAll()
} else {
alert('修改失败: ' + data.message)
}
} catch (err) {
alert('操作失败: ' + err.message)
}
}
// ==================== 日志查看 ====================
async function showUserLogs(userId, username) {
document.getElementById('userLogsUsername').textContent = username
document.getElementById('userLogsContent').innerHTML = '<p style="text-align:center">加载中...</p>'
document.getElementById('userLogsModal').style.display = 'flex'
try {
const data = await authFetch(`${API_BASE}/api/admin/logs/user/${userId}`)
if (data.success && data.logs.length > 0) {
document.getElementById('userLogsContent').innerHTML = `
<table style="width:100%;font-size:13px">
<thead>
<tr>
<th>时间</th>
<th>操作</th>
<th>详情</th>
<th>IP</th>
</tr>
</thead>
<tbody>
${data.logs.map(log => `
<tr>
<td>${new Date(log.created_at).toLocaleString()}</td>
<td><span class="badge" style="background:#667eea">${log.action}</span></td>
<td>${log.message || '-'}</td>
<td style="font-size:11px;color:#888">${log.ip_address || '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
`
} else {
document.getElementById('userLogsContent').innerHTML = '<p style="text-align:center;color:#888">暂无日志记录</p>'
}
} catch (err) {
document.getElementById('userLogsContent').innerHTML = '<p style="text-align:center;color:#f56565">加载失败</p>'
}
}
async function showAdminLogs() {
document.getElementById('adminLogsContent').innerHTML = '<p style="text-align:center">加载中...</p>'
document.getElementById('adminLogsModal').style.display = 'flex'
try {
const data = await authFetch(`${API_BASE}/api/admin/logs/all?limit=100`)
if (data.success && data.logs.length > 0) {
document.getElementById('adminLogsContent').innerHTML = `
<table style="width:100%;font-size:13px">
<thead>
<tr>
<th>时间</th>
<th>用户</th>
<th>操作</th>
<th>详情</th>
</tr>
</thead>
<tbody>
${data.logs.map(log => `
<tr>
<td>${new Date(log.created_at).toLocaleString()}</td>
<td>${log.username || 'ID:' + log.user_id}</td>
<td><span class="badge" style="background:${getActionColor(log.action)}">${formatAction(log.action)}</span></td>
<td>${log.message || '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
`
} else {
document.getElementById('adminLogsContent').innerHTML = '<p style="text-align:center;color:#888">暂无日志记录</p>'
}
} catch (err) {
document.getElementById('adminLogsContent').innerHTML = '<p style="text-align:center;color:#f56565">加载失败</p>'
}
}
function getActionColor(action) {
const colors = {
'login_success': '#48bb78',
'login_failed': '#f56565',
'register': '#667eea',
'create_admin': '#9f7aea',
'status_change': '#ed8936',
'password_reset': '#f6ad55',
'points_change': '#f6ad55',
'device_unbind': '#4a5568',
'role_change': '#9f7aea'
}
return colors[action] || '#4a5568'
}
function formatAction(action) {
const labels = {
'login_success': '登录成功',
'login_failed': '登录失败',
'login_blocked': '登录拦截',
'register': '注册',
'create_admin': '创建管理员',
'status_change': '状态变更',
'password_reset': '密码重置',
'points_change': '积分变动',
'device_unbind': '解绑设备',
'device_unbind_all': '解绑全部',
'role_change': '角色变更'
}
return labels[action] || action
}
async function loadAdmins() {
if (!isSuperAdmin()) return
try {
const data = await authFetch(`${API_BASE}/api/admin/admins`)
if (data.success) adminList = data.admins || []
} catch (err) {
console.error('加载管理员列表失败:', err)
}
}
async function showAssignOwner(userId, username, ownerAdminId) {
if (!isSuperAdmin()) return
if (!adminList.length) await loadAdmins()
document.getElementById('assignOwnerUserId').value = userId
document.getElementById('assignOwnerUsername').value = username
const select = document.getElementById('assignOwnerAdminId')
select.innerHTML = '<option value="">总后台</option>' + adminList
.filter(a => !a.is_super_admin)
.map(a => `<option value="${a.id}">${a.username} (${a.invite_code || '无邀请码'})</option>`)
.join('')
select.value = ownerAdminId || ''
document.getElementById('assignOwnerModal').style.display = 'flex'
}
async function confirmAssignOwner() {
const userId = document.getElementById('assignOwnerUserId').value
const ownerAdminId = document.getElementById('assignOwnerAdminId').value
const data = await authFetch(`${API_BASE}/api/admin/user/assign-owner`, {
method: 'POST',
body: JSON.stringify({ user_id: userId, owner_admin_id: ownerAdminId || null })
})
if (data.success) {
closeModal('assignOwnerModal')
loadAll()
} else {
alert('分配失败: ' + (data.message || data.error))
}
}
async function updateAdminInviteCode(adminId, currentCode) {
const inviteCode = prompt('请输入该子管理员的邀请码(留空则清除)', currentCode || '')
if (inviteCode === null) return
const data = await authFetch(`${API_BASE}/api/admin/admin/invite-code`, {
method: 'PUT',
body: JSON.stringify({ admin_id: adminId, invite_code: inviteCode })
})
if (data.success) {
loadAll()
} else {
alert('邀请码更新失败: ' + (data.message || data.error))
}
}
async function promoteToSubAdmin(userId, username, currentCode) {
const inviteCode = prompt(`${username} 升级为子管理员,请设置邀请码(可留空后续再设置)`, currentCode || '')
if (inviteCode === null) return
if (!confirm(`确定把 ${username} 升级为子管理员吗?升级后他可以管理自己邀请码注册进来的用户。`)) return
const data = await authFetch(`${API_BASE}/api/admin/user/role`, {
method: 'POST',
body: JSON.stringify({ user_id: userId, role: 'admin', invite_code: inviteCode })
})
if (data.success) {
loadAll()
} else {
alert('升级失败: ' + (data.message || data.error))
}
}
function loadAll() {
loadStats()
if (isSuperAdmin()) loadAdmins()
loadUsers()
if (isSuperAdmin()) loadSystemConfig()
}
async function showGenerationStats(userId) {
document.getElementById('generationStatsModal').style.display = 'flex'
document.getElementById('generationStatsContent').innerHTML = '<p style="text-align:center">加载中...</p>'
document.getElementById('generationLogsContent').innerHTML = ''
try {
const url = userId ? `${API_BASE}/api/admin/generation-stats?userId=${userId}` : `${API_BASE}/api/admin/generation-stats`
const data = await authFetch(url)
if (data.success) {
const s = data.stats
document.getElementById('generationStatsContent').innerHTML = `
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:12px;">
<div style="background:#2d3748;padding:16px;border-radius:10px;text-align:center;">
<div style="font-size:28px;font-weight:bold;color:#667eea">${s.script || 0}</div>
<div style="color:#a0aec0;margin-top:4px">📝 生成文案</div>
</div>
<div style="background:#2d3748;padding:16px;border-radius:10px;text-align:center;">
<div style="font-size:28px;font-weight:bold;color:#48bb78">${s.voice || 0}</div>
<div style="color:#a0aec0;margin-top:4px">🎙️ 生成语音</div>
</div>
<div style="background:#2d3748;padding:16px;border-radius:10px;text-align:center;">
<div style="font-size:28px;font-weight:bold;color:#ed8936">${s.video || 0}</div>
<div style="color:#a0aec0;margin-top:4px">🎬 生成视频</div>
</div>
<div style="background:#2d3748;padding:16px;border-radius:10px;text-align:center;">
<div style="font-size:28px;font-weight:bold;color:#e53e3e">${s.subtitle || 0}</div>
<div style="color:#a0aec0;margin-top:4px">📄 生成字幕</div>
</div>
<div style="background:#2d3748;padding:16px;border-radius:10px;text-align:center;">
<div style="font-size:28px;font-weight:bold;color:#fff">${s.total || 0}</div>
<div style="color:#a0aec0;margin-top:4px">📊 总计</div>
</div>
</div>
`
loadGenerationLogs(userId)
} else {
document.getElementById('generationStatsContent').innerHTML = '<p style="text-align:center;color:#f56565">加载失败</p>'
}
} catch (e) {
document.getElementById('generationStatsContent').innerHTML = '<p style="text-align:center;color:#f56565">加载失败: ' + e.message + '</p>'
}
}
async function loadGenerationLogs(userId) {
try {
const url = userId ? `${API_BASE}/api/admin/generation-logs?userId=${userId}&limit=100` : `${API_BASE}/api/admin/generation-logs?limit=100`
const data = await authFetch(url)
if (data.success && data.logs.length > 0) {
const typeLabels = { script: '📝 文案', voice: '🎙️ 语音', video: '🎬 视频', subtitle: '📄 字幕' }
const typeColors = { script: '#667eea', voice: '#48bb78', video: '#ed8936', subtitle: '#e53e3e' }
document.getElementById('generationLogsContent').innerHTML = `
<h4 style="margin:0 0 10px 0">最近生成记录 (共${data.total}条)</h4>
<table class="data-table">
<thead><tr><th>时间</th><th>用户</th><th>类型</th><th>详情</th></tr></thead>
<tbody>
${data.logs.map(l => `
<tr>
<td>${new Date(l.created_at).toLocaleString()}</td>
<td>${l.username || 'ID:' + l.user_id}</td>
<td><span class="badge" style="background:${typeColors[l.type] || '#667'}">${typeLabels[l.type] || l.type}</span></td>
<td style="font-size:12px;color:#a0aec0;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${l.detail || '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
`
} else {
document.getElementById('generationLogsContent').innerHTML = '<p style="text-align:center;color:#888">暂无生成记录</p>'
}
} catch (e) {}
}
function switchTab(tab) {
if (tab === 'apiConfig' && !isSuperAdmin()) return
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'))
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'))
if (tab === 'users') {
document.querySelectorAll('.tab-btn')[0].classList.add('active')
document.getElementById('tab-users').classList.add('active')
} else if (tab === 'apiConfig') {
document.querySelectorAll('.tab-btn')[1].classList.add('active')
document.getElementById('tab-apiConfig').classList.add('active')
loadSystemConfig()
}
}
async function loadSystemConfig() {
try {
const data = await authFetch(`${API_BASE}/api/admin/system/config`)
if (data.success) {
const configKeys = [
'aliyun_bailian_api_key', 'aliyun_bailian_base_url',
'aliyun_oss_access_key_id', 'aliyun_oss_access_key_secret',
'aliyun_oss_bucket', 'aliyun_oss_region',
'volcengine_api_key', 'volcengine_model',
'runninghub_api_key', 'runninghub_workflow_id_1', 'runninghub_workflow_id_2',
'runninghub_tts_webapp_id_v2', 'runninghub_tts_webapp_id_v3',
'runninghub_tts_v2_audio_node', 'runninghub_tts_v2_text_node', 'runninghub_tts_v2_emotion_node',
'cloud_beauty_base_url', 'cloud_beauty_api_key', 'cloud_beauty_default_mode', 'cloud_beauty_enabled',
'trial_duration_hours'
]
data.configs.forEach(c => {
if (configKeys.includes(c.config_key)) {
const el = document.getElementById('cfg_' + c.config_key)
if (el) {
if (el.type === 'checkbox') {
el.checked = c.config_value === 'true'
const toggle = el.nextElementSibling
if (toggle) {
toggle.style.backgroundColor = el.checked ? '#22c55e' : '#2d3748'
const dot = toggle.querySelector('span')
if (dot) dot.style.transform = el.checked ? 'translateX(20px)' : 'translateX(0)'
}
} else {
el.value = c.config_value || ''
}
}
}
})
}
} catch (err) {
console.error('加载配置失败:', err)
}
}
async function saveSystemConfig() {
const configs = {
aliyun_bailian_api_key: document.getElementById('cfg_aliyun_bailian_api_key').value,
aliyun_bailian_base_url: document.getElementById('cfg_aliyun_bailian_base_url').value,
aliyun_oss_access_key_id: document.getElementById('cfg_aliyun_oss_access_key_id').value,
aliyun_oss_access_key_secret: document.getElementById('cfg_aliyun_oss_access_key_secret').value,
aliyun_oss_bucket: document.getElementById('cfg_aliyun_oss_bucket').value,
aliyun_oss_region: document.getElementById('cfg_aliyun_oss_region').value,
volcengine_api_key: document.getElementById('cfg_volcengine_api_key').value,
volcengine_model: document.getElementById('cfg_volcengine_model').value,
runninghub_api_key: document.getElementById('cfg_runninghub_api_key').value,
runninghub_workflow_id_1: document.getElementById('cfg_runninghub_workflow_id_1').value,
runninghub_workflow_id_2: document.getElementById('cfg_runninghub_workflow_id_2').value,
runninghub_tts_webapp_id_v2: document.getElementById('cfg_runninghub_tts_webapp_id_v2').value,
runninghub_tts_webapp_id_v3: document.getElementById('cfg_runninghub_tts_webapp_id_v3').value,
runninghub_tts_v2_audio_node: document.getElementById('cfg_runninghub_tts_v2_audio_node').value,
runninghub_tts_v2_text_node: document.getElementById('cfg_runninghub_tts_v2_text_node').value,
runninghub_tts_v2_emotion_node: document.getElementById('cfg_runninghub_tts_v2_emotion_node').value,
cloud_beauty_base_url: document.getElementById('cfg_cloud_beauty_base_url').value,
cloud_beauty_api_key: document.getElementById('cfg_cloud_beauty_api_key').value,
cloud_beauty_default_mode: document.getElementById('cfg_cloud_beauty_default_mode').value,
cloud_beauty_enabled: document.getElementById('cfg_cloud_beauty_enabled').checked ? 'true' : 'false',
trial_duration_hours: document.getElementById('cfg_trial_duration_hours').value || '24'
}
const msgEl = document.getElementById('configSaveMsg')
try {
const data = await authFetch(`${API_BASE}/api/admin/system/config`, {
method: 'PUT',
body: JSON.stringify({ configs })
})
if (data.success) {
msgEl.style.display = 'block'
msgEl.style.color = '#48bb78'
msgEl.textContent = '✅ 配置保存成功!客户端将在下次请求时自动读取新配置。'
setTimeout(() => { msgEl.style.display = 'none' }, 5000)
} else {
msgEl.style.display = 'block'
msgEl.style.color = '#f56565'
msgEl.textContent = '❌ 保存失败: ' + (data.message || '未知错误')
}
} catch (err) {
msgEl.style.display = 'block'
msgEl.style.color = '#f56565'
msgEl.textContent = '❌ 网络错误: ' + err.message
}
}
async function toggleCloudDigitalHuman(userId, currentDisabled) {
const action = currentDisabled ? '开启' : '关闭'
if (!confirm(`确定要${action}该用户的云端数字人功能吗?`)) return
try {
const data = await authFetch(`${API_BASE}/api/admin/user/cloud-digital-human`, {
method: 'PUT',
body: JSON.stringify({ user_id: userId, disabled: !currentDisabled })
})
if (data.success) {
loadUsers()
} else {
alert('操作失败: ' + data.message)
}
} catch (err) {
alert('操作失败: ' + err.message)
}
}
</script>
</body>
</html>