backend v2.1: 公告管理功能 + 系统重构
- 新增 Announcement 数据模型,支持公告的增删改查 - 后台管理新增"公告管理"Tab(创建/编辑/删除/启用禁用) - 客户端 /api/announcement 改为从数据库读取 - 账号服务重构,新增无感换号、自动分析等功能 - 新增后台任务调度器、数据库迁移脚本 - Schema/Service/Config 全面升级至 v2.1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,11 @@
|
||||
class="px-3 py-2 font-medium text-sm rounded-md">
|
||||
批量补偿
|
||||
</button>
|
||||
<button @click="currentTab = 'announcements'; loadAnnouncements()"
|
||||
:class="currentTab === 'announcements' ? '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">
|
||||
@@ -141,7 +146,7 @@
|
||||
"access_token": "eyJhbG...",
|
||||
"refresh_token": "xxx", // 可选
|
||||
"workos_session_token": "xxx", // 可选
|
||||
"membership_type": "free", // free=Auto账号, pro=高级账号
|
||||
"membership_type": "free", // free=Free账号, pro=Pro账号
|
||||
"remark": "备注" // 可选
|
||||
}
|
||||
],
|
||||
@@ -174,7 +179,7 @@
|
||||
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"
|
||||
<button @click="openCreateAccountModal"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
|
||||
添加账号
|
||||
</button>
|
||||
@@ -207,7 +212,7 @@
|
||||
<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() }}
|
||||
{{ formatMembershipLabel(account.membership_type) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm space-x-1">
|
||||
@@ -233,6 +238,11 @@
|
||||
</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="openAnalyzeModal(account)"
|
||||
class="text-green-600 hover:text-green-900">
|
||||
手动检测
|
||||
</button>
|
||||
<span class="text-gray-300">|</span>
|
||||
<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>
|
||||
@@ -335,7 +345,7 @@
|
||||
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>
|
||||
<option value="auto">Auto (无限换号)</option>
|
||||
</select>
|
||||
<button @click="resetKeySearch" class="px-3 py-2 text-gray-600 hover:text-gray-800 text-sm">
|
||||
重置
|
||||
@@ -478,13 +488,13 @@
|
||||
<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"
|
||||
<input v-model.number="globalSettings.auto_switch_interval" 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">两次换号之间至少等待的时间</p>
|
||||
<p class="text-xs text-gray-500 mt-1">两次换号之间至少等待的时间,0表示无限制</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"
|
||||
<input v-model.number="globalSettings.auto_daily_switches" 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>
|
||||
@@ -493,7 +503,7 @@
|
||||
<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"
|
||||
<input v-model.number="globalSettings.pro_quota_per_switch" 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>
|
||||
@@ -529,7 +539,7 @@
|
||||
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>
|
||||
<option value="auto">Auto (无限换号)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
@@ -621,6 +631,58 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 公告管理 -->
|
||||
<div v-if="currentTab === 'announcements'">
|
||||
<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>
|
||||
<button @click="openCreateAnnouncementModal"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
|
||||
发布公告
|
||||
</button>
|
||||
</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="ann in announcements" :key="ann.id">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ ann.title }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span :class="announcementTypeClass(ann.type)"
|
||||
class="px-2 py-1 text-xs font-medium rounded-full">
|
||||
{{ announcementTypeLabel(ann.type) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<button @click="toggleAnnouncement(ann)"
|
||||
:class="ann.is_active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'"
|
||||
class="px-2 py-1 text-xs font-medium rounded-full cursor-pointer hover:opacity-80 transition">
|
||||
{{ ann.is_active ? '已启用' : '已禁用' }}
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDate(ann.created_at) }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm space-x-2">
|
||||
<button @click="editAnnouncement(ann)" class="text-blue-600 hover:text-blue-900">编辑</button>
|
||||
<button @click="deleteAnnouncement(ann.id)" class="text-red-600 hover:text-red-900">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="announcements.length === 0">
|
||||
<td colspan="5" class="px-6 py-4 text-center text-gray-500">暂无公告</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用日志 -->
|
||||
<div v-if="currentTab === 'logs'">
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
@@ -696,7 +758,7 @@
|
||||
</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"
|
||||
<textarea v-model="accountForm.access_token" 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>
|
||||
@@ -710,12 +772,9 @@
|
||||
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"
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">备注</label>
|
||||
<input v-model="accountForm.remark" type="text" placeholder="来源、部门等"
|
||||
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"
|
||||
@@ -727,6 +786,76 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手动检测弹窗 -->
|
||||
<div v-if="showAnalyzeModal" 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 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">手动检测账号</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">可粘贴最新的 WorkosCursorSessionToken 或 user_xxx::jwt 用于调试</p>
|
||||
<p class="text-sm text-gray-600 mt-2">
|
||||
当前账号: <span class="font-mono text-blue-600">{{ analyzeTarget ? analyzeTarget.email : '' }}</span>
|
||||
<span class="ml-4">状态: {{ getStatusText(analyzeTarget ? analyzeTarget.status : null) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<button @click="closeAnalyzeModal" class="text-gray-400 hover:text-gray-600">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-4 max-h-[70vh] overflow-y-auto">
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-md px-3 py-2 text-sm">
|
||||
<p class="text-gray-600">Workos Session</p>
|
||||
<p class="mt-1 font-mono break-all text-gray-800">
|
||||
{{ analyzeTarget && analyzeTarget.workos_session_token ? analyzeTarget.workos_session_token : '未保存' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-md px-3 py-2 text-sm">
|
||||
<p class="text-gray-600">Access Token</p>
|
||||
<p class="mt-1 font-mono break-all text-gray-800">
|
||||
{{ analyzeTarget && analyzeTarget.access_token ? analyzeTarget.access_token : '未保存' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-md px-3 py-2 text-sm">
|
||||
<p class="text-gray-600">Refresh Token</p>
|
||||
<p class="mt-1 font-mono break-all text-gray-800">
|
||||
{{ analyzeTarget && analyzeTarget.refresh_token ? analyzeTarget.refresh_token : '未保存' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">检测用 Token</label>
|
||||
<textarea v-model="analyzeForm.token" rows="4"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="WorkosCursorSessionToken=... 或 user_xxx::jwt"></textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">若留空,将使用系统当前保存的 Token</p>
|
||||
</div>
|
||||
<label class="flex items-center space-x-2 text-sm text-gray-700">
|
||||
<input type="checkbox" v-model="analyzeForm.saveToken"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
<span>检测成功后,将本次 Token 同步到账号</span>
|
||||
</label>
|
||||
<div v-if="analyzeResult" class="rounded-md border px-4 py-3"
|
||||
:class="analyzeResult.success ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50'">
|
||||
<p class="font-medium" :class="analyzeResult.success ? 'text-green-700' : 'text-red-700'">
|
||||
{{ analyzeResult.success ? '检测成功' : '检测失败' }}
|
||||
</p>
|
||||
<p v-if="analyzeResult.message" class="text-sm mt-1">{{ analyzeResult.message }}</p>
|
||||
<pre v-if="analyzeResult.data" class="mt-3 text-xs bg-white border rounded p-2 overflow-auto max-h-48">{{ JSON.stringify(analyzeResult.data, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3">
|
||||
<button @click="closeAnalyzeModal"
|
||||
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">取消</button>
|
||||
<button @click="submitAnalyze" :disabled="analyzeLoading"
|
||||
:class="analyzeLoading ? 'bg-blue-300 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700'"
|
||||
class="px-4 py-2 text-white rounded-md">
|
||||
{{ analyzeLoading ? '检测中...' : '开始检测' }}
|
||||
</button>
|
||||
</div>
|
||||
</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">
|
||||
@@ -744,7 +873,7 @@
|
||||
<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>
|
||||
<option value="auto">Auto (无限换号) - 按时间计费</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -757,7 +886,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Auto密钥: 只设置有效天数 -->
|
||||
<div v-if="keyForm.membership_type === 'free'">
|
||||
<div v-if="keyForm.membership_type === 'auto'">
|
||||
<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">
|
||||
@@ -958,6 +1087,49 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 公告编辑弹窗 -->
|
||||
<div v-if="showAnnouncementModal" 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-lg mx-4">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">{{ editingAnnouncement ? '编辑公告' : '发布公告' }}</h3>
|
||||
</div>
|
||||
<form @submit.prevent="saveAnnouncement" class="p-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">标题</label>
|
||||
<input v-model="announcementForm.title" type="text" required
|
||||
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>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">内容</label>
|
||||
<textarea v-model="announcementForm.content" rows="5" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="公告内容,支持换行"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">类型</label>
|
||||
<select v-model="announcementForm.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="info">信息 (info)</option>
|
||||
<option value="warning">警告 (warning)</option>
|
||||
<option value="error">错误 (error)</option>
|
||||
<option value="success">成功 (success)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="checkbox" v-model="announcementForm.is_active" id="ann-active"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
<label for="ann-active" class="text-sm text-gray-700">启用</label>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button type="button" @click="showAnnouncementModal = 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>
|
||||
|
||||
<script>
|
||||
@@ -998,7 +1170,13 @@
|
||||
|
||||
// 表单
|
||||
const loginForm = reactive({ username: '', password: '' })
|
||||
const accountForm = reactive({ email: '', access_token: '', refresh_token: '', workos_session_token: '', membership_type: 'pro' })
|
||||
const accountForm = reactive({
|
||||
email: '',
|
||||
access_token: '',
|
||||
refresh_token: '',
|
||||
workos_session_token: '',
|
||||
remark: ''
|
||||
})
|
||||
const keyForm = reactive({
|
||||
count: 1,
|
||||
membership_type: 'pro',
|
||||
@@ -1010,11 +1188,13 @@
|
||||
|
||||
// 弹窗
|
||||
const showAccountModal = ref(false)
|
||||
const showAnalyzeModal = ref(false)
|
||||
const showKeyModal = ref(false)
|
||||
const showImportModal = ref(false)
|
||||
const showQuotaModal = ref(false)
|
||||
const showExtendModal = ref(false)
|
||||
const editingAccount = ref(null)
|
||||
const analyzeTarget = ref(null)
|
||||
const editingKey = ref(null)
|
||||
const importData = ref('')
|
||||
const importResult = ref(null)
|
||||
@@ -1029,11 +1209,22 @@
|
||||
const keyDevices = ref([])
|
||||
const keyLogs = ref([])
|
||||
|
||||
// 公告管理
|
||||
const announcements = ref([])
|
||||
const showAnnouncementModal = ref(false)
|
||||
const editingAnnouncement = ref(null)
|
||||
const announcementForm = reactive({
|
||||
title: '',
|
||||
content: '',
|
||||
type: 'info',
|
||||
is_active: true
|
||||
})
|
||||
|
||||
// 全局设置
|
||||
const globalSettings = reactive({
|
||||
auto_switch_interval_minutes: 20,
|
||||
auto_max_switches_per_day: 50,
|
||||
pro_quota_cost: 50
|
||||
auto_switch_interval: 0,
|
||||
auto_daily_switches: 999,
|
||||
pro_quota_per_switch: 1
|
||||
})
|
||||
|
||||
// 批量补偿
|
||||
@@ -1120,23 +1311,47 @@
|
||||
}
|
||||
|
||||
// 账号操作
|
||||
const openCreateAccountModal = () => {
|
||||
editingAccount.value = null
|
||||
resetAccountForm()
|
||||
showAccountModal.value = true
|
||||
}
|
||||
|
||||
const editAccount = (account) => {
|
||||
editingAccount.value = account
|
||||
Object.assign(accountForm, account)
|
||||
prepareAccountForm(account)
|
||||
showAccountModal.value = true
|
||||
}
|
||||
|
||||
const buildAccountPayload = () => {
|
||||
const workosToken = accountForm.workos_session_token?.trim() || ''
|
||||
const accessToken = accountForm.access_token?.trim() || ''
|
||||
if (!workosToken && !accessToken) {
|
||||
throw new Error('请至少填写 Access Token 或 Workos Token')
|
||||
}
|
||||
return {
|
||||
email: accountForm.email,
|
||||
token: workosToken || accessToken,
|
||||
workos_session_token: workosToken || null,
|
||||
access_token: accessToken || null,
|
||||
refresh_token: accountForm.refresh_token?.trim() || null,
|
||||
remark: accountForm.remark?.trim() || ''
|
||||
}
|
||||
}
|
||||
|
||||
const saveAccount = async () => {
|
||||
try {
|
||||
const payload = buildAccountPayload()
|
||||
if (editingAccount.value) {
|
||||
await api.put(`/accounts/${editingAccount.value.id}`, accountForm)
|
||||
await api.put(`/accounts/${editingAccount.value.id}`, payload)
|
||||
} else {
|
||||
await api.post('/accounts', accountForm)
|
||||
await api.post('/accounts', payload)
|
||||
}
|
||||
showAccountModal.value = false
|
||||
loadAccounts()
|
||||
} catch (e) {
|
||||
alert(e.response?.data?.detail || '保存失败')
|
||||
const message = e.message || e.response?.data?.detail
|
||||
alert(message || '保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1150,6 +1365,72 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 手动分析单个账号
|
||||
const analyzeForm = reactive({
|
||||
token: '',
|
||||
saveToken: false
|
||||
})
|
||||
const analyzeResult = ref(null)
|
||||
const analyzeLoading = ref(false)
|
||||
|
||||
const resetAccountForm = () => {
|
||||
accountForm.email = ''
|
||||
accountForm.access_token = ''
|
||||
accountForm.refresh_token = ''
|
||||
accountForm.workos_session_token = ''
|
||||
accountForm.remark = ''
|
||||
}
|
||||
|
||||
const prepareAccountForm = (account) => {
|
||||
accountForm.email = account.email || ''
|
||||
accountForm.access_token = account.access_token || ''
|
||||
accountForm.refresh_token = account.refresh_token || ''
|
||||
accountForm.workos_session_token = account.workos_session_token || account.token || ''
|
||||
accountForm.remark = account.remark || ''
|
||||
}
|
||||
|
||||
const openAnalyzeModal = (account) => {
|
||||
analyzeTarget.value = account
|
||||
analyzeForm.token = account?.workos_session_token || account?.access_token || account?.token || ''
|
||||
analyzeForm.saveToken = false
|
||||
analyzeResult.value = null
|
||||
showAnalyzeModal.value = true
|
||||
}
|
||||
|
||||
const closeAnalyzeModal = () => {
|
||||
showAnalyzeModal.value = false
|
||||
analyzeTarget.value = null
|
||||
analyzeForm.token = ''
|
||||
analyzeForm.saveToken = false
|
||||
analyzeResult.value = null
|
||||
}
|
||||
|
||||
const submitAnalyze = async () => {
|
||||
if (!analyzeTarget.value) return
|
||||
analyzeLoading.value = true
|
||||
analyzeResult.value = null
|
||||
try {
|
||||
const payload = {
|
||||
save_token: analyzeForm.saveToken
|
||||
}
|
||||
if (analyzeForm.token?.trim()) {
|
||||
payload.token = analyzeForm.token.trim()
|
||||
}
|
||||
const res = await api.post(`/accounts/${analyzeTarget.value.id}/analyze`, payload)
|
||||
analyzeResult.value = res.data
|
||||
if (res.data?.success) {
|
||||
await loadAccounts()
|
||||
}
|
||||
} catch (e) {
|
||||
analyzeResult.value = {
|
||||
success: false,
|
||||
message: e.response?.data?.detail || '分析失败'
|
||||
}
|
||||
} finally {
|
||||
analyzeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 快捷切换账号状态
|
||||
const toggleAccountStatus = async (account) => {
|
||||
const statusMap = { 'active': '可用', 'in_use': '使用中', 'disabled': '禁用', 'expired': '过期' }
|
||||
@@ -1545,6 +1826,82 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 公告管理
|
||||
const loadAnnouncements = async () => {
|
||||
try {
|
||||
const res = await api.get('/announcements')
|
||||
announcements.value = res.data
|
||||
} catch (e) {
|
||||
console.error('加载公告失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
const openCreateAnnouncementModal = () => {
|
||||
editingAnnouncement.value = null
|
||||
announcementForm.title = ''
|
||||
announcementForm.content = ''
|
||||
announcementForm.type = 'info'
|
||||
announcementForm.is_active = true
|
||||
showAnnouncementModal.value = true
|
||||
}
|
||||
|
||||
const editAnnouncement = (ann) => {
|
||||
editingAnnouncement.value = ann
|
||||
announcementForm.title = ann.title
|
||||
announcementForm.content = ann.content
|
||||
announcementForm.type = ann.type
|
||||
announcementForm.is_active = ann.is_active
|
||||
showAnnouncementModal.value = true
|
||||
}
|
||||
|
||||
const saveAnnouncement = async () => {
|
||||
try {
|
||||
if (editingAnnouncement.value) {
|
||||
await api.put(`/announcements/${editingAnnouncement.value.id}`, announcementForm)
|
||||
} else {
|
||||
await api.post('/announcements', announcementForm)
|
||||
}
|
||||
showAnnouncementModal.value = false
|
||||
loadAnnouncements()
|
||||
} catch (e) {
|
||||
alert(e.response?.data?.detail || '保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAnnouncement = async (id) => {
|
||||
if (!confirm('确定删除此公告?')) return
|
||||
try {
|
||||
await api.delete(`/announcements/${id}`)
|
||||
loadAnnouncements()
|
||||
} catch (e) {
|
||||
alert(e.response?.data?.detail || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleAnnouncement = async (ann) => {
|
||||
try {
|
||||
await api.post(`/announcements/${ann.id}/toggle`)
|
||||
loadAnnouncements()
|
||||
} catch (e) {
|
||||
alert(e.response?.data?.detail || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const announcementTypeClass = (type) => {
|
||||
const map = {
|
||||
'info': 'bg-blue-100 text-blue-800',
|
||||
'warning': 'bg-yellow-100 text-yellow-800',
|
||||
'error': 'bg-red-100 text-red-800',
|
||||
'success': 'bg-green-100 text-green-800'
|
||||
}
|
||||
return map[type] || 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
|
||||
const announcementTypeLabel = (type) => {
|
||||
const map = { 'info': '信息', 'warning': '警告', 'error': '错误', 'success': '成功' }
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
const getStatusClass = (status) => {
|
||||
const map = {
|
||||
@@ -1556,6 +1913,11 @@
|
||||
return map[status] || 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
|
||||
const formatMembershipLabel = (type) => {
|
||||
if (!type) return '未知'
|
||||
return String(type).toUpperCase()
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const map = { 'active': '可用', 'in_use': '使用中', 'disabled': '禁用', 'expired': '过期' }
|
||||
return map[status] || status
|
||||
@@ -1578,7 +1940,9 @@
|
||||
|
||||
// 监听标签页切换
|
||||
watch(currentTab, (newTab) => {
|
||||
if (newTab !== 'settings' && newTab !== 'compensate') {
|
||||
if (newTab === 'announcements') {
|
||||
loadAnnouncements()
|
||||
} else if (newTab !== 'settings' && newTab !== 'compensate') {
|
||||
loadData()
|
||||
}
|
||||
})
|
||||
@@ -1589,24 +1953,29 @@
|
||||
keys, keysTotal, selectedKeys, keysPagination, isAllKeysSelected,
|
||||
logs, logFilter, keySearch,
|
||||
accountForm, keyForm,
|
||||
showAccountModal, showKeyModal, showImportModal, showQuotaModal, showExtendModal,
|
||||
showAccountModal, showAnalyzeModal, showKeyModal, showImportModal, showQuotaModal, showExtendModal,
|
||||
showKeyDetailModal, keyDetail, keyDevices, keyLogs,
|
||||
editingAccount, editingKey,
|
||||
editingAccount, editingKey, analyzeTarget,
|
||||
importData, importResult,
|
||||
quotaTarget, addQuotaAmount,
|
||||
extendTarget, extendDays,
|
||||
globalSettings,
|
||||
compensateForm, compensatePreview, compensateResult,
|
||||
announcements, showAnnouncementModal, editingAnnouncement, announcementForm,
|
||||
loadAnnouncements, openCreateAnnouncementModal, editAnnouncement, saveAnnouncement,
|
||||
deleteAnnouncement, toggleAnnouncement, announcementTypeClass, announcementTypeLabel,
|
||||
login, logout, loadData, loadAccounts, loadLogs, searchKeys, resetKeySearch,
|
||||
toggleSelectAll, batchEnableAccounts, batchDisableAccounts, batchDeleteAccounts,
|
||||
toggleSelectAllKeys, batchCopyKeys, batchEnableKeys, batchDisableKeys, batchDeleteKeys,
|
||||
editAccount, saveAccount, deleteAccount, importAccounts, toggleAccountStatus,
|
||||
openCreateAccountModal, editAccount, saveAccount, deleteAccount, importAccounts, toggleAccountStatus,
|
||||
openAnalyzeModal, closeAnalyzeModal, submitAnalyze,
|
||||
resetKeyForm, editKey, saveKey, deleteKey, addQuota, submitAddQuota, copyKey, copyToken,
|
||||
viewKeyDetail, deleteDevice, disableKey, enableKey,
|
||||
extendKey, submitExtend,
|
||||
analyzeForm, analyzeResult, analyzeLoading,
|
||||
loadSettings, saveSettings,
|
||||
previewCompensate, executeCompensate,
|
||||
getStatusClass, getStatusText, formatDate
|
||||
getStatusClass, getStatusText, formatDate, formatMembershipLabel
|
||||
}
|
||||
}
|
||||
}).mount('#app')
|
||||
|
||||
Reference in New Issue
Block a user