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:
2026-03-09 19:58:05 +08:00
parent 73a71f198f
commit ac19d029da
20 changed files with 3341 additions and 1440 deletions

View File

@@ -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')