mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
Merge branch 'dev' into main
This commit is contained in:
@@ -934,6 +934,64 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Claude 统一客户端标识配置 -->
|
||||
<div v-if="form.platform === 'claude'" class="mt-4">
|
||||
<label class="flex items-start">
|
||||
<input
|
||||
v-model="form.useUnifiedClientId"
|
||||
class="mt-1 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
@change="handleUnifiedClientIdChange"
|
||||
/>
|
||||
<div class="ml-3 flex-1">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
使用统一的客户端标识
|
||||
</span>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
开启后将使用固定的客户端标识,使所有请求看起来来自同一个客户端,减少特征
|
||||
</p>
|
||||
<div v-if="form.useUnifiedClientId" class="mt-3">
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800/50"
|
||||
>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
>客户端标识 ID</span
|
||||
>
|
||||
<button
|
||||
class="rounded-md bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:hover:bg-blue-900/50"
|
||||
type="button"
|
||||
@click="regenerateClientId"
|
||||
>
|
||||
<i class="fas fa-sync-alt mr-1" />
|
||||
重新生成
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code
|
||||
class="block w-full select-all break-all rounded bg-gray-100 px-3 py-2 font-mono text-xs text-gray-700 dark:bg-gray-900 dark:text-gray-300"
|
||||
>
|
||||
<span class="text-blue-600 dark:text-blue-400">{{
|
||||
form.unifiedClientId.substring(0, 8)
|
||||
}}</span
|
||||
><span class="text-gray-500 dark:text-gray-500">{{
|
||||
form.unifiedClientId.substring(8, 56)
|
||||
}}</span
|
||||
><span class="text-blue-600 dark:text-blue-400">{{
|
||||
form.unifiedClientId.substring(56)
|
||||
}}</span>
|
||||
</code>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-info-circle mr-1 text-blue-500" />
|
||||
此ID将替换请求中的user_id客户端部分,保留session部分用于粘性会话
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 所有平台的优先级设置 -->
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
@@ -1553,6 +1611,64 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Claude 统一客户端标识配置(编辑模式) -->
|
||||
<div v-if="form.platform === 'claude'" class="mt-4">
|
||||
<label class="flex items-start">
|
||||
<input
|
||||
v-model="form.useUnifiedClientId"
|
||||
class="mt-1 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
@change="handleUnifiedClientIdChange"
|
||||
/>
|
||||
<div class="ml-3 flex-1">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
使用统一的客户端标识
|
||||
</span>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
开启后将使用固定的客户端标识,使所有请求看起来来自同一个客户端,减少特征
|
||||
</p>
|
||||
<div v-if="form.useUnifiedClientId" class="mt-3">
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800/50"
|
||||
>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
>客户端标识 ID</span
|
||||
>
|
||||
<button
|
||||
class="rounded-md bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:hover:bg-blue-900/50"
|
||||
type="button"
|
||||
@click="regenerateClientId"
|
||||
>
|
||||
<i class="fas fa-sync-alt mr-1" />
|
||||
重新生成
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code
|
||||
class="block w-full select-all break-all rounded bg-gray-100 px-3 py-2 font-mono text-xs text-gray-700 dark:bg-gray-900 dark:text-gray-300"
|
||||
>
|
||||
<span class="text-blue-600 dark:text-blue-400">{{
|
||||
form.unifiedClientId.substring(0, 8)
|
||||
}}</span
|
||||
><span class="text-gray-500 dark:text-gray-500">{{
|
||||
form.unifiedClientId.substring(8, 56)
|
||||
}}</span
|
||||
><span class="text-blue-600 dark:text-blue-400">{{
|
||||
form.unifiedClientId.substring(56)
|
||||
}}</span>
|
||||
</code>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-info-circle mr-1 text-blue-500" />
|
||||
此ID将替换请求中的user_id客户端部分,保留session部分用于粘性会话
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 所有平台的优先级设置(编辑模式) -->
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
@@ -2155,6 +2271,8 @@ const setupTokenSessionId = ref('')
|
||||
// Claude Code 统一 User-Agent 信息
|
||||
const unifiedUserAgent = ref('')
|
||||
const clearingCache = ref(false)
|
||||
// 客户端标识编辑状态(已废弃,不再需要编辑功能)
|
||||
// const editingClientId = ref(false)
|
||||
|
||||
// 初始化代理配置
|
||||
const initProxyConfig = () => {
|
||||
@@ -2193,6 +2311,8 @@ const form = ref({
|
||||
subscriptionType: 'claude_max', // 默认为 Claude Max,兼容旧数据
|
||||
autoStopOnWarning: props.account?.autoStopOnWarning || false, // 5小时限制自动停止调度
|
||||
useUnifiedUserAgent: props.account?.useUnifiedUserAgent || false, // 使用统一Claude Code版本
|
||||
useUnifiedClientId: props.account?.useUnifiedClientId || false, // 使用统一的客户端标识
|
||||
unifiedClientId: props.account?.unifiedClientId || '', // 统一的客户端标识
|
||||
groupId: '',
|
||||
groupIds: [],
|
||||
projectId: props.account?.projectId || '',
|
||||
@@ -2477,6 +2597,11 @@ const exchangeSetupTokenCode = async () => {
|
||||
|
||||
const tokenInfo = await accountsStore.exchangeClaudeSetupTokenCode(data)
|
||||
|
||||
// Setup Token模式也需要确保生成客户端ID
|
||||
if (form.value.useUnifiedClientId && !form.value.unifiedClientId) {
|
||||
form.value.unifiedClientId = generateClientId()
|
||||
}
|
||||
|
||||
// 调用相同的成功处理函数
|
||||
await handleOAuthSuccess(tokenInfo)
|
||||
} catch (error) {
|
||||
@@ -2490,6 +2615,15 @@ const exchangeSetupTokenCode = async () => {
|
||||
const handleOAuthSuccess = async (tokenInfo) => {
|
||||
loading.value = true
|
||||
try {
|
||||
// OAuth模式也需要确保生成客户端ID
|
||||
if (
|
||||
form.value.platform === 'claude' &&
|
||||
form.value.useUnifiedClientId &&
|
||||
!form.value.unifiedClientId
|
||||
) {
|
||||
form.value.unifiedClientId = generateClientId()
|
||||
}
|
||||
|
||||
const data = {
|
||||
name: form.value.name,
|
||||
description: form.value.description,
|
||||
@@ -2513,6 +2647,8 @@ const handleOAuthSuccess = async (tokenInfo) => {
|
||||
data.priority = form.value.priority || 50
|
||||
data.autoStopOnWarning = form.value.autoStopOnWarning || false
|
||||
data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false
|
||||
data.useUnifiedClientId = form.value.useUnifiedClientId || false
|
||||
data.unifiedClientId = form.value.unifiedClientId || ''
|
||||
// 添加订阅类型信息
|
||||
data.subscriptionInfo = {
|
||||
accountType: form.value.subscriptionType || 'claude_max',
|
||||
@@ -2697,6 +2833,11 @@ const createAccount = async () => {
|
||||
? 10 * 60 * 1000 // 10分钟
|
||||
: 365 * 24 * 60 * 60 * 1000 // 1年
|
||||
|
||||
// 手动模式也需要确保生成客户端ID
|
||||
if (form.value.useUnifiedClientId && !form.value.unifiedClientId) {
|
||||
form.value.unifiedClientId = generateClientId()
|
||||
}
|
||||
|
||||
data.claudeAiOauth = {
|
||||
accessToken: form.value.accessToken,
|
||||
refreshToken: form.value.refreshToken || '',
|
||||
@@ -2706,6 +2847,8 @@ const createAccount = async () => {
|
||||
data.priority = form.value.priority || 50
|
||||
data.autoStopOnWarning = form.value.autoStopOnWarning || false
|
||||
data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false
|
||||
data.useUnifiedClientId = form.value.useUnifiedClientId || false
|
||||
data.unifiedClientId = form.value.unifiedClientId || ''
|
||||
// 添加订阅类型信息
|
||||
data.subscriptionInfo = {
|
||||
accountType: form.value.subscriptionType || 'claude_max',
|
||||
@@ -2972,9 +3115,16 @@ const updateAccount = async () => {
|
||||
|
||||
// Claude 官方账号优先级和订阅类型更新
|
||||
if (props.account.platform === 'claude') {
|
||||
// 更新模式也需要确保生成客户端ID
|
||||
if (form.value.useUnifiedClientId && !form.value.unifiedClientId) {
|
||||
form.value.unifiedClientId = generateClientId()
|
||||
}
|
||||
|
||||
data.priority = form.value.priority || 50
|
||||
data.autoStopOnWarning = form.value.autoStopOnWarning || false
|
||||
data.useUnifiedUserAgent = form.value.useUnifiedUserAgent || false
|
||||
data.useUnifiedClientId = form.value.useUnifiedClientId || false
|
||||
data.unifiedClientId = form.value.unifiedClientId || ''
|
||||
// 更新订阅类型信息
|
||||
data.subscriptionInfo = {
|
||||
accountType: form.value.subscriptionType || 'claude_max',
|
||||
@@ -3408,6 +3558,8 @@ watch(
|
||||
subscriptionType: subscriptionType,
|
||||
autoStopOnWarning: newAccount.autoStopOnWarning || false,
|
||||
useUnifiedUserAgent: newAccount.useUnifiedUserAgent || false,
|
||||
useUnifiedClientId: newAccount.useUnifiedClientId || false,
|
||||
unifiedClientId: newAccount.unifiedClientId || '',
|
||||
groupId: groupId,
|
||||
groupIds: [],
|
||||
projectId: newAccount.projectId || '',
|
||||
@@ -3530,6 +3682,32 @@ const clearUnifiedCache = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 生成客户端标识
|
||||
const generateClientId = () => {
|
||||
// 生成64位十六进制字符串(32字节)
|
||||
const bytes = new Uint8Array(32)
|
||||
crypto.getRandomValues(bytes)
|
||||
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')
|
||||
}
|
||||
|
||||
// 重新生成客户端标识
|
||||
const regenerateClientId = () => {
|
||||
form.value.unifiedClientId = generateClientId()
|
||||
showToast('已生成新的客户端标识', 'success')
|
||||
}
|
||||
|
||||
// 处理统一客户端标识复选框变化
|
||||
const handleUnifiedClientIdChange = () => {
|
||||
if (form.value.useUnifiedClientId) {
|
||||
// 如果启用了统一客户端标识,自动启用统一User-Agent
|
||||
form.value.useUnifiedUserAgent = true
|
||||
// 如果没有客户端标识,自动生成一个
|
||||
if (!form.value.unifiedClientId) {
|
||||
form.value.unifiedClientId = generateClientId()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取统一 User-Agent 信息
|
||||
onMounted(() => {
|
||||
// 获取Claude Code统一User-Agent信息
|
||||
|
||||
@@ -115,7 +115,19 @@
|
||||
<span class="mt-1 flex-shrink-0 text-sm text-gray-600 dark:text-gray-400 md:text-base"
|
||||
>过期时间</span
|
||||
>
|
||||
<div v-if="statsData.expiresAt" class="text-right">
|
||||
<!-- 未激活状态 -->
|
||||
<div
|
||||
v-if="statsData.expirationMode === 'activation' && !statsData.isActivated"
|
||||
class="text-sm font-medium text-amber-600 dark:text-amber-500 md:text-base"
|
||||
>
|
||||
<i class="fas fa-pause-circle mr-1 text-xs md:text-sm" />
|
||||
未激活
|
||||
<span class="ml-1 text-xs text-gray-500 dark:text-gray-400"
|
||||
>(首次使用后{{ statsData.activationDays || 30 }}天过期)</span
|
||||
>
|
||||
</div>
|
||||
<!-- 已设置过期时间 -->
|
||||
<div v-else-if="statsData.expiresAt" class="text-right">
|
||||
<div
|
||||
v-if="isApiKeyExpired(statsData.expiresAt)"
|
||||
class="text-sm font-medium text-red-600 md:text-base"
|
||||
@@ -137,6 +149,7 @@
|
||||
{{ formatExpireDate(statsData.expiresAt) }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 永不过期 -->
|
||||
<div v-else class="text-sm font-medium text-gray-400 dark:text-gray-500 md:text-base">
|
||||
<i class="fas fa-infinity mr-1 text-xs md:text-sm" />
|
||||
永不过期
|
||||
|
||||
@@ -8,6 +8,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
siteName: 'Claude Relay Service',
|
||||
siteIcon: '',
|
||||
siteIconData: '',
|
||||
showAdminButton: true, // 控制管理后台按钮的显示
|
||||
updatedAt: null
|
||||
})
|
||||
|
||||
@@ -64,6 +65,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
siteName: 'Claude Relay Service',
|
||||
siteIcon: '',
|
||||
siteIconData: '',
|
||||
showAdminButton: true,
|
||||
updatedAt: null
|
||||
}
|
||||
|
||||
|
||||
@@ -272,7 +272,12 @@
|
||||
>
|
||||
<i class="fas fa-share-alt mr-1" />共享
|
||||
</span>
|
||||
<!-- 显示所有分组 -->
|
||||
</div>
|
||||
<!-- 显示所有分组 - 换行显示 -->
|
||||
<div
|
||||
v-if="account.groupInfos && account.groupInfos.length > 0"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<span
|
||||
v-for="group in account.groupInfos"
|
||||
:key="group.id"
|
||||
@@ -424,7 +429,7 @@
|
||||
typeof account.rateLimitStatus === 'object' &&
|
||||
account.rateLimitStatus.minutesRemaining > 0
|
||||
"
|
||||
>({{ account.rateLimitStatus.minutesRemaining }}分钟)</span
|
||||
>({{ formatRateLimitTime(account.rateLimitStatus.minutesRemaining) }})</span
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
@@ -636,7 +641,9 @@
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<button
|
||||
v-if="
|
||||
account.platform === 'claude' &&
|
||||
(account.platform === 'claude' ||
|
||||
account.platform === 'claude-console' ||
|
||||
account.platform === 'openai') &&
|
||||
(account.status === 'unauthorized' ||
|
||||
account.status !== 'active' ||
|
||||
account.rateLimitStatus?.isRateLimited ||
|
||||
@@ -1336,7 +1343,7 @@ const loadApiKeys = async (forceReload = false) => {
|
||||
apiKeysLoaded.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load API keys:', error)
|
||||
// 静默处理错误
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1353,7 +1360,7 @@ const loadAccountGroups = async (forceReload = false) => {
|
||||
groupsLoaded.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load account groups:', error)
|
||||
// 静默处理错误
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1426,6 +1433,38 @@ const formatRemainingTime = (minutes) => {
|
||||
return `${mins}分钟`
|
||||
}
|
||||
|
||||
// 格式化限流时间(支持显示天数)
|
||||
const formatRateLimitTime = (minutes) => {
|
||||
if (!minutes || minutes <= 0) return ''
|
||||
|
||||
// 转换为整数,避免小数
|
||||
minutes = Math.floor(minutes)
|
||||
|
||||
// 计算天数、小时和分钟
|
||||
const days = Math.floor(minutes / 1440) // 1天 = 1440分钟
|
||||
const remainingAfterDays = minutes % 1440
|
||||
const hours = Math.floor(remainingAfterDays / 60)
|
||||
const mins = remainingAfterDays % 60
|
||||
|
||||
// 根据时间长度返回不同格式
|
||||
if (days > 0) {
|
||||
// 超过1天,显示天数和小时
|
||||
if (hours > 0) {
|
||||
return `${days}天${hours}小时`
|
||||
}
|
||||
return `${days}天`
|
||||
} else if (hours > 0) {
|
||||
// 超过1小时但不到1天,显示小时和分钟
|
||||
if (mins > 0) {
|
||||
return `${hours}小时${mins}分钟`
|
||||
}
|
||||
return `${hours}小时`
|
||||
} else {
|
||||
// 不到1小时,只显示分钟
|
||||
return `${mins}分钟`
|
||||
}
|
||||
}
|
||||
|
||||
// 打开创建账户模态框
|
||||
const openCreateAccountModal = () => {
|
||||
showCreateAccountModal.value = true
|
||||
@@ -1515,7 +1554,22 @@ const resetAccountStatus = async (account) => {
|
||||
|
||||
try {
|
||||
account.isResetting = true
|
||||
const data = await apiClient.post(`/admin/claude-accounts/${account.id}/reset-status`)
|
||||
|
||||
// 根据账户平台选择不同的 API 端点
|
||||
let endpoint = ''
|
||||
if (account.platform === 'openai') {
|
||||
endpoint = `/admin/openai-accounts/${account.id}/reset-status`
|
||||
} else if (account.platform === 'claude') {
|
||||
endpoint = `/admin/claude-accounts/${account.id}/reset-status`
|
||||
} else if (account.platform === 'claude-console') {
|
||||
endpoint = `/admin/claude-console-accounts/${account.id}/reset-status`
|
||||
} else {
|
||||
showToast('不支持的账户类型', 'error')
|
||||
account.isResetting = false
|
||||
return
|
||||
}
|
||||
|
||||
const data = await apiClient.post(endpoint)
|
||||
|
||||
if (data.success) {
|
||||
showToast('账户状态已重置', 'success')
|
||||
@@ -1621,13 +1675,7 @@ const getClaudeAccountType = (account) => {
|
||||
? JSON.parse(account.subscriptionInfo)
|
||||
: account.subscriptionInfo
|
||||
|
||||
// 添加调试日志
|
||||
console.log('Account subscription info:', {
|
||||
accountName: account.name,
|
||||
subscriptionInfo: info,
|
||||
hasClaudeMax: info.hasClaudeMax,
|
||||
hasClaudePro: info.hasClaudePro
|
||||
})
|
||||
// 订阅信息已解析
|
||||
|
||||
// 根据 has_claude_max 和 has_claude_pro 判断
|
||||
if (info.hasClaudeMax === true) {
|
||||
@@ -1639,13 +1687,11 @@ const getClaudeAccountType = (account) => {
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析失败,返回默认值
|
||||
console.error('Failed to parse subscription info:', e)
|
||||
return 'Claude'
|
||||
}
|
||||
}
|
||||
|
||||
// 没有订阅信息,保持原有显示
|
||||
console.log('No subscription info for account:', account.name)
|
||||
return 'Claude'
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div
|
||||
v-if="oemSettings.ldapEnabled || oemSettings.showAdminButton !== false"
|
||||
class="h-8 w-px bg-gradient-to-b from-transparent via-gray-300 to-transparent opacity-50 dark:via-gray-600"
|
||||
/>
|
||||
|
||||
@@ -31,6 +32,7 @@
|
||||
</router-link>
|
||||
<!-- 管理后台按钮 -->
|
||||
<router-link
|
||||
v-if="oemSettings.showAdminButton !== false"
|
||||
class="admin-button-refined flex items-center gap-2 rounded-2xl px-4 py-2 transition-all duration-300 md:px-5 md:py-2.5"
|
||||
to="/dashboard"
|
||||
>
|
||||
|
||||
@@ -147,6 +147,41 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 管理后台按钮显示控制 -->
|
||||
<tr class="table-row">
|
||||
<td class="w-48 whitespace-nowrap px-6 py-4">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="mr-3 flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600"
|
||||
>
|
||||
<i class="fas fa-eye-slash text-xs text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
管理入口
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">登录按钮显示</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center">
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input v-model="hideAdminButton" class="peer sr-only" type="checkbox" />
|
||||
<div
|
||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
|
||||
></div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{
|
||||
hideAdminButton ? '隐藏登录按钮' : '显示登录按钮'
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
隐藏后,用户需要直接访问 /admin/login 页面登录
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<tr>
|
||||
<td class="px-6 py-6" colspan="2">
|
||||
@@ -189,7 +224,148 @@
|
||||
|
||||
<!-- 移动端卡片视图 -->
|
||||
<div class="space-y-4 sm:hidden">
|
||||
<!-- 省略移动端视图代码... -->
|
||||
<!-- 站点名称卡片 -->
|
||||
<div class="glass-card p-4">
|
||||
<div class="mb-3 flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-cyan-600 text-white shadow-md"
|
||||
>
|
||||
<i class="fas fa-tag"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">站点名称</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">自定义您的站点品牌名称</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-model="oemSettings.siteName"
|
||||
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
maxlength="100"
|
||||
placeholder="Claude Relay Service"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 站点图标卡片 -->
|
||||
<div class="glass-card p-4">
|
||||
<div class="mb-3 flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-purple-500 to-pink-600 text-white shadow-md"
|
||||
>
|
||||
<i class="fas fa-image"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">站点图标</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
上传自定义图标或输入图标URL
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<!-- 图标预览 -->
|
||||
<div
|
||||
v-if="oemSettings.siteIconData || oemSettings.siteIcon"
|
||||
class="inline-flex items-center gap-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-700"
|
||||
>
|
||||
<img
|
||||
alt="图标预览"
|
||||
class="h-8 w-8"
|
||||
:src="oemSettings.siteIconData || oemSettings.siteIcon"
|
||||
@error="handleIconError"
|
||||
/>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">当前图标</span>
|
||||
<button
|
||||
class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
|
||||
@click="removeIcon"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 上传按钮 -->
|
||||
<div>
|
||||
<input
|
||||
ref="iconFileInputMobile"
|
||||
accept=".ico,.png,.jpg,.jpeg,.svg"
|
||||
class="hidden"
|
||||
type="file"
|
||||
@change="handleIconUpload"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-success px-4 py-2"
|
||||
@click="$refs.iconFileInputMobile.click()"
|
||||
>
|
||||
<i class="fas fa-upload mr-2" />
|
||||
上传图标
|
||||
</button>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
支持 .ico, .png, .jpg, .svg 格式,最大 350KB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理后台按钮显示控制卡片 -->
|
||||
<div class="glass-card p-4">
|
||||
<div class="mb-3 flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 text-white shadow-md"
|
||||
>
|
||||
<i class="fas fa-eye-slash"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">管理入口</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">控制登录按钮在首页的显示</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input v-model="hideAdminButton" class="peer sr-only" type="checkbox" />
|
||||
<div
|
||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
|
||||
></div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{
|
||||
hideAdminButton ? '隐藏登录按钮' : '显示登录按钮'
|
||||
}}</span>
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
隐藏后,用户需要直接访问 /admin/login 页面登录
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮卡片 -->
|
||||
<div class="glass-card p-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<button
|
||||
class="btn btn-primary w-full px-6 py-3"
|
||||
:class="{ 'cursor-not-allowed opacity-50': saving }"
|
||||
:disabled="saving"
|
||||
@click="saveOemSettings"
|
||||
>
|
||||
<div v-if="saving" class="loading-spinner mr-2"></div>
|
||||
<i v-else class="fas fa-save mr-2" />
|
||||
{{ saving ? '保存中...' : '保存设置' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn w-full bg-gray-100 px-6 py-3 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
:disabled="saving"
|
||||
@click="resetOemSettings"
|
||||
>
|
||||
<i class="fas fa-undo mr-2" />
|
||||
重置为默认
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="oemSettings.updatedAt"
|
||||
class="text-center text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<i class="fas fa-clock mr-1" />
|
||||
上次更新: {{ formatDateTime(oemSettings.updatedAt) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -791,6 +967,16 @@ const isMounted = ref(true)
|
||||
// API请求取消控制器
|
||||
const abortController = ref(new AbortController())
|
||||
|
||||
// 计算属性:隐藏管理后台按钮(反转 showAdminButton 的值)
|
||||
const hideAdminButton = computed({
|
||||
get() {
|
||||
return !oemSettings.value.showAdminButton
|
||||
},
|
||||
set(value) {
|
||||
oemSettings.value.showAdminButton = !value
|
||||
}
|
||||
})
|
||||
|
||||
// URL 验证状态
|
||||
const urlError = ref(false)
|
||||
const urlValid = ref(false)
|
||||
@@ -1286,7 +1472,8 @@ const saveOemSettings = async () => {
|
||||
const settings = {
|
||||
siteName: oemSettings.value.siteName,
|
||||
siteIcon: oemSettings.value.siteIcon,
|
||||
siteIconData: oemSettings.value.siteIconData
|
||||
siteIconData: oemSettings.value.siteIconData,
|
||||
showAdminButton: oemSettings.value.showAdminButton
|
||||
}
|
||||
const result = await settingsStore.saveOemSettings(settings)
|
||||
if (result && result.success) {
|
||||
|
||||
@@ -434,6 +434,7 @@
|
||||
<div class="whitespace-nowrap text-gray-300">model = "gpt-5"</div>
|
||||
<div class="whitespace-nowrap text-gray-300">model_reasoning_effort = "high"</div>
|
||||
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
|
||||
<div class="whitespace-nowrap text-gray-300">preferred_auth_method = "apikey"</div>
|
||||
<div class="mt-2"></div>
|
||||
<div class="whitespace-nowrap text-gray-300">[model_providers.crs]</div>
|
||||
<div class="whitespace-nowrap text-gray-300">name = "crs"</div>
|
||||
@@ -920,6 +921,7 @@
|
||||
<div class="whitespace-nowrap text-gray-300">model = "gpt-5"</div>
|
||||
<div class="whitespace-nowrap text-gray-300">model_reasoning_effort = "high"</div>
|
||||
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
|
||||
<div class="whitespace-nowrap text-gray-300">preferred_auth_method = "apikey"</div>
|
||||
<div class="mt-2"></div>
|
||||
<div class="whitespace-nowrap text-gray-300">[model_providers.crs]</div>
|
||||
<div class="whitespace-nowrap text-gray-300">name = "crs"</div>
|
||||
@@ -1397,6 +1399,7 @@
|
||||
<div class="whitespace-nowrap text-gray-300">model = "gpt-5"</div>
|
||||
<div class="whitespace-nowrap text-gray-300">model_reasoning_effort = "high"</div>
|
||||
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
|
||||
<div class="whitespace-nowrap text-gray-300">preferred_auth_method = "apikey"</div>
|
||||
<div class="mt-2"></div>
|
||||
<div class="whitespace-nowrap text-gray-300">[model_providers.crs]</div>
|
||||
<div class="whitespace-nowrap text-gray-300">name = "crs"</div>
|
||||
|
||||
Reference in New Issue
Block a user