备份: 完整开发状态(含反混淆脚本和临时文件)

This commit is contained in:
ccdojox-crypto
2025-12-17 17:18:02 +08:00
parent 9e2333c90c
commit 7e9ea173a7
2872 changed files with 326818 additions and 249 deletions

View File

@@ -119,26 +119,79 @@
<!-- 账号管理 -->
<div v-if="currentTab === 'accounts'">
<!-- 外部API说明 -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="ml-3 flex-1">
<h3 class="text-sm font-medium text-blue-800">外部系统批量上传接口</h3>
<div class="mt-2 text-sm text-blue-700">
<p class="font-mono bg-blue-100 px-2 py-1 rounded mb-2">POST /admin/external/accounts/batch</p>
<p class="mb-1"><strong>Header:</strong> X-API-Token: hb-api-token-change-in-production</p>
<details class="cursor-pointer">
<summary class="text-blue-800 hover:text-blue-900">查看请求格式 ▼</summary>
<pre class="mt-2 bg-gray-800 text-green-400 p-3 rounded text-xs overflow-x-auto">{
"accounts": [
{
"email": "user@example.com",
"access_token": "eyJhbG...",
"refresh_token": "xxx", // 可选
"workos_session_token": "xxx", // 可选
"membership_type": "free", // free=Auto账号, pro=高级账号
"remark": "备注" // 可选
}
],
"update_existing": true // 是否更新已存在的账号
}</pre>
</details>
</div>
</div>
</div>
</div>
<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 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">账号列表 (共 {{ accountsTotal }} 条)</h2>
<div class="space-x-2">
<button v-if="selectedAccounts.length > 0" @click="batchEnableAccounts"
class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm">
批量启用 ({{ selectedAccounts.length }})
</button>
<button v-if="selectedAccounts.length > 0" @click="batchDisableAccounts"
class="px-4 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 text-sm">
批量禁用 ({{ selectedAccounts.length }})
</button>
<button v-if="selectedAccounts.length > 0" @click="batchDeleteAccounts"
class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 text-sm">
批量删除 ({{ selectedAccounts.length }})
</button>
<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>
<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">
<input type="checkbox" @change="toggleSelectAll" :checked="isAllSelected"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
</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">Token</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>
@@ -146,6 +199,10 @@
</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">
<input type="checkbox" :value="account.id" v-model="selectedAccounts"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
</td>
<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'"
@@ -153,9 +210,24 @@
{{ account.membership_type.toUpperCase() }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm space-x-1">
<button @click="copyToken(account.access_token, 'AccessToken')"
class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs hover:bg-blue-200" title="复制 Access Token">
AT
</button>
<button v-if="account.refresh_token" @click="copyToken(account.refresh_token, 'RefreshToken')"
class="px-2 py-1 bg-green-100 text-green-700 rounded text-xs hover:bg-green-200" title="复制 Refresh Token">
RT
</button>
<button v-if="account.workos_session_token" @click="copyToken(account.workos_session_token, 'WorkosSessionToken')"
class="px-2 py-1 bg-purple-100 text-purple-700 rounded text-xs hover:bg-purple-200" title="复制 Workos Session Token">
WT
</button>
</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">
<span @click="toggleAccountStatus(account)" :class="getStatusClass(account.status)"
class="px-2 py-1 text-xs font-medium rounded-full cursor-pointer hover:opacity-80 transition"
:title="'点击切换状态'">
{{ getStatusText(account.status) }}
</span>
</td>
@@ -168,6 +240,49 @@
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-700">每页显示</span>
<select v-model="accountsPagination.pageSize" @change="loadAccounts"
class="px-3 py-1 border border-gray-300 rounded-md text-sm">
<option :value="10">10</option>
<option :value="20">20</option>
<option :value="50">50</option>
<option :value="100">100</option>
</select>
<span class="text-sm text-gray-700"></span>
</div>
<div class="flex items-center space-x-2">
<button @click="accountsPagination.currentPage = 1; loadAccounts()"
:disabled="accountsPagination.currentPage === 1"
:class="accountsPagination.currentPage === 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-100'"
class="px-3 py-1 border border-gray-300 rounded-md text-sm">
首页
</button>
<button @click="accountsPagination.currentPage--; loadAccounts()"
:disabled="accountsPagination.currentPage === 1"
:class="accountsPagination.currentPage === 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-100'"
class="px-3 py-1 border border-gray-300 rounded-md text-sm">
上一页
</button>
<span class="text-sm text-gray-700">
第 {{ accountsPagination.currentPage }} / {{ accountsPagination.totalPages }} 页
</span>
<button @click="accountsPagination.currentPage++; loadAccounts()"
:disabled="accountsPagination.currentPage >= accountsPagination.totalPages"
:class="accountsPagination.currentPage >= accountsPagination.totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-100'"
class="px-3 py-1 border border-gray-300 rounded-md text-sm">
下一页
</button>
<button @click="accountsPagination.currentPage = accountsPagination.totalPages; loadAccounts()"
:disabled="accountsPagination.currentPage >= accountsPagination.totalPages"
:class="accountsPagination.currentPage >= accountsPagination.totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-100'"
class="px-3 py-1 border border-gray-300 rounded-md text-sm">
末页
</button>
</div>
</div>
</div>
</div>
@@ -176,11 +291,29 @@
<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>
<h2 class="text-lg font-medium text-gray-900">激活码列表 (共 {{ keysTotal }} 条)</h2>
<div class="space-x-2">
<button v-if="selectedKeys.length > 0" @click="batchCopyKeys"
class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700 text-sm">
批量复制 ({{ selectedKeys.length }})
</button>
<button v-if="selectedKeys.length > 0" @click="batchEnableKeys"
class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm">
批量启用 ({{ selectedKeys.length }})
</button>
<button v-if="selectedKeys.length > 0" @click="batchDisableKeys"
class="px-4 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 text-sm">
批量禁用 ({{ selectedKeys.length }})
</button>
<button v-if="selectedKeys.length > 0" @click="batchDeleteKeys"
class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 text-sm">
批量删除 ({{ selectedKeys.length }})
</button>
<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>
<!-- 搜索和筛选 -->
<div class="flex flex-wrap gap-3">
@@ -213,6 +346,10 @@
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left">
<input type="checkbox" @change="toggleSelectAllKeys" :checked="isAllKeysSelected"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
</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>
@@ -223,6 +360,10 @@
</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">
<input type="checkbox" :value="key.id" v-model="selectedKeys"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
</td>
<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">
@@ -273,6 +414,49 @@
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-700">每页显示</span>
<select v-model="keysPagination.pageSize" @change="searchKeys"
class="px-3 py-1 border border-gray-300 rounded-md text-sm">
<option :value="10">10</option>
<option :value="20">20</option>
<option :value="50">50</option>
<option :value="100">100</option>
</select>
<span class="text-sm text-gray-700"></span>
</div>
<div class="flex items-center space-x-2">
<button @click="keysPagination.currentPage = 1; searchKeys()"
:disabled="keysPagination.currentPage === 1"
:class="keysPagination.currentPage === 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-100'"
class="px-3 py-1 border border-gray-300 rounded-md text-sm">
首页
</button>
<button @click="keysPagination.currentPage--; searchKeys()"
:disabled="keysPagination.currentPage === 1"
:class="keysPagination.currentPage === 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-100'"
class="px-3 py-1 border border-gray-300 rounded-md text-sm">
上一页
</button>
<span class="text-sm text-gray-700">
第 {{ keysPagination.currentPage }} / {{ keysPagination.totalPages }} 页
</span>
<button @click="keysPagination.currentPage++; searchKeys()"
:disabled="keysPagination.currentPage >= keysPagination.totalPages"
:class="keysPagination.currentPage >= keysPagination.totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-100'"
class="px-3 py-1 border border-gray-300 rounded-md text-sm">
下一页
</button>
<button @click="keysPagination.currentPage = keysPagination.totalPages; searchKeys()"
:disabled="keysPagination.currentPage >= keysPagination.totalPages"
:class="keysPagination.currentPage >= keysPagination.totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-100'"
class="px-3 py-1 border border-gray-300 rounded-md text-sm">
末页
</button>
</div>
</div>
</div>
</div>
@@ -777,7 +961,7 @@
</div>
<script>
const { createApp, ref, reactive, onMounted, watch } = Vue
const { createApp, ref, reactive, computed, onMounted, watch } = Vue
createApp({
setup() {
@@ -791,7 +975,21 @@
// 数据
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 accountsTotal = ref(0)
const selectedAccounts = ref([])
const accountsPagination = reactive({
currentPage: 1,
pageSize: 20,
totalPages: 1
})
const keys = ref([])
const keysTotal = ref(0)
const selectedKeys = ref([])
const keysPagination = reactive({
currentPage: 1,
pageSize: 20,
totalPages: 1
})
const logs = ref([])
const logFilter = reactive({ action: '' })
@@ -883,19 +1081,44 @@
// 加载数据
const loadData = async () => {
try {
const [statsRes, accountsRes, keysRes] = await Promise.all([
api.get('/dashboard'),
api.get('/accounts'),
api.get('/keys')
])
const statsRes = await api.get('/dashboard')
stats.value = statsRes.data
accounts.value = accountsRes.data
keys.value = keysRes.data
await Promise.all([loadAccounts(), searchKeys()])
} catch (e) {
console.error('加载数据失败', e)
}
}
// 加载账号列表(分页)
const loadAccounts = async () => {
try {
const skip = (accountsPagination.currentPage - 1) * accountsPagination.pageSize
const [accountsRes, countRes] = await Promise.all([
api.get(`/accounts?skip=${skip}&limit=${accountsPagination.pageSize}`),
api.get('/accounts/count')
])
accounts.value = accountsRes.data
accountsTotal.value = countRes.data.total
accountsPagination.totalPages = Math.ceil(accountsTotal.value / accountsPagination.pageSize)
selectedAccounts.value = []
} catch (e) {
console.error('加载账号失败', e)
}
}
// 全选/取消全选
const isAllSelected = computed(() => {
return accounts.value.length > 0 && selectedAccounts.value.length === accounts.value.length
})
const toggleSelectAll = (event) => {
if (event.target.checked) {
selectedAccounts.value = accounts.value.map(a => a.id)
} else {
selectedAccounts.value = []
}
}
// 账号操作
const editAccount = (account) => {
editingAccount.value = account
@@ -911,7 +1134,7 @@
await api.post('/accounts', accountForm)
}
showAccountModal.value = false
loadData()
loadAccounts()
} catch (e) {
alert(e.response?.data?.detail || '保存失败')
}
@@ -921,18 +1144,72 @@
if (!confirm('确定删除此账号?')) return
try {
await api.delete(`/accounts/${id}`)
loadData()
loadAccounts()
} catch (e) {
alert('删除失败')
}
}
// 快捷切换账号状态
const toggleAccountStatus = async (account) => {
const statusMap = { 'active': '可用', 'in_use': '使用中', 'disabled': '禁用', 'expired': '过期' }
const nextMap = { 'in_use': 'active', 'active': 'disabled', 'disabled': 'active', 'expired': 'active' }
const nextStatus = nextMap[account.status] || 'active'
const msg = `确定将账号状态从「${statusMap[account.status]}」切换为「${statusMap[nextStatus]}」?`
if (!confirm(msg)) return
try {
const res = await api.post(`/accounts/${account.id}/toggle-status`)
loadAccounts()
} catch (e) {
alert(e.response?.data?.detail || '切换失败')
}
}
// 批量启用账号
const batchEnableAccounts = async () => {
if (selectedAccounts.value.length === 0) return
if (!confirm(`确定启用选中的 ${selectedAccounts.value.length} 个账号?`)) return
try {
const res = await api.post('/accounts/batch-enable', selectedAccounts.value)
alert(res.data.message)
loadAccounts()
} catch (e) {
alert(e.response?.data?.detail || '批量启用失败')
}
}
// 批量禁用账号
const batchDisableAccounts = async () => {
if (selectedAccounts.value.length === 0) return
if (!confirm(`确定禁用选中的 ${selectedAccounts.value.length} 个账号?`)) return
try {
const res = await api.post('/accounts/batch-disable', selectedAccounts.value)
alert(res.data.message)
loadAccounts()
} catch (e) {
alert(e.response?.data?.detail || '批量禁用失败')
}
}
// 批量删除账号
const batchDeleteAccounts = async () => {
if (selectedAccounts.value.length === 0) return
if (!confirm(`确定删除选中的 ${selectedAccounts.value.length} 个账号?此操作不可撤销!`)) return
try {
const res = await api.post('/accounts/batch-delete', selectedAccounts.value)
alert(res.data.message)
loadAccounts()
} catch (e) {
alert(e.response?.data?.detail || '批量删除失败')
}
}
const importAccounts = async () => {
try {
const data = JSON.parse(importData.value)
const res = await api.post('/accounts/import', { accounts: data })
importResult.value = res.data
loadData()
loadAccounts()
} catch (e) {
alert('导入失败: ' + (e.response?.data?.detail || e.message))
}
@@ -964,7 +1241,7 @@
await api.post('/keys', keyForm)
}
showKeyModal.value = false
loadData()
searchKeys()
} catch (e) {
alert(e.response?.data?.detail || '保存失败')
}
@@ -974,7 +1251,7 @@
if (!confirm('确定删除此激活码?')) return
try {
await api.delete(`/keys/${id}`)
loadData()
searchKeys()
} catch (e) {
alert('删除失败')
}
@@ -990,7 +1267,7 @@
try {
await api.post(`/keys/${quotaTarget.value.id}/add-quota?add_quota=${addQuotaAmount.value}`)
showQuotaModal.value = false
loadData()
searchKeys()
alert('充值成功')
} catch (e) {
alert(e.response?.data?.detail || '充值失败')
@@ -1002,6 +1279,11 @@
alert('已复制')
}
const copyToken = (token, name) => {
navigator.clipboard.writeText(token)
alert(`${name} 已复制`)
}
// 延期操作
const extendKey = (key) => {
extendTarget.value = key
@@ -1017,7 +1299,7 @@
add_quota: 0
})
showExtendModal.value = false
loadData()
searchKeys()
alert('延期成功')
} catch (e) {
alert(e.response?.data?.detail || '延期失败')
@@ -1072,7 +1354,7 @@
const res = await api.post('/keys/batch-compensate?' + params.toString())
compensateResult.value = res.data
compensatePreview.value = null
loadData()
searchKeys()
} catch (e) {
alert(e.response?.data?.detail || '补偿失败')
}
@@ -1116,19 +1398,38 @@
}
}
// 搜索激活码
// 搜索激活码(支持分页)
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
const skip = (keysPagination.currentPage - 1) * keysPagination.pageSize
// 构建激活码列表查询参数
const listParams = new URLSearchParams()
listParams.append('skip', skip)
listParams.append('limit', keysPagination.pageSize)
if (keySearch.search) listParams.append('search', keySearch.search)
if (keySearch.status) listParams.append('status', keySearch.status)
if (keySearch.activated) listParams.append('activated', keySearch.activated)
if (keySearch.membership_type) listParams.append('membership_type', keySearch.membership_type)
// 构建计数查询参数不包含skip和limit
const countParams = new URLSearchParams()
if (keySearch.search) countParams.append('search', keySearch.search)
if (keySearch.status) countParams.append('status', keySearch.status)
if (keySearch.activated) countParams.append('activated', keySearch.activated)
if (keySearch.membership_type) countParams.append('membership_type', keySearch.membership_type)
const [keysRes, countRes] = await Promise.all([
api.get('/keys?' + listParams.toString()),
api.get('/keys/count' + (countParams.toString() ? '?' + countParams.toString() : ''))
])
keys.value = keysRes.data
keysTotal.value = countRes.data.total
keysPagination.totalPages = Math.ceil(keysTotal.value / keysPagination.pageSize)
selectedKeys.value = []
} catch (e) {
console.error('搜索失败', e)
}
@@ -1140,7 +1441,74 @@
keySearch.status = ''
keySearch.activated = ''
keySearch.membership_type = ''
loadData()
keysPagination.currentPage = 1
searchKeys()
}
// 全选/取消全选激活码
const isAllKeysSelected = computed(() => {
return keys.value.length > 0 && selectedKeys.value.length === keys.value.length
})
const toggleSelectAllKeys = (event) => {
if (event.target.checked) {
selectedKeys.value = keys.value.map(k => k.id)
} else {
selectedKeys.value = []
}
}
// 批量复制激活码
const batchCopyKeys = async () => {
if (selectedKeys.value.length === 0) return
try {
// 获取选中的激活码
const selectedKeyObjects = keys.value.filter(k => selectedKeys.value.includes(k.id))
const keyStrings = selectedKeyObjects.map(k => k.key).join('\n')
await navigator.clipboard.writeText(keyStrings)
alert(`已复制 ${selectedKeys.value.length} 个激活码到剪贴板`)
} catch (e) {
alert('复制失败: ' + e.message)
}
}
// 批量启用激活码
const batchEnableKeys = async () => {
if (selectedKeys.value.length === 0) return
if (!confirm(`确定启用选中的 ${selectedKeys.value.length} 个激活码?`)) return
try {
const res = await api.post('/keys/batch-enable', selectedKeys.value)
alert(res.data.message)
searchKeys()
} catch (e) {
alert(e.response?.data?.detail || '批量启用失败')
}
}
// 批量禁用激活码
const batchDisableKeys = async () => {
if (selectedKeys.value.length === 0) return
if (!confirm(`确定禁用选中的 ${selectedKeys.value.length} 个激活码?`)) return
try {
const res = await api.post('/keys/batch-disable', selectedKeys.value)
alert(res.data.message)
searchKeys()
} catch (e) {
alert(e.response?.data?.detail || '批量禁用失败')
}
}
// 批量删除激活码
const batchDeleteKeys = async () => {
if (selectedKeys.value.length === 0) return
if (!confirm(`确定删除选中的 ${selectedKeys.value.length} 个激活码?此操作不可撤销!`)) return
try {
const res = await api.post('/keys/batch-delete', selectedKeys.value)
alert(res.data.message)
searchKeys()
} catch (e) {
alert(e.response?.data?.detail || '批量删除失败')
}
}
// 禁用/启用激活码
@@ -1217,7 +1585,9 @@
return {
isLoggedIn, currentUser, currentTab, loginError, loginForm,
stats, accounts, keys, logs, logFilter, keySearch,
stats, accounts, accountsTotal, selectedAccounts, accountsPagination, isAllSelected,
keys, keysTotal, selectedKeys, keysPagination, isAllKeysSelected,
logs, logFilter, keySearch,
accountForm, keyForm,
showAccountModal, showKeyModal, showImportModal, showQuotaModal, showExtendModal,
showKeyDetailModal, keyDetail, keyDevices, keyLogs,
@@ -1227,9 +1597,11 @@
extendTarget, extendDays,
globalSettings,
compensateForm, compensatePreview, compensateResult,
login, logout, loadData, loadLogs, searchKeys, resetKeySearch,
editAccount, saveAccount, deleteAccount, importAccounts,
resetKeyForm, editKey, saveKey, deleteKey, addQuota, submitAddQuota, copyKey,
login, logout, loadData, loadAccounts, loadLogs, searchKeys, resetKeySearch,
toggleSelectAll, batchEnableAccounts, batchDisableAccounts, batchDeleteAccounts,
toggleSelectAllKeys, batchCopyKeys, batchEnableKeys, batchDisableKeys, batchDeleteKeys,
editAccount, saveAccount, deleteAccount, importAccounts, toggleAccountStatus,
resetKeyForm, editKey, saveKey, deleteKey, addQuota, submitAddQuota, copyKey, copyToken,
viewKeyDetail, deleteDevice, disableKey, enableKey,
extendKey, submitExtend,
loadSettings, saveSettings,