mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix(admin-spa): 修复多个管理后台问题
- 修复代理设置导致页面卡死的问题(循环更新) - 修复Gemini账号授权码自动提取功能 - 修复账户名称验证无错误提示的问题 - 修复网站图标只在settings页面显示的问题 - 修复删除账户使用自定义确认弹窗 - 修复账号添加成功提示重复显示的问题 - 修复代理配置字段格式与原版不一致的问题 - 添加.gitignore忽略旧版web/admin和web/apiStats目录 所有问题已按照原版逻辑完整修复,提升了用户体验。
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -229,3 +229,7 @@ out/
|
|||||||
|
|
||||||
# Runtime files
|
# Runtime files
|
||||||
*.sock
|
*.sock
|
||||||
|
|
||||||
|
# Old admin interface (deprecated)
|
||||||
|
web/admin/
|
||||||
|
web/apiStats/
|
||||||
1
web/admin-spa/.gitignore
vendored
1
web/admin-spa/.gitignore
vendored
@@ -21,6 +21,7 @@ dist-ssr
|
|||||||
.env.*.local
|
.env.*.local
|
||||||
.env
|
.env
|
||||||
vite.config.js.timestamp-*.mjs
|
vite.config.js.timestamp-*.mjs
|
||||||
|
web/admin/
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ const confirmRef = ref()
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 检查本地存储的认证状态
|
// 检查本地存储的认证状态
|
||||||
authStore.checkAuth()
|
authStore.checkAuth()
|
||||||
|
|
||||||
|
// 加载OEM设置(包括网站图标)
|
||||||
|
authStore.loadOemSettings()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -96,8 +96,10 @@
|
|||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
|
:class="{ 'border-red-500': errors.name }"
|
||||||
placeholder="为账户设置一个易识别的名称"
|
placeholder="为账户设置一个易识别的名称"
|
||||||
>
|
>
|
||||||
|
<p v-if="errors.name" class="text-red-500 text-xs mt-1">{{ errors.name }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -204,8 +206,10 @@
|
|||||||
rows="4"
|
rows="4"
|
||||||
required
|
required
|
||||||
class="form-input w-full resize-none font-mono text-xs"
|
class="form-input w-full resize-none font-mono text-xs"
|
||||||
|
:class="{ 'border-red-500': errors.accessToken }"
|
||||||
placeholder="请输入 Access Token..."
|
placeholder="请输入 Access Token..."
|
||||||
></textarea>
|
></textarea>
|
||||||
|
<p v-if="errors.accessToken" class="text-red-500 text-xs mt-1">{{ errors.accessToken }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -234,7 +238,7 @@
|
|||||||
v-if="form.addType === 'oauth'"
|
v-if="form.addType === 'oauth'"
|
||||||
type="button"
|
type="button"
|
||||||
@click="nextStep"
|
@click="nextStep"
|
||||||
:disabled="!canProceed"
|
:disabled="loading"
|
||||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||||
>
|
>
|
||||||
下一步
|
下一步
|
||||||
@@ -243,7 +247,7 @@
|
|||||||
v-else
|
v-else
|
||||||
type="button"
|
type="button"
|
||||||
@click="createAccount"
|
@click="createAccount"
|
||||||
:disabled="loading || !canCreate"
|
:disabled="loading"
|
||||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||||
>
|
>
|
||||||
<div v-if="loading" class="loading-spinner mr-2"></div>
|
<div v-if="loading" class="loading-spinner mr-2"></div>
|
||||||
@@ -450,24 +454,33 @@ const form = ref({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 表单验证错误
|
||||||
|
const errors = ref({
|
||||||
|
name: '',
|
||||||
|
accessToken: ''
|
||||||
|
})
|
||||||
|
|
||||||
// 计算是否可以进入下一步
|
// 计算是否可以进入下一步
|
||||||
const canProceed = computed(() => {
|
const canProceed = computed(() => {
|
||||||
return form.value.name && form.value.platform
|
return form.value.name?.trim() && form.value.platform
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算是否可以创建
|
// 计算是否可以创建
|
||||||
const canCreate = computed(() => {
|
const canCreate = computed(() => {
|
||||||
if (form.value.addType === 'manual') {
|
if (form.value.addType === 'manual') {
|
||||||
return form.value.name && form.value.accessToken
|
return form.value.name?.trim() && form.value.accessToken?.trim()
|
||||||
}
|
}
|
||||||
return form.value.name
|
return form.value.name?.trim()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 下一步
|
// 下一步
|
||||||
const nextStep = async () => {
|
const nextStep = async () => {
|
||||||
|
// 清除之前的错误
|
||||||
|
errors.value.name = ''
|
||||||
|
|
||||||
if (!canProceed.value) {
|
if (!canProceed.value) {
|
||||||
if (!form.value.name || form.value.name.trim() === '') {
|
if (!form.value.name || form.value.name.trim() === '') {
|
||||||
showToast('请填写账户名称', 'error')
|
errors.value.name = '请填写账户名称'
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -499,15 +512,25 @@ const handleOAuthSuccess = async (tokenInfo) => {
|
|||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
description: form.value.description,
|
description: form.value.description,
|
||||||
accountType: form.value.accountType,
|
accountType: form.value.accountType,
|
||||||
accessToken: tokenInfo.access_token,
|
proxy: form.value.proxy.enabled ? {
|
||||||
refreshToken: tokenInfo.refresh_token,
|
type: form.value.proxy.type,
|
||||||
scopes: tokenInfo.scopes || [],
|
host: form.value.proxy.host,
|
||||||
proxy: form.value.proxy.enabled ? form.value.proxy : null
|
port: parseInt(form.value.proxy.port),
|
||||||
|
username: form.value.proxy.username || null,
|
||||||
|
password: form.value.proxy.password || null
|
||||||
|
} : null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.value.platform === 'gemini' && form.value.projectId) {
|
if (form.value.platform === 'claude') {
|
||||||
|
// Claude使用claudeAiOauth字段
|
||||||
|
data.claudeAiOauth = tokenInfo.claudeAiOauth || tokenInfo
|
||||||
|
} else if (form.value.platform === 'gemini') {
|
||||||
|
// Gemini使用geminiOauth字段
|
||||||
|
data.geminiOauth = tokenInfo.tokens || tokenInfo
|
||||||
|
if (form.value.projectId) {
|
||||||
data.projectId = form.value.projectId
|
data.projectId = form.value.projectId
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let result
|
let result
|
||||||
if (form.value.platform === 'claude') {
|
if (form.value.platform === 'claude') {
|
||||||
@@ -516,7 +539,6 @@ const handleOAuthSuccess = async (tokenInfo) => {
|
|||||||
result = await accountsStore.createGeminiAccount(data)
|
result = await accountsStore.createGeminiAccount(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast('账户创建成功', 'success')
|
|
||||||
emit('success', result)
|
emit('success', result)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error.message || '账户创建失败', 'error')
|
showToast(error.message || '账户创建失败', 'error')
|
||||||
@@ -527,12 +549,23 @@ const handleOAuthSuccess = async (tokenInfo) => {
|
|||||||
|
|
||||||
// 创建账户(手动模式)
|
// 创建账户(手动模式)
|
||||||
const createAccount = async () => {
|
const createAccount = async () => {
|
||||||
if (!canCreate.value) {
|
// 清除之前的错误
|
||||||
|
errors.value.name = ''
|
||||||
|
errors.value.accessToken = ''
|
||||||
|
|
||||||
|
let hasError = false
|
||||||
|
|
||||||
if (!form.value.name || form.value.name.trim() === '') {
|
if (!form.value.name || form.value.name.trim() === '') {
|
||||||
showToast('请填写账户名称', 'error')
|
errors.value.name = '请填写账户名称'
|
||||||
} else if (!form.value.accessToken || form.value.accessToken.trim() === '') {
|
hasError = true
|
||||||
showToast('请填写 Access Token', 'error')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (form.value.addType === 'manual' && (!form.value.accessToken || form.value.accessToken.trim() === '')) {
|
||||||
|
errors.value.accessToken = '请填写 Access Token'
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,14 +575,45 @@ const createAccount = async () => {
|
|||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
description: form.value.description,
|
description: form.value.description,
|
||||||
accountType: form.value.accountType,
|
accountType: form.value.accountType,
|
||||||
accessToken: form.value.accessToken,
|
proxy: form.value.proxy.enabled ? {
|
||||||
refreshToken: form.value.refreshToken || undefined,
|
type: form.value.proxy.type,
|
||||||
proxy: form.value.proxy.enabled ? form.value.proxy : null
|
host: form.value.proxy.host,
|
||||||
|
port: parseInt(form.value.proxy.port),
|
||||||
|
username: form.value.proxy.username || null,
|
||||||
|
password: form.value.proxy.password || null
|
||||||
|
} : null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.value.platform === 'gemini' && form.value.projectId) {
|
if (form.value.platform === 'claude') {
|
||||||
|
// Claude手动模式需要构建claudeAiOauth对象
|
||||||
|
const expiresInMs = form.value.refreshToken
|
||||||
|
? (10 * 60 * 1000) // 10分钟
|
||||||
|
: (365 * 24 * 60 * 60 * 1000) // 1年
|
||||||
|
|
||||||
|
data.claudeAiOauth = {
|
||||||
|
accessToken: form.value.accessToken,
|
||||||
|
refreshToken: form.value.refreshToken || '',
|
||||||
|
expiresAt: Date.now() + expiresInMs,
|
||||||
|
scopes: ['user:inference']
|
||||||
|
}
|
||||||
|
} else if (form.value.platform === 'gemini') {
|
||||||
|
// Gemini手动模式需要构建geminiOauth对象
|
||||||
|
const expiresInMs = form.value.refreshToken
|
||||||
|
? (10 * 60 * 1000) // 10分钟
|
||||||
|
: (365 * 24 * 60 * 60 * 1000) // 1年
|
||||||
|
|
||||||
|
data.geminiOauth = {
|
||||||
|
access_token: form.value.accessToken,
|
||||||
|
refresh_token: form.value.refreshToken || '',
|
||||||
|
scope: 'https://www.googleapis.com/auth/cloud-platform',
|
||||||
|
token_type: 'Bearer',
|
||||||
|
expiry_date: Date.now() + expiresInMs
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.value.projectId) {
|
||||||
data.projectId = form.value.projectId
|
data.projectId = form.value.projectId
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let result
|
let result
|
||||||
if (form.value.platform === 'claude') {
|
if (form.value.platform === 'claude') {
|
||||||
@@ -558,7 +622,6 @@ const createAccount = async () => {
|
|||||||
result = await accountsStore.createGeminiAccount(data)
|
result = await accountsStore.createGeminiAccount(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast('账户创建成功', 'success')
|
|
||||||
emit('success', result)
|
emit('success', result)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error.message || '账户创建失败', 'error')
|
showToast(error.message || '账户创建失败', 'error')
|
||||||
@@ -569,6 +632,15 @@ const createAccount = async () => {
|
|||||||
|
|
||||||
// 更新账户
|
// 更新账户
|
||||||
const updateAccount = async () => {
|
const updateAccount = async () => {
|
||||||
|
// 清除之前的错误
|
||||||
|
errors.value.name = ''
|
||||||
|
|
||||||
|
// 验证账户名称
|
||||||
|
if (!form.value.name || form.value.name.trim() === '') {
|
||||||
|
errors.value.name = '请填写账户名称'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 对于Gemini账户,检查项目编号
|
// 对于Gemini账户,检查项目编号
|
||||||
if (form.value.platform === 'gemini') {
|
if (form.value.platform === 'gemini') {
|
||||||
if (!form.value.projectId || form.value.projectId.trim() === '') {
|
if (!form.value.projectId || form.value.projectId.trim() === '') {
|
||||||
@@ -591,15 +663,43 @@ const updateAccount = async () => {
|
|||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
description: form.value.description,
|
description: form.value.description,
|
||||||
accountType: form.value.accountType,
|
accountType: form.value.accountType,
|
||||||
proxy: form.value.proxy.enabled ? form.value.proxy : null
|
proxy: form.value.proxy.enabled ? {
|
||||||
|
type: form.value.proxy.type,
|
||||||
|
host: form.value.proxy.host,
|
||||||
|
port: parseInt(form.value.proxy.port),
|
||||||
|
username: form.value.proxy.username || null,
|
||||||
|
password: form.value.proxy.password || null
|
||||||
|
} : null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只有非空时才更新token
|
// 只有非空时才更新token
|
||||||
if (form.value.accessToken) {
|
if (form.value.accessToken || form.value.refreshToken) {
|
||||||
data.accessToken = form.value.accessToken
|
if (props.account.platform === 'claude') {
|
||||||
|
// Claude需要构建claudeAiOauth对象
|
||||||
|
const expiresInMs = form.value.refreshToken
|
||||||
|
? (10 * 60 * 1000) // 10分钟
|
||||||
|
: (365 * 24 * 60 * 60 * 1000) // 1年
|
||||||
|
|
||||||
|
data.claudeAiOauth = {
|
||||||
|
accessToken: form.value.accessToken || '',
|
||||||
|
refreshToken: form.value.refreshToken || '',
|
||||||
|
expiresAt: Date.now() + expiresInMs,
|
||||||
|
scopes: ['user:inference']
|
||||||
|
}
|
||||||
|
} else if (props.account.platform === 'gemini') {
|
||||||
|
// Gemini需要构建geminiOauth对象
|
||||||
|
const expiresInMs = form.value.refreshToken
|
||||||
|
? (10 * 60 * 1000) // 10分钟
|
||||||
|
: (365 * 24 * 60 * 60 * 1000) // 1年
|
||||||
|
|
||||||
|
data.geminiOauth = {
|
||||||
|
access_token: form.value.accessToken || '',
|
||||||
|
refresh_token: form.value.refreshToken || '',
|
||||||
|
scope: 'https://www.googleapis.com/auth/cloud-platform',
|
||||||
|
token_type: 'Bearer',
|
||||||
|
expiry_date: Date.now() + expiresInMs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (form.value.refreshToken) {
|
|
||||||
data.refreshToken = form.value.refreshToken
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.account.platform === 'gemini' && form.value.projectId) {
|
if (props.account.platform === 'gemini' && form.value.projectId) {
|
||||||
@@ -620,6 +720,20 @@ const updateAccount = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听表单名称变化,清除错误
|
||||||
|
watch(() => form.value.name, () => {
|
||||||
|
if (errors.value.name && form.value.name?.trim()) {
|
||||||
|
errors.value.name = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听Access Token变化,清除错误
|
||||||
|
watch(() => form.value.accessToken, () => {
|
||||||
|
if (errors.value.accessToken && form.value.accessToken?.trim()) {
|
||||||
|
errors.value.accessToken = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 监听账户变化,更新表单
|
// 监听账户变化,更新表单
|
||||||
watch(() => props.account, (newAccount) => {
|
watch(() => props.account, (newAccount) => {
|
||||||
if (newAccount) {
|
if (newAccount) {
|
||||||
|
|||||||
@@ -253,7 +253,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { showToast } from '@/utils/toast'
|
import { showToast } from '@/utils/toast'
|
||||||
import { useAccountsStore } from '@/stores/accounts'
|
import { useAccountsStore } from '@/stores/accounts'
|
||||||
|
|
||||||
@@ -278,28 +278,91 @@ const exchanging = ref(false)
|
|||||||
const authUrl = ref('')
|
const authUrl = ref('')
|
||||||
const authCode = ref('')
|
const authCode = ref('')
|
||||||
const copied = ref(false)
|
const copied = ref(false)
|
||||||
|
const sessionId = ref('') // 保存sessionId用于后续交换
|
||||||
|
|
||||||
// 计算是否可以交换code
|
// 计算是否可以交换code
|
||||||
const canExchange = computed(() => {
|
const canExchange = computed(() => {
|
||||||
return authUrl.value && authCode.value.trim()
|
return authUrl.value && authCode.value.trim()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听授权码输入,自动提取URL中的code参数
|
||||||
|
watch(authCode, (newValue) => {
|
||||||
|
if (!newValue || typeof newValue !== 'string') return
|
||||||
|
|
||||||
|
const trimmedValue = newValue.trim()
|
||||||
|
|
||||||
|
// 如果内容为空,不处理
|
||||||
|
if (!trimmedValue) return
|
||||||
|
|
||||||
|
// 检查是否是 URL 格式(包含 http:// 或 https://)
|
||||||
|
const isUrl = trimmedValue.startsWith('http://') || trimmedValue.startsWith('https://')
|
||||||
|
|
||||||
|
// 如果是 URL 格式
|
||||||
|
if (isUrl) {
|
||||||
|
// 检查是否是正确的 localhost:45462 开头的 URL
|
||||||
|
if (trimmedValue.startsWith('http://localhost:45462')) {
|
||||||
|
try {
|
||||||
|
const url = new URL(trimmedValue)
|
||||||
|
const code = url.searchParams.get('code')
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
// 成功提取授权码
|
||||||
|
authCode.value = code
|
||||||
|
showToast('成功提取授权码!', 'success')
|
||||||
|
console.log('Successfully extracted authorization code from URL')
|
||||||
|
} else {
|
||||||
|
// URL 中没有 code 参数
|
||||||
|
showToast('URL 中未找到授权码参数,请检查链接是否正确', 'error')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// URL 解析失败
|
||||||
|
console.error('Failed to parse URL:', error)
|
||||||
|
showToast('链接格式错误,请检查是否为完整的 URL', 'error')
|
||||||
|
}
|
||||||
|
} else if (props.platform === 'gemini') {
|
||||||
|
// Gemini 平台可能使用不同的回调URL
|
||||||
|
// 尝试从任何URL中提取code参数
|
||||||
|
try {
|
||||||
|
const url = new URL(trimmedValue)
|
||||||
|
const code = url.searchParams.get('code')
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
authCode.value = code
|
||||||
|
showToast('成功提取授权码!', 'success')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 不是有效的URL,保持原值
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 错误的 URL(不是 localhost:45462 开头)
|
||||||
|
showToast('请粘贴以 http://localhost:45462 开头的链接', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果不是 URL,保持原值(兼容直接输入授权码)
|
||||||
|
})
|
||||||
|
|
||||||
// 生成授权URL
|
// 生成授权URL
|
||||||
const generateAuthUrl = async () => {
|
const generateAuthUrl = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const proxyConfig = props.proxy?.enabled ? {
|
const proxyConfig = props.proxy?.enabled ? {
|
||||||
|
proxy: {
|
||||||
type: props.proxy.type,
|
type: props.proxy.type,
|
||||||
host: props.proxy.host,
|
host: props.proxy.host,
|
||||||
port: props.proxy.port,
|
port: parseInt(props.proxy.port),
|
||||||
username: props.proxy.username,
|
username: props.proxy.username || null,
|
||||||
password: props.proxy.password
|
password: props.proxy.password || null
|
||||||
} : null
|
}
|
||||||
|
} : {}
|
||||||
|
|
||||||
if (props.platform === 'claude') {
|
if (props.platform === 'claude') {
|
||||||
authUrl.value = await accountsStore.generateClaudeAuthUrl(proxyConfig)
|
const result = await accountsStore.generateClaudeAuthUrl(proxyConfig)
|
||||||
|
authUrl.value = result.authUrl
|
||||||
|
sessionId.value = result.sessionId
|
||||||
} else if (props.platform === 'gemini') {
|
} else if (props.platform === 'gemini') {
|
||||||
authUrl.value = await accountsStore.generateGeminiAuthUrl(proxyConfig)
|
const result = await accountsStore.generateGeminiAuthUrl(proxyConfig)
|
||||||
|
authUrl.value = result.authUrl
|
||||||
|
sessionId.value = result.sessionId
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error.message || '生成授权链接失败', 'error')
|
showToast(error.message || '生成授权链接失败', 'error')
|
||||||
@@ -346,17 +409,30 @@ const exchangeCode = async () => {
|
|||||||
|
|
||||||
exchanging.value = true
|
exchanging.value = true
|
||||||
try {
|
try {
|
||||||
const data = {
|
let data = {}
|
||||||
code: authCode.value.trim()
|
|
||||||
|
if (props.platform === 'claude') {
|
||||||
|
// Claude使用sessionId和callbackUrl(即授权码)
|
||||||
|
data = {
|
||||||
|
sessionId: sessionId.value,
|
||||||
|
callbackUrl: authCode.value.trim()
|
||||||
|
}
|
||||||
|
} else if (props.platform === 'gemini') {
|
||||||
|
// Gemini使用code和sessionId
|
||||||
|
data = {
|
||||||
|
code: authCode.value.trim(),
|
||||||
|
sessionId: sessionId.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加代理配置(如果启用)
|
||||||
if (props.proxy?.enabled) {
|
if (props.proxy?.enabled) {
|
||||||
data.proxy = {
|
data.proxy = {
|
||||||
type: props.proxy.type,
|
type: props.proxy.type,
|
||||||
host: props.proxy.host,
|
host: props.proxy.host,
|
||||||
port: props.proxy.port,
|
port: parseInt(props.proxy.port),
|
||||||
username: props.proxy.username,
|
username: props.proxy.username || null,
|
||||||
password: props.proxy.password
|
password: props.proxy.password || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -140,27 +140,75 @@ const proxy = ref({ ...props.modelValue })
|
|||||||
const showAuth = ref(!!(proxy.value.username || proxy.value.password))
|
const showAuth = ref(!!(proxy.value.username || proxy.value.password))
|
||||||
const showPassword = ref(false)
|
const showPassword = ref(false)
|
||||||
|
|
||||||
// 监听modelValue变化
|
// 监听modelValue变化,只在真正需要更新时才更新
|
||||||
watch(() => props.modelValue, (newVal) => {
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
// 只有当值真正不同时才更新,避免循环
|
||||||
|
if (JSON.stringify(newVal) !== JSON.stringify(proxy.value)) {
|
||||||
proxy.value = { ...newVal }
|
proxy.value = { ...newVal }
|
||||||
showAuth.value = !!(newVal.username || newVal.password)
|
showAuth.value = !!(newVal.username || newVal.password)
|
||||||
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
// 监听proxy变化,更新父组件
|
// 监听各个字段单独变化,而不是整个对象
|
||||||
watch(proxy, (newVal) => {
|
watch(() => proxy.value.enabled, (newVal) => {
|
||||||
// 如果不需要认证,清空用户名密码
|
emitUpdate()
|
||||||
if (!showAuth.value) {
|
})
|
||||||
newVal.username = ''
|
|
||||||
newVal.password = ''
|
watch(() => proxy.value.type, (newVal) => {
|
||||||
}
|
emitUpdate()
|
||||||
emit('update:modelValue', { ...newVal })
|
})
|
||||||
}, { deep: true })
|
|
||||||
|
watch(() => proxy.value.host, (newVal) => {
|
||||||
|
emitUpdate()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => proxy.value.port, (newVal) => {
|
||||||
|
emitUpdate()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => proxy.value.username, (newVal) => {
|
||||||
|
emitUpdate()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => proxy.value.password, (newVal) => {
|
||||||
|
emitUpdate()
|
||||||
|
})
|
||||||
|
|
||||||
// 监听认证开关
|
// 监听认证开关
|
||||||
watch(showAuth, (newVal) => {
|
watch(showAuth, (newVal) => {
|
||||||
if (!newVal) {
|
if (!newVal) {
|
||||||
proxy.value.username = ''
|
proxy.value.username = ''
|
||||||
proxy.value.password = ''
|
proxy.value.password = ''
|
||||||
|
emitUpdate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 防抖的更新函数
|
||||||
|
let updateTimer = null
|
||||||
|
function emitUpdate() {
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (updateTimer) {
|
||||||
|
clearTimeout(updateTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新的定时器,延迟发送更新
|
||||||
|
updateTimer = setTimeout(() => {
|
||||||
|
const data = { ...proxy.value }
|
||||||
|
|
||||||
|
// 如果不需要认证,清空用户名密码
|
||||||
|
if (!showAuth.value) {
|
||||||
|
data.username = ''
|
||||||
|
data.password = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('update:modelValue', data)
|
||||||
|
}, 100) // 100ms 延迟
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件销毁时清理定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (updateTimer) {
|
||||||
|
clearTimeout(updateTimer)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -23,15 +23,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
|
||||||
import AppHeader from './AppHeader.vue'
|
import AppHeader from './AppHeader.vue'
|
||||||
import TabBar from './TabBar.vue'
|
import TabBar from './TabBar.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
|
||||||
|
|
||||||
// 根据路由设置当前激活的标签
|
// 根据路由设置当前激活的标签
|
||||||
const activeTab = ref('dashboard')
|
const activeTab = ref('dashboard')
|
||||||
@@ -60,10 +58,7 @@ const handleTabChange = (tabKey) => {
|
|||||||
router.push(tabRouteMap[tabKey])
|
router.push(tabRouteMap[tabKey])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载OEM设置
|
// OEM设置已在App.vue中加载,无需重复加载
|
||||||
onMounted(() => {
|
|
||||||
authStore.loadOemSettings()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export const useAccountsStore = defineStore('accounts', () => {
|
|||||||
try {
|
try {
|
||||||
const response = await apiClient.post('/admin/claude-accounts/generate-auth-url', proxyConfig)
|
const response = await apiClient.post('/admin/claude-accounts/generate-auth-url', proxyConfig)
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
return response.data.authUrl // 返回authUrl字符串而不是整个对象
|
return response.data // 返回整个对象,包含authUrl和sessionId
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.message || '生成授权URL失败')
|
throw new Error(response.message || '生成授权URL失败')
|
||||||
}
|
}
|
||||||
@@ -259,7 +259,7 @@ export const useAccountsStore = defineStore('accounts', () => {
|
|||||||
try {
|
try {
|
||||||
const response = await apiClient.post('/admin/gemini-accounts/generate-auth-url', proxyConfig)
|
const response = await apiClient.post('/admin/gemini-accounts/generate-auth-url', proxyConfig)
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
return response.data.authUrl // 返回authUrl字符串而不是整个对象
|
return response.data // 返回整个对象,包含authUrl和sessionId
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.message || '生成授权URL失败')
|
throw new Error(response.message || '生成授权URL失败')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,11 +92,11 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
oemSettings.value = { ...oemSettings.value, ...result.data }
|
oemSettings.value = { ...oemSettings.value, ...result.data }
|
||||||
|
|
||||||
// 设置favicon
|
// 设置favicon
|
||||||
if (result.data.faviconData) {
|
if (result.data.siteIconData || result.data.siteIcon) {
|
||||||
const link = document.querySelector("link[rel*='icon']") || document.createElement('link')
|
const link = document.querySelector("link[rel*='icon']") || document.createElement('link')
|
||||||
link.type = 'image/x-icon'
|
link.type = 'image/x-icon'
|
||||||
link.rel = 'shortcut icon'
|
link.rel = 'shortcut icon'
|
||||||
link.href = result.data.faviconData
|
link.href = result.data.siteIconData || result.data.siteIcon
|
||||||
document.getElementsByTagName('head')[0].appendChild(link)
|
document.getElementsByTagName('head')[0].appendChild(link)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -232,6 +232,17 @@
|
|||||||
@close="showEditAccountModal = false"
|
@close="showEditAccountModal = false"
|
||||||
@success="handleEditSuccess"
|
@success="handleEditSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 确认弹窗 -->
|
||||||
|
<ConfirmModal
|
||||||
|
:show="showConfirmModal"
|
||||||
|
:title="confirmOptions.title"
|
||||||
|
:message="confirmOptions.message"
|
||||||
|
:confirm-text="confirmOptions.confirmText"
|
||||||
|
:cancel-text="confirmOptions.cancelText"
|
||||||
|
@confirm="handleConfirm"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -239,7 +250,12 @@
|
|||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { showToast } from '@/utils/toast'
|
import { showToast } from '@/utils/toast'
|
||||||
import { apiClient } from '@/config/api'
|
import { apiClient } from '@/config/api'
|
||||||
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
import AccountForm from '@/components/accounts/AccountForm.vue'
|
import AccountForm from '@/components/accounts/AccountForm.vue'
|
||||||
|
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
||||||
|
|
||||||
|
// 使用确认弹窗
|
||||||
|
const { showConfirmModal, confirmOptions, showConfirm, handleConfirm, handleCancel } = useConfirm()
|
||||||
|
|
||||||
// 数据状态
|
// 数据状态
|
||||||
const accounts = ref([])
|
const accounts = ref([])
|
||||||
@@ -375,6 +391,18 @@ const formatLastUsed = (dateString) => {
|
|||||||
return date.toLocaleDateString('zh-CN')
|
return date.toLocaleDateString('zh-CN')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载API Keys列表
|
||||||
|
const loadApiKeys = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get('/admin/api-keys')
|
||||||
|
if (response.success) {
|
||||||
|
apiKeys.value = response.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load API keys:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 格式化会话窗口时间
|
// 格式化会话窗口时间
|
||||||
const formatSessionWindow = (windowStart, windowEnd) => {
|
const formatSessionWindow = (windowStart, windowEnd) => {
|
||||||
if (!windowStart || !windowEnd) return '--'
|
if (!windowStart || !windowEnd) return '--'
|
||||||
@@ -417,7 +445,24 @@ const editAccount = (account) => {
|
|||||||
|
|
||||||
// 删除账户
|
// 删除账户
|
||||||
const deleteAccount = async (account) => {
|
const deleteAccount = async (account) => {
|
||||||
if (!confirm(`确定要删除账户 "${account.name}" 吗?此操作不可恢复。`)) return
|
// 检查是否有API Key绑定到此账号
|
||||||
|
const boundKeysCount = apiKeys.value.filter(key =>
|
||||||
|
key.claudeAccountId === account.id || key.geminiAccountId === account.id
|
||||||
|
).length
|
||||||
|
|
||||||
|
if (boundKeysCount > 0) {
|
||||||
|
showToast(`无法删除此账号,有 ${boundKeysCount} 个API Key绑定到此账号,请先解绑所有API Key`, 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await showConfirm(
|
||||||
|
'删除账户',
|
||||||
|
`确定要删除账户 "${account.name}" 吗?\n\n此操作不可恢复。`,
|
||||||
|
'删除',
|
||||||
|
'取消'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const endpoint = account.platform === 'claude'
|
const endpoint = account.platform === 'claude'
|
||||||
@@ -489,6 +534,7 @@ watch(accountSortBy, (newVal) => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadAccounts()
|
loadAccounts()
|
||||||
|
loadApiKeys()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user