Spaces:
Paused
Paused
| {% extends "base.html" %} | |
| {% block title %}{% if active_page == "welfare" %}福利车位 - GPT Team 管理系统{% else %}控制台 - GPT Team 管理系统{% endif %}{% endblock %} | |
| {% block content %} | |
| <div class="page-header"> | |
| <h2>{% if active_page == "welfare" %}福利车位{% else %}控制台{% endif %}</h2> | |
| </div> | |
| <!-- 统计卡片 --> | |
| <div class="stats-container"> | |
| <div class="stat-card"> | |
| <div class="stat-icon"><i data-lucide="users"></i></div> | |
| <div class="stat-info-group"> | |
| <div class="stat-value">{{ stats.total_teams }}</div> | |
| <div class="stat-label">Team 总数</div> | |
| </div> | |
| </div> | |
| <div class="stat-card stat-success"> | |
| <div class="stat-icon"><i data-lucide="check-circle"></i></div> | |
| <div class="stat-info-group"> | |
| <div class="stat-value">{{ stats.available_teams }}</div> | |
| <div class="stat-label">可用 Team</div> | |
| </div> | |
| </div> | |
| {% if active_page == "welfare" %} | |
| <div class="stat-card stat-info"> | |
| <div class="stat-icon"><i data-lucide="car-front"></i></div> | |
| <div class="stat-info-group"> | |
| <div class="stat-value">{{ stats.remaining_spots }}</div> | |
| <div class="stat-label">福利剩余车位</div> | |
| </div> | |
| </div> | |
| <div class="stat-card stat-warning welfare-code-card"> | |
| <div class="stat-icon"><i data-lucide="ticket"></i></div> | |
| <div class="stat-info-group"> | |
| <div class="stat-label">通用兑换码</div> | |
| <div class="welfare-code-value" id="welfareCommonCodeText" title="{{ stats.welfare_code or '' }}">{{ stats.welfare_code or '-' }}</div> | |
| <div class="stat-sub-label" id="welfareCodeUsageText">剩余次数 {{ stats.welfare_code_remaining }} / {{ stats.welfare_code_limit }}</div> | |
| <div class="welfare-code-row"> | |
| <button type="button" class="btn btn-secondary btn-sm" onclick="copyWelfareCode()" {% if not stats.welfare_code %}disabled{% endif %} id="copyWelfareCodeBtn"> | |
| <i data-lucide="copy" style="width: 14px; height: 14px;"></i> 复制兑换码 | |
| </button> | |
| </div> | |
| <input type="hidden" id="welfareCommonCodeValue" value="{{ stats.welfare_code or '' }}"> | |
| </div> | |
| </div> | |
| {% else %} | |
| <div class="stat-card stat-info"> | |
| <div class="stat-icon"><i data-lucide="ticket"></i></div> | |
| <div class="stat-info-group"> | |
| <div class="stat-value">{{ stats.total_codes }}</div> | |
| <div class="stat-label">兑换码总数</div> | |
| </div> | |
| </div> | |
| <div class="stat-card stat-warning"> | |
| <div class="stat-icon"><i data-lucide="clipboard-list"></i></div> | |
| <div class="stat-info-group"> | |
| <div class="stat-value">{{ stats.used_codes }}</div> | |
| <div class="stat-label">已使用兑换码</div> | |
| </div> | |
| </div> | |
| {% endif %} | |
| </div> | |
| <!-- Team 列表 --> | |
| <div class="content-section"> | |
| <div class="section-header"> | |
| <h3>{% if active_page == "welfare" %}福利 Team 列表{% else %}Team 列表{% endif %}</h3> | |
| <div class="header-group"> | |
| <!-- 批量操作 --> | |
| <div id="batchActions" class="batch-actions-group" style="display: none;"> | |
| <button onclick="handleBatchRefresh()" class="btn btn-secondary btn-sm"> | |
| <i data-lucide="refresh-cw" style="width: 14px; height: 14px;"></i> 批量刷新 | |
| </button> | |
| <button onclick="handleBatchEnableDeviceAuth()" class="btn btn-success btn-sm"> | |
| <i data-lucide="shield-check" style="width: 14px; height: 14px;"></i> 批量开启验证 | |
| </button> | |
| <button onclick="handleBatchDelete()" class="btn btn-danger btn-sm"> | |
| <i data-lucide="trash-2" style="width: 14px; height: 14px;"></i> 批量删除 | |
| </button> | |
| <span class="selected-count text-muted small" style="margin-left: 8px;">已选 0 项</span> | |
| </div> | |
| <!-- 状态筛选 --> | |
| <div class="dropdown-wrapper"> | |
| <button class="btn btn-secondary dropdown-toggle" onclick="toggleDropdown('statusFilterDropdown')"> | |
| <i data-lucide="filter" style="width: 16px; height: 16px;"></i> | |
| <span> | |
| {% if status_filter == 'active' %}可用 | |
| {% elif status_filter == 'full' %}已满 | |
| {% elif status_filter == 'expired' %}已过期 | |
| {% elif status_filter == 'error' %}异常 | |
| {% elif status_filter == 'banned' %}已封禁 | |
| {% else %}状态筛选 | |
| {% endif %} | |
| </span> | |
| </button> | |
| <div id="statusFilterDropdown" class="dropdown-menu"> | |
| <div class="dropdown-header">选择状态</div> | |
| <div class="dropdown-item" onclick="filterByStatus('')"> | |
| <span class="text-muted">所有状态</span> | |
| </div> | |
| <div class="dropdown-item" onclick="filterByStatus('active')"> | |
| <span class="status-badge status-active">可用 (Active)</span> | |
| </div> | |
| <div class="dropdown-item" onclick="filterByStatus('full')"> | |
| <span class="status-badge status-full">已满 (Full)</span> | |
| </div> | |
| <div class="dropdown-item" onclick="filterByStatus('expired')"> | |
| <span class="status-badge status-expired">已过期 (Expired)</span> | |
| </div> | |
| <div class="dropdown-item" onclick="filterByStatus('error')"> | |
| <span class="status-badge status-error">异常 (Error)</span> | |
| </div> | |
| <div class="dropdown-item" onclick="filterByStatus('banned')"> | |
| <span class="status-badge status-banned">已封禁 (Banned)</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 搜索框 --> | |
| <form action="{% if active_page == 'welfare' %}/admin/welfare{% else %}/admin{% endif %}" method="get" class="search-form"> | |
| <div class="input-group-clean"> | |
| <i data-lucide="search" style="width: 14px; height: 14px;"></i> | |
| <input type="text" name="search" value="{{ search or '' }}" placeholder="搜索邮箱/Account ID..." | |
| style="width: 200px;"> | |
| {% if search %} | |
| <a href="{% if active_page == 'welfare' %}/admin/welfare{% else %}/admin{% endif %}" title="清除搜索"> | |
| <i data-lucide="x-circle" style="width: 14px; height: 14px;"></i> | |
| </a> | |
| {% endif %} | |
| {% if status_filter %} | |
| <input type="hidden" name="status" value="{{ status_filter }}"> | |
| {% endif %} | |
| </div> | |
| </form> | |
| <!-- 列设置 --> | |
| <div class="dropdown-wrapper"> | |
| <button class="btn btn-secondary dropdown-toggle" onclick="toggleDropdown('columnToggleDropdown')"> | |
| <i data-lucide="columns" style="width: 16px; height: 16px;"></i> 列设置 | |
| </button> | |
| <div id="columnToggleDropdown" class="dropdown-menu"> | |
| <!-- Column checkboxes will be generated here --> | |
| </div> | |
| </div> | |
| {% if active_page == "welfare" %} | |
| <button onclick="generateWelfareCode()" class="btn btn-secondary" id="generateWelfareCodeBtn"> | |
| <i data-lucide="ticket-plus" style="width: 16px; height: 16px;"></i> 生成通用兑换码 | |
| </button> | |
| {% endif %} | |
| <button onclick="showModal('importTeamModal')" class="btn btn-primary"> | |
| <i data-lucide="plus-circle" style="width: 16px; height: 16px;"></i> 导入 Team | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| {% if teams %} | |
| <div class="table-container"> | |
| <table class="data-table"> | |
| <thead> | |
| <tr> | |
| <th style="width: 40px;"><input type="checkbox" id="selectAll" onchange="toggleSelectAll(this.checked)"> | |
| </th> | |
| <th>{% if active_page == "welfare" %}编号{% else %}ID{% endif %}</th> | |
| <th>邮箱</th> | |
| <th>Account ID</th> | |
| <th>Team 名称</th> | |
| <th>成员数</th> | |
| <th>订阅计划</th> | |
| <th>到期时间</th> | |
| <th>设备验证</th> | |
| <th>状态</th> | |
| <th>操作</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for team in teams %} | |
| <tr> | |
| <td><input type="checkbox" class="team-checkbox" value="{{ team.id }}" onchange="updateSelectedCount()"> | |
| </td> | |
| <td><span class="text-muted">{% if active_page == "welfare" %}{{ ((pagination.current_page - 1) * pagination.per_page) + loop.index }}{% else %}{{ team.id }}{% endif %}</span></td> | |
| <td> | |
| {{ team.email }} | |
| {% if team.account_role and team.account_role != 'account-owner' %} | |
| <span class="role-hint" title="账号角色: {{ team.account_role }}">已降级</span> | |
| {% endif %} | |
| </td> | |
| <td><span class="text-muted small">{{ team.account_id or '-' }}</span></td> | |
| <td>{{ team.team_name or '-' }}</td> | |
| <td> | |
| <span class="member-count"> | |
| {{ team.current_members }}/{{ team.max_members }} | |
| </span> | |
| </td> | |
| <td>{{ team.subscription_plan or '-' }}</td> | |
| <td> | |
| {% if team.expires_at %} | |
| {{ team.expires_at|format_datetime }} | |
| {% else %} | |
| - | |
| {% endif %} | |
| </td> | |
| <td> | |
| {% if team.device_code_auth_enabled %} | |
| <span class="status-badge status-active">已开启</span> | |
| {% else %} | |
| <span class="status-badge status-banned">未开启</span> | |
| {% endif %} | |
| </td> | |
| <td> | |
| <span class="status-badge status-{{ team.status }}"> | |
| {% if team.status == 'active' %} | |
| 可用 | |
| {% elif team.status == 'full' %} | |
| 已满 | |
| {% elif team.status == 'expired' %} | |
| 已过期 | |
| {% elif team.status == 'banned' %} | |
| 已封禁 | |
| {% elif team.status == 'error' %} | |
| 异常 | |
| {% else %} | |
| 未知 | |
| {% endif %} | |
| </span> | |
| </td> | |
| <td> | |
| <div class="action-buttons"> | |
| <button class="btn btn-sm btn-icon btn-minimal btn-info btn-view-members" | |
| data-id="{{ team.id }}" data-email="{{ team.email }}" title="查看成员"> | |
| <i data-lucide="users" style="width: 16px; height: 16px;"></i> | |
| </button> | |
| <button class="btn btn-sm btn-icon btn-minimal btn-primary btn-edit-team" | |
| data-id="{{ team.id }}" title="编辑"> | |
| <i data-lucide="edit-3" style="width: 16px; height: 16px;"></i> | |
| </button> | |
| <button class="btn btn-sm btn-icon btn-minimal btn-secondary btn-refresh-team" | |
| data-id="{{ team.id }}" title="刷新"> | |
| <i data-lucide="refresh-cw" style="width: 16px; height: 16px;"></i> | |
| </button> | |
| <button class="btn btn-sm btn-icon btn-minimal btn-success btn-enable-device-auth" | |
| data-id="{{ team.id }}" title="一键开启设备代码验证"> | |
| <i data-lucide="shield-check" style="width: 16px; height: 16px;"></i> | |
| </button> | |
| <button type="button" class="btn btn-sm btn-icon btn-minimal btn-danger btn-delete-team" | |
| data-id="{{ team.id }}" data-email="{{ team.email }}" title="删除" | |
| onclick='deleteTeam({{ team.id }}, {{ team.email|tojson }})'> | |
| <i data-lucide="trash-2" style="width: 16px; height: 16px;"></i> | |
| </button> | |
| </div> | |
| </td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- 分页 --> | |
| <!-- 分页 --> | |
| {% if pagination %} | |
| <div class="pagination"> | |
| <div class="per-page-selector"> | |
| 每页 | |
| <select class="per-page-select" onchange="changePerPage(this.value)"> | |
| <option value="20" {% if pagination.per_page==20 %}selected{% endif %}>20</option> | |
| <option value="50" {% if pagination.per_page==50 %}selected{% endif %}>50</option> | |
| <option value="100" {% if pagination.per_page==100 %}selected{% endif %}>100</option> | |
| </select> | |
| </div> | |
| {% if pagination.total_pages > 1 %} | |
| {% set search_param = '&search=' + search|urlencode if search else '' %} | |
| {% set per_page_param = '&per_page=' + pagination.per_page|string if pagination.per_page != 20 else '' %} | |
| {% set base_query = search_param + per_page_param %} | |
| <div class="pagination-controls"> | |
| <!-- 首页 --> | |
| <a href="?page=1{{ base_query }}" | |
| class="btn btn-sm btn-secondary {% if pagination.current_page == 1 %}disabled{% endif %}" title="首页"> | |
| <i data-lucide="chevrons-left" style="width: 14px; height: 14px;"></i> | |
| </a> | |
| <!-- 上一页 --> | |
| {% if pagination.current_page > 1 %} | |
| <a href="?page={{ pagination.current_page - 1 }}{{ base_query }}" class="btn btn-sm btn-secondary" title="上一页"> | |
| <i data-lucide="chevron-left" style="width: 14px; height: 14px;"></i> | |
| </a> | |
| {% else %} | |
| <button class="btn btn-sm btn-secondary" disabled> | |
| <i data-lucide="chevron-left" style="width: 14px; height: 14px;"></i> | |
| </button> | |
| {% endif %} | |
| <!-- 页码 --> | |
| <div class="pagination-numbers"> | |
| {% set start_page = pagination.current_page - 2 %} | |
| {% set end_page = pagination.current_page + 2 %} | |
| {% if start_page < 1 %} {% set end_page=end_page + (1 - start_page) %} {% set start_page=1 %} {% endif %} {% | |
| if end_page> pagination.total_pages %} | |
| {% set start_page = start_page - (end_page - pagination.total_pages) %} | |
| {% set end_page = pagination.total_pages %} | |
| {% if start_page < 1 %} {% set start_page=1 %} {% endif %} {% endif %} {% if start_page> 1 %} | |
| <a href="?page=1{{ base_query }}" class="page-number">1</a> | |
| {% if start_page > 2 %} | |
| <span class="page-dots">...</span> | |
| {% endif %} | |
| {% endif %} | |
| {% for p in range(start_page, end_page + 1) %} | |
| <a href="?page={{ p }}{{ base_query }}" | |
| class="page-number {% if p == pagination.current_page %}active{% endif %}">{{ p }}</a> | |
| {% endfor %} | |
| {% if end_page < pagination.total_pages %} {% if end_page < pagination.total_pages - 1 %} <span | |
| class="page-dots">...</span> | |
| {% endif %} | |
| <a href="?page={{ pagination.total_pages }}{{ base_query }}" class="page-number">{{ | |
| pagination.total_pages }}</a> | |
| {% endif %} | |
| </div> | |
| <!-- 下一页 --> | |
| {% if pagination.current_page < pagination.total_pages %} <a | |
| href="?page={{ pagination.current_page + 1 }}{{ base_query }}" class="btn btn-sm btn-secondary" title="下一页"> | |
| <i data-lucide="chevron-right" style="width: 14px; height: 14px;"></i> | |
| </a> | |
| {% else %} | |
| <button class="btn btn-sm btn-secondary" disabled> | |
| <i data-lucide="chevron-right" style="width: 14px; height: 14px;"></i> | |
| </button> | |
| {% endif %} | |
| <!-- 末页 --> | |
| <a href="?page={{ pagination.total_pages }}{{ base_query }}" | |
| class="btn btn-sm btn-secondary {% if pagination.current_page == pagination.total_pages %}disabled{% endif %}" | |
| title="末页"> | |
| <i data-lucide="chevrons-right" style="width: 14px; height: 14px;"></i> | |
| </a> | |
| </div> | |
| <span class="pagination-info" style="margin-left: 1rem;">共 {{ pagination.total }} 条</span> | |
| {% endif %} | |
| </div> | |
| {% endif %} | |
| {% else %} | |
| <div class="empty-state"> | |
| <p>暂无 Team 数据</p> | |
| <button onclick="showModal('importTeamModal')" class="btn btn-primary">立即导入</button> | |
| </div> | |
| {% endif %} | |
| </div> | |
| <!-- 编辑 Team 模态框 --> | |
| <div id="editTeamModal" class="modal-overlay"> | |
| <div class="modal"> | |
| <div class="modal-header"> | |
| <h3>编辑 Team 信息</h3> | |
| <button class="modal-close" onclick="hideModal('editTeamModal')">×</button> | |
| </div> | |
| <div class="modal-body"> | |
| <form id="editTeamForm" onsubmit="handleEditTeam(event)"> | |
| <input type="hidden" id="edit-team-id" name="teamId"> | |
| <div class="form-group"> | |
| <label>邮箱 <span class="required">*</span></label> | |
| <input type="email" id="edit-team-email" name="email" class="form-control" required> | |
| </div> | |
| <div class="form-group"> | |
| <label>Access Token (AT) <span class="required">*</span></label> | |
| <div class="input-group-btn"> | |
| <input type="text" id="edit-team-token" name="accessToken" class="form-control" required> | |
| <button type="button" class="btn btn-secondary" onclick="refreshATInModal()" | |
| title="立即从 ST/RT 刷新 AT"> | |
| <i data-lucide="refresh-cw" style="width: 14px; height: 14px;"></i> 刷新 | |
| </button> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label>Refresh Token (RT)</label> | |
| <input type="text" id="edit-team-refresh-token" name="refreshToken" class="form-control"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Session Token</label> | |
| <input type="text" id="edit-team-session-token" name="sessionToken" class="form-control"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Client ID</label> | |
| <input type="text" id="edit-team-client-id" name="clientId" class="form-control"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Account ID <span class="required">*</span></label> | |
| <input type="text" id="edit-team-account-id" name="accountId" class="form-control" required> | |
| </div> | |
| <div class="form-group"> | |
| <label>Team 名称</label> | |
| <input type="text" id="edit-team-name" name="teamName" class="form-control"> | |
| </div> | |
| <div class="form-group"> | |
| <label>账号角色</label> | |
| <input type="text" id="edit-team-role" class="form-control" readonly title="检测到的账号角色"> | |
| </div> | |
| <div class="form-group"> | |
| <label>设备代码身份验证</label> | |
| <input type="text" id="edit-team-device-auth" class="form-control" readonly title="检测到的验证开启状态"> | |
| </div> | |
| <div class="form-group"> | |
| <label>最大成员数 <span class="required">*</span></label> | |
| <input type="number" id="edit-team-max-members" name="maxMembers" class="form-control" min="1" | |
| max="100" required> | |
| </div> | |
| <div class="form-group"> | |
| <label>状态 <span class="required">*</span></label> | |
| <select id="edit-team-status" name="status" class="form-control" required> | |
| <option value="active">活跃 (Active)</option> | |
| <option value="full">已满 (Full)</option> | |
| <option value="expired">已过期 (Expired)</option> | |
| <option value="error">异常 (Error)</option> | |
| <option value="banned">已封禁 (Banned)</option> | |
| </select> | |
| </div> | |
| <button type="submit" class="btn btn-primary">保存修改</button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block extra_css %} | |
| {% endblock %} | |
| {% block extra_js %} | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Init Column Toggler | |
| initColumnToggler('.data-table', 'team_list_columns'); | |
| // 绑定查看成员按钮 | |
| document.querySelectorAll('.btn-view-members').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const id = btn.getAttribute('data-id'); | |
| const email = btn.getAttribute('data-email'); | |
| viewMembers(id, email); | |
| }); | |
| }); | |
| // 绑定刷新按钮 | |
| document.querySelectorAll('.btn-refresh-team').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const id = btn.getAttribute('data-id'); | |
| refreshTeam(id); | |
| }); | |
| }); | |
| // 绑定编辑按钮 | |
| document.querySelectorAll('.btn-edit-team').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const id = btn.getAttribute('data-id'); | |
| editTeam(id); | |
| }); | |
| }); | |
| // 绑定开启设备认证按钮 | |
| document.querySelectorAll('.btn-enable-device-auth').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const id = btn.getAttribute('data-id'); | |
| enableDeviceAuth(id); | |
| }); | |
| }); | |
| }); | |
| async function enableDeviceAuth(teamId) { | |
| if (!confirm('确定要为该 Team 开启设备代码身份验证吗?')) { | |
| return; | |
| } | |
| try { | |
| showToast('正在开启...', 'info'); | |
| const response = await fetch(`/admin/teams/${teamId}/enable-device-auth`, { | |
| method: 'POST' | |
| }); | |
| const data = await response.json(); | |
| if (response.ok && data.success) { | |
| showToast('开启成功', 'success'); | |
| setTimeout(() => location.reload(), 1000); | |
| } else { | |
| showToast(data.error || '开启失败', 'error'); | |
| } | |
| } catch (error) { | |
| showToast('网络错误', 'error'); | |
| } | |
| } | |
| async function refreshTeam(teamId) { | |
| if (!confirm('确定要刷新此 Team 的信息吗?')) { | |
| return; | |
| } | |
| try { | |
| showToast('正在刷新...', 'info'); | |
| const response = await fetch(`/api/teams/${teamId}/refresh?force=true`, { | |
| method: 'GET' | |
| }); | |
| const data = await response.json(); | |
| if (response.ok && data.success) { | |
| showToast('刷新成功', 'success'); | |
| setTimeout(() => location.reload(), 1000); | |
| } else { | |
| showToast(data.error || '刷新失败', 'error'); | |
| } | |
| } catch (error) { | |
| showToast('网络错误', 'error'); | |
| } | |
| } | |
| async function refreshATInModal() { | |
| const teamId = document.getElementById('edit-team-id').value; | |
| if (!teamId) return; | |
| if (!confirm('确定要刷新此 Team 的 Access Token 吗?')) { | |
| return; | |
| } | |
| try { | |
| showToast('正在刷新...', 'info'); | |
| const response = await fetch(`/api/teams/${teamId}/refresh?force=true`, { | |
| method: 'GET' | |
| }); | |
| const data = await response.json(); | |
| if (response.ok && data.success) { | |
| showToast('刷新成功', 'success'); | |
| // 刷新成功后,重新获取 Team 信息以更新模态框中的 AT | |
| const infoResponse = await fetch(`/admin/teams/${teamId}/info`); | |
| const infoData = await infoResponse.json(); | |
| if (infoResponse.ok && infoData.success) { | |
| const team = infoData.team; | |
| document.getElementById('edit-team-token').value = team.access_token || ''; | |
| document.getElementById('edit-team-refresh-token').value = team.refresh_token || ''; | |
| document.getElementById('edit-team-session-token').value = team.session_token || ''; | |
| document.getElementById('edit-team-role').value = team.account_role || '未知'; | |
| // 重新创建图标以免刷新后丢失 | |
| if (window.lucide) { | |
| lucide.createIcons(); | |
| } | |
| } | |
| } else { | |
| showToast(data.error || '刷新失败', 'error'); | |
| } | |
| } catch (error) { | |
| showToast('网络错误', 'error'); | |
| } | |
| } | |
| async function deleteTeam(teamId, email) { | |
| if (!confirm(`确定要删除 Team "${email}" 吗?\n\n此操作不可恢复!`)) { | |
| return; | |
| } | |
| try { | |
| showToast('正在删除...', 'info'); | |
| const response = await fetch(`/admin/teams/${teamId}/delete`, { | |
| method: 'POST' | |
| }); | |
| const data = await response.json(); | |
| if (response.ok && data.success) { | |
| showToast('删除成功', 'success'); | |
| setTimeout(() => location.reload(), 1000); | |
| } else { | |
| showToast(data.error || '删除失败', 'error'); | |
| } | |
| } catch (error) { | |
| showToast('网络错误', 'error'); | |
| } | |
| } | |
| async function editTeam(teamId) { | |
| try { | |
| const response = await fetch(`/admin/teams/${teamId}/info`); | |
| const data = await response.json(); | |
| if (response.ok && data.success) { | |
| const team = data.team; | |
| document.getElementById('edit-team-id').value = team.id; | |
| document.getElementById('edit-team-email').value = team.email; | |
| document.getElementById('edit-team-token').value = team.access_token || ''; | |
| document.getElementById('edit-team-refresh-token').value = team.refresh_token || ''; | |
| document.getElementById('edit-team-session-token').value = team.session_token || ''; | |
| document.getElementById('edit-team-client-id').value = team.client_id || ''; | |
| document.getElementById('edit-team-account-id').value = team.account_id || ''; | |
| document.getElementById('edit-team-max-members').value = team.max_members || 6; | |
| document.getElementById('edit-team-name').value = team.team_name || ''; | |
| document.getElementById('edit-team-status').value = team.status || 'active'; | |
| document.getElementById('edit-team-role').value = team.account_role || '未知'; | |
| document.getElementById('edit-team-device-auth').value = team.device_code_auth_enabled ? '已开启' : '未开启'; | |
| showModal('editTeamModal'); | |
| } else { | |
| showToast(data.error || '获取信息失败', 'error'); | |
| } | |
| } catch (error) { | |
| showToast('网络错误', 'error'); | |
| } | |
| } | |
| async function handleEditTeam(event) { | |
| event.preventDefault(); | |
| const teamId = document.getElementById('edit-team-id').value; | |
| const payload = { | |
| email: document.getElementById('edit-team-email').value, | |
| access_token: document.getElementById('edit-team-token').value, | |
| refresh_token: document.getElementById('edit-team-refresh-token').value, | |
| session_token: document.getElementById('edit-team-session-token').value, | |
| client_id: document.getElementById('edit-team-client-id').value, | |
| account_id: document.getElementById('edit-team-account-id').value, | |
| max_members: parseInt(document.getElementById('edit-team-max-members').value), | |
| team_name: document.getElementById('edit-team-name').value, | |
| status: document.getElementById('edit-team-status').value | |
| }; | |
| try { | |
| const response = await fetch(`/admin/teams/${teamId}/update`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(payload) | |
| }); | |
| const result = await response.json(); | |
| if (response.ok && result.success) { | |
| showToast('更新成功', 'success'); | |
| hideModal('editTeamModal'); | |
| setTimeout(() => location.reload(), 1000); | |
| } else { | |
| showToast(result.error || '更新失败', 'error'); | |
| } | |
| } catch (error) { | |
| showToast('网络错误', 'error'); | |
| } | |
| } | |
| // Column Toggler Logic | |
| function initColumnToggler(tableSelector, storageKey) { | |
| const table = document.querySelector(tableSelector); | |
| if (!table) return; | |
| const headers = table.querySelectorAll('thead th'); | |
| const container = document.getElementById('columnToggleDropdown'); | |
| if (!container) return; | |
| container.innerHTML = '<div class="dropdown-header">显示/隐藏列</div>'; | |
| // Load saved preferences | |
| const saved = JSON.parse(localStorage.getItem(storageKey) || '[]'); | |
| headers.forEach((th, index) => { | |
| const text = th.innerText.trim(); | |
| if (!text || text === '操作' || th.querySelector('input')) return; // Checkbox column or Action | |
| const isVisible = !saved.includes(index); | |
| if (!isVisible) { | |
| th.style.display = 'none'; | |
| table.querySelectorAll(`tbody tr td:nth-child(${index + 1})`).forEach(td => td.style.display = 'none'); | |
| } | |
| const item = document.createElement('label'); | |
| item.className = 'dropdown-item'; | |
| item.innerHTML = ` | |
| <input type="checkbox" ${isVisible ? 'checked' : ''}> | |
| <span>${text}</span> | |
| `; | |
| item.querySelector('input').addEventListener('change', (e) => { | |
| const checked = e.target.checked; | |
| // Toggle header | |
| th.style.display = checked ? '' : 'none'; | |
| // Toggle cells | |
| table.querySelectorAll(`tbody tr td:nth-child(${index + 1})`).forEach(td => td.style.display = checked ? '' : 'none'); | |
| // Save to localStorage | |
| const currentSaved = JSON.parse(localStorage.getItem(storageKey) || '[]'); | |
| if (checked) { | |
| const idx = currentSaved.indexOf(index); | |
| if (idx > -1) currentSaved.splice(idx, 1); | |
| } else { | |
| if (!currentSaved.includes(index)) currentSaved.push(index); | |
| } | |
| localStorage.setItem(storageKey, JSON.stringify(currentSaved)); | |
| }); | |
| container.appendChild(item); | |
| }); | |
| } | |
| function toggleDropdown(id) { | |
| const el = document.getElementById(id); | |
| if (el) { | |
| el.classList.toggle('show'); | |
| document.querySelectorAll('.dropdown-menu').forEach(d => { | |
| if (d.id !== id) d.classList.remove('show'); | |
| }); | |
| } | |
| } | |
| // Close dropdowns | |
| document.addEventListener('click', (e) => { | |
| if (!e.target.closest('.dropdown-wrapper') && !e.target.closest('.dropdown-toggle')) { | |
| document.querySelectorAll('.dropdown-menu').forEach(d => d.classList.remove('show')); | |
| } | |
| }); | |
| const dashboardBasePath = "{{ '/admin/welfare' if active_page == 'welfare' else '/admin' }}"; | |
| function filterByStatus(status) { | |
| const url = new URL(window.location.href); | |
| if (status) { | |
| url.searchParams.set('status', status); | |
| } else { | |
| url.searchParams.delete('status'); | |
| } | |
| url.searchParams.set('page', 1); | |
| window.location.href = url.toString(); | |
| } | |
| function toggleSelectAll(checked) { | |
| document.querySelectorAll('.team-checkbox').forEach(cb => { | |
| cb.checked = checked; | |
| }); | |
| updateSelectedCount(); | |
| } | |
| function updateSelectedCount() { | |
| const checkboxes = document.querySelectorAll('.team-checkbox'); | |
| const selectedCount = Array.from(checkboxes).filter(cb => cb.checked).length; | |
| const totalCount = checkboxes.length; | |
| const batchActions = document.getElementById('batchActions'); | |
| const countSpan = batchActions.querySelector('.selected-count'); | |
| const selectAll = document.getElementById('selectAll'); | |
| if (selectedCount > 0) { | |
| batchActions.style.display = 'flex'; | |
| batchActions.style.alignItems = 'center'; | |
| countSpan.innerText = `已选 ${selectedCount} 项`; | |
| } else { | |
| batchActions.style.display = 'none'; | |
| } | |
| // Update Select All state | |
| if (selectAll) { | |
| selectAll.checked = selectedCount === totalCount && totalCount > 0; | |
| selectAll.indeterminate = selectedCount > 0 && selectedCount < totalCount; | |
| } | |
| } | |
| async function handleBatchAction(endpoint, actionName) { | |
| const selectedIds = Array.from(document.querySelectorAll('.team-checkbox:checked')) | |
| .map(cb => parseInt(cb.value)); | |
| if (selectedIds.length === 0) { | |
| showToast('请选择要操作的 Team', 'warning'); | |
| return; | |
| } | |
| if (!confirm(`确定要对选中的 ${selectedIds.length} 个 Team 进行 "${actionName}" 操作吗?`)) { | |
| return; | |
| } | |
| try { | |
| showToast(`正在${actionName}...`, 'info'); | |
| const response = await fetch(endpoint, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ ids: selectedIds }) | |
| }); | |
| const result = await response.json(); | |
| if (response.ok && result.success) { | |
| showToast(result.message || `${actionName}成功`, 'success'); | |
| setTimeout(() => location.reload(), 1500); | |
| } else { | |
| showToast(result.error || `${actionName}失败`, 'error'); | |
| } | |
| } catch (error) { | |
| console.error(error); | |
| showToast('网络错误', 'error'); | |
| } | |
| } | |
| function handleBatchRefresh() { | |
| handleBatchAction('/admin/teams/batch-refresh', '批量刷新'); | |
| } | |
| function handleBatchDelete() { | |
| handleBatchAction('/admin/teams/batch-delete', '批量删除'); | |
| } | |
| function handleBatchEnableDeviceAuth() { | |
| handleBatchAction('/admin/teams/batch-enable-device-auth', '批量开启验证'); | |
| } | |
| function changePerPage(val) { | |
| const url = new URL(window.location.href); | |
| url.searchParams.set('per_page', val); | |
| url.searchParams.set('page', 1); | |
| window.location.href = url.toString(); | |
| } | |
| </script> | |
| {% endblock %} |