feat: 全新的Vue3管理后台(admin-spa)和路由重构

🎨 新增功能:
- 使用Vue3 + Vite构建的全新管理后台界面
- 支持Tab切换的API统计页面(统计查询/使用教程)
- 优雅的胶囊式Tab切换设计
- 同步了PR #106的会话窗口管理功能
- 完整的响应式设计和骨架屏加载状态

🔧 路由调整:
- 新版管理后台部署在 /admin-next/ 路径
- 将根路径 / 重定向到 /admin-next/api-stats
- 将 /web 页面路由重定向到新版,保留 /web/auth/* 认证路由
- 将 /apiStats 页面路由重定向到新版,保留API端点

🗑️ 清理工作:
- 删除旧版 web/admin/ 静态文件
- 删除旧版 web/apiStats/ 静态文件
- 清理相关的文件服务代码

🐛 修复问题:
- 修复重定向循环问题
- 修复环境变量配置
- 修复路由404错误
- 优化构建配置

🚀 生成方式:使用 Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-07-29 12:40:51 +08:00
parent c98de2aca5
commit 414856f152
70 changed files with 18748 additions and 10314 deletions

View File

@@ -0,0 +1,377 @@
<template>
<div class="space-y-6">
<!-- Claude OAuth流程 -->
<div v-if="platform === 'claude'">
<div class="bg-blue-50 p-6 rounded-lg border border-blue-200">
<div class="flex items-start gap-4">
<div class="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-link text-white"></i>
</div>
<div class="flex-1">
<h4 class="font-semibold text-blue-900 mb-3">Claude 账户授权</h4>
<p class="text-sm text-blue-800 mb-4">
请按照以下步骤完成 Claude 账户的授权
</p>
<div class="space-y-4">
<!-- 步骤1: 生成授权链接 -->
<div class="bg-white/80 rounded-lg p-4 border border-blue-300">
<div class="flex items-start gap-3">
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">1</div>
<div class="flex-1">
<p class="font-medium text-blue-900 mb-2">点击下方按钮生成授权链接</p>
<button
v-if="!authUrl"
@click="generateAuthUrl"
:disabled="loading"
class="btn btn-primary px-4 py-2 text-sm"
>
<i v-if="!loading" class="fas fa-link mr-2"></i>
<div v-else class="loading-spinner mr-2"></div>
{{ loading ? '生成中...' : '生成授权链接' }}
</button>
<div v-else class="space-y-3">
<div class="flex items-center gap-2">
<input
type="text"
:value="authUrl"
readonly
class="form-input flex-1 text-xs font-mono bg-gray-50"
>
<button
@click="copyAuthUrl"
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
title="复制链接"
>
<i :class="copied ? 'fas fa-check text-green-500' : 'fas fa-copy'"></i>
</button>
</div>
<button
@click="regenerateAuthUrl"
class="text-xs text-blue-600 hover:text-blue-700"
>
<i class="fas fa-sync-alt mr-1"></i>重新生成
</button>
</div>
</div>
</div>
</div>
<!-- 步骤2: 访问链接并授权 -->
<div class="bg-white/80 rounded-lg p-4 border border-blue-300">
<div class="flex items-start gap-3">
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">2</div>
<div class="flex-1">
<p class="font-medium text-blue-900 mb-2">在浏览器中打开链接并完成授权</p>
<p class="text-sm text-blue-700 mb-2">
请在新标签页中打开授权链接登录您的 Claude 账户并授权
</p>
<div class="bg-yellow-50 p-3 rounded border border-yellow-300">
<p class="text-xs text-yellow-800">
<i class="fas fa-exclamation-triangle mr-1"></i>
<strong>注意</strong>如果您设置了代理请确保浏览器也使用相同的代理访问授权页面
</p>
</div>
</div>
</div>
</div>
<!-- 步骤3: 输入授权码 -->
<div class="bg-white/80 rounded-lg p-4 border border-blue-300">
<div class="flex items-start gap-3">
<div class="w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">3</div>
<div class="flex-1">
<p class="font-medium text-blue-900 mb-2">输入 Authorization Code</p>
<p class="text-sm text-blue-700 mb-3">
授权完成后页面会显示一个 <strong>Authorization Code</strong>请将其复制并粘贴到下方输入框
</p>
<div class="space-y-3">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">
<i class="fas fa-key text-blue-500 mr-2"></i>Authorization Code
</label>
<textarea
v-model="authCode"
rows="3"
class="form-input w-full resize-none font-mono text-sm"
placeholder="粘贴从Claude页面获取的Authorization Code..."
></textarea>
</div>
<p class="text-xs text-gray-500 mt-2">
<i class="fas fa-info-circle mr-1"></i>
请粘贴从Claude页面复制的Authorization Code
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Gemini OAuth流程 -->
<div v-else-if="platform === 'gemini'">
<div class="bg-green-50 p-6 rounded-lg border border-green-200">
<div class="flex items-start gap-4">
<div class="w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center flex-shrink-0">
<i class="fas fa-robot text-white"></i>
</div>
<div class="flex-1">
<h4 class="font-semibold text-green-900 mb-3">Gemini 账户授权</h4>
<p class="text-sm text-green-800 mb-4">
请按照以下步骤完成 Gemini 账户的授权
</p>
<div class="space-y-4">
<!-- 步骤1: 生成授权链接 -->
<div class="bg-white/80 rounded-lg p-4 border border-green-300">
<div class="flex items-start gap-3">
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">1</div>
<div class="flex-1">
<p class="font-medium text-green-900 mb-2">点击下方按钮生成授权链接</p>
<button
v-if="!authUrl"
@click="generateAuthUrl"
:disabled="loading"
class="btn btn-primary px-4 py-2 text-sm"
>
<i v-if="!loading" class="fas fa-link mr-2"></i>
<div v-else class="loading-spinner mr-2"></div>
{{ loading ? '生成中...' : '生成授权链接' }}
</button>
<div v-else class="space-y-3">
<div class="flex items-center gap-2">
<input
type="text"
:value="authUrl"
readonly
class="form-input flex-1 text-xs font-mono bg-gray-50"
>
<button
@click="copyAuthUrl"
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
title="复制链接"
>
<i :class="copied ? 'fas fa-check text-green-500' : 'fas fa-copy'"></i>
</button>
</div>
<button
@click="regenerateAuthUrl"
class="text-xs text-green-600 hover:text-green-700"
>
<i class="fas fa-sync-alt mr-1"></i>重新生成
</button>
</div>
</div>
</div>
</div>
<!-- 步骤2: 操作说明 -->
<div class="bg-white/80 rounded-lg p-4 border border-green-300">
<div class="flex items-start gap-3">
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">2</div>
<div class="flex-1">
<p class="font-medium text-green-900 mb-2">在浏览器中打开链接并完成授权</p>
<ol class="text-sm text-green-800 space-y-1 list-decimal list-inside mb-3">
<li>点击上方的授权链接在新页面中完成Google账号登录</li>
<li>点击登录按钮后可能会加载很慢这是正常的</li>
<li>如果超过1分钟还在加载请按 F5 刷新页面</li>
<li>授权完成后会跳转到 http://localhost:45462 (可能显示无法访问)</li>
</ol>
<div class="bg-green-100 p-3 rounded border border-green-300">
<p class="text-xs text-green-700">
<i class="fas fa-lightbulb mr-1"></i>
<strong>提示</strong>如果页面一直无法跳转可以打开浏览器开发者工具F12F5刷新一下授权页再点击页面的登录按钮网络标签中找到以 localhost:45462 开头的请求复制其完整URL
</p>
</div>
</div>
</div>
</div>
<!-- 步骤3: 输入授权码 -->
<div class="bg-white/80 rounded-lg p-4 border border-green-300">
<div class="flex items-start gap-3">
<div class="w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0">3</div>
<div class="flex-1">
<p class="font-medium text-green-900 mb-2">复制oauth后的链接</p>
<p class="text-sm text-green-700 mb-3">
复制浏览器地址栏的完整链接并粘贴到下方输入框
</p>
<div class="space-y-3">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2">
<i class="fas fa-key text-green-500 mr-2"></i>复制oauth后的链接
</label>
<textarea
v-model="authCode"
rows="3"
class="form-input w-full resize-none font-mono text-sm"
placeholder="粘贴以 http://localhost:45462 开头的完整链接..."
></textarea>
</div>
<div class="mt-2 space-y-1">
<p class="text-xs text-gray-600">
<i class="fas fa-check-circle text-green-500 mr-1"></i>
支持粘贴完整链接系统会自动提取授权码
</p>
<p class="text-xs text-gray-600">
<i class="fas fa-check-circle text-green-500 mr-1"></i>
也可以直接粘贴授权码code参数的值
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex gap-3 pt-4">
<button
type="button"
@click="$emit('back')"
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
>
上一步
</button>
<button
type="button"
@click="exchangeCode"
:disabled="!canExchange || exchanging"
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
>
<div v-if="exchanging" class="loading-spinner mr-2"></div>
{{ exchanging ? '验证中...' : '完成授权' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { showToast } from '@/utils/toast'
import { useAccountsStore } from '@/stores/accounts'
const props = defineProps({
platform: {
type: String,
required: true
},
proxy: {
type: Object,
default: null
}
})
const emit = defineEmits(['success', 'back'])
const accountsStore = useAccountsStore()
// 状态
const loading = ref(false)
const exchanging = ref(false)
const authUrl = ref('')
const authCode = ref('')
const copied = ref(false)
// 计算是否可以交换code
const canExchange = computed(() => {
return authUrl.value && authCode.value.trim()
})
// 生成授权URL
const generateAuthUrl = async () => {
loading.value = true
try {
const proxyConfig = props.proxy?.enabled ? {
type: props.proxy.type,
host: props.proxy.host,
port: props.proxy.port,
username: props.proxy.username,
password: props.proxy.password
} : null
if (props.platform === 'claude') {
authUrl.value = await accountsStore.generateClaudeAuthUrl(proxyConfig)
} else if (props.platform === 'gemini') {
authUrl.value = await accountsStore.generateGeminiAuthUrl(proxyConfig)
}
} catch (error) {
showToast(error.message || '生成授权链接失败', 'error')
} finally {
loading.value = false
}
}
// 重新生成授权URL
const regenerateAuthUrl = () => {
authUrl.value = ''
authCode.value = ''
generateAuthUrl()
}
// 复制授权URL
const copyAuthUrl = async () => {
try {
await navigator.clipboard.writeText(authUrl.value)
copied.value = true
showToast('链接已复制', 'success')
setTimeout(() => {
copied.value = false
}, 2000)
} catch (error) {
// 降级方案
const input = document.createElement('input')
input.value = authUrl.value
document.body.appendChild(input)
input.select()
document.execCommand('copy')
document.body.removeChild(input)
copied.value = true
showToast('链接已复制', 'success')
setTimeout(() => {
copied.value = false
}, 2000)
}
}
// 交换授权码
const exchangeCode = async () => {
if (!canExchange.value) return
exchanging.value = true
try {
const data = {
code: authCode.value.trim()
}
if (props.proxy?.enabled) {
data.proxy = {
type: props.proxy.type,
host: props.proxy.host,
port: props.proxy.port,
username: props.proxy.username,
password: props.proxy.password
}
}
let tokenInfo
if (props.platform === 'claude') {
tokenInfo = await accountsStore.exchangeClaudeCode(data)
} else if (props.platform === 'gemini') {
tokenInfo = await accountsStore.exchangeGeminiCode(data)
}
emit('success', tokenInfo)
} catch (error) {
showToast(error.message || '授权失败,请检查授权码是否正确', 'error')
} finally {
exchanging.value = false
}
}
</script>