1.我们的Nmpostor服务器管理面板
https://auserverpanel.fanchuanovo.cn/
由清风AmongUs服务器的开源代码修改
2.我们的开源htmlCDJ面板
https://aucdj.fanchuanovo.cn/
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>帆船Nmpostor车队姬管理面板</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<style>
:root {
--primary-color: #3498db;
--secondary-color: #2c3e50;
--accent-color: #e74c3c;
--success-color: #2ecc71;
--warning-color: #f39c12;
--dark-bg: #1a1d29;
--card-bg: #252836;
--card-hover: #2e3243;
--text-light: #f8f9fa;
--text-muted: #adb5bd;
--border-color: rgba(255, 255, 255, 0.1);
--input-bg: rgba(0, 0, 0, 0.3);
--table-header-bg: rgba(0, 0, 0, 0.2);
--table-striped-bg: rgba(255, 255, 255, 0.03);
--table-hover-bg: rgba(255, 255, 255, 0.08);
--header-bg: linear-gradient(135deg, rgba(52, 152, 219, 0.1), rgba(231, 76, 60, 0.1));
}
[data-theme="light"] {
--dark-bg: #f8f9fa;
--card-bg: #ffffff;
--card-hover: #f0f0f0;
--text-light: #212529;
--text-muted: #6c757d;
--border-color: rgba(0, 0, 0, 0.1);
--input-bg: rgba(0, 0, 0, 0.05);
--table-header-bg: rgba(0, 0, 0, 0.05);
--table-striped-bg: rgba(0, 0, 0, 0.02);
--table-hover-bg: rgba(0, 0, 0, 0.05);
--header-bg: linear-gradient(135deg, rgba(52, 152, 219, 0.05), rgba(231, 76, 60, 0.05));
}
body {
background-color: var(--dark-bg);
color: var(--text-light);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding-top: 20px;
min-height: 100vh;
transition: background-color 0.3s, color 0.3s;
}
.container {
max-width: 1200px;
}
.card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
transition: transform 0.3s, box-shadow 0.3s, background-color 0.3s, border-color 0.3s;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--table-header-bg);
border-bottom: 1px solid var(--border-color);
padding: 15px 20px;
border-radius: 12px 12px 0 0 !important;
transition: background-color 0.3s, border-color 0.3s;
}
.card-header h5 {
margin: 0;
font-weight: 600;
color: var(--text-light);
}
.card-body {
padding: 20px;
}
.table {
color: var(--text-light);
margin-bottom: 0;
transition: color 0.3s;
}
.table th {
border-top: none;
background-color: var(--table-header-bg);
color: var(--text-muted);
font-weight: 600;
padding: 12px 15px;
transition: background-color 0.3s, color 0.3s;
}
.table td {
padding: 12px 15px;
border-color: var(--border-color);
vertical-align: middle;
transition: border-color 0.3s;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: var(--table-striped-bg);
transition: background-color 0.3s;
}
.table-hover tbody tr:hover {
background-color: var(--table-hover-bg);
transition: background-color 0.3s;
}
.nav-tabs {
border-bottom: 1px solid var(--border-color);
margin-bottom: 20px;
transition: border-color 0.3s;
}
.nav-tabs .nav-link {
color: var(--text-muted);
border: none;
padding: 12px 20px;
border-radius: 8px 8px 0 0;
margin-right: 5px;
transition: all 0.3s;
font-weight: 500;
}
.nav-tabs .nav-link:hover {
color: var(--text-light);
background-color: var(--table-striped-bg);
}
.nav-tabs .nav-link.active {
color: var(--primary-color);
background-color: var(--card-bg);
border-bottom: 2px solid var(--primary-color);
}
.form-control, .form-select {
background-color: var(--input-bg);
border: 1px solid var(--border-color);
color: var(--text-light);
border-radius: 6px;
padding: 10px 15px;
transition: background-color 0.3s, border-color 0.3s, color 0.3s;
}
.form-control:focus, .form-select:focus {
background-color: var(--input-bg);
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
color: var(--text-light);
}
.form-label {
color: var(--text-light);
font-weight: 500;
margin-bottom: 8px;
transition: color 0.3s;
}
.form-text {
color: var(--text-muted);
transition: color 0.3s;
}
.btn {
border-radius: 6px;
font-weight: 500;
padding: 8px 16px;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 5px;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover {
background-color: #2980b9;
border-color: #2980b9;
transform: translateY(-2px);
}
.btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
}
.btn-success {
background-color: var(--success-color);
border-color: var(--success-color);
}
.btn-warning {
background-color: var(--warning-color);
border-color: var(--warning-color);
color: white;
}
.btn-danger {
background-color: var(--accent-color);
border-color: var(--accent-color);
}
.btn-outline-secondary {
color: var(--text-muted);
border-color: var(--text-muted);
}
.btn-outline-secondary:hover {
background-color: var(--text-muted);
border-color: var(--text-muted);
color: var(--dark-bg);
}
.alert {
border-radius: 8px;
border: none;
}
.alert-info {
background-color: rgba(52, 152, 219, 0.2);
color: var(--text-light);
}
.modal-content {
background-color: var(--card-bg);
border-radius: 12px;
border: 1px solid var(--border-color);
transition: background-color 0.3s, border-color 0.3s;
}
.modal-header {
border-bottom: 1px solid var(--border-color);
transition: border-color 0.3s;
}
.modal-footer {
border-top: 1px solid var(--border-color);
transition: border-color 0.3s;
}
.modal-title {
color: var(--text-light);
transition: color 0.3s;
}
.form-check-input:checked {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
header h1 {
color: var(--text-light);
font-weight: 700;
margin-bottom: 25px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
display: inline-block;
transition: color 0.3s;
}
header h1::after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 3px;
background: linear-gradient(to right, var(--primary-color), var(--accent-color));
border-radius: 3px;
}
.header-section {
background: var(--header-bg);
border-radius: 12px;
padding: 20px;
margin-bottom: 25px;
border: 1px solid var(--border-color);
transition: background 0.3s, border-color 0.3s;
}
footer {
margin-top: 40px;
padding: 20px 0;
border-top: 1px solid var(--border-color);
color: var(--text-muted);
transition: border-color 0.3s, color 0.3s;
}
footer a {
color: var(--text-muted);
text-decoration: none;
transition: color 0.3s;
}
footer a:hover {
color: var(--primary-color);
}
.dropdown-menu {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s, border-color 0.3s;
}
.dropdown-item {
color: var(--text-light);
transition: color 0.3s, background-color 0.3s;
}
.dropdown-item:hover {
background-color: var(--table-striped-bg);
color: var(--text-light);
}
.dropdown-item-text {
color: var(--text-muted);
}
.input-group-text {
background-color: var(--input-bg);
border: 1px solid var(--border-color);
color: var(--text-muted);
transition: background-color 0.3s, border-color 0.3s, color 0.3s;
}
.badge {
font-weight: 500;
padding: 6px 10px;
border-radius: 6px;
}
/* 主题切换按钮 */
.theme-switcher {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.theme-switcher .btn {
border-radius: 50%;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--table-striped-bg);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* 响应式调整 */
@media (max-width: 768px) {
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.card-header div {
width: 100%;
display: flex;
gap: 10px;
}
.card-header div .btn {
flex: 1;
}
.theme-switcher {
position: static;
margin-bottom: 20px;
display: flex;
justify-content: flex-end;
}
}
</style>
</head>
<body data-theme="dark">
<div class="theme-switcher">
<button class="btn btn-primary" id="themeToggle">
<i class="bi bi-sun" id="themeIcon"></i>
</button>
</div>
<div class="container">
<header class="mb-4 text-center">
<h1><i class="bi bi-speedometer2 me-2"></i>帆船Nmpostor车队姬管理面板</h1>
<div class="header-section">
<div class="row g-3 align-items-center">
<div class="col-md-5">
<label for="serverAddress" class="form-label"><i class="bi bi-server me-1"></i>API 服务器地址</label>
<div class="input-group">
<input type="text" class="form-control" id="serverAddress"
placeholder="例如: http://localhost:5000/api/cdj">
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
data-bs-toggle="dropdown" title="历史记录">
<i class="bi bi-clock-history"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end" id="historyDropdown">
<li><span class="dropdown-item-text text-muted">加载中...</span></li>
</ul>
</div>
</div>
</div>
<div class="col-md-5">
<label for="authToken" class="form-label"><i class="bi bi-key me-1"></i>认证 Token (可选)</label>
<input type="text" class="form-control" id="authToken" placeholder="如果需要认证,请在此处输入 Token">
</div>
<div class="col-md-2">
<label class="form-label"> </label>
<div class="d-grid">
<button class="btn btn-outline-danger btn-sm" onclick="clearAllHistory()" title="清除所有历史记录">
<i class="bi bi-trash"></i> 清除历史
</button>
</div>
</div>
</div>
</div>
</header>
<ul class="nav nav-tabs" id="mainTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="players-tab" data-bs-toggle="tab" data-bs-target="#players-tab-pane"
type="button" role="tab">
<i class="bi bi-people me-1"></i>玩家管理
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="onebot-tab" data-bs-toggle="tab" data-bs-target="#onebot-tab-pane"
type="button" role="tab">
<i class="bi bi-robot me-1"></i>OneBot 配置
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="templates-tab" data-bs-toggle="tab" data-bs-target="#templates-tab-pane"
type="button" role="tab">
<i class="bi bi-chat-text me-1"></i>消息模板
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="server-tab" data-bs-toggle="tab" data-bs-target="#server-tab-pane"
type="button" role="tab">
<i class="bi bi-gear me-1"></i>服务器设置
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="token-tab" data-bs-toggle="tab" data-bs-target="#token-tab-pane"
type="button" role="tab">
<i class="bi bi-shield-lock me-1"></i>Token 管理
</button>
</li>
</ul>
<div class="tab-content" id="mainTabContent">
<!-- 玩家管理 Tab -->
<div class="tab-pane fade show active" id="players-tab-pane" role="tabpanel">
<div class="card mt-3">
<div class="card-header">
<h5><i class="bi bi-people me-2"></i>玩家列表</h5>
<div>
<button class="btn btn-primary btn-sm" id="addPlayerBtn">
<i class="bi bi-plus-circle"></i> 添加玩家
</button>
<button class="btn btn-secondary btn-sm" id="refreshPlayersBtn">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>PUID</th>
<th>Token Platform</th>
<th>QQ ID</th>
<th>使用CDJ</th>
<th>OneBot 配置</th>
<th>操作</th>
</tr>
</thead>
<tbody id="playersTableBody">
<!-- 玩家数据将在这里动态生成 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- OneBot 配置 Tab -->
<div class="tab-pane fade" id="onebot-tab-pane" role="tabpanel">
<div class="card mt-3">
<div class="card-header">
<h5><i class="bi bi-robot me-2"></i>OneBot 配置列表</h5>
<div>
<button class="btn btn-primary btn-sm" id="addOneBotConfigBtn">
<i class="bi bi-plus-circle"></i> 添加配置
</button>
<button class="btn btn-secondary btn-sm" id="refreshOneBotConfigsBtn">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>配置名称</th>
<th>启用状态</th>
<th>URL</th>
<th>类型</th>
<th>目标ID</th>
<th>操作</th>
</tr>
</thead>
<tbody id="oneBotConfigsTableBody">
<!-- OneBot 配置将在这里动态生成 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 消息模板 Tab -->
<div class="tab-pane fade" id="templates-tab-pane" role="tabpanel">
<div class="card mt-3">
<div class="card-header">
<h5><i class="bi bi-chat-text me-2"></i>消息模板</h5>
<div>
<button class="btn btn-primary btn-sm" id="saveMessageTemplatesBtn">
<i class="bi bi-check-circle"></i> 保存更改
</button>
<button class="btn btn-secondary btn-sm" id="refreshMessageTemplatesBtn">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
</div>
</div>
<div class="card-body">
<form id="messageTemplatesForm">
<div class="mb-3">
<label for="templateRoomCreated" class="form-label">房间创建</label>
<textarea class="form-control" id="templateRoomCreated" name="RoomCreated"
rows="2"></textarea>
</div>
<div class="mb-3">
<label for="templateGameStarted" class="form-label">游戏开始</label>
<textarea class="form-control" id="templateGameStarted" name="GameStarted"
rows="2"></textarea>
</div>
<div class="mb-3">
<label for="templateGameEnded" class="form-label">游戏结束</label>
<textarea class="form-control" id="templateGameEnded" name="GameEnded"
rows="2"></textarea>
</div>
<div class="mb-3">
<label for="templateHostChanged" class="form-label">房主变更</label>
<textarea class="form-control" id="templateHostChanged" name="HostChanged"
rows="2"></textarea>
</div>
<div class="mb-3">
<label for="templateCdjAvailable" class="form-label">CDJ可用通知</label>
<textarea class="form-control" id="templateCdjAvailable" name="CdjAvailableNotice"
rows="2"></textarea>
</div>
</form>
</div>
</div>
</div>
<!-- 服务器设置 Tab -->
<div class="tab-pane fade" id="server-tab-pane" role="tabpanel">
<div class="card mt-3">
<div class="card-header">
<h5><i class="bi bi-gear me-2"></i>服务器名称</h5>
</div>
<div class="card-body">
<div class="input-group">
<input type="text" class="form-control" id="serverNameInput" placeholder="输入服务器名称">
<button class="btn btn-primary" id="updateServerNameBtn">
<i class="bi bi-check-lg"></i> 更新
</button>
<button class="btn btn-secondary" id="refreshServerNameBtn">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
</div>
</div>
</div>
</div>
<!-- Token 管理 Tab -->
<div class="tab-pane fade" id="token-tab-pane" role="tabpanel">
<div class="card mt-3">
<div class="card-header">
<h5><i class="bi bi-shield-lock me-2"></i>管理 Token</h5>
</div>
<div class="card-body">
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle me-2"></i>
<strong>提示:</strong>如果您是首次设置或忘记了当前 Token,可以在服务器配置文件 <code>Niko.CDJPluginConfig.json</code> 中查看初始 Token 设置。
</div>
<div class="mb-3">
<label for="currentTokenDisplay" class="form-label">当前 Token</label>
<div class="input-group">
<input type="password" class="form-control" id="currentTokenDisplay" readonly>
<button class="btn btn-outline-secondary" type="button" id="toggleCurrentTokenBtn" title="显示/隐藏">
<i class="bi bi-eye"></i>
</button>
</div>
<div class="form-text">当前正在使用的管理 Token</div>
</div>
<div class="mb-3">
<label for="newTokenInput" class="form-label">新 Token</label>
<div class="input-group">
<input type="text" class="form-control" id="newTokenInput" placeholder="输入新的管理 Token">
<button class="btn btn-success" id="generateTokenBtn" title="生成随机 Token">
<i class="bi bi-dice-5"></i> 生成随机 Token
</button>
</div>
<div class="form-text">留空将禁用 Token 验证,任何人都可以访问管理接口</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<button class="btn btn-primary" id="updateTokenBtn">
<i class="bi bi-check-lg"></i> 更新 Token
</button>
<button class="btn btn-secondary" id="refreshTokenBtn">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
</div>
</div>
</div>
</div>
</div>
<footer class="text-center">
<p class="text-secondary small mb-2">
<i class="bi bi-c-circle me-1"></i>2025 By
<a href="https://fcaugame.cn" target="_blank"
class="link-secondary link-offset-1 link-underline-opacity-25 link-underline-opacity-100-hover">
帆船AmongUs服务器丨
</a>
<a href="https://fcaugame.cn" target="_blank"
class="link-secondary link-offset-1 link-underline-opacity-25 link-underline-opacity-100-hover">
当前版本:sabcdjwebui-2.0丨
</a>
<a href="https://beian.miit.gov.cn/" target="_blank">粤ICP备2025466846号-2</a>
</p>
</footer>
</div>
<!-- 玩家编辑/添加 Modal -->
<div class="modal fade" id="playerModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="playerModalTitle">玩家信息</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="playerForm">
<input type="hidden" id="player-original-puid">
<input type="hidden" id="player-original-tokenPlatform">
<div class="row">
<div class="col-md-6 mb-3">
<label for="player-puid" class="form-label">PUID</label>
<input type="text" class="form-control" id="player-puid" required>
</div>
<div class="col-md-6 mb-3">
<label for="player-tokenPlatform" class="form-label">Token Platform</label>
<input type="text" class="form-control" id="player-tokenPlatform" required>
</div>
</div>
<div class="mb-3">
<label for="player-qqId" class="form-label">QQ ID</label>
<input type="number" class="form-control" id="player-qqId" required>
</div>
<div class="mb-3">
<label for="player-oneBotConfigNames" class="form-label">OneBot 配置名称 (用逗号分隔)</label>
<input type="text" class="form-control" id="player-oneBotConfigNames">
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="player-announceRoomOnCreated">
<label class="form-check-label" for="player-announceRoomOnCreated">创建房间时通知</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="player-announceGameStartEnd">
<label class="form-check-label" for="player-announceGameStartEnd">游戏开始/结束时通知</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="player-useCDJ">
<label class="form-check-label" for="player-useCDJ">启用 CDJ 功能</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" id="savePlayerBtn">保存</button>
</div>
</div>
</div>
</div>
<!-- OneBot 配置编辑/添加 Modal -->
<div class="modal fade" id="oneBotConfigModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="oneBotConfigModalTitle">OneBot 配置</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="oneBotConfigForm">
<input type="hidden" id="onebot-original-name">
<div class="mb-3">
<label for="onebot-name" class="form-label">配置名称</label>
<input type="text" class="form-control" id="onebot-name" required>
</div>
<div class="mb-3">
<label for="onebot-url" class="form-label">URL</label>
<input type="text" class="form-control" id="onebot-url" required>
</div>
<div class="mb-3">
<label for="onebot-token" class="form-label">Token</label>
<input type="text" class="form-control" id="onebot-token">
</div>
<div class="mb-3">
<label for="onebot-type" class="form-label">类型</label>
<select class="form-select" id="onebot-type">
<option value="private">private</option>
<option value="group">group</option>
</select>
</div>
<div class="mb-3">
<label for="onebot-targetId" class="form-label">目标 ID</label>
<input type="number" class="form-control" id="onebot-targetId" required>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="onebot-enabled">
<label class="form-check-label" for="onebot-enabled">启用</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" id="saveOneBotConfigBtn">保存</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 主题切换功能
const themeToggle = document.getElementById('themeToggle');
const themeIcon = document.getElementById('themeIcon');
const body = document.body;
// 初始化主题
function initTheme() {
const savedTheme = localStorage.getItem('theme') || 'dark';
body.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
}
// 更新主题图标
function updateThemeIcon(theme) {
if (theme === 'dark') {
themeIcon.className = 'bi bi-sun';
themeToggle.title = '切换到浅色模式';
} else {
themeIcon.className = 'bi bi-moon';
themeToggle.title = '切换到深色模式';
}
}
// 切换主题
themeToggle.addEventListener('click', () => {
const currentTheme = body.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
body.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
});
// 全局变量和辅助函数
const getApiBase = () => document.getElementById('serverAddress').value;
const getAuthToken = () => document.getElementById('authToken').value;
// 历史记录管理
const STORAGE_KEY = 'cdj-web-manager-history';
function saveToHistory() {
const serverAddress = getApiBase();
const authToken = getAuthToken();
if (!serverAddress) return;
let history = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
// 移除重复项
history = history.filter(item => item.serverAddress !== serverAddress);
// 添加到开头
history.unshift({
serverAddress,
authToken,
lastUsed: Date.now()
});
// 只保留最近10条记录
history = history.slice(0, 10);
localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
updateHistoryDropdown();
}
function loadFromHistory() {
const history = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
if (history.length > 0) {
const latest = history[0];
document.getElementById('serverAddress').value = latest.serverAddress;
document.getElementById('authToken').value = latest.authToken || '';
}
updateHistoryDropdown();
}
function updateHistoryDropdown() {
const history = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
const dropdown = document.getElementById('historyDropdown');
if (history.length === 0) {
dropdown.innerHTML = '<li><span class="dropdown-item-text text-muted">暂无历史记录</span></li>';
return;
}
dropdown.innerHTML = history.map((item, index) => {
const date = new Date(item.lastUsed).toLocaleString();
return `
<li>
<a class="dropdown-item" href="#" onclick="loadHistoryItem(${index})" title="使用时间: ${date}">
<div class="d-flex justify-content-between align-items-center">
<span class="text-truncate" style="max-width: 200px;">${item.serverAddress}</span>
<button class="btn btn-outline-danger btn-sm ms-2" onclick="removeHistoryItem(${index}); event.stopPropagation();" title="删除">×</button>
</div>
</a>
</li>
`;
}).join('');
}
function loadHistoryItem(index) {
const history = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
if (history[index]) {
document.getElementById('serverAddress').value = history[index].serverAddress;
document.getElementById('authToken').value = history[index].authToken || '';
// 更新最后使用时间
history[index].lastUsed = Date.now();
localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
updateHistoryDropdown();
// 自动加载当前标签页数据
loadCurrentTabData();
}
}
function removeHistoryItem(index) {
let history = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
history.splice(index, 1);
localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
updateHistoryDropdown();
}
function clearAllHistory() {
if (confirm('确定要清除所有历史记录吗?')) {
localStorage.removeItem(STORAGE_KEY);
updateHistoryDropdown();
}
}
function loadCurrentTabData() {
const activeTab = document.querySelector('.tab-pane.active');
if (!activeTab || !getApiBase()) return;
const tabId = activeTab.id;
switch (tabId) {
case 'players-tab-pane':
loadPlayers();
break;
case 'onebot-tab-pane':
loadOneBotConfigs();
break;
case 'templates-tab-pane':
loadMessageTemplates();
break;
case 'server-tab-pane':
loadServerName();
break;
case 'token-tab-pane':
loadManageToken();
break;
}
}
// 清除当前标签页的提示信息并显示正确内容
function clearTabPlaceholder() {
const activeTab = document.querySelector('.tab-pane.active');
if (activeTab && activeTab.querySelector('.alert-info')) {
// 移除提示信息,恢复原始内容结构
if (activeTab.id === 'players-tab-pane') {
activeTab.innerHTML = `
<div class="card mt-3">
<div class="card-header">
<h5><i class="bi bi-people me-2"></i>玩家列表</h5>
<div>
<button class="btn btn-primary btn-sm" id="addPlayerBtn">
<i class="bi bi-plus-circle"></i> 添加玩家
</button>
<button class="btn btn-secondary btn-sm" id="refreshPlayersBtn">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>PUID</th>
<th>Token Platform</th>
<th>QQ ID</th>
<th>使用CDJ</th>
<th>OneBot 配置</th>
<th>操作</th>
</tr>
</thead>
<tbody id="playersTableBody">
<!-- 玩家数据将在这里动态生成 -->
</tbody>
</table>
</div>
</div>
</div>
`;
// 重新绑定事件监听器
rebindPlayerEvents();
}
}
}
// 重新绑定玩家管理相关的事件监听器
function rebindPlayerEvents() {
const addPlayerBtn = document.getElementById('addPlayerBtn');
const refreshPlayersBtn = document.getElementById('refreshPlayersBtn');
if (addPlayerBtn) {
addPlayerBtn.addEventListener('click', () => {
isEditingPlayer = false;
document.getElementById('playerForm').reset();
document.getElementById('playerModalTitle').textContent = '添加新玩家';
document.getElementById('player-puid').readOnly = false;
document.getElementById('player-tokenPlatform').readOnly = false;
playerModal.show();
});
}
if (refreshPlayersBtn) {
refreshPlayersBtn.addEventListener('click', loadPlayers);
}
}
async function apiFetch(endpoint, options = {}) {
const url = `${getApiBase()}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
const token = getAuthToken();
if (token) {
headers['Authorization'] = token;
}
try {
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: '请求失败', message: response.statusText }));
throw new Error(errorData.message || errorData.error || '未知错误');
}
// DELETE 请求可能没有响应体
if (response.status === 204 || response.headers.get("content-length") === "0") {
return { success: true };
}
return await response.json();
} catch (error) {
alert(`API 操作失败: ${error.message}`);
throw error;
}
}
// 玩家管理
const playersTableBody = document.getElementById('playersTableBody');
const playerModal = new bootstrap.Modal(document.getElementById('playerModal'));
let isEditingPlayer = false;
async function loadPlayers() {
if (!getApiBase()) return;
try {
const players = await apiFetch('/players');
playersTableBody.innerHTML = '';
players.forEach(p => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${p.Puid}</td>
<td>${p.TokenPlatform}</td>
<td>${p.QqId}</td>
<td><span class="badge ${p.UseCDJ ? 'bg-success' : 'bg-secondary'}">${p.UseCDJ ? '是' : '否'}</span></td>
<td>${p.OneBotConfigNames.join(', ')}</td>
<td>
<button class="btn btn-warning btn-sm" onclick="editPlayer(this)"><i class="bi bi-pencil"></i> 编辑</button>
<button class="btn btn-danger btn-sm" onclick="deletePlayer('${p.Puid}', '${p.TokenPlatform}')"><i class="bi bi-trash"></i> 删除</button>
</td>
`;
row.dataset.player = JSON.stringify(p);
playersTableBody.appendChild(row);
});
} catch (error) {
console.error('加载玩家列表失败:', error);
}
}
document.getElementById('addPlayerBtn').addEventListener('click', () => {
isEditingPlayer = false;
document.getElementById('playerForm').reset();
document.getElementById('playerModalTitle').textContent = '添加新玩家';
document.getElementById('player-puid').readOnly = false;
document.getElementById('player-tokenPlatform').readOnly = false;
playerModal.show();
});
function editPlayer(btn) {
isEditingPlayer = true;
const playerData = JSON.parse(btn.closest('tr').dataset.player);
document.getElementById('playerModalTitle').textContent = '编辑玩家信息';
document.getElementById('player-original-puid').value = playerData.Puid;
document.getElementById('player-original-tokenPlatform').value = playerData.TokenPlatform;
document.getElementById('player-puid').value = playerData.Puid;
document.getElementById('player-tokenPlatform').value = playerData.TokenPlatform;
document.getElementById('player-puid').readOnly = true;
document.getElementById('player-tokenPlatform').readOnly = true;
document.getElementById('player-qqId').value = playerData.QqId;
document.getElementById('player-oneBotConfigNames').value = playerData.OneBotConfigNames.join(', ');
document.getElementById('player-announceRoomOnCreated').checked = playerData.AnnounceRoomOnCreated;
document.getElementById('player-announceGameStartEnd').checked = playerData.AnnounceGameStartEnd;
document.getElementById('player-useCDJ').checked = playerData.UseCDJ;
playerModal.show();
}
document.getElementById('savePlayerBtn').addEventListener('click', async () => {
const puid = document.getElementById('player-puid').value;
const tokenPlatform = document.getElementById('player-tokenPlatform').value;
const playerConfig = {
Puid: puid,
TokenPlatform: tokenPlatform,
QqId: parseInt(document.getElementById('player-qqId').value, 10),
AnnounceRoomOnCreated: document.getElementById('player-announceRoomOnCreated').checked,
AnnounceGameStartEnd: document.getElementById('player-announceGameStartEnd').checked,
UseCDJ: document.getElementById('player-useCDJ').checked,
OneBotConfigNames: document.getElementById('player-oneBotConfigNames').value.split(',').map(s => s.trim()).filter(Boolean),
};
try {
if (isEditingPlayer) {
const originalPuid = document.getElementById('player-original-puid').value;
const originalTokenPlatform = document.getElementById('player-original-tokenPlatform').value;
await apiFetch(`/players/${originalPuid}/${originalTokenPlatform}`, {
method: 'PUT',
body: JSON.stringify(playerConfig),
});
} else {
await apiFetch('/players', {
method: 'POST',
body: JSON.stringify(playerConfig),
});
}
playerModal.hide();
loadPlayers();
} catch (error) {
console.error('保存玩家失败:', error);
}
});
async function deletePlayer(puid, tokenPlatform) {
if (confirm(`确定要删除玩家 ${puid} (${tokenPlatform}) 吗?`)) {
try {
await apiFetch(`/players/${puid}/${tokenPlatform}`, { method: 'DELETE' });
loadPlayers();
} catch (error) {
console.error('删除玩家失败:', error);
}
}
}
document.getElementById('refreshPlayersBtn').addEventListener('click', loadPlayers);
// OneBot 配置管理
const oneBotConfigsTableBody = document.getElementById('oneBotConfigsTableBody');
const oneBotConfigModal = new bootstrap.Modal(document.getElementById('oneBotConfigModal'));
let isEditingOneBotConfig = false;
async function loadOneBotConfigs() {
if (!getApiBase()) return;
try {
const configs = await apiFetch('/onebot');
oneBotConfigsTableBody.innerHTML = '';
Object.entries(configs).forEach(([name, config]) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${name}</td>
<td><span class="badge ${config.Enabled ? 'bg-success' : 'bg-secondary'}">${config.Enabled ? '是' : '否'}</span></td>
<td>${config.Url}</td>
<td><span class="badge ${config.Type === 'private' ? 'bg-info' : 'bg-warning'}">${config.Type}</span></td>
<td>${config.TargetId}</td>
<td>
<button class="btn btn-warning btn-sm" onclick="editOneBotConfig(this)"><i class="bi bi-pencil"></i> 编辑</button>
<button class="btn btn-danger btn-sm" onclick="deleteOneBotConfig('${name}')"><i class="bi bi-trash"></i> 删除</button>
</td>
`;
row.dataset.name = name;
row.dataset.config = JSON.stringify(config);
oneBotConfigsTableBody.appendChild(row);
});
} catch (error) {
console.error('加载OneBot配置失败:', error);
}
}
document.getElementById('addOneBotConfigBtn').addEventListener('click', () => {
isEditingOneBotConfig = false;
document.getElementById('oneBotConfigForm').reset();
document.getElementById('oneBotConfigModalTitle').textContent = '添加新OneBot配置';
document.getElementById('onebot-name').readOnly = false;
oneBotConfigModal.show();
});
function editOneBotConfig(btn) {
isEditingOneBotConfig = true;
const name = btn.closest('tr').dataset.name;
const config = JSON.parse(btn.closest('tr').dataset.config);
document.getElementById('oneBotConfigModalTitle').textContent = '编辑OneBot配置';
document.getElementById('onebot-original-name').value = name;
document.getElementById('onebot-name').value = name;
document.getElementById('onebot-name').readOnly = true;
document.getElementById('onebot-url').value = config.Url;
document.getElementById('onebot-token').value = config.Token;
document.getElementById('onebot-type').value = config.Type;
document.getElementById('onebot-targetId').value = config.TargetId;
document.getElementById('onebot-enabled').checked = config.Enabled;
oneBotConfigModal.show();
}
document.getElementById('saveOneBotConfigBtn').addEventListener('click', async () => {
const name = document.getElementById('onebot-name').value;
const config = {
Enabled: document.getElementById('onebot-enabled').checked,
Url: document.getElementById('onebot-url').value,
Token: document.getElementById('onebot-token').value,
Type: document.getElementById('onebot-type').value,
TargetId: parseInt(document.getElementById('onebot-targetId').value, 10),
};
try {
const originalName = document.getElementById('onebot-original-name').value;
const endpoint = isEditingOneBotConfig ? `/onebot/${originalName}` : `/onebot/${name}`;
const method = isEditingOneBotConfig ? 'PUT' : 'POST';
if (isEditingOneBotConfig) {
await apiFetch(`/onebot/${originalName}`, { method: 'PUT', body: JSON.stringify(config) });
} else {
await apiFetch(`/onebot/${name}`, { method: 'POST', body: JSON.stringify(config) });
}
oneBotConfigModal.hide();
loadOneBotConfigs();
} catch (error) {
console.error('保存OneBot配置失败:', error);
}
});
async function deleteOneBotConfig(name) {
if (confirm(`确定要删除配置 ${name} 吗?`)) {
try {
await apiFetch(`/onebot/${name}`, { method: 'DELETE' });
loadOneBotConfigs();
} catch (error) {
console.error('删除OneBot配置失败:', error);
}
}
}
document.getElementById('refreshOneBotConfigsBtn').addEventListener('click', loadOneBotConfigs);
// 消息模板管理
async function loadMessageTemplates() {
if (!getApiBase()) return;
try {
const templates = await apiFetch('/messages');
document.getElementById('templateRoomCreated').value = templates.RoomCreated;
document.getElementById('templateGameStarted').value = templates.GameStarted;
document.getElementById('templateGameEnded').value = templates.GameEnded;
document.getElementById('templateHostChanged').value = templates.HostChanged;
document.getElementById('templateCdjAvailable').value = templates.CdjAvailableNotice;
} catch (error) {
console.error('加载消息模板失败:', error);
}
}
document.getElementById('saveMessageTemplatesBtn').addEventListener('click', async () => {
const templates = {
RoomCreated: document.getElementById('templateRoomCreated').value,
GameStarted: document.getElementById('templateGameStarted').value,
GameEnded: document.getElementById('templateGameEnded').value,
HostChanged: document.getElementById('templateHostChanged').value,
CdjAvailableNotice: document.getElementById('templateCdjAvailable').value,
};
try {
await apiFetch('/messages', {
method: 'PUT',
body: JSON.stringify(templates),
});
alert('消息模板更新成功!');
loadMessageTemplates();
} catch (error) {
console.error('更新消息模板失败:', error);
}
});
document.getElementById('refreshMessageTemplatesBtn').addEventListener('click', loadMessageTemplates);
// 服务器名称管理
async function loadServerName() {
if (!getApiBase()) return;
try {
const data = await apiFetch('/server-name');
document.getElementById('serverNameInput').value = data.serverName;
} catch (error) {
console.error('加载服务器名称失败:', error);
}
}
document.getElementById('updateServerNameBtn').addEventListener('click', async () => {
const serverName = document.getElementById('serverNameInput').value;
try {
await apiFetch('/server-name', {
method: 'PUT',
body: JSON.stringify({ serverName: serverName }), // 修改:发送对象而不是纯字符串
headers: { 'Content-Type': 'application/json' }
});
alert('服务器名称更新成功!');
loadServerName();
} catch (error) {
console.error('更新服务器名称失败:', error);
}
});
document.getElementById('refreshServerNameBtn').addEventListener('click', loadServerName);
// Token 管理
async function loadManageToken() {
if (!getApiBase()) return;
try {
const data = await apiFetch('/token');
const currentToken = data.manageToken || '';
document.getElementById('currentTokenDisplay').value = currentToken;
document.getElementById('newTokenInput').value = '';
} catch (error) {
console.error('加载管理Token失败:', error);
}
}
// 生成随机 Token
function generateRandomToken() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 16; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
document.getElementById('generateTokenBtn').addEventListener('click', () => {
const newToken = generateRandomToken();
document.getElementById('newTokenInput').value = newToken;
});
// 切换当前 Token 显示/隐藏
document.getElementById('toggleCurrentTokenBtn').addEventListener('click', () => {
const tokenDisplay = document.getElementById('currentTokenDisplay');
const toggleBtn = document.getElementById('toggleCurrentTokenBtn');
if (tokenDisplay.type === 'password') {
tokenDisplay.type = 'text';
toggleBtn.innerHTML = '<i class="bi bi-eye-slash"></i>';
} else {
tokenDisplay.type = 'password';
toggleBtn.innerHTML = '<i class="bi bi-eye"></i>';
}
});
document.getElementById('updateTokenBtn').addEventListener('click', async () => {
const newToken = document.getElementById('newTokenInput').value.trim();
if (newToken === '') {
if (!confirm('您即将清空管理 Token,这将禁用 Token 验证,任何人都可以访问管理接口。确定要继续吗?')) {
return;
}
}
try {
const result = await apiFetch('/token', {
method: 'PUT',
body: JSON.stringify({ manageToken: newToken }),
headers: { 'Content-Type': 'application/json' }
});
if (result.success) {
alert('管理 Token 更新成功!\n\n提示:新的 Token 已自动保存到配置文件中。如果您设置了新的 Token,请记得在上方的 "认证 Token" 字段中输入新的 Token 以继续管理。');
// 如果设置了新 Token,自动更新认证字段
if (newToken !== '') {
document.getElementById('authToken').value = newToken;
saveToHistory(); // 保存到历史记录
}
loadManageToken(); // 刷新显示
} else {
alert('更新失败:' + (result.message || '未知错误'));
}
} catch (error) {
console.error('更新管理Token失败:', error);
}
});
document.getElementById('refreshTokenBtn').addEventListener('click', loadManageToken);
// 初始化加载
document.addEventListener('DOMContentLoaded', () => {
// 初始化主题
initTheme();
// 加载历史记录
loadFromHistory();
// 监听服务器地址和token变化,自动保存
const serverAddressInput = document.getElementById('serverAddress');
const authTokenInput = document.getElementById('authToken');
function handleInputChange() {
if (getApiBase()) {
clearTabPlaceholder(); // 清除提示信息
saveToHistory();
loadCurrentTabData();
}
}
serverAddressInput.addEventListener('blur', handleInputChange);
authTokenInput.addEventListener('blur', handleInputChange);
// 当切换标签页时,加载对应的数据
const tabElms = document.querySelectorAll('button[data-bs-toggle="tab"]');
tabElms.forEach(tabElm => {
tabElm.addEventListener('shown.bs.tab', (event) => {
const targetId = event.target.getAttribute('data-bs-target');
switch (targetId) {
case '#players-tab-pane':
loadPlayers();
break;
case '#onebot-tab-pane':
loadOneBotConfigs();
break;
case '#templates-tab-pane':
loadMessageTemplates();
break;
case '#server-tab-pane':
loadServerName();
break;
case '#token-tab-pane':
loadManageToken();
break;
}
});
});
// 默认加载第一个标签页的数据
if (getApiBase()) {
loadPlayers();
} else {
// 提示用户输入服务器地址
const firstTabPane = document.querySelector('.tab-pane.active');
if (firstTabPane) {
firstTabPane.innerHTML = '<div class="alert alert-info mt-3">请输入API服务器地址以开始。</div>';
}
}
});
</script>
</body>
</html>
0 条评论