692 lines
39 KiB
HTML
692 lines
39 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>Paper Burner X - 管理面板</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script src="https://gcore.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||
<script src="https://gcore.jsdelivr.net/npm/chart.js"></script>
|
||
</head>
|
||
<body class="bg-gray-100">
|
||
<!-- 登录页面 -->
|
||
<div id="loginPage" class="min-h-screen flex items-center justify-center">
|
||
<div class="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
|
||
<h1 class="text-2xl font-bold mb-6 text-center">Paper Burner X 管理面板</h1>
|
||
<form id="loginForm" class="space-y-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">邮箱</label>
|
||
<input type="email" id="email" required
|
||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">密码</label>
|
||
<input type="password" id="password" required
|
||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||
</div>
|
||
<button type="submit"
|
||
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||
登录
|
||
</button>
|
||
</form>
|
||
<div id="loginError" class="mt-4 text-red-600 text-sm hidden"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 管理面板主界面 -->
|
||
<div id="adminPanel" class="hidden min-h-screen">
|
||
<!-- 顶部导航 -->
|
||
<nav class="bg-white shadow-sm">
|
||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div class="flex justify-between h-16">
|
||
<div class="flex items-center">
|
||
<h1 class="text-xl font-bold">Paper Burner X 管理面板</h1>
|
||
</div>
|
||
<div class="flex items-center space-x-4">
|
||
<span id="adminName" class="text-gray-700"></span>
|
||
<button onclick="logout()"
|
||
class="bg-gray-200 px-4 py-2 rounded-md hover:bg-gray-300">
|
||
退出登录
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- 主内容区 -->
|
||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
<!-- 统计卡片 -->
|
||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||
<div class="bg-white p-6 rounded-lg shadow">
|
||
<div class="text-gray-500 text-sm">总用户数</div>
|
||
<div id="totalUsers" class="text-3xl font-bold mt-2">0</div>
|
||
</div>
|
||
<div class="bg-white p-6 rounded-lg shadow">
|
||
<div class="text-gray-500 text-sm">活跃用户</div>
|
||
<div id="activeUsers" class="text-3xl font-bold mt-2">0</div>
|
||
</div>
|
||
<div class="bg-white p-6 rounded-lg shadow">
|
||
<div class="text-gray-500 text-sm">总文档数</div>
|
||
<div id="totalDocuments" class="text-3xl font-bold mt-2">0</div>
|
||
</div>
|
||
<div class="bg-white p-6 rounded-lg shadow">
|
||
<div class="text-gray-500 text-sm">今日处理</div>
|
||
<div id="documentsToday" class="text-3xl font-bold mt-2">0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 额外统计卡片(周/月处理量与总存储) -->
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||
<div class="bg-white p-6 rounded-lg shadow">
|
||
<div class="text-gray-500 text-sm mb-2">本周处理</div>
|
||
<div id="documentsThisWeek" class="text-2xl font-bold">-</div>
|
||
</div>
|
||
<div class="bg-white p-6 rounded-lg shadow">
|
||
<div class="text-gray-500 text-sm mb-2">本月处理</div>
|
||
<div id="documentsThisMonth" class="text-2xl font-bold">-</div>
|
||
</div>
|
||
<div class="bg-white p-6 rounded-lg shadow">
|
||
<div class="text-gray-500 text-sm mb-2">总存储使用</div>
|
||
<div id="totalStorageMB" class="text-2xl font-bold">- MB</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 选项卡导航 -->
|
||
<div class="bg-white rounded-lg shadow mb-4">
|
||
<div class="border-b border-gray-200">
|
||
<nav class="flex -mb-px overflow-x-auto">
|
||
<button onclick="switchTab('overview')" id="tab-overview"
|
||
class="tab-button px-6 py-3 border-b-2 border-transparent font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap">
|
||
概览
|
||
</button>
|
||
<button onclick="switchTab('users')" id="tab-users"
|
||
class="tab-button px-6 py-3 border-b-2 border-blue-500 font-medium text-blue-600 whitespace-nowrap">
|
||
用户管理
|
||
</button>
|
||
<button onclick="switchTab('quotas')" id="tab-quotas"
|
||
class="tab-button px-6 py-3 border-b-2 border-transparent font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap">
|
||
配额管理
|
||
</button>
|
||
<button onclick="switchTab('activity')" id="tab-activity"
|
||
class="tab-button px-6 py-3 border-b-2 border-transparent font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap">
|
||
活动日志
|
||
</button>
|
||
<button onclick="switchTab('models')" id="tab-models"
|
||
class="tab-button px-6 py-3 border-b-2 border-transparent font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap">
|
||
模型配置
|
||
</button>
|
||
<button onclick="switchTab('system')" id="tab-system"
|
||
class="tab-button px-6 py-3 border-b-2 border-transparent font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap">
|
||
系统设置
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
|
||
<!-- 概览标签页 -->
|
||
<div id="content-overview" class="p-6 hidden">
|
||
<!-- 日期筛选 -->
|
||
<div class="mb-6 flex flex-wrap items-end gap-3">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">开始日期</label>
|
||
<input type="date" id="statsStartDate" class="px-3 py-2 border rounded-md">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">结束日期</label>
|
||
<input type="date" id="statsEndDate" class="px-3 py-2 border rounded-md">
|
||
</div>
|
||
<div class="flex items-center gap-2 pb-1">
|
||
<button onclick="applyStatsRange()" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">应用</button>
|
||
<button onclick="clearStatsRange()" class="px-4 py-2 border rounded-md hover:bg-gray-50">清除</button>
|
||
</div>
|
||
</div>
|
||
<div id="statsRangeHint" class="mb-6 text-sm text-gray-600">当前筛选:全部</div>
|
||
<div class="mb-8">
|
||
<h3 class="text-lg font-medium mb-4">使用趋势(最近30天)</h3>
|
||
<div class="bg-white p-4 rounded-lg border">
|
||
<canvas id="trendChart" height="80"></canvas>
|
||
</div>
|
||
</div>
|
||
<div class="mb-8">
|
||
<h3 class="text-lg font-medium mb-4">文档状态分布</h3>
|
||
<div id="documentsByStatus" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4"></div>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-lg font-medium mb-4">Top 10 活跃用户(本月)</h3>
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full divide-y divide-gray-200">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">排名</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">用户</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">邮箱</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">文档数</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="topUsersList" class="bg-white divide-y divide-gray-200"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 用户管理 -->
|
||
<div id="content-users" class="p-6">
|
||
<div class="mb-4 flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||
<h3 class="text-lg font-medium">用户列表</h3>
|
||
<div class="flex items-center gap-2">
|
||
<input id="usersSearch" type="text" placeholder="搜索邮箱/姓名" class="px-3 py-2 border rounded-md" onkeypress="if(event.key==='Enter'){ usersPage=1; loadUsers(); }">
|
||
<select id="usersPageSize" class="px-2 py-2 border rounded-md" onchange="usersPage=1; usersPageSize=parseInt(this.value); loadUsers();">
|
||
<option value="10">10/页</option>
|
||
<option value="20" selected>20/页</option>
|
||
<option value="50">50/页</option>
|
||
</select>
|
||
<button onclick="usersPage=1; loadUsers();" class="bg-gray-200 px-3 py-2 rounded-md hover:bg-gray-300">搜索</button>
|
||
<button onclick="openCreateUser()" class="bg-blue-600 text-white px-3 py-2 rounded-md hover:bg-blue-700">新建用户</button>
|
||
</div>
|
||
</div>
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full divide-y divide-gray-200">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">邮箱</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">姓名</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">角色</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">状态</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">注册时间</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="usersList" class="bg-white divide-y divide-gray-200"></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="flex items-center justify-between mt-3 text-sm text-gray-600">
|
||
<div id="usersPagerInfo">共 0 条</div>
|
||
<div class="flex items-center gap-2">
|
||
<button class="px-3 py-1 border rounded-md" onclick="if(usersPage>1){usersPage--; loadUsers();}">上一页</button>
|
||
<span id="usersPagerPage">第 1/1 页</span>
|
||
<button class="px-3 py-1 border rounded-md" onclick="if(usersPage<usersTotalPages){usersPage++; loadUsers();}">下一页</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 配额管理 -->
|
||
<div id="content-quotas" class="p-6 hidden">
|
||
<div class="mb-4">
|
||
<h3 class="text-lg font-medium mb-2">配额管理</h3>
|
||
<p class="text-sm text-gray-600">为用户设置文档数量和存储空间限制(-1 表示无限制)</p>
|
||
</div>
|
||
|
||
<div class="mb-6">
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">选择用户</label>
|
||
<select id="quotaUserId" onchange="loadUserQuota()"
|
||
class="block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
<option value="">请选择用户...</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="quotaForm" class="hidden space-y-6">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">每日文档限制</label>
|
||
<input type="number" id="maxDocumentsPerDay"
|
||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
<p class="mt-1 text-xs text-gray-500">-1 表示无限制</p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">每月文档限制</label>
|
||
<input type="number" id="maxDocumentsPerMonth"
|
||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
<p class="mt-1 text-xs text-gray-500">-1 表示无限制</p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">存储空间限制(MB)</label>
|
||
<input type="number" id="maxStorageSize"
|
||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
<p class="mt-1 text-xs text-gray-500">-1 表示无限制</p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">API Keys 数量限制</label>
|
||
<input type="number" id="maxApiKeysCount"
|
||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
<p class="mt-1 text-xs text-gray-500">-1 表示无限制</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-gray-50 p-4 rounded-lg">
|
||
<h4 class="text-sm font-medium text-gray-700 mb-3">当前使用量</h4>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<div class="text-sm text-gray-600">本月文档数</div>
|
||
<div id="documentsThisMonthQuota" class="text-lg font-semibold">0</div>
|
||
<div class="w-full bg-gray-200 rounded-full h-2 mt-2">
|
||
<div id="documentsProgressBar" class="bg-blue-600 h-2 rounded-full transition-all" style="width: 0%"></div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="text-sm text-gray-600">存储使用(MB)</div>
|
||
<div id="currentStorageUsed" class="text-lg font-semibold">0</div>
|
||
<div class="w-full bg-gray-200 rounded-full h-2 mt-2">
|
||
<div id="storageProgressBar" class="bg-green-600 h-2 rounded-full transition-all" style="width: 0%"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex space-x-4">
|
||
<button onclick="saveUserQuota()"
|
||
class="bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700">
|
||
保存配额
|
||
</button>
|
||
<button onclick="resetUserQuota()"
|
||
class="bg-gray-200 px-6 py-2 rounded-md hover:bg-gray-300">
|
||
重置使用量
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 活动日志 -->
|
||
<div id="content-activity" class="p-6 hidden">
|
||
<div class="mb-6">
|
||
<h3 class="text-lg font-medium mb-2">用户活动日志</h3>
|
||
<div class="flex items-center space-x-4">
|
||
<div class="flex-1">
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">选择用户</label>
|
||
<select id="activityUserId" onchange="loadUserActivity()"
|
||
class="block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
<option value="">请选择用户...</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">显示条数</label>
|
||
<select id="activityLimit" onchange="loadUserActivity()"
|
||
class="block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
<option value="50">50</option>
|
||
<option value="100">100</option>
|
||
<option value="200">200</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full divide-y divide-gray-200">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">时间</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">操作</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">资源ID</th>
|
||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">详情</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="activityLogsList" class="bg-white divide-y divide-gray-200">
|
||
<tr>
|
||
<td colspan="4" class="px-6 py-4 text-center text-gray-500">
|
||
请选择用户查看活动日志
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模型配置 -->
|
||
<div id="content-models" class="p-6 hidden">
|
||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||
<button onclick="addSourceSite()"
|
||
class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
|
||
添加自定义源站
|
||
</button>
|
||
<button id="importSourceSitesBtn"
|
||
class="bg-gray-200 px-4 py-2 rounded-md hover:bg-gray-300">
|
||
导入 JSON
|
||
</button>
|
||
<input id="importSourceSitesFile" type="file" accept="application/json" class="hidden">
|
||
<button id="exportSourceSitesBtn"
|
||
class="bg-gray-200 px-4 py-2 rounded-md hover:bg-gray-300">
|
||
导出 JSON
|
||
</button>
|
||
</div>
|
||
<div id="sourceSitesList" class="space-y-4">
|
||
<!-- 源站配置将在这里动态加载 -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 系统设置 -->
|
||
<div id="content-system" class="p-6 hidden">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<h3 class="text-lg font-medium mb-4">系统配置</h3>
|
||
<div class="space-y-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">允许用户注册</label>
|
||
<select id="allowRegistration"
|
||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
<option value="true">是</option>
|
||
<option value="false">否</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">最大上传大小(MB)</label>
|
||
<input type="number" id="maxUploadSize" value="100"
|
||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
</div>
|
||
<button onclick="saveSystemSettings()"
|
||
class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
|
||
保存设置
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pt-6 border-t">
|
||
<h3 class="text-lg font-medium mb-4">代理与下载设置</h3>
|
||
<div class="space-y-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">代理白名单域(手动追加,逗号分隔)</label>
|
||
<input type="text" id="proxyWhitelistDomains" placeholder="mineru.net, v2.doc2x.noedgeai.com"
|
||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
<p class="mt-1 text-xs text-gray-500">与“自定义源站点(全局)”“Workers 域”动态合并;子域名自动匹配。</p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">Workers 代理域(逗号分隔)</label>
|
||
<input type="text" id="workerProxyDomains" placeholder="ocr-proxy.xxx.workers.dev, my-proxy.example.com"
|
||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
</div>
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">允许 HTTP 代理</label>
|
||
<select id="allowHttpProxy" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
<option value="false">否(推荐)</option>
|
||
<option value="true">是(仅测试环境)</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">上游超时(毫秒)</label>
|
||
<input type="number" id="ocrUpstreamTimeoutMs" placeholder="30000"
|
||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">下载体积上限(MB)</label>
|
||
<input type="number" id="maxProxyDownloadMb" placeholder="100"
|
||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<button onclick="saveProxySettings()" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">保存代理设置</button>
|
||
<span class="text-xs text-gray-500">保存后约 60 秒内生效(服务端定时刷新)。</span>
|
||
<button onclick="refreshEffectiveProxySettings()" class="px-3 py-2 border rounded-md hover:bg-gray-50 text-sm">刷新生效配置</button>
|
||
<button onclick="applyProxySettingsNow()" class="px-3 py-2 border rounded-md hover:bg-gray-50 text-sm">立即应用配置</button>
|
||
<button onclick="clearProxyDomains()" class="px-3 py-2 border rounded-md hover:bg-gray-50 text-sm">清空手动域/Workers 域</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const API_BASE = window.location.origin + '/api';
|
||
let authToken = localStorage.getItem('admin_token');
|
||
let usersPage = 1;
|
||
let usersPageSize = 20;
|
||
let usersTotalPages = 1;
|
||
|
||
// 初始化
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
if (authToken) {
|
||
checkAuth();
|
||
} else {
|
||
showLoginPage();
|
||
}
|
||
});
|
||
|
||
// 登录
|
||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const email = document.getElementById('email').value;
|
||
const password = document.getElementById('password').value;
|
||
|
||
try {
|
||
const response = await axios.post(`${API_BASE}/auth/login`, {
|
||
email,
|
||
password
|
||
});
|
||
|
||
if (response.data.success && response.data.user.role === 'ADMIN') {
|
||
authToken = response.data.token;
|
||
localStorage.setItem('admin_token', authToken);
|
||
showAdminPanel(response.data.user);
|
||
} else {
|
||
showError('您没有管理员权限');
|
||
}
|
||
} catch (error) {
|
||
showError('登录失败:' + (error.response?.data?.error || error.message));
|
||
}
|
||
});
|
||
|
||
function showError(message) {
|
||
const errorDiv = document.getElementById('loginError');
|
||
errorDiv.textContent = message;
|
||
errorDiv.classList.remove('hidden');
|
||
setTimeout(() => errorDiv.classList.add('hidden'), 5000);
|
||
}
|
||
|
||
async function checkAuth() {
|
||
try {
|
||
const response = await axios.get(`${API_BASE}/auth/me`, {
|
||
headers: { Authorization: `Bearer ${authToken}` }
|
||
});
|
||
|
||
if (response.data.user.role === 'ADMIN') {
|
||
showAdminPanel(response.data.user);
|
||
} else {
|
||
logout();
|
||
}
|
||
} catch (error) {
|
||
logout();
|
||
}
|
||
}
|
||
|
||
function showLoginPage() {
|
||
document.getElementById('loginPage').classList.remove('hidden');
|
||
document.getElementById('adminPanel').classList.add('hidden');
|
||
}
|
||
|
||
async function showAdminPanel(user) {
|
||
document.getElementById('loginPage').classList.add('hidden');
|
||
document.getElementById('adminPanel').classList.remove('hidden');
|
||
// 使用 textContent 而不是 innerHTML 来防止 XSS
|
||
const adminNameElement = document.getElementById('adminName');
|
||
adminNameElement.textContent = user.name || user.email;
|
||
|
||
await loadStats();
|
||
await loadUsers();
|
||
}
|
||
|
||
async function loadStats() {
|
||
try {
|
||
const response = await axios.get(`${API_BASE}/admin/stats`, {
|
||
headers: { Authorization: `Bearer ${authToken}` }
|
||
});
|
||
|
||
const stats = response.data;
|
||
document.getElementById('totalUsers').textContent = stats.totalUsers;
|
||
document.getElementById('activeUsers').textContent = stats.activeUsers;
|
||
document.getElementById('totalDocuments').textContent = stats.totalDocuments;
|
||
document.getElementById('documentsToday').textContent = stats.documentsToday;
|
||
} catch (error) {
|
||
console.error('Failed to load stats:', error);
|
||
}
|
||
}
|
||
|
||
// 安全转义
|
||
function escapeHtml(text) {
|
||
if (!text) return '-';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
async function loadUsers() {
|
||
try {
|
||
const search = (document.getElementById('usersSearch')?.value || '').trim();
|
||
const params = new URLSearchParams({ page: String(usersPage), pageSize: String(usersPageSize), search });
|
||
const response = await axios.get(`${API_BASE}/admin/users?${params.toString()}`, { headers: { Authorization: `Bearer ${authToken}` } });
|
||
|
||
const data = response.data;
|
||
const usersList = document.getElementById('usersList');
|
||
usersList.innerHTML = data.items.map(user => {
|
||
// 额外安全处理:为 onclick 组装参数时转义引号,避免属性破坏
|
||
const safe = (s) => String(s ?? '').replaceAll('"', '"').replaceAll("'", ''');
|
||
const id = safe(user.id);
|
||
const email = safe(user.email);
|
||
const name = safe(user.name);
|
||
const role = safe(user.role);
|
||
const createdAt = new Date(user.createdAt).toLocaleDateString();
|
||
const isActive = !!user.isActive;
|
||
return `
|
||
<tr>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm">${escapeHtml(user.email)}</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm">${escapeHtml(user.name)}</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm">${escapeHtml(user.role)}</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||
<span class="px-2 py-1 rounded-full text-xs ${isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}">
|
||
${isActive ? '活跃' : '禁用'}
|
||
</span>
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm">${createdAt}</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||
<div class="flex items-center gap-3">
|
||
<button onclick="openEditUser('${id}','${email}','${name}','${role}')" class="text-blue-600 hover:text-blue-900">编辑</button>
|
||
<button onclick="toggleUserStatus('${id}', ${!isActive})" class="text-yellow-600 hover:text-yellow-800">${isActive ? '禁用' : '启用'}</button>
|
||
<button onclick="resetUserPasswordPrompt('${id}')" class="text-indigo-600 hover:text-indigo-800">重置密码</button>
|
||
<button onclick="deleteUserConfirm('${id}')" class="text-red-600 hover:text-red-800">删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
|
||
// 分页信息
|
||
usersTotalPages = Math.max(1, Math.ceil((data.total || 0) / data.pageSize));
|
||
document.getElementById('usersPagerInfo').textContent = `共 ${data.total} 条`;
|
||
document.getElementById('usersPagerPage').textContent = `第 ${data.page}/${usersTotalPages} 页`;
|
||
} catch (error) {
|
||
console.error('Failed to load users:', error);
|
||
}
|
||
}
|
||
|
||
async function toggleUserStatus(userId, isActive) {
|
||
try {
|
||
await axios.put(`${API_BASE}/admin/users/${userId}/status`,
|
||
{ isActive },
|
||
{ headers: { Authorization: `Bearer ${authToken}` } }
|
||
);
|
||
await loadUsers();
|
||
} catch (error) {
|
||
alert('操作失败:' + error.message);
|
||
}
|
||
}
|
||
|
||
// 创建/编辑/重置/删除用户
|
||
function openCreateUser() {
|
||
const email = prompt('请输入邮箱:');
|
||
if (!email) return;
|
||
const name = prompt('请输入姓名(可留空):') || '';
|
||
const role = prompt('角色(USER/ADMIN,默认 USER):') || 'USER';
|
||
const password = prompt('初始密码(至少 8 位,留空则自动生成):') || undefined;
|
||
createUser({ email, name, role, password });
|
||
}
|
||
|
||
async function createUser(payload) {
|
||
try {
|
||
const res = await axios.post(`${API_BASE}/admin/users`, payload, { headers: { Authorization: `Bearer ${authToken}` } });
|
||
alert('创建成功' + (res.data.tempPassword ? `,临时密码:${res.data.tempPassword}` : ''));
|
||
await loadUsers();
|
||
} catch (e) { alert('创建失败:' + (e.response?.data?.error || e.message)); }
|
||
}
|
||
|
||
function openEditUser(id, email, name, role) {
|
||
const newEmail = prompt('修改邮箱(留空不变):', email) || undefined;
|
||
const newName = prompt('修改姓名(可留空):', name) || undefined;
|
||
const newRole = prompt('修改角色(USER/ADMIN,留空不变):', role) || undefined;
|
||
const data = {};
|
||
if (newEmail && newEmail !== email) data.email = newEmail;
|
||
if (newName !== undefined && newName !== name) data.name = newName;
|
||
if (newRole && newRole !== role) data.role = newRole;
|
||
if (Object.keys(data).length === 0) return;
|
||
updateUser(id, data);
|
||
}
|
||
|
||
async function updateUser(id, data) {
|
||
try {
|
||
await axios.put(`${API_BASE}/admin/users/${id}`, data, { headers: { Authorization: `Bearer ${authToken}` } });
|
||
await loadUsers();
|
||
} catch (e) { alert('更新失败:' + (e.response?.data?.error || e.message)); }
|
||
}
|
||
|
||
function resetUserPasswordPrompt(id) {
|
||
const password = prompt('输入新密码(>=8 位):');
|
||
if (!password) return;
|
||
resetUserPassword(id, password);
|
||
}
|
||
|
||
async function resetUserPassword(id, password) {
|
||
try {
|
||
await axios.put(`${API_BASE}/admin/users/${id}/password`, { password }, { headers: { Authorization: `Bearer ${authToken}` } });
|
||
alert('密码已重置');
|
||
} catch (e) { alert('重置失败:' + (e.response?.data?.error || e.message)); }
|
||
}
|
||
|
||
function deleteUserConfirm(id) {
|
||
if (!confirm('确认删除该用户?此操作不可恢复。')) return;
|
||
deleteUser(id);
|
||
}
|
||
|
||
async function deleteUser(id) {
|
||
try {
|
||
await axios.delete(`${API_BASE}/admin/users/${id}`, { headers: { Authorization: `Bearer ${authToken}` } });
|
||
await loadUsers();
|
||
} catch (e) { alert('删除失败:' + (e.response?.data?.error || e.message)); }
|
||
}
|
||
|
||
function switchTab(tab) {
|
||
// 更新选项卡样式
|
||
document.querySelectorAll('.tab-button').forEach(btn => {
|
||
btn.classList.remove('border-blue-500', 'text-blue-600');
|
||
btn.classList.add('border-transparent', 'text-gray-500');
|
||
});
|
||
const activeBtn = document.getElementById(`tab-${tab}`);
|
||
if (activeBtn) {
|
||
activeBtn.classList.add('border-blue-500', 'text-blue-600');
|
||
activeBtn.classList.remove('border-transparent', 'text-gray-500');
|
||
}
|
||
|
||
// 切换内容
|
||
['overview', 'users', 'quotas', 'activity', 'models', 'system'].forEach(t => {
|
||
const el = document.getElementById(`content-${t}`);
|
||
if (el) el.classList.toggle('hidden', t !== tab);
|
||
});
|
||
}
|
||
|
||
function logout() {
|
||
localStorage.removeItem('admin_token');
|
||
authToken = null;
|
||
showLoginPage();
|
||
}
|
||
|
||
// 占位函数
|
||
function addSourceSite() {
|
||
alert('自定义源站管理功能开发中...');
|
||
}
|
||
|
||
function saveSystemSettings() {
|
||
alert('系统设置保存功能开发中...');
|
||
}
|
||
</script>
|
||
<script src="admin-enhanced.js"></script>
|
||
</body>
|
||
</html>
|
||
|