Files
cursornew2026/backend/templates/index.html
ccdojox-crypto 9e2333c90c CursorPro 后台管理系统 v1.0
功能:
- 激活码管理 (Pro/Auto 两种类型)
- 账号池管理
- 设备绑定记录
- 使用日志
- 搜索/筛选功能
- 禁用/启用功能 (支持退款参考)
- 全局设置 (换号间隔、额度消耗等)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 20:54:44 +08:00

1244 lines
72 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CursorPro 管理后台</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
</head>
<body class="bg-gray-100">
<div id="app">
<!-- 登录页面 -->
<div v-if="!isLoggedIn" class="min-h-screen flex items-center justify-center">
<div class="bg-white p-8 rounded-lg shadow-lg w-96">
<h1 class="text-2xl font-bold text-center mb-6 text-gray-800">CursorPro 管理后台</h1>
<form @submit.prevent="login">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">用户名</label>
<input v-model="loginForm.username" type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="请输入用户名">
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">密码</label>
<input v-model="loginForm.password" type="password"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="请输入密码">
</div>
<button type="submit"
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition">
登录
</button>
</form>
<p v-if="loginError" class="mt-4 text-red-500 text-sm text-center">{{ loginError }}</p>
</div>
</div>
<!-- 主界面 -->
<div v-else class="min-h-screen">
<!-- 顶部导航 -->
<nav class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<h1 class="text-xl font-bold text-gray-800">CursorPro 管理后台</h1>
</div>
<div class="flex items-center space-x-4">
<span class="text-gray-600">{{ currentUser }}</span>
<button @click="logout" class="text-red-600 hover:text-red-800">退出</button>
</div>
</div>
</div>
</nav>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 标签页 -->
<div class="mb-6">
<nav class="flex space-x-4">
<button @click="currentTab = 'dashboard'"
:class="currentTab === 'dashboard' ? 'bg-blue-100 text-blue-700' : 'text-gray-500 hover:text-gray-700'"
class="px-3 py-2 font-medium text-sm rounded-md">
仪表盘
</button>
<button @click="currentTab = 'accounts'"
:class="currentTab === 'accounts' ? 'bg-blue-100 text-blue-700' : 'text-gray-500 hover:text-gray-700'"
class="px-3 py-2 font-medium text-sm rounded-md">
账号管理
</button>
<button @click="currentTab = 'keys'"
:class="currentTab === 'keys' ? 'bg-blue-100 text-blue-700' : 'text-gray-500 hover:text-gray-700'"
class="px-3 py-2 font-medium text-sm rounded-md">
激活码管理
</button>
<button @click="currentTab = 'settings'; loadSettings()"
:class="currentTab === 'settings' ? 'bg-blue-100 text-blue-700' : 'text-gray-500 hover:text-gray-700'"
class="px-3 py-2 font-medium text-sm rounded-md">
全局设置
</button>
<button @click="currentTab = 'compensate'"
:class="currentTab === 'compensate' ? 'bg-blue-100 text-blue-700' : 'text-gray-500 hover:text-gray-700'"
class="px-3 py-2 font-medium text-sm rounded-md">
批量补偿
</button>
<button @click="currentTab = 'logs'; loadLogs()"
:class="currentTab === 'logs' ? 'bg-blue-100 text-blue-700' : 'text-gray-500 hover:text-gray-700'"
class="px-3 py-2 font-medium text-sm rounded-md">
使用日志
</button>
</nav>
</div>
<!-- 仪表盘 -->
<div v-if="currentTab === 'dashboard'">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-lg shadow p-6">
<div class="text-sm font-medium text-gray-500">总账号数</div>
<div class="mt-2 text-3xl font-semibold text-gray-900">{{ stats.total_accounts }}</div>
<div class="mt-1 text-sm text-green-600">{{ stats.active_accounts }} 可用</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="text-sm font-medium text-gray-500">总激活码</div>
<div class="mt-2 text-3xl font-semibold text-gray-900">{{ stats.total_keys }}</div>
<div class="mt-1 text-sm text-green-600">{{ stats.active_keys }} 有效</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="text-sm font-medium text-gray-500">今日使用</div>
<div class="mt-2 text-3xl font-semibold text-gray-900">{{ stats.today_usage }}</div>
<div class="mt-1 text-sm text-blue-600">Pro账号: {{ stats.pro_accounts }}</div>
</div>
</div>
</div>
<!-- 账号管理 -->
<div v-if="currentTab === 'accounts'">
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-medium text-gray-900">账号列表</h2>
<div class="space-x-2">
<button @click="showImportModal = true"
class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm">
批量导入
</button>
<button @click="showAccountModal = true; editingAccount = null"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
添加账号
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">邮箱</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">类型</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">状态</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">使用次数</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="account in accounts" :key="account.id">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ account.email }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="account.membership_type === 'pro' ? 'bg-purple-100 text-purple-800' : 'bg-gray-100 text-gray-800'"
class="px-2 py-1 text-xs font-medium rounded-full">
{{ account.membership_type.toUpperCase() }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="getStatusClass(account.status)"
class="px-2 py-1 text-xs font-medium rounded-full">
{{ getStatusText(account.status) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ account.usage_count }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm space-x-2">
<button @click="editAccount(account)" class="text-blue-600 hover:text-blue-900">编辑</button>
<button @click="deleteAccount(account.id)" class="text-red-600 hover:text-red-900">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 激活码管理 -->
<div v-if="currentTab === 'keys'">
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-medium text-gray-900">激活码列表</h2>
<button @click="showKeyModal = true; editingKey = null; resetKeyForm()"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
生成激活码
</button>
</div>
<!-- 搜索和筛选 -->
<div class="flex flex-wrap gap-3">
<input v-model="keySearch.search" @input="searchKeys" type="text" placeholder="搜索激活码..."
class="px-3 py-2 border border-gray-300 rounded-md text-sm w-64 focus:outline-none focus:ring-2 focus:ring-blue-500">
<select v-model="keySearch.status" @change="searchKeys"
class="px-3 py-2 border border-gray-300 rounded-md text-sm">
<option value="">全部状态</option>
<option value="active">有效</option>
<option value="disabled">禁用</option>
</select>
<select v-model="keySearch.activated" @change="searchKeys"
class="px-3 py-2 border border-gray-300 rounded-md text-sm">
<option value="">全部</option>
<option value="true">已激活</option>
<option value="false">未激活</option>
</select>
<select v-model="keySearch.membership_type" @change="searchKeys"
class="px-3 py-2 border border-gray-300 rounded-md text-sm">
<option value="">全部类型</option>
<option value="pro">Pro (高级模型)</option>
<option value="free">Auto (无限换号)</option>
</select>
<button @click="resetKeySearch" class="px-3 py-2 text-gray-600 hover:text-gray-800 text-sm">
重置
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">激活码</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">类型</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">额度/天数</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">有效期</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">状态</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="key in keys" :key="key.id">
<td class="px-6 py-4 whitespace-nowrap">
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{ key.key }}</code>
<button @click="copyKey(key.key)" class="ml-2 text-gray-400 hover:text-gray-600">
<svg class="w-4 h-4 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="key.membership_type === 'pro' ? 'bg-purple-100 text-purple-800' : 'bg-blue-100 text-blue-800'"
class="px-2 py-1 text-xs font-medium rounded-full">
{{ key.membership_type === 'pro' ? '高级模型' : '无限Auto' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<template v-if="key.membership_type === 'pro'">
<span class="text-green-600 font-medium">{{ key.quota - key.quota_used }}</span>
<span class="text-gray-400"> / {{ key.quota }}</span>
</template>
<template v-else>
<span class="text-blue-600">{{ key.valid_days }}天</span>
</template>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<template v-if="key.first_activated_at">
{{ key.expire_at ? formatDate(key.expire_at) : '永久' }}
</template>
<template v-else>
<span class="text-yellow-600">{{ key.valid_days }}天 (未激活)</span>
</template>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="key.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
class="px-2 py-1 text-xs font-medium rounded-full">
{{ key.status === 'active' ? '有效' : '禁用' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm space-x-2">
<button @click="viewKeyDetail(key)" class="text-purple-600 hover:text-purple-900">查看</button>
<button v-if="key.membership_type === 'pro'" @click="addQuota(key)" class="text-green-600 hover:text-green-900">充值</button>
<button @click="extendKey(key)" class="text-yellow-600 hover:text-yellow-900">延期</button>
<button v-if="key.status === 'active'" @click="disableKey(key)" class="text-orange-600 hover:text-orange-900">禁用</button>
<button v-else @click="enableKey(key)" class="text-green-600 hover:text-green-900">启用</button>
<button @click="editKey(key)" class="text-blue-600 hover:text-blue-900">编辑</button>
<button @click="deleteKey(key.id)" class="text-red-600 hover:text-red-900">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 全局设置 -->
<div v-if="currentTab === 'settings'">
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-lg font-medium text-gray-900 mb-6">全局设置</h2>
<!-- 说明 -->
<div class="bg-gray-50 border border-gray-200 rounded-md p-4 mb-6">
<p class="text-sm text-gray-700">
<strong>Auto密钥</strong>:按时间限制,全局控制换号频率<br>
<strong>Pro密钥</strong>:按额度限制,全局控制每次扣费
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<h3 class="font-medium text-gray-700 border-b pb-2">Auto密钥 (无限换号)</h3>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">换号最小间隔 (分钟)</label>
<input v-model.number="globalSettings.auto_switch_interval_minutes" type="number" min="1"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">两次换号之间至少等待的时间</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">每天最大换号次数</label>
<input v-model.number="globalSettings.auto_max_switches_per_day" type="number" min="1"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">每天0点重置计数</p>
</div>
</div>
<div class="space-y-4">
<h3 class="font-medium text-gray-700 border-b pb-2">Pro密钥 (高级模型)</h3>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">每次换号扣除额度</label>
<input v-model.number="globalSettings.pro_quota_cost" type="number" min="1"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">例如50点/次500点总额度可换10次</p>
</div>
</div>
</div>
<div class="mt-6 flex justify-end">
<button @click="saveSettings" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
保存设置
</button>
</div>
</div>
</div>
<!-- 批量补偿 -->
<div v-if="currentTab === 'compensate'">
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">批量补偿</h2>
<!-- 说明 -->
<div class="bg-blue-50 border border-blue-200 rounded-md p-4 mb-6">
<p class="text-sm text-blue-800 font-medium mb-2">补偿逻辑说明:</p>
<ul class="text-sm text-blue-700 list-disc list-inside space-y-1">
<li>筛选:在指定日期之前激活 且 在指定日期还未过期的密钥</li>
<li>未过期的卡:直接在过期时间上增加天数</li>
<li>已过期的卡(符合条件):恢复使用,过期时间 = 今天 + 补偿天数</li>
</ul>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">套餐类型</label>
<select v-model="compensateForm.membership_type"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">全部</option>
<option value="pro">Pro (高级模型)</option>
<option value="free">Free (无限Auto)</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">在此日期之前激活</label>
<input v-model="compensateForm.activated_before" type="date"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">例如12月4号之前激活 填 2024-12-05</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">在此日期还未过期</label>
<input v-model="compensateForm.not_expired_on" type="date"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">例如12月4号还没过期 填 2024-12-04</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">延长天数</label>
<input v-model.number="compensateForm.extend_days" type="number" min="0"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">Auto和Pro都可延长</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">增加额度 (仅Pro)</label>
<input v-model.number="compensateForm.add_quota" type="number" min="0"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">仅对Pro密钥有效</p>
</div>
</div>
<div class="flex space-x-4 mb-6">
<button @click="previewCompensate" class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700">
预览
</button>
<button @click="executeCompensate" class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700">
执行补偿
</button>
</div>
<!-- 预览结果 -->
<div v-if="compensatePreview" class="mt-4 p-4 bg-gray-50 rounded-md">
<p class="font-medium text-gray-700 mb-2">{{ compensatePreview.message }}</p>
<div v-if="compensatePreview.keys && compensatePreview.keys.length > 0" class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead>
<tr class="text-left text-gray-500">
<th class="py-1 pr-4">ID</th>
<th class="py-1 pr-4">激活码</th>
<th class="py-1 pr-4">类型</th>
<th class="py-1 pr-4">激活时间</th>
<th class="py-1 pr-4">到期时间</th>
<th class="py-1">状态</th>
</tr>
</thead>
<tbody>
<tr v-for="k in compensatePreview.keys" :key="k.id">
<td class="py-1 pr-4">{{ k.id }}</td>
<td class="py-1 pr-4"><code class="bg-gray-200 px-1 rounded">{{ k.key }}</code></td>
<td class="py-1 pr-4">{{ k.membership_type === 'pro' ? 'Pro' : 'Auto' }}</td>
<td class="py-1 pr-4">{{ k.activated_at }}</td>
<td class="py-1 pr-4">{{ k.expire_at }}</td>
<td class="py-1">
<span :class="k.is_expired ? 'text-red-600' : 'text-green-600'">
{{ k.is_expired ? '已过期' : '有效' }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 执行结果 -->
<div v-if="compensateResult" class="mt-4 p-4 rounded-md" :class="compensateResult.failed > 0 ? 'bg-yellow-50' : 'bg-green-50'">
<p class="font-medium">补偿完成</p>
<p class="text-sm">
匹配: {{ compensateResult.total_matched }} 个,
成功: {{ compensateResult.success }} 个,
失败: {{ compensateResult.failed }} 个
<span v-if="compensateResult.recovered > 0" class="text-blue-600">
(其中 {{ compensateResult.recovered }} 个已过期卡被恢复使用)
</span>
</p>
<ul v-if="compensateResult.errors && compensateResult.errors.length > 0" class="text-sm text-red-600 mt-2">
<li v-for="err in compensateResult.errors" :key="err">{{ err }}</li>
</ul>
</div>
</div>
</div>
<!-- 使用日志 -->
<div v-if="currentTab === 'logs'">
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-medium text-gray-900">使用日志</h2>
<div class="flex space-x-2">
<select v-model="logFilter.action" @change="loadLogs"
class="px-3 py-2 border border-gray-300 rounded-md text-sm">
<option value="">全部操作</option>
<option value="verify">验证</option>
<option value="switch">换号</option>
</select>
<button @click="loadLogs" class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 text-sm">
刷新
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">时间</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">激活码</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">操作</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">IP</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">状态</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">消息</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="log in logs" :key="log.id">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ log.created_at }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{ log.key_preview }}</code>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="log.action === 'switch' ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'"
class="px-2 py-1 text-xs font-medium rounded-full">
{{ log.action === 'switch' ? '换号' : '验证' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ log.ip_address || '-' }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="log.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
class="px-2 py-1 text-xs font-medium rounded-full">
{{ log.success ? '成功' : '失败' }}
</span>
</td>
<td class="px-6 py-4 text-sm text-gray-500">{{ log.message || '-' }}</td>
</tr>
<tr v-if="logs.length === 0">
<td colspan="6" class="px-6 py-4 text-center text-gray-500">暂无日志</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- 账号编辑弹窗 -->
<div v-if="showAccountModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">{{ editingAccount ? '编辑账号' : '添加账号' }}</h3>
</div>
<form @submit.prevent="saveAccount" class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
<input v-model="accountForm.email" type="email" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Access Token</label>
<textarea v-model="accountForm.access_token" required rows="3"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Refresh Token</label>
<textarea v-model="accountForm.refresh_token" rows="2"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">WorkosCursorSessionToken</label>
<textarea v-model="accountForm.workos_session_token" rows="2" placeholder="可选,用于刷新 Token"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">会员类型</label>
<select v-model="accountForm.membership_type"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="pro">Pro</option>
<option value="free">Free</option>
</select>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button type="button" @click="showAccountModal = false"
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">取消</button>
<button type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">保存</button>
</div>
</form>
</div>
</div>
<!-- 激活码编辑弹窗 -->
<div v-if="showKeyModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4 max-h-[90vh] overflow-y-auto">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">{{ editingKey ? '编辑激活码' : '生成激活码' }}</h3>
</div>
<form @submit.prevent="saveKey" class="p-6 space-y-4">
<div v-if="!editingKey">
<label class="block text-sm font-medium text-gray-700 mb-1">生成数量</label>
<input v-model.number="keyForm.count" type="number" min="1" max="100"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">套餐类型</label>
<select v-model="keyForm.membership_type"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="pro">Pro (高级模型) - 按额度计费</option>
<option value="free">Auto (无限换号) - 按时间计费</option>
</select>
</div>
<!-- Pro密钥: 只设置总额度 -->
<div v-if="keyForm.membership_type === 'pro'">
<label class="block text-sm font-medium text-gray-700 mb-1">总额度</label>
<input v-model.number="keyForm.quota" type="number" min="1"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">每次换号扣除额度由全局设置控制</p>
</div>
<!-- Auto密钥: 只设置有效天数 -->
<div v-if="keyForm.membership_type === 'free'">
<label class="block text-sm font-medium text-gray-700 mb-1">有效天数</label>
<select v-model.number="keyForm.valid_days"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option :value="0">永久</option>
<option :value="1">1天</option>
<option :value="7">7天</option>
<option :value="30">30天</option>
<option :value="90">90天</option>
<option :value="365">365天</option>
</select>
<p class="text-xs text-gray-500 mt-1">首次使用激活码时开始计时,换号频率由全局设置控制</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">最大设备数</label>
<input v-model.number="keyForm.max_devices" type="number" min="1" max="10"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<p class="text-xs text-gray-500 mt-1">同一激活码最多绑定的设备数量</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">备注</label>
<input v-model="keyForm.remark" type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex justify-end space-x-3 pt-4">
<button type="button" @click="showKeyModal = false"
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">取消</button>
<button type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">保存</button>
</div>
</form>
</div>
</div>
<!-- 充值额度弹窗 -->
<div v-if="showQuotaModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-sm mx-4">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">充值额度</h3>
</div>
<div class="p-6 space-y-4">
<p class="text-sm text-gray-600">当前额度: {{ quotaTarget?.quota - quotaTarget?.quota_used }} / {{ quotaTarget?.quota }}</p>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">增加额度</label>
<input v-model.number="addQuotaAmount" type="number" min="1"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex justify-end space-x-3 pt-4">
<button type="button" @click="showQuotaModal = false"
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">取消</button>
<button @click="submitAddQuota"
class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700">充值</button>
</div>
</div>
</div>
</div>
<!-- 批量导入弹窗 -->
<div v-if="showImportModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">批量导入账号</h3>
</div>
<div class="p-6">
<p class="text-sm text-gray-600 mb-4">请输入 JSON 格式的账号数据,每行一个账号对象:</p>
<textarea v-model="importData" rows="10" placeholder='[
{"email": "xxx@example.com", "access_token": "...", "refresh_token": "...", "membership_type": "pro"},
...
]'
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm"></textarea>
<div v-if="importResult" class="mt-4 p-3 rounded-md" :class="importResult.failed > 0 ? 'bg-yellow-50' : 'bg-green-50'">
<p class="text-sm">导入完成:成功 {{ importResult.success }} 个,失败 {{ importResult.failed }} 个</p>
</div>
<div class="flex justify-end space-x-3 mt-6">
<button @click="showImportModal = false; importResult = null"
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">关闭</button>
<button @click="importAccounts"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">导入</button>
</div>
</div>
</div>
</div>
<!-- 延期弹窗 -->
<div v-if="showExtendModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-sm mx-4">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">延长有效期</h3>
</div>
<div class="p-6 space-y-4">
<p class="text-sm text-gray-600">
当前状态:
<span v-if="extendTarget?.expire_at">{{ formatDate(extendTarget.expire_at) }}</span>
<span v-else-if="extendTarget?.first_activated_at">{{ extendTarget.valid_days }}天 (已激活)</span>
<span v-else>{{ extendTarget?.valid_days }}天 (未激活)</span>
</p>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">延长天数</label>
<input v-model.number="extendDays" type="number" min="1"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex justify-end space-x-3 pt-4">
<button type="button" @click="showExtendModal = false"
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">取消</button>
<button @click="submitExtend"
class="px-4 py-2 bg-yellow-600 text-white rounded-md hover:bg-yellow-700">延期</button>
</div>
</div>
</div>
</div>
<!-- 激活码详情弹窗 -->
<div v-if="showKeyDetailModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-3xl mx-4 max-h-[90vh] overflow-y-auto">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900">激活码详情</h3>
<button @click="showKeyDetailModal = false" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="p-6" v-if="keyDetail">
<!-- 基本信息 -->
<div class="mb-6">
<h4 class="font-medium text-gray-700 mb-2">基本信息</h4>
<div class="bg-gray-50 rounded-md p-4 grid grid-cols-2 gap-4 text-sm">
<div><span class="text-gray-500">激活码:</span><code class="bg-gray-200 px-2 py-1 rounded">{{ keyDetail.key }}</code></div>
<div><span class="text-gray-500">类型:</span>{{ keyDetail.membership_type === 'pro' ? 'Pro (高级模型)' : 'Auto (无限换号)' }}</div>
<div><span class="text-gray-500">状态:</span>{{ keyDetail.status === 'active' ? '有效' : '禁用' }}</div>
<div><span class="text-gray-500">激活时间:</span>{{ keyDetail.first_activated_at ? formatDate(keyDetail.first_activated_at) : '未激活' }}</div>
<div><span class="text-gray-500">过期时间:</span>{{ keyDetail.expire_at ? formatDate(keyDetail.expire_at) : '永久' }}</div>
<div><span class="text-gray-500">换号次数:</span>{{ keyDetail.switch_count }}</div>
</div>
</div>
<!-- 绑定设备 -->
<div class="mb-6">
<h4 class="font-medium text-gray-700 mb-2">绑定设备 ({{ keyDevices.length }}/{{ keyDetail.max_devices }})</h4>
<div class="bg-gray-50 rounded-md overflow-hidden">
<table class="min-w-full text-sm" v-if="keyDevices.length > 0">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-2 text-left text-gray-600">设备ID</th>
<th class="px-4 py-2 text-left text-gray-600">绑定时间</th>
<th class="px-4 py-2 text-left text-gray-600">最后活跃</th>
<th class="px-4 py-2 text-left text-gray-600">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="device in keyDevices" :key="device.id" class="border-t border-gray-200">
<td class="px-4 py-2"><code class="text-xs bg-gray-200 px-1 rounded">{{ device.device_id.slice(0, 16) }}...</code></td>
<td class="px-4 py-2 text-gray-500">{{ device.created_at }}</td>
<td class="px-4 py-2 text-gray-500">{{ device.last_active_at || '-' }}</td>
<td class="px-4 py-2">
<button @click="deleteDevice(device.id)" class="text-red-600 hover:text-red-900 text-xs">解绑</button>
</td>
</tr>
</tbody>
</table>
<p v-else class="px-4 py-3 text-gray-500 text-center">暂无绑定设备</p>
</div>
</div>
<!-- 使用记录 -->
<div>
<h4 class="font-medium text-gray-700 mb-2">使用记录</h4>
<div class="bg-gray-50 rounded-md overflow-hidden">
<table class="min-w-full text-sm" v-if="keyLogs.length > 0">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-2 text-left text-gray-600">时间</th>
<th class="px-4 py-2 text-left text-gray-600">操作</th>
<th class="px-4 py-2 text-left text-gray-600">IP</th>
<th class="px-4 py-2 text-left text-gray-600">状态</th>
<th class="px-4 py-2 text-left text-gray-600">消息</th>
</tr>
</thead>
<tbody>
<tr v-for="log in keyLogs" :key="log.id" class="border-t border-gray-200">
<td class="px-4 py-2 text-gray-500">{{ log.created_at }}</td>
<td class="px-4 py-2">
<span :class="log.action === 'switch' ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'" class="px-2 py-0.5 text-xs rounded-full">
{{ log.action === 'switch' ? '换号' : '验证' }}
</span>
</td>
<td class="px-4 py-2 text-gray-500">{{ log.ip_address || '-' }}</td>
<td class="px-4 py-2">
<span :class="log.success ? 'text-green-600' : 'text-red-600'">{{ log.success ? '成功' : '失败' }}</span>
</td>
<td class="px-4 py-2 text-gray-500">{{ log.message || '-' }}</td>
</tr>
</tbody>
</table>
<p v-else class="px-4 py-3 text-gray-500 text-center">暂无使用记录</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, reactive, onMounted, watch } = Vue
createApp({
setup() {
// 状态
const isLoggedIn = ref(false)
const currentUser = ref('')
const token = ref('')
const currentTab = ref('dashboard')
const loginError = ref('')
// 数据
const stats = ref({ total_accounts: 0, active_accounts: 0, pro_accounts: 0, total_keys: 0, active_keys: 0, today_usage: 0 })
const accounts = ref([])
const keys = ref([])
const logs = ref([])
const logFilter = reactive({ action: '' })
// 搜索
const keySearch = reactive({ search: '', status: '', activated: '', membership_type: '' })
// 表单
const loginForm = reactive({ username: '', password: '' })
const accountForm = reactive({ email: '', access_token: '', refresh_token: '', workos_session_token: '', membership_type: 'pro' })
const keyForm = reactive({
count: 1,
membership_type: 'pro',
quota: 500, // Pro用
valid_days: 30, // Auto用
max_devices: 2,
remark: ''
})
// 弹窗
const showAccountModal = ref(false)
const showKeyModal = ref(false)
const showImportModal = ref(false)
const showQuotaModal = ref(false)
const showExtendModal = ref(false)
const editingAccount = ref(null)
const editingKey = ref(null)
const importData = ref('')
const importResult = ref(null)
const quotaTarget = ref(null)
const addQuotaAmount = ref(100)
const extendTarget = ref(null)
const extendDays = ref(7)
// 激活码详情
const showKeyDetailModal = ref(false)
const keyDetail = ref(null)
const keyDevices = ref([])
const keyLogs = ref([])
// 全局设置
const globalSettings = reactive({
auto_switch_interval_minutes: 20,
auto_max_switches_per_day: 50,
pro_quota_cost: 50
})
// 批量补偿
const compensateForm = reactive({
membership_type: '',
activated_before: '',
not_expired_on: '',
extend_days: 0,
add_quota: 0
})
const compensatePreview = ref(null)
const compensateResult = ref(null)
// API 请求封装
const api = axios.create({ baseURL: '/admin' })
api.interceptors.request.use(config => {
if (token.value) {
config.headers.Authorization = `Bearer ${token.value}`
}
return config
})
// 登录
const login = async () => {
try {
const res = await axios.post('/admin/login', loginForm)
token.value = res.data.access_token
localStorage.setItem('token', token.value)
isLoggedIn.value = true
currentUser.value = loginForm.username
loginError.value = ''
loadData()
} catch (e) {
loginError.value = e.response?.data?.detail || '登录失败'
}
}
// 登出
const logout = () => {
token.value = ''
localStorage.removeItem('token')
isLoggedIn.value = false
}
// 加载数据
const loadData = async () => {
try {
const [statsRes, accountsRes, keysRes] = await Promise.all([
api.get('/dashboard'),
api.get('/accounts'),
api.get('/keys')
])
stats.value = statsRes.data
accounts.value = accountsRes.data
keys.value = keysRes.data
} catch (e) {
console.error('加载数据失败', e)
}
}
// 账号操作
const editAccount = (account) => {
editingAccount.value = account
Object.assign(accountForm, account)
showAccountModal.value = true
}
const saveAccount = async () => {
try {
if (editingAccount.value) {
await api.put(`/accounts/${editingAccount.value.id}`, accountForm)
} else {
await api.post('/accounts', accountForm)
}
showAccountModal.value = false
loadData()
} catch (e) {
alert(e.response?.data?.detail || '保存失败')
}
}
const deleteAccount = async (id) => {
if (!confirm('确定删除此账号?')) return
try {
await api.delete(`/accounts/${id}`)
loadData()
} catch (e) {
alert('删除失败')
}
}
const importAccounts = async () => {
try {
const data = JSON.parse(importData.value)
const res = await api.post('/accounts/import', { accounts: data })
importResult.value = res.data
loadData()
} catch (e) {
alert('导入失败: ' + (e.response?.data?.detail || e.message))
}
}
// 激活码操作
const resetKeyForm = () => {
Object.assign(keyForm, {
count: 1,
membership_type: 'pro',
quota: 500,
valid_days: 30,
max_devices: 2,
remark: ''
})
}
const editKey = (key) => {
editingKey.value = key
Object.assign(keyForm, key)
showKeyModal.value = true
}
const saveKey = async () => {
try {
if (editingKey.value) {
await api.put(`/keys/${editingKey.value.id}`, keyForm)
} else {
await api.post('/keys', keyForm)
}
showKeyModal.value = false
loadData()
} catch (e) {
alert(e.response?.data?.detail || '保存失败')
}
}
const deleteKey = async (id) => {
if (!confirm('确定删除此激活码?')) return
try {
await api.delete(`/keys/${id}`)
loadData()
} catch (e) {
alert('删除失败')
}
}
const addQuota = (key) => {
quotaTarget.value = key
addQuotaAmount.value = 100
showQuotaModal.value = true
}
const submitAddQuota = async () => {
try {
await api.post(`/keys/${quotaTarget.value.id}/add-quota?add_quota=${addQuotaAmount.value}`)
showQuotaModal.value = false
loadData()
alert('充值成功')
} catch (e) {
alert(e.response?.data?.detail || '充值失败')
}
}
const copyKey = (key) => {
navigator.clipboard.writeText(key)
alert('已复制')
}
// 延期操作
const extendKey = (key) => {
extendTarget.value = key
extendDays.value = 7
showExtendModal.value = true
}
const submitExtend = async () => {
try {
await api.post('/keys/batch-extend', {
key_ids: [extendTarget.value.id],
extend_days: extendDays.value,
add_quota: 0
})
showExtendModal.value = false
loadData()
alert('延期成功')
} catch (e) {
alert(e.response?.data?.detail || '延期失败')
}
}
// 全局设置
const loadSettings = async () => {
try {
const res = await api.get('/settings')
Object.assign(globalSettings, res.data)
} catch (e) {
console.error('加载设置失败', e)
}
}
const saveSettings = async () => {
try {
await api.put('/settings', globalSettings)
alert('设置已保存')
} catch (e) {
alert(e.response?.data?.detail || '保存失败')
}
}
// 批量补偿
const previewCompensate = async () => {
try {
const params = new URLSearchParams()
if (compensateForm.membership_type) params.append('membership_type', compensateForm.membership_type)
if (compensateForm.activated_before) params.append('activated_before', compensateForm.activated_before)
if (compensateForm.not_expired_on) params.append('not_expired_on', compensateForm.not_expired_on)
const res = await api.get('/keys/preview-compensate?' + params.toString())
compensatePreview.value = res.data
compensateResult.value = null
} catch (e) {
alert(e.response?.data?.detail || '预览失败')
}
}
const executeCompensate = async () => {
if (!confirm('确定执行批量补偿?此操作不可撤销!')) return
try {
const params = new URLSearchParams()
if (compensateForm.membership_type) params.append('membership_type', compensateForm.membership_type)
if (compensateForm.activated_before) params.append('activated_before', compensateForm.activated_before)
if (compensateForm.not_expired_on) params.append('not_expired_on', compensateForm.not_expired_on)
params.append('extend_days', compensateForm.extend_days)
params.append('add_quota', compensateForm.add_quota)
const res = await api.post('/keys/batch-compensate?' + params.toString())
compensateResult.value = res.data
compensatePreview.value = null
loadData()
} catch (e) {
alert(e.response?.data?.detail || '补偿失败')
}
}
// 日志
const loadLogs = async () => {
try {
const params = new URLSearchParams()
if (logFilter.action) params.append('action', logFilter.action)
const res = await api.get('/logs?' + params.toString())
logs.value = res.data
} catch (e) {
console.error('加载日志失败', e)
}
}
// 激活码详情
const viewKeyDetail = async (key) => {
keyDetail.value = key
showKeyDetailModal.value = true
try {
const [devicesRes, logsRes] = await Promise.all([
api.get(`/keys/${key.id}/devices`),
api.get(`/keys/${key.id}/logs`)
])
keyDevices.value = devicesRes.data
keyLogs.value = logsRes.data
} catch (e) {
console.error('加载详情失败', e)
}
}
const deleteDevice = async (deviceId) => {
if (!confirm('确定解绑此设备?')) return
try {
await api.delete(`/devices/${deviceId}`)
keyDevices.value = keyDevices.value.filter(d => d.id !== deviceId)
} catch (e) {
alert(e.response?.data?.detail || '解绑失败')
}
}
// 搜索激活码
let searchTimeout = null
const searchKeys = () => {
clearTimeout(searchTimeout)
searchTimeout = setTimeout(async () => {
try {
const params = new URLSearchParams()
if (keySearch.search) params.append('search', keySearch.search)
if (keySearch.status) params.append('status', keySearch.status)
if (keySearch.activated) params.append('activated', keySearch.activated)
if (keySearch.membership_type) params.append('membership_type', keySearch.membership_type)
const res = await api.get('/keys?' + params.toString())
keys.value = res.data
} catch (e) {
console.error('搜索失败', e)
}
}, 300)
}
const resetKeySearch = () => {
keySearch.search = ''
keySearch.status = ''
keySearch.activated = ''
keySearch.membership_type = ''
loadData()
}
// 禁用/启用激活码
const disableKey = async (key) => {
try {
const res = await api.get(`/keys/${key.id}/usage-info`)
const info = res.data
const msg = `确定禁用此激活码?\n\n` +
`激活码: ${info.key}\n` +
`类型: ${info.membership_type === 'pro' ? 'Pro' : 'Auto'}\n` +
`激活时间: ${info.first_activated_at || '未激活'}\n` +
`已使用: ${info.used_days}\n` +
`换号次数: ${info.switch_count}\n` +
(info.membership_type === 'pro' ? `已用额度: ${info.quota_used}/${info.quota}\n` : '') +
`绑定设备: ${info.device_count}/${info.max_devices}`
if (confirm(msg)) {
const disableRes = await api.post(`/keys/${key.id}/disable`)
alert(`禁用成功!\n已使用 ${disableRes.data.used_days} 天,换号 ${disableRes.data.switch_count}`)
searchKeys()
}
} catch (e) {
alert(e.response?.data?.detail || '操作失败')
}
}
const enableKey = async (key) => {
if (!confirm('确定启用此激活码?')) return
try {
await api.post(`/keys/${key.id}/enable`)
searchKeys()
} catch (e) {
alert(e.response?.data?.detail || '操作失败')
}
}
// 辅助函数
const getStatusClass = (status) => {
const map = {
'active': 'bg-green-100 text-green-800',
'in_use': 'bg-blue-100 text-blue-800',
'disabled': 'bg-red-100 text-red-800',
'expired': 'bg-gray-100 text-gray-800'
}
return map[status] || 'bg-gray-100 text-gray-800'
}
const getStatusText = (status) => {
const map = { 'active': '可用', 'in_use': '使用中', 'disabled': '禁用', 'expired': '过期' }
return map[status] || status
}
const formatDate = (date) => {
return new Date(date).toLocaleString('zh-CN')
}
// 初始化
onMounted(() => {
const savedToken = localStorage.getItem('token')
if (savedToken) {
token.value = savedToken
isLoggedIn.value = true
currentUser.value = 'admin'
loadData()
}
})
// 监听标签页切换
watch(currentTab, (newTab) => {
if (newTab !== 'settings' && newTab !== 'compensate') {
loadData()
}
})
return {
isLoggedIn, currentUser, currentTab, loginError, loginForm,
stats, accounts, keys, logs, logFilter, keySearch,
accountForm, keyForm,
showAccountModal, showKeyModal, showImportModal, showQuotaModal, showExtendModal,
showKeyDetailModal, keyDetail, keyDevices, keyLogs,
editingAccount, editingKey,
importData, importResult,
quotaTarget, addQuotaAmount,
extendTarget, extendDays,
globalSettings,
compensateForm, compensatePreview, compensateResult,
login, logout, loadData, loadLogs, searchKeys, resetKeySearch,
editAccount, saveAccount, deleteAccount, importAccounts,
resetKeyForm, editKey, saveKey, deleteKey, addQuota, submitAddQuota, copyKey,
viewKeyDetail, deleteDevice, disableKey, enableKey,
extendKey, submitExtend,
loadSettings, saveSettings,
previewCompensate, executeCompensate,
getStatusClass, getStatusText, formatDate
}
}
}).mount('#app')
</script>
</body>
</html>