备份: 完整开发状态(含反混淆脚本和临时文件)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user