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">&nbsp;</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>

感谢您选择帆船AmongUs服务器开发平台
←返回主页
←返回文章页

分类: 其他

0 条评论

发表回复

Avatar placeholder

您的邮箱地址不会被公开。 必填项已用 * 标注