1682 lines
73 KiB
HTML
1682 lines
73 KiB
HTML
<!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="默认: 2013514129943826433">
|
||
</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="0" step="0.01" 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 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) => ({
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
}[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>
|