mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 21:17:30 +00:00
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:
646
web/admin-spa/src/components/accounts/AccountForm.vue
Normal file
646
web/admin-spa/src/components/accounts/AccountForm.vue
Normal file
@@ -0,0 +1,646 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="show" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
||||
<div class="modal-content w-full max-w-2xl p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-user-circle text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900">{{ isEdit ? '编辑账户' : '添加账户' }}</h3>
|
||||
</div>
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 步骤指示器 -->
|
||||
<div v-if="!isEdit && form.addType === 'oauth'" class="flex items-center justify-center mb-8">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center">
|
||||
<div :class="['w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold',
|
||||
oauthStep >= 1 ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-500']">
|
||||
1
|
||||
</div>
|
||||
<span class="ml-2 text-sm font-medium text-gray-700">基本信息</span>
|
||||
</div>
|
||||
<div class="w-8 h-0.5 bg-gray-300"></div>
|
||||
<div class="flex items-center">
|
||||
<div :class="['w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold',
|
||||
oauthStep >= 2 ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-500']">
|
||||
2
|
||||
</div>
|
||||
<span class="ml-2 text-sm font-medium text-gray-700">授权认证</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤1: 基本信息和代理设置 -->
|
||||
<div v-if="oauthStep === 1 && !isEdit">
|
||||
<div class="space-y-6">
|
||||
<div v-if="!isEdit">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">平台</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.platform"
|
||||
value="claude"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">Claude</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.platform"
|
||||
value="gemini"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">Gemini</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isEdit">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">添加方式</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.addType"
|
||||
value="oauth"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">OAuth 授权 (推荐)</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.addType"
|
||||
value="manual"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">手动输入 Access Token</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">账户名称</label>
|
||||
<input
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
required
|
||||
class="form-input w-full"
|
||||
placeholder="为账户设置一个易识别的名称"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">描述 (可选)</label>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
rows="3"
|
||||
class="form-input w-full resize-none"
|
||||
placeholder="账户用途说明..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">账户类型</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.accountType"
|
||||
value="shared"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">共享账户</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.accountType"
|
||||
value="dedicated"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">专属账户</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
共享账户:供所有API Key使用;专属账户:仅供特定API Key使用
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Gemini 项目编号字段 -->
|
||||
<div v-if="form.platform === 'gemini'">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">项目编号 (可选)</label>
|
||||
<input
|
||||
v-model="form.projectId"
|
||||
type="text"
|
||||
class="form-input w-full"
|
||||
placeholder="例如:123456789012(纯数字)"
|
||||
>
|
||||
<div class="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="fas fa-info-circle text-yellow-600 mt-0.5"></i>
|
||||
<div class="text-xs text-yellow-700">
|
||||
<p class="font-medium mb-1">Google Cloud/Workspace 账号需要提供项目编号</p>
|
||||
<p>某些 Google 账号(特别是绑定了 Google Cloud 的账号)会被识别为 Workspace 账号,需要提供额外的项目编号。</p>
|
||||
<div class="mt-2 p-2 bg-white rounded border border-yellow-300">
|
||||
<p class="font-medium mb-1">如何获取项目编号:</p>
|
||||
<ol class="list-decimal list-inside space-y-1 ml-2">
|
||||
<li>访问 <a href="https://console.cloud.google.com/welcome" target="_blank" class="text-blue-600 hover:underline font-medium">Google Cloud Console</a></li>
|
||||
<li>复制<span class="font-semibold text-red-600">项目编号(Project Number)</span>,通常是12位纯数字</li>
|
||||
<li class="text-red-600">⚠️ 注意:不要复制项目ID(Project ID),要复制项目编号!</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p class="mt-2"><strong>提示:</strong>如果您的账号是普通个人账号(未绑定 Google Cloud),请留空此字段。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手动输入 Token 字段 -->
|
||||
<div v-if="form.addType === 'manual'" class="space-y-4 bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||
<div class="flex items-start gap-3 mb-4">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
|
||||
<i class="fas fa-info text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="font-semibold text-blue-900 mb-2">手动输入 Token</h5>
|
||||
<p v-if="form.platform === 'claude'" class="text-sm text-blue-800 mb-2">
|
||||
请输入有效的 Claude Access Token。如果您有 Refresh Token,建议也一并填写以支持自动刷新。
|
||||
</p>
|
||||
<p v-else-if="form.platform === 'gemini'" class="text-sm text-blue-800 mb-2">
|
||||
请输入有效的 Gemini Access Token。如果您有 Refresh Token,建议也一并填写以支持自动刷新。
|
||||
</p>
|
||||
<div class="bg-white/80 rounded-lg p-3 mt-2 mb-2 border border-blue-300">
|
||||
<p class="text-sm text-blue-900 font-medium mb-1">
|
||||
<i class="fas fa-folder-open mr-1"></i>
|
||||
获取 Access Token 的方法:
|
||||
</p>
|
||||
<p v-if="form.platform === 'claude'" class="text-xs text-blue-800">
|
||||
请从已登录 Claude Code 的机器上获取 <code class="bg-blue-100 px-1 py-0.5 rounded font-mono">~/.claude/.credentials.json</code> 文件中的凭证,
|
||||
请勿使用 Claude 官网 API Keys 页面的密钥。
|
||||
</p>
|
||||
<p v-else-if="form.platform === 'gemini'" class="text-xs text-blue-800">
|
||||
请从已登录 Gemini CLI 的机器上获取 <code class="bg-blue-100 px-1 py-0.5 rounded font-mono">~/.config/gemini/credentials.json</code> 文件中的凭证。
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xs text-blue-600">💡 如果未填写 Refresh Token,Token 过期后需要手动更新。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">Access Token *</label>
|
||||
<textarea
|
||||
v-model="form.accessToken"
|
||||
rows="4"
|
||||
required
|
||||
class="form-input w-full resize-none font-mono text-xs"
|
||||
placeholder="请输入 Access Token..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">Refresh Token (可选)</label>
|
||||
<textarea
|
||||
v-model="form.refreshToken"
|
||||
rows="4"
|
||||
class="form-input w-full resize-none font-mono text-xs"
|
||||
placeholder="请输入 Refresh Token..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 代理设置 -->
|
||||
<ProxyConfig v-model="form.proxy" />
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
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
|
||||
v-if="form.addType === 'oauth'"
|
||||
type="button"
|
||||
@click="nextStep"
|
||||
:disabled="!canProceed"
|
||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||
>
|
||||
下一步
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
@click="createAccount"
|
||||
:disabled="loading || !canCreate"
|
||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||
>
|
||||
<div v-if="loading" class="loading-spinner mr-2"></div>
|
||||
{{ loading ? '创建中...' : '创建' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤2: OAuth授权 -->
|
||||
<OAuthFlow
|
||||
v-if="oauthStep === 2 && form.addType === 'oauth'"
|
||||
:platform="form.platform"
|
||||
:proxy="form.proxy"
|
||||
@success="handleOAuthSuccess"
|
||||
@back="oauthStep = 1"
|
||||
/>
|
||||
|
||||
<!-- 编辑模式 -->
|
||||
<div v-if="isEdit" class="space-y-6">
|
||||
<!-- 基本信息 -->
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">账户名称</label>
|
||||
<input
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
required
|
||||
class="form-input w-full"
|
||||
placeholder="为账户设置一个易识别的名称"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">描述 (可选)</label>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
rows="3"
|
||||
class="form-input w-full resize-none"
|
||||
placeholder="账户用途说明..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">账户类型</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.accountType"
|
||||
value="shared"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">共享账户</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="form.accountType"
|
||||
value="dedicated"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">专属账户</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
共享账户:供所有API Key使用;专属账户:仅供特定API Key使用
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Gemini 项目编号字段 -->
|
||||
<div v-if="form.platform === 'gemini'">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">项目编号 (可选)</label>
|
||||
<input
|
||||
v-model="form.projectId"
|
||||
type="text"
|
||||
class="form-input w-full"
|
||||
placeholder="例如:123456789012(纯数字)"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
Google Cloud/Workspace 账号可能需要提供项目编号
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Token 更新 -->
|
||||
<div class="bg-amber-50 p-4 rounded-lg border border-amber-200">
|
||||
<div class="flex items-start gap-3 mb-4">
|
||||
<div class="w-8 h-8 bg-amber-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
|
||||
<i class="fas fa-key text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="font-semibold text-amber-900 mb-2">更新 Token</h5>
|
||||
<p class="text-sm text-amber-800 mb-2">可以更新 Access Token 和 Refresh Token。为了安全起见,不会显示当前的 Token 值。</p>
|
||||
<p class="text-xs text-amber-600">💡 留空表示不更新该字段。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">新的 Access Token</label>
|
||||
<textarea
|
||||
v-model="form.accessToken"
|
||||
rows="4"
|
||||
class="form-input w-full resize-none font-mono text-xs"
|
||||
placeholder="留空表示不更新..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">新的 Refresh Token</label>
|
||||
<textarea
|
||||
v-model="form.refreshToken"
|
||||
rows="4"
|
||||
class="form-input w-full resize-none font-mono text-xs"
|
||||
placeholder="留空表示不更新..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 代理设置 -->
|
||||
<ProxyConfig v-model="form.proxy" />
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
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="updateAccount"
|
||||
:disabled="loading"
|
||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||
>
|
||||
<div v-if="loading" class="loading-spinner mr-2"></div>
|
||||
{{ loading ? '更新中...' : '更新' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认弹窗 -->
|
||||
<ConfirmModal
|
||||
:show="showConfirmModal"
|
||||
:title="confirmOptions.title"
|
||||
:message="confirmOptions.message"
|
||||
:confirm-text="confirmOptions.confirmText"
|
||||
:cancel-text="confirmOptions.cancelText"
|
||||
@confirm="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { useAccountsStore } from '@/stores/accounts'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import ProxyConfig from './ProxyConfig.vue'
|
||||
import OAuthFlow from './OAuthFlow.vue'
|
||||
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
account: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
|
||||
const accountsStore = useAccountsStore()
|
||||
const { showConfirmModal, confirmOptions, showConfirm, handleConfirm, handleCancel } = useConfirm()
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.account)
|
||||
const show = ref(true)
|
||||
|
||||
// OAuth步骤
|
||||
const oauthStep = ref(1)
|
||||
const loading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
platform: props.account?.platform || 'claude',
|
||||
addType: 'oauth',
|
||||
name: props.account?.name || '',
|
||||
description: props.account?.description || '',
|
||||
accountType: props.account?.accountType || 'shared',
|
||||
projectId: props.account?.projectId || '',
|
||||
accessToken: '',
|
||||
refreshToken: '',
|
||||
proxy: props.account?.proxy || {
|
||||
enabled: false,
|
||||
type: 'socks5',
|
||||
host: '',
|
||||
port: '',
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
})
|
||||
|
||||
// 计算是否可以进入下一步
|
||||
const canProceed = computed(() => {
|
||||
return form.value.name && form.value.platform
|
||||
})
|
||||
|
||||
// 计算是否可以创建
|
||||
const canCreate = computed(() => {
|
||||
if (form.value.addType === 'manual') {
|
||||
return form.value.name && form.value.accessToken
|
||||
}
|
||||
return form.value.name
|
||||
})
|
||||
|
||||
// 下一步
|
||||
const nextStep = async () => {
|
||||
if (!canProceed.value) {
|
||||
if (!form.value.name || form.value.name.trim() === '') {
|
||||
showToast('请填写账户名称', 'error')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 对于Gemini账户,检查项目编号
|
||||
if (form.value.platform === 'gemini' && oauthStep.value === 1 && form.value.addType === 'oauth') {
|
||||
if (!form.value.projectId || form.value.projectId.trim() === '') {
|
||||
// 使用自定义确认弹窗
|
||||
const confirmed = await showConfirm(
|
||||
'项目编号未填写',
|
||||
'您尚未填写项目编号。\n\n如果您的Google账号绑定了Google Cloud或被识别为Workspace账号,需要提供项目编号。\n如果您使用的是普通个人账号,可以继续不填写。',
|
||||
'继续',
|
||||
'返回填写'
|
||||
)
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oauthStep.value = 2
|
||||
}
|
||||
|
||||
// 处理OAuth成功
|
||||
const handleOAuthSuccess = async (tokenInfo) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = {
|
||||
name: form.value.name,
|
||||
description: form.value.description,
|
||||
accountType: form.value.accountType,
|
||||
accessToken: tokenInfo.access_token,
|
||||
refreshToken: tokenInfo.refresh_token,
|
||||
scopes: tokenInfo.scopes || [],
|
||||
proxy: form.value.proxy.enabled ? form.value.proxy : null
|
||||
}
|
||||
|
||||
if (form.value.platform === 'gemini' && form.value.projectId) {
|
||||
data.projectId = form.value.projectId
|
||||
}
|
||||
|
||||
let result
|
||||
if (form.value.platform === 'claude') {
|
||||
result = await accountsStore.createClaudeAccount(data)
|
||||
} else {
|
||||
result = await accountsStore.createGeminiAccount(data)
|
||||
}
|
||||
|
||||
showToast('账户创建成功', 'success')
|
||||
emit('success', result)
|
||||
} catch (error) {
|
||||
showToast(error.message || '账户创建失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建账户(手动模式)
|
||||
const createAccount = async () => {
|
||||
if (!canCreate.value) {
|
||||
if (!form.value.name || form.value.name.trim() === '') {
|
||||
showToast('请填写账户名称', 'error')
|
||||
} else if (!form.value.accessToken || form.value.accessToken.trim() === '') {
|
||||
showToast('请填写 Access Token', 'error')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const data = {
|
||||
name: form.value.name,
|
||||
description: form.value.description,
|
||||
accountType: form.value.accountType,
|
||||
accessToken: form.value.accessToken,
|
||||
refreshToken: form.value.refreshToken || undefined,
|
||||
proxy: form.value.proxy.enabled ? form.value.proxy : null
|
||||
}
|
||||
|
||||
if (form.value.platform === 'gemini' && form.value.projectId) {
|
||||
data.projectId = form.value.projectId
|
||||
}
|
||||
|
||||
let result
|
||||
if (form.value.platform === 'claude') {
|
||||
result = await accountsStore.createClaudeAccount(data)
|
||||
} else {
|
||||
result = await accountsStore.createGeminiAccount(data)
|
||||
}
|
||||
|
||||
showToast('账户创建成功', 'success')
|
||||
emit('success', result)
|
||||
} catch (error) {
|
||||
showToast(error.message || '账户创建失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新账户
|
||||
const updateAccount = async () => {
|
||||
// 对于Gemini账户,检查项目编号
|
||||
if (form.value.platform === 'gemini') {
|
||||
if (!form.value.projectId || form.value.projectId.trim() === '') {
|
||||
// 使用自定义确认弹窗
|
||||
const confirmed = await showConfirm(
|
||||
'项目编号未填写',
|
||||
'您尚未填写项目编号。\n\n如果您的Google账号绑定了Google Cloud或被识别为Workspace账号,需要提供项目编号。\n如果您使用的是普通个人账号,可以继续不填写。',
|
||||
'继续保存',
|
||||
'返回填写'
|
||||
)
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const data = {
|
||||
name: form.value.name,
|
||||
description: form.value.description,
|
||||
accountType: form.value.accountType,
|
||||
proxy: form.value.proxy.enabled ? form.value.proxy : null
|
||||
}
|
||||
|
||||
// 只有非空时才更新token
|
||||
if (form.value.accessToken) {
|
||||
data.accessToken = form.value.accessToken
|
||||
}
|
||||
if (form.value.refreshToken) {
|
||||
data.refreshToken = form.value.refreshToken
|
||||
}
|
||||
|
||||
if (props.account.platform === 'gemini' && form.value.projectId) {
|
||||
data.projectId = form.value.projectId
|
||||
}
|
||||
|
||||
if (props.account.platform === 'claude') {
|
||||
await accountsStore.updateClaudeAccount(props.account.id, data)
|
||||
} else {
|
||||
await accountsStore.updateGeminiAccount(props.account.id, data)
|
||||
}
|
||||
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
showToast(error.message || '账户更新失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听账户变化,更新表单
|
||||
watch(() => props.account, (newAccount) => {
|
||||
if (newAccount) {
|
||||
form.value = {
|
||||
platform: newAccount.platform,
|
||||
addType: 'oauth',
|
||||
name: newAccount.name,
|
||||
description: newAccount.description || '',
|
||||
accountType: newAccount.accountType || 'shared',
|
||||
projectId: newAccount.projectId || '',
|
||||
accessToken: '',
|
||||
refreshToken: '',
|
||||
proxy: newAccount.proxy || {
|
||||
enabled: false,
|
||||
type: 'socks5',
|
||||
host: '',
|
||||
port: '',
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
377
web/admin-spa/src/components/accounts/OAuthFlow.vue
Normal file
377
web/admin-spa/src/components/accounts/OAuthFlow.vue
Normal 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>如果页面一直无法跳转,可以打开浏览器开发者工具(F12),F5刷新一下授权页再点击页面的登录按钮,在“网络”标签中找到以 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>
|
||||
166
web/admin-spa/src/components/accounts/ProxyConfig.vue
Normal file
166
web/admin-spa/src/components/accounts/ProxyConfig.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-semibold text-gray-700">代理设置 (可选)</h4>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="proxy.enabled"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<span class="ml-2 text-sm text-gray-700">启用代理</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="proxy.enabled" class="bg-gray-50 p-4 rounded-lg border border-gray-200 space-y-4">
|
||||
<div class="flex items-start gap-3 mb-3">
|
||||
<div class="w-8 h-8 bg-gray-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-server text-white text-sm"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-gray-700">
|
||||
配置代理以访问受限的网络资源。支持 SOCKS5 和 HTTP 代理。
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
请确保代理服务器稳定可用,否则会影响账户的正常使用。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">代理类型</label>
|
||||
<select
|
||||
v-model="proxy.type"
|
||||
class="form-input w-full"
|
||||
>
|
||||
<option value="socks5">SOCKS5</option>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">主机地址</label>
|
||||
<input
|
||||
v-model="proxy.host"
|
||||
type="text"
|
||||
placeholder="例如: 192.168.1.100"
|
||||
class="form-input w-full"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">端口</label>
|
||||
<input
|
||||
v-model="proxy.port"
|
||||
type="number"
|
||||
placeholder="例如: 1080"
|
||||
class="form-input w-full"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="showAuth"
|
||||
id="proxyAuth"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
>
|
||||
<label for="proxyAuth" class="ml-2 text-sm text-gray-700 cursor-pointer">
|
||||
需要身份验证
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="showAuth" class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">用户名</label>
|
||||
<input
|
||||
v-model="proxy.username"
|
||||
type="text"
|
||||
placeholder="代理用户名"
|
||||
class="form-input w-full"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">密码</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="proxy.password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="代理密码"
|
||||
class="form-input w-full pr-10"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 p-3 rounded-lg border border-blue-200">
|
||||
<p class="text-xs text-blue-700">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
<strong>提示:</strong>代理设置将用于所有与此账户相关的API请求。请确保代理服务器支持HTTPS流量转发。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
enabled: false,
|
||||
type: 'socks5',
|
||||
host: '',
|
||||
port: '',
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// 内部代理数据
|
||||
const proxy = ref({ ...props.modelValue })
|
||||
|
||||
// UI状态
|
||||
const showAuth = ref(!!(proxy.value.username || proxy.value.password))
|
||||
const showPassword = ref(false)
|
||||
|
||||
// 监听modelValue变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
proxy.value = { ...newVal }
|
||||
showAuth.value = !!(newVal.username || newVal.password)
|
||||
}, { deep: true })
|
||||
|
||||
// 监听proxy变化,更新父组件
|
||||
watch(proxy, (newVal) => {
|
||||
// 如果不需要认证,清空用户名密码
|
||||
if (!showAuth.value) {
|
||||
newVal.username = ''
|
||||
newVal.password = ''
|
||||
}
|
||||
emit('update:modelValue', { ...newVal })
|
||||
}, { deep: true })
|
||||
|
||||
// 监听认证开关
|
||||
watch(showAuth, (newVal) => {
|
||||
if (!newVal) {
|
||||
proxy.value.username = ''
|
||||
proxy.value.password = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user