mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
feat: 完善管理界面功能和用户体验
- 添加 API Key 窗口倒计时组件 (WindowCountdown) - 添加自定义下拉菜单组件 (CustomDropdown) - 优化账户和 API Key 管理界面交互 - 改进教程页面布局和说明文字 - 完善账户状态显示和错误处理 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -620,6 +620,7 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
concurrencyLimit,
|
concurrencyLimit,
|
||||||
rateLimitWindow,
|
rateLimitWindow,
|
||||||
rateLimitRequests,
|
rateLimitRequests,
|
||||||
|
isActive,
|
||||||
claudeAccountId,
|
claudeAccountId,
|
||||||
claudeConsoleAccountId,
|
claudeConsoleAccountId,
|
||||||
geminiAccountId,
|
geminiAccountId,
|
||||||
@@ -726,6 +727,7 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
if (expiresAt === null) {
|
if (expiresAt === null) {
|
||||||
// null 表示永不过期
|
// null 表示永不过期
|
||||||
updates.expiresAt = null
|
updates.expiresAt = null
|
||||||
|
updates.isActive = true
|
||||||
} else {
|
} else {
|
||||||
// 验证日期格式
|
// 验证日期格式
|
||||||
const expireDate = new Date(expiresAt)
|
const expireDate = new Date(expiresAt)
|
||||||
@@ -733,6 +735,7 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Invalid expiration date format' })
|
return res.status(400).json({ error: 'Invalid expiration date format' })
|
||||||
}
|
}
|
||||||
updates.expiresAt = expiresAt
|
updates.expiresAt = expiresAt
|
||||||
|
updates.isActive = expireDate > new Date() // 如果过期时间在当前时间之后,则设置为激活状态
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -756,6 +759,14 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
updates.tags = tags
|
updates.tags = tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理活跃/禁用状态状态, 放在过期处理后,以确保后续增加禁用key功能
|
||||||
|
if (isActive !== undefined) {
|
||||||
|
if (typeof isActive !== 'boolean') {
|
||||||
|
return res.status(400).json({ error: 'isActive must be a boolean' })
|
||||||
|
}
|
||||||
|
updates.isActive = isActive
|
||||||
|
}
|
||||||
|
|
||||||
await apiKeyService.updateApiKey(keyId, updates)
|
await apiKeyService.updateApiKey(keyId, updates)
|
||||||
|
|
||||||
logger.success(`📝 Admin updated API key: ${keyId}`)
|
logger.success(`📝 Admin updated API key: ${keyId}`)
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
>
|
>
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700">添加方式</label>
|
<label class="mb-3 block text-sm font-semibold text-gray-700">添加方式</label>
|
||||||
<div class="flex flex-wrap gap-4">
|
<div class="flex flex-wrap gap-4">
|
||||||
<label class="flex cursor-pointer items-center">
|
<label v-if="form.platform === 'claude'" class="flex cursor-pointer items-center">
|
||||||
<input v-model="form.addType" class="mr-2" type="radio" value="setup-token" />
|
<input v-model="form.addType" class="mr-2" type="radio" value="setup-token" />
|
||||||
<span class="text-sm text-gray-700">Setup Token (推荐)</span>
|
<span class="text-sm text-gray-700">Setup Token (推荐)</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -1203,7 +1203,7 @@ const initProxyConfig = () => {
|
|||||||
// 表单数据
|
// 表单数据
|
||||||
const form = ref({
|
const form = ref({
|
||||||
platform: props.account?.platform || 'claude',
|
platform: props.account?.platform || 'claude',
|
||||||
addType: 'setup-token',
|
addType: props.account?.platform === 'gemini' ? 'oauth' : 'setup-token',
|
||||||
name: props.account?.name || '',
|
name: props.account?.name || '',
|
||||||
description: props.account?.description || '',
|
description: props.account?.description || '',
|
||||||
accountType: props.account?.accountType || 'shared',
|
accountType: props.account?.accountType || 'shared',
|
||||||
|
|||||||
@@ -442,48 +442,71 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-if="form.enableModelRestriction" class="space-y-3">
|
||||||
v-if="form.enableModelRestriction"
|
|
||||||
class="space-y-2 rounded-lg border border-red-200 bg-red-50 p-3"
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-xs font-medium text-gray-700">限制的模型列表</label>
|
<label class="mb-2 block text-sm font-medium text-gray-600">限制的模型列表</label>
|
||||||
<div class="mb-2 flex min-h-[24px] flex-wrap gap-1">
|
<div
|
||||||
|
class="mb-3 flex min-h-[32px] flex-wrap gap-2 rounded-lg border border-gray-200 bg-gray-50 p-2"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
v-for="(model, index) in form.restrictedModels"
|
v-for="(model, index) in form.restrictedModels"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="inline-flex items-center rounded-full bg-red-100 px-2 py-1 text-xs text-red-800"
|
class="inline-flex items-center rounded-full bg-red-100 px-3 py-1 text-sm text-red-800"
|
||||||
>
|
>
|
||||||
{{ model }}
|
{{ model }}
|
||||||
<button
|
<button
|
||||||
class="ml-1 text-red-600 hover:text-red-800"
|
class="ml-2 text-red-600 hover:text-red-800"
|
||||||
type="button"
|
type="button"
|
||||||
@click="removeRestrictedModel(index)"
|
@click="removeRestrictedModel(index)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-times text-xs" />
|
<i class="fas fa-times text-xs" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="form.restrictedModels.length === 0" class="text-xs text-gray-400">
|
<span v-if="form.restrictedModels.length === 0" class="text-sm text-gray-400">
|
||||||
暂无限制的模型
|
暂无限制的模型
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<!-- 快速添加按钮 -->
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
v-for="model in availableQuickModels"
|
||||||
|
:key="model"
|
||||||
|
class="flex-shrink-0 rounded-lg bg-gray-100 px-3 py-1 text-xs text-gray-700 transition-colors hover:bg-gray-200 sm:text-sm"
|
||||||
|
type="button"
|
||||||
|
@click="quickAddRestrictedModel(model)"
|
||||||
|
>
|
||||||
|
{{ model }}
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
v-if="availableQuickModels.length === 0"
|
||||||
|
class="text-sm italic text-gray-400"
|
||||||
|
>
|
||||||
|
所有常用模型已在限制列表中
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 手动输入 -->
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
v-model="form.modelInput"
|
v-model="form.modelInput"
|
||||||
class="form-input flex-1 text-sm"
|
class="form-input flex-1"
|
||||||
placeholder="输入模型名称,按回车添加"
|
placeholder="输入模型名称,按回车添加"
|
||||||
type="text"
|
type="text"
|
||||||
@keydown.enter.prevent="addRestrictedModel"
|
@keydown.enter.prevent="addRestrictedModel"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="rounded-lg bg-red-500 px-3 py-1.5 text-sm text-white transition-colors hover:bg-red-600"
|
class="rounded-lg bg-red-500 px-4 py-2 text-white transition-colors hover:bg-red-600"
|
||||||
type="button"
|
type="button"
|
||||||
@click="addRestrictedModel"
|
@click="addRestrictedModel"
|
||||||
>
|
>
|
||||||
<i class="fas fa-plus" />
|
<i class="fas fa-plus" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-gray-500">例如:claude-opus-4-20250514</p>
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-gray-500">
|
||||||
|
设置此API Key无法访问的模型,例如:claude-opus-4-20250514
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -769,6 +792,21 @@ const removeRestrictedModel = (index) => {
|
|||||||
form.restrictedModels.splice(index, 1)
|
form.restrictedModels.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 常用模型列表
|
||||||
|
const commonModels = ref(['claude-opus-4-20250514', 'claude-opus-4-1-20250805'])
|
||||||
|
|
||||||
|
// 可用的快捷模型(过滤掉已在限制列表中的)
|
||||||
|
const availableQuickModels = computed(() => {
|
||||||
|
return commonModels.value.filter((model) => !form.restrictedModels.includes(model))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 快速添加限制的模型
|
||||||
|
const quickAddRestrictedModel = (model) => {
|
||||||
|
if (!form.restrictedModels.includes(model)) {
|
||||||
|
form.restrictedModels.push(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 标签管理方法
|
// 标签管理方法
|
||||||
const addTag = () => {
|
const addTag = () => {
|
||||||
if (newTag.value && newTag.value.trim()) {
|
if (newTag.value && newTag.value.trim()) {
|
||||||
|
|||||||
@@ -238,6 +238,27 @@
|
|||||||
<p class="mt-2 text-xs text-gray-500">设置此 API Key 可同时处理的最大请求数</p>
|
<p class="mt-2 text-xs text-gray-500">设置此 API Key 可同时处理的最大请求数</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 激活账号 -->
|
||||||
|
<div>
|
||||||
|
<div class="mb-3 flex items-center">
|
||||||
|
<input
|
||||||
|
id="editIsActive"
|
||||||
|
v-model="form.isActive"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700"
|
||||||
|
for="editIsActive"
|
||||||
|
>
|
||||||
|
激活账号
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="mb-4 text-xs text-gray-500">
|
||||||
|
取消勾选将禁用此 API Key,暂停所有请求,客户端返回 401 错误
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700">服务权限</label>
|
<label class="mb-3 block text-sm font-semibold text-gray-700">服务权限</label>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
@@ -511,7 +532,8 @@ const form = reactive({
|
|||||||
modelInput: '',
|
modelInput: '',
|
||||||
enableClientRestriction: false,
|
enableClientRestriction: false,
|
||||||
allowedClients: [],
|
allowedClients: [],
|
||||||
tags: []
|
tags: [],
|
||||||
|
isActive: true
|
||||||
})
|
})
|
||||||
|
|
||||||
// 添加限制的模型
|
// 添加限制的模型
|
||||||
@@ -628,6 +650,9 @@ const updateApiKey = async () => {
|
|||||||
data.enableClientRestriction = form.enableClientRestriction
|
data.enableClientRestriction = form.enableClientRestriction
|
||||||
data.allowedClients = form.allowedClients
|
data.allowedClients = form.allowedClients
|
||||||
|
|
||||||
|
// 活跃状态
|
||||||
|
data.isActive = form.isActive
|
||||||
|
|
||||||
const result = await apiClient.put(`/admin/api-keys/${props.apiKey.id}`, data)
|
const result = await apiClient.put(`/admin/api-keys/${props.apiKey.id}`, data)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -737,6 +762,8 @@ onMounted(async () => {
|
|||||||
// 从后端数据中获取实际的启用状态,而不是根据数组长度推断
|
// 从后端数据中获取实际的启用状态,而不是根据数组长度推断
|
||||||
form.enableModelRestriction = props.apiKey.enableModelRestriction || false
|
form.enableModelRestriction = props.apiKey.enableModelRestriction || false
|
||||||
form.enableClientRestriction = props.apiKey.enableClientRestriction || false
|
form.enableClientRestriction = props.apiKey.enableClientRestriction || false
|
||||||
|
// 初始化活跃状态,默认为 true
|
||||||
|
form.isActive = props.apiKey.isActive !== undefined ? props.apiKey.isActive : true
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
254
web/admin-spa/src/components/apikeys/WindowCountdown.vue
Normal file
254
web/admin-spa/src/components/apikeys/WindowCountdown.vue
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="flex items-center justify-between text-xs">
|
||||||
|
<span class="text-gray-500">{{ label }}</span>
|
||||||
|
<span v-if="windowState === 'active'" class="font-medium text-gray-700">
|
||||||
|
<i class="fas fa-clock mr-1 text-blue-500" />
|
||||||
|
{{ formatTime(remainingSeconds) }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="windowState === 'expired'" class="font-medium text-orange-600">
|
||||||
|
<i class="fas fa-sync-alt mr-1" />
|
||||||
|
窗口已过期
|
||||||
|
</span>
|
||||||
|
<span v-else-if="windowState === 'notStarted'" class="font-medium text-gray-500">
|
||||||
|
<i class="fas fa-pause-circle mr-1" />
|
||||||
|
窗口未激活
|
||||||
|
</span>
|
||||||
|
<span v-else class="font-medium text-gray-400"> {{ rateLimitWindow }} 分钟 </span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 进度条(仅在有限制时显示) -->
|
||||||
|
<div v-if="showProgress" class="space-y-0.5">
|
||||||
|
<div v-if="hasRequestLimit" class="space-y-0.5">
|
||||||
|
<div class="flex items-center justify-between text-xs">
|
||||||
|
<span class="text-gray-400">请求</span>
|
||||||
|
<span class="text-gray-600"> {{ currentRequests || 0 }}/{{ requestLimit }} </span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1 w-full rounded-full bg-gray-200">
|
||||||
|
<div
|
||||||
|
class="h-1 rounded-full transition-all duration-300"
|
||||||
|
:class="getRequestProgressColor()"
|
||||||
|
:style="{ width: getRequestProgress() + '%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasTokenLimit" class="space-y-0.5">
|
||||||
|
<div class="flex items-center justify-between text-xs">
|
||||||
|
<span class="text-gray-400">Token</span>
|
||||||
|
<span class="text-gray-600">
|
||||||
|
{{ formatTokenCount(currentTokens || 0) }}/{{ formatTokenCount(tokenLimit) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1 w-full rounded-full bg-gray-200">
|
||||||
|
<div
|
||||||
|
class="h-1 rounded-full transition-all duration-300"
|
||||||
|
:class="getTokenProgressColor()"
|
||||||
|
:style="{ width: getTokenProgress() + '%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 额外提示信息 -->
|
||||||
|
<div v-if="windowState === 'active' && showTooltip" class="text-xs text-gray-500">
|
||||||
|
<i class="fas fa-info-circle mr-1" />
|
||||||
|
<span v-if="remainingSeconds < 60">即将重置</span>
|
||||||
|
<span v-else-if="remainingSeconds < 300"
|
||||||
|
>{{ Math.ceil(remainingSeconds / 60) }} 分钟后重置</span
|
||||||
|
>
|
||||||
|
<span v-else>{{ formatDetailedTime(remainingSeconds) }}后重置</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '窗口限制'
|
||||||
|
},
|
||||||
|
rateLimitWindow: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
windowStartTime: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
windowEndTime: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
windowRemainingSeconds: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
currentRequests: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
requestLimit: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
currentTokens: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
tokenLimit: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
showProgress: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
showTooltip: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const remainingSeconds = ref(props.windowRemainingSeconds)
|
||||||
|
let intervalId = null
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const windowState = computed(() => {
|
||||||
|
if (props.windowStartTime === null) {
|
||||||
|
return 'notStarted' // 窗口未开始
|
||||||
|
}
|
||||||
|
if (remainingSeconds.value === 0) {
|
||||||
|
return 'expired' // 窗口已过期
|
||||||
|
}
|
||||||
|
if (remainingSeconds.value > 0) {
|
||||||
|
return 'active' // 窗口活跃中
|
||||||
|
}
|
||||||
|
return 'unknown'
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasRequestLimit = computed(() => props.requestLimit > 0)
|
||||||
|
const hasTokenLimit = computed(() => props.tokenLimit > 0)
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const formatTime = (seconds) => {
|
||||||
|
if (seconds === null || seconds === undefined) return '--:--'
|
||||||
|
|
||||||
|
const hours = Math.floor(seconds / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
const secs = seconds % 60
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}h ${minutes}m`
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}m ${secs}s`
|
||||||
|
} else {
|
||||||
|
return `${secs}s`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDetailedTime = (seconds) => {
|
||||||
|
const hours = Math.floor(seconds / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}小时${minutes}分钟`
|
||||||
|
} else {
|
||||||
|
return `${minutes}分钟`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTokenCount = (count) => {
|
||||||
|
if (count >= 1000000) {
|
||||||
|
return (count / 1000000).toFixed(1) + 'M'
|
||||||
|
} else if (count >= 1000) {
|
||||||
|
return (count / 1000).toFixed(1) + 'K'
|
||||||
|
}
|
||||||
|
return count.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRequestProgress = () => {
|
||||||
|
if (!props.requestLimit || props.requestLimit === 0) return 0
|
||||||
|
const percentage = ((props.currentRequests || 0) / props.requestLimit) * 100
|
||||||
|
return Math.min(percentage, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRequestProgressColor = () => {
|
||||||
|
const progress = getRequestProgress()
|
||||||
|
if (progress >= 100) return 'bg-red-500'
|
||||||
|
if (progress >= 80) return 'bg-yellow-500'
|
||||||
|
return 'bg-blue-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTokenProgress = () => {
|
||||||
|
if (!props.tokenLimit || props.tokenLimit === 0) return 0
|
||||||
|
const percentage = ((props.currentTokens || 0) / props.tokenLimit) * 100
|
||||||
|
return Math.min(percentage, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTokenProgressColor = () => {
|
||||||
|
const progress = getTokenProgress()
|
||||||
|
if (progress >= 100) return 'bg-red-500'
|
||||||
|
if (progress >= 80) return 'bg-yellow-500'
|
||||||
|
return 'bg-purple-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新倒计时
|
||||||
|
const updateCountdown = () => {
|
||||||
|
if (props.windowEndTime && remainingSeconds.value > 0) {
|
||||||
|
const now = Date.now()
|
||||||
|
const remaining = Math.max(0, Math.floor((props.windowEndTime - now) / 1000))
|
||||||
|
remainingSeconds.value = remaining
|
||||||
|
|
||||||
|
if (remaining === 0) {
|
||||||
|
// 窗口已过期,停止倒计时
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
intervalId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听props变化
|
||||||
|
watch(
|
||||||
|
() => props.windowRemainingSeconds,
|
||||||
|
(newVal) => {
|
||||||
|
remainingSeconds.value = newVal
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.windowEndTime,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
// 重新计算剩余时间
|
||||||
|
updateCountdown()
|
||||||
|
|
||||||
|
// 如果窗口活跃且没有定时器,启动定时器
|
||||||
|
if (!intervalId && remainingSeconds.value > 0) {
|
||||||
|
intervalId = setInterval(updateCountdown, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.windowEndTime && remainingSeconds.value > 0) {
|
||||||
|
// 立即更新一次
|
||||||
|
updateCountdown()
|
||||||
|
// 启动定时器
|
||||||
|
intervalId = setInterval(updateCountdown, 1000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
219
web/admin-spa/src/components/common/CustomDropdown.vue
Normal file
219
web/admin-spa/src/components/common/CustomDropdown.vue
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<!-- 触发器 -->
|
||||||
|
<div
|
||||||
|
ref="triggerRef"
|
||||||
|
class="relative flex cursor-pointer items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2 shadow-sm transition-all duration-200 hover:shadow-md"
|
||||||
|
:class="[isOpen && 'border-blue-400 shadow-md']"
|
||||||
|
@click="toggleDropdown"
|
||||||
|
>
|
||||||
|
<i v-if="icon" :class="['fas', icon, 'text-sm', iconColor]"></i>
|
||||||
|
<span class="select-none whitespace-nowrap text-sm font-medium text-gray-700">
|
||||||
|
{{ selectedLabel || placeholder }}
|
||||||
|
</span>
|
||||||
|
<i
|
||||||
|
:class="[
|
||||||
|
'fas fa-chevron-down ml-auto text-xs text-gray-400 transition-transform duration-200',
|
||||||
|
isOpen && 'rotate-180'
|
||||||
|
]"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 下拉选项 - 使用 Teleport 将其移动到 body -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition duration-200 ease-out"
|
||||||
|
enter-from-class="transform scale-95 opacity-0"
|
||||||
|
enter-to-class="transform scale-100 opacity-100"
|
||||||
|
leave-active-class="transition duration-150 ease-in"
|
||||||
|
leave-from-class="transform scale-100 opacity-100"
|
||||||
|
leave-to-class="transform scale-95 opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
ref="dropdownRef"
|
||||||
|
class="fixed z-[9999] min-w-max overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg"
|
||||||
|
:style="dropdownStyle"
|
||||||
|
>
|
||||||
|
<div class="max-h-60 overflow-y-auto py-1">
|
||||||
|
<div
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option.value"
|
||||||
|
class="flex cursor-pointer items-center gap-2 whitespace-nowrap px-3 py-2 text-sm transition-colors duration-150"
|
||||||
|
:class="[
|
||||||
|
option.value === modelValue
|
||||||
|
? 'bg-blue-50 font-medium text-blue-700'
|
||||||
|
: 'text-gray-700 hover:bg-gray-50'
|
||||||
|
]"
|
||||||
|
@click="selectOption(option)"
|
||||||
|
>
|
||||||
|
<i v-if="option.icon" :class="['fas', option.icon, 'text-xs']"></i>
|
||||||
|
<span>{{ option.label }}</span>
|
||||||
|
<i
|
||||||
|
v-if="option.value === modelValue"
|
||||||
|
class="fas fa-check ml-auto pl-3 text-xs text-blue-600"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '请选择'
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
iconColor: {
|
||||||
|
type: String,
|
||||||
|
default: 'text-gray-500'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const triggerRef = ref(null)
|
||||||
|
const dropdownRef = ref(null)
|
||||||
|
const dropdownStyle = ref({})
|
||||||
|
|
||||||
|
const selectedLabel = computed(() => {
|
||||||
|
const selected = props.options.find((opt) => opt.value === props.modelValue)
|
||||||
|
return selected ? selected.label : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleDropdown = async () => {
|
||||||
|
isOpen.value = !isOpen.value
|
||||||
|
if (isOpen.value) {
|
||||||
|
await nextTick()
|
||||||
|
updateDropdownPosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDropdown = () => {
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectOption = (option) => {
|
||||||
|
emit('update:modelValue', option.value)
|
||||||
|
emit('change', option.value)
|
||||||
|
closeDropdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDropdownPosition = () => {
|
||||||
|
if (!triggerRef.value || !isOpen.value) return
|
||||||
|
|
||||||
|
const trigger = triggerRef.value.getBoundingClientRect()
|
||||||
|
const dropdownHeight = 250 // 预估高度
|
||||||
|
const spaceBelow = window.innerHeight - trigger.bottom
|
||||||
|
const spaceAbove = trigger.top
|
||||||
|
|
||||||
|
let top, left
|
||||||
|
|
||||||
|
// 计算垂直位置
|
||||||
|
if (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove) {
|
||||||
|
// 显示在下方
|
||||||
|
top = trigger.bottom + 8
|
||||||
|
} else {
|
||||||
|
// 显示在上方
|
||||||
|
top = trigger.top - dropdownHeight - 8
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算水平位置
|
||||||
|
left = trigger.left
|
||||||
|
|
||||||
|
// 确保不超出右边界
|
||||||
|
const dropdownWidth = 200 // 预估宽度
|
||||||
|
if (left + dropdownWidth > window.innerWidth) {
|
||||||
|
left = window.innerWidth - dropdownWidth - 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保不超出左边界
|
||||||
|
if (left < 10) {
|
||||||
|
left = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdownStyle.value = {
|
||||||
|
top: `${top}px`,
|
||||||
|
left: `${left}px`,
|
||||||
|
minWidth: `${trigger.width}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听窗口大小变化和滚动
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (isOpen.value) {
|
||||||
|
updateDropdownPosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (isOpen.value) {
|
||||||
|
closeDropdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理点击外部关闭
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (!triggerRef.value || !isOpen.value) return
|
||||||
|
|
||||||
|
// 如果点击不在触发器内,且下拉框存在时也不在下拉框内,则关闭
|
||||||
|
if (!triggerRef.value.contains(event.target)) {
|
||||||
|
if (dropdownRef.value && !dropdownRef.value.contains(event.target)) {
|
||||||
|
closeDropdown()
|
||||||
|
} else if (!dropdownRef.value) {
|
||||||
|
closeDropdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('scroll', handleScroll, true)
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('scroll', handleScroll, true)
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
document.removeEventListener('click', handleClickOutside)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 自定义滚动条 */
|
||||||
|
.max-h-60::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-h-60::-webkit-scrollbar-track {
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-h-60::-webkit-scrollbar-thumb {
|
||||||
|
background: #d1d5db;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-h-60::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,36 +6,65 @@
|
|||||||
<h3 class="mb-1 text-lg font-bold text-gray-900 sm:mb-2 sm:text-xl">账户管理</h3>
|
<h3 class="mb-1 text-lg font-bold text-gray-900 sm:mb-2 sm:text-xl">账户管理</h3>
|
||||||
<p class="text-sm text-gray-600 sm:text-base">管理您的 Claude 和 Gemini 账户及代理配置</p>
|
<p class="text-sm text-gray-600 sm:text-base">管理您的 Claude 和 Gemini 账户及代理配置</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div class="flex flex-col gap-2 sm:flex-row">
|
<!-- 筛选器组 -->
|
||||||
<select
|
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-3">
|
||||||
|
<!-- 排序选择器 -->
|
||||||
|
<div class="group relative min-w-[160px]">
|
||||||
|
<div
|
||||||
|
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-indigo-500 to-blue-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||||
|
></div>
|
||||||
|
<CustomDropdown
|
||||||
v-model="accountSortBy"
|
v-model="accountSortBy"
|
||||||
class="form-input w-full px-3 py-2 text-sm sm:w-auto"
|
icon="fa-sort-amount-down"
|
||||||
|
icon-color="text-indigo-500"
|
||||||
|
:options="sortOptions"
|
||||||
|
placeholder="选择排序"
|
||||||
@change="sortAccounts()"
|
@change="sortAccounts()"
|
||||||
>
|
/>
|
||||||
<option value="name">按名称排序</option>
|
|
||||||
<option value="dailyTokens">按今日Token排序</option>
|
|
||||||
<option value="dailyRequests">按今日请求数排序</option>
|
|
||||||
<option value="totalTokens">按总Token排序</option>
|
|
||||||
<option value="lastUsed">按最后使用排序</option>
|
|
||||||
</select>
|
|
||||||
<select
|
|
||||||
v-model="groupFilter"
|
|
||||||
class="form-input w-full px-3 py-2 text-sm sm:w-auto"
|
|
||||||
@change="filterByGroup"
|
|
||||||
>
|
|
||||||
<option value="all">所有账户</option>
|
|
||||||
<option value="ungrouped">未分组账户</option>
|
|
||||||
<option v-for="group in accountGroups" :key="group.id" :value="group.id">
|
|
||||||
{{ group.name }} ({{ group.platform === 'claude' ? 'Claude' : 'Gemini' }})
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 分组筛选器 -->
|
||||||
|
<div class="group relative min-w-[160px]">
|
||||||
|
<div
|
||||||
|
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||||
|
></div>
|
||||||
|
<CustomDropdown
|
||||||
|
v-model="groupFilter"
|
||||||
|
icon="fa-layer-group"
|
||||||
|
icon-color="text-purple-500"
|
||||||
|
:options="groupOptions"
|
||||||
|
placeholder="选择分组"
|
||||||
|
@change="filterByGroup"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 刷新按钮 -->
|
||||||
<button
|
<button
|
||||||
class="btn btn-success flex w-full items-center justify-center gap-2 px-4 py-2 sm:w-auto sm:px-6 sm:py-3"
|
class="group relative flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
||||||
|
:disabled="accountsLoading"
|
||||||
|
@click="loadAccounts()"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-green-500 to-teal-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||||
|
></div>
|
||||||
|
<i
|
||||||
|
:class="[
|
||||||
|
'fas relative text-green-500',
|
||||||
|
accountsLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<span class="relative">刷新</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加账户按钮 -->
|
||||||
|
<button
|
||||||
|
class="flex w-full items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-green-500 to-green-600 px-5 py-2.5 text-sm font-medium text-white shadow-md transition-all duration-200 hover:from-green-600 hover:to-green-700 hover:shadow-lg sm:w-auto"
|
||||||
@click.stop="openCreateAccountModal"
|
@click.stop="openCreateAccountModal"
|
||||||
>
|
>
|
||||||
<i class="fas fa-plus" />添加账户
|
<i class="fas fa-plus"></i>
|
||||||
|
<span>添加账户</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -710,6 +739,7 @@ import { apiClient } from '@/config/api'
|
|||||||
import { useConfirm } from '@/composables/useConfirm'
|
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'
|
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
||||||
|
import CustomDropdown from '@/components/common/CustomDropdown.vue'
|
||||||
|
|
||||||
// 使用确认弹窗
|
// 使用确认弹窗
|
||||||
const { showConfirmModal, confirmOptions, showConfirm, handleConfirm, handleCancel } = useConfirm()
|
const { showConfirmModal, confirmOptions, showConfirm, handleConfirm, handleCancel } = useConfirm()
|
||||||
@@ -726,6 +756,30 @@ const accountGroups = ref([])
|
|||||||
const groupFilter = ref('all')
|
const groupFilter = ref('all')
|
||||||
const filteredAccounts = ref([])
|
const filteredAccounts = ref([])
|
||||||
|
|
||||||
|
// 下拉选项数据
|
||||||
|
const sortOptions = ref([
|
||||||
|
{ value: 'name', label: '按名称排序', icon: 'fa-font' },
|
||||||
|
{ value: 'dailyTokens', label: '按今日Token排序', icon: 'fa-coins' },
|
||||||
|
{ value: 'dailyRequests', label: '按今日请求数排序', icon: 'fa-chart-line' },
|
||||||
|
{ value: 'totalTokens', label: '按总Token排序', icon: 'fa-database' },
|
||||||
|
{ value: 'lastUsed', label: '按最后使用排序', icon: 'fa-clock' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const groupOptions = computed(() => {
|
||||||
|
const options = [
|
||||||
|
{ value: 'all', label: '所有账户', icon: 'fa-globe' },
|
||||||
|
{ value: 'ungrouped', label: '未分组账户', icon: 'fa-user' }
|
||||||
|
]
|
||||||
|
accountGroups.value.forEach((group) => {
|
||||||
|
options.push({
|
||||||
|
value: group.id,
|
||||||
|
label: `${group.name} (${group.platform === 'claude' ? 'Claude' : 'Gemini'})`,
|
||||||
|
icon: group.platform === 'claude' ? 'fa-brain' : 'fa-robot'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
|
||||||
// 模态框状态
|
// 模态框状态
|
||||||
const showCreateAccountModal = ref(false)
|
const showCreateAccountModal = ref(false)
|
||||||
const showEditAccountModal = ref(false)
|
const showEditAccountModal = ref(false)
|
||||||
|
|||||||
@@ -7,35 +7,71 @@
|
|||||||
<p class="text-sm text-gray-600 sm:text-base">管理和监控您的 API 密钥</p>
|
<p class="text-sm text-gray-600 sm:text-base">管理和监控您的 API 密钥</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<!-- Token统计时间范围选择 -->
|
<!-- 筛选器组 -->
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-3">
|
||||||
<select
|
<!-- 时间范围筛选 -->
|
||||||
|
<div class="group relative min-w-[140px]">
|
||||||
|
<div
|
||||||
|
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-blue-500 to-purple-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||||
|
></div>
|
||||||
|
<CustomDropdown
|
||||||
v-model="apiKeyStatsTimeRange"
|
v-model="apiKeyStatsTimeRange"
|
||||||
class="rounded-md border border-gray-200 bg-white px-3 py-2 text-sm text-gray-700 transition-colors hover:border-gray-300 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500"
|
icon="fa-calendar-alt"
|
||||||
|
icon-color="text-blue-500"
|
||||||
|
:options="timeRangeOptions"
|
||||||
|
placeholder="选择时间范围"
|
||||||
@change="loadApiKeys()"
|
@change="loadApiKeys()"
|
||||||
>
|
/>
|
||||||
<option value="today">今日</option>
|
|
||||||
<option value="7days">最近7天</option>
|
|
||||||
<option value="monthly">本月</option>
|
|
||||||
<option value="all">全部时间</option>
|
|
||||||
</select>
|
|
||||||
<!-- 标签筛选器 -->
|
|
||||||
<select
|
|
||||||
v-model="selectedTagFilter"
|
|
||||||
class="rounded-md border border-gray-200 bg-white px-3 py-2 text-sm text-gray-700 transition-colors hover:border-gray-300 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
@change="currentPage = 1"
|
|
||||||
>
|
|
||||||
<option value="">所有标签</option>
|
|
||||||
<option v-for="tag in availableTags" :key="tag" :value="tag">
|
|
||||||
{{ tag }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 标签筛选器 -->
|
||||||
|
<div class="group relative min-w-[140px]">
|
||||||
|
<div
|
||||||
|
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||||
|
></div>
|
||||||
|
<div class="relative">
|
||||||
|
<CustomDropdown
|
||||||
|
v-model="selectedTagFilter"
|
||||||
|
icon="fa-tags"
|
||||||
|
icon-color="text-purple-500"
|
||||||
|
:options="tagOptions"
|
||||||
|
placeholder="所有标签"
|
||||||
|
@change="currentPage = 1"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="selectedTagFilter"
|
||||||
|
class="absolute -right-2 -top-2 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-purple-500 text-xs text-white shadow-sm"
|
||||||
|
>
|
||||||
|
{{ selectedTagCount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 刷新按钮 -->
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary flex w-full items-center justify-center gap-2 px-4 py-2 text-sm sm:w-auto"
|
class="group relative flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
||||||
|
:disabled="apiKeysLoading"
|
||||||
|
@click="loadApiKeys()"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-green-500 to-teal-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||||
|
></div>
|
||||||
|
<i
|
||||||
|
:class="[
|
||||||
|
'fas relative text-green-500',
|
||||||
|
apiKeysLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<span class="relative">刷新</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- 创建按钮 -->
|
||||||
|
<button
|
||||||
|
class="flex w-full items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-blue-500 to-blue-600 px-5 py-2.5 text-sm font-medium text-white shadow-md transition-all duration-200 hover:from-blue-600 hover:to-blue-700 hover:shadow-lg sm:w-auto"
|
||||||
@click.stop="openCreateApiKeyModal"
|
@click.stop="openCreateApiKeyModal"
|
||||||
>
|
>
|
||||||
<i class="fas fa-plus" />创建新 Key
|
<i class="fas fa-plus"></i>
|
||||||
|
<span>创建新 Key</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -368,6 +404,21 @@
|
|||||||
<i class="fas fa-clock" />
|
<i class="fas fa-clock" />
|
||||||
<span class="ml-1 hidden xl:inline">续期</span>
|
<span class="ml-1 hidden xl:inline">续期</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
key.isActive
|
||||||
|
? 'text-orange-600 hover:bg-orange-50 hover:text-orange-900'
|
||||||
|
: 'text-green-600 hover:bg-green-50 hover:text-green-900',
|
||||||
|
'rounded px-2 py-1 text-xs font-medium transition-colors'
|
||||||
|
]"
|
||||||
|
:title="key.isActive ? '禁用' : '激活'"
|
||||||
|
@click="toggleApiKeyStatus(key)"
|
||||||
|
>
|
||||||
|
<i :class="['fas', key.isActive ? 'fa-ban' : 'fa-check-circle']" />
|
||||||
|
<span class="ml-1 hidden xl:inline">{{
|
||||||
|
key.isActive ? '禁用' : '激活'
|
||||||
|
}}</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded px-2 py-1 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
|
class="rounded px-2 py-1 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
|
||||||
title="删除"
|
title="删除"
|
||||||
@@ -801,6 +852,18 @@
|
|||||||
<i class="fas fa-clock mr-1" />
|
<i class="fas fa-clock mr-1" />
|
||||||
续期
|
续期
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
key.isActive
|
||||||
|
? 'bg-orange-50 text-orange-600 hover:bg-orange-100'
|
||||||
|
: 'bg-green-50 text-green-600 hover:bg-green-100',
|
||||||
|
'rounded-lg px-3 py-2 text-xs transition-colors'
|
||||||
|
]"
|
||||||
|
@click="toggleApiKeyStatus(key)"
|
||||||
|
>
|
||||||
|
<i :class="['fas', key.isActive ? 'fa-ban' : 'fa-check-circle', 'mr-1']" />
|
||||||
|
{{ key.isActive ? '禁用' : '激活' }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded-lg bg-red-50 px-3 py-2 text-xs text-red-600 transition-colors hover:bg-red-100"
|
class="rounded-lg bg-red-50 px-3 py-2 text-xs text-red-600 transition-colors hover:bg-red-100"
|
||||||
@click="deleteApiKey(key.id)"
|
@click="deleteApiKey(key.id)"
|
||||||
@@ -964,6 +1027,7 @@ import BatchApiKeyModal from '@/components/apikeys/BatchApiKeyModal.vue'
|
|||||||
import ExpiryEditModal from '@/components/apikeys/ExpiryEditModal.vue'
|
import ExpiryEditModal from '@/components/apikeys/ExpiryEditModal.vue'
|
||||||
import UsageDetailModal from '@/components/apikeys/UsageDetailModal.vue'
|
import UsageDetailModal from '@/components/apikeys/UsageDetailModal.vue'
|
||||||
import WindowCountdown from '@/components/apikeys/WindowCountdown.vue'
|
import WindowCountdown from '@/components/apikeys/WindowCountdown.vue'
|
||||||
|
import CustomDropdown from '@/components/common/CustomDropdown.vue'
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const clientsStore = useClientsStore()
|
const clientsStore = useClientsStore()
|
||||||
@@ -986,6 +1050,28 @@ const selectedApiKeyForDetail = ref(null)
|
|||||||
const selectedTagFilter = ref('')
|
const selectedTagFilter = ref('')
|
||||||
const availableTags = ref([])
|
const availableTags = ref([])
|
||||||
|
|
||||||
|
// 下拉选项数据
|
||||||
|
const timeRangeOptions = ref([
|
||||||
|
{ value: 'today', label: '今日', icon: 'fa-clock' },
|
||||||
|
{ value: '7days', label: '最近7天', icon: 'fa-calendar-week' },
|
||||||
|
{ value: 'monthly', label: '本月', icon: 'fa-calendar' },
|
||||||
|
{ value: 'all', label: '全部时间', icon: 'fa-infinity' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const tagOptions = computed(() => {
|
||||||
|
const options = [{ value: '', label: '所有标签', icon: 'fa-asterisk' }]
|
||||||
|
availableTags.value.forEach((tag) => {
|
||||||
|
options.push({ value: tag, label: tag, icon: 'fa-tag' })
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedTagCount = computed(() => {
|
||||||
|
if (!selectedTagFilter.value) return 0
|
||||||
|
return apiKeys.value.filter((key) => key.tags && key.tags.includes(selectedTagFilter.value))
|
||||||
|
.length
|
||||||
|
})
|
||||||
|
|
||||||
// 分页相关
|
// 分页相关
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
@@ -1455,6 +1541,49 @@ const handleRenewSuccess = () => {
|
|||||||
loadApiKeys()
|
loadApiKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换API Key状态(激活/禁用)
|
||||||
|
const toggleApiKeyStatus = async (key) => {
|
||||||
|
let confirmed = true
|
||||||
|
|
||||||
|
// 禁用时需要二次确认
|
||||||
|
if (key.isActive) {
|
||||||
|
if (window.showConfirm) {
|
||||||
|
confirmed = await window.showConfirm(
|
||||||
|
'禁用 API Key',
|
||||||
|
`确定要禁用 API Key "${key.name}" 吗?禁用后所有使用此 Key 的请求将返回 401 错误。`,
|
||||||
|
'确定禁用',
|
||||||
|
'取消'
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 降级方案
|
||||||
|
confirmed = confirm(
|
||||||
|
`确定要禁用 API Key "${key.name}" 吗?禁用后所有使用此 Key 的请求将返回 401 错误。`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await apiClient.put(`/admin/api-keys/${key.id}`, {
|
||||||
|
isActive: !key.isActive
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
showToast(`API Key 已${key.isActive ? '禁用' : '激活'}`, 'success')
|
||||||
|
// 更新本地数据
|
||||||
|
const localKey = apiKeys.value.find((k) => k.id === key.id)
|
||||||
|
if (localKey) {
|
||||||
|
localKey.isActive = !key.isActive
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast(data.message || '操作失败', 'error')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast('操作失败', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 删除API Key
|
// 删除API Key
|
||||||
const deleteApiKey = async (keyId) => {
|
const deleteApiKey = async (keyId) => {
|
||||||
let confirmed = false
|
let confirmed = false
|
||||||
|
|||||||
@@ -114,19 +114,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 第二步:安装 Git Bash -->
|
<!-- 第二步:安装 Claude Code -->
|
||||||
<div class="mb-4 sm:mb-10 sm:mb-6">
|
<div class="mb-4 sm:mb-10 sm:mb-6">
|
||||||
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl">
|
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl">
|
||||||
<span
|
<span
|
||||||
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-green-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
|
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-green-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
|
||||||
>2</span
|
>2</span
|
||||||
>
|
>
|
||||||
安装 Git Bash
|
安装 Claude Code
|
||||||
</h4>
|
</h4>
|
||||||
<p class="mb-4 text-sm text-gray-600 sm:mb-4 sm:mb-6 sm:text-base">
|
|
||||||
Windows 环境下需要使用 Git Bash 安装Claude code。安装完成后,环境变量设置和使用 Claude
|
|
||||||
Code 仍然在普通的 PowerShell 或 CMD 中进行。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="mb-4 rounded-xl border border-green-100 bg-gradient-to-r from-green-50 to-emerald-50 p-4 sm:mb-6 sm:p-6"
|
class="mb-4 rounded-xl border border-green-100 bg-gradient-to-r from-green-50 to-emerald-50 p-4 sm:mb-6 sm:p-6"
|
||||||
@@ -134,78 +130,16 @@
|
|||||||
<h5
|
<h5
|
||||||
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg"
|
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg"
|
||||||
>
|
>
|
||||||
<i class="fab fa-git-alt mr-2 text-green-600" />
|
<i class="fas fa-download mr-2 text-green-600" />
|
||||||
下载并安装 Git for Windows
|
|
||||||
</h5>
|
|
||||||
<ol
|
|
||||||
class="mb-4 ml-2 list-inside list-decimal space-y-1 text-xs text-gray-600 sm:ml-4 sm:space-y-2 sm:text-sm"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
访问
|
|
||||||
<code class="rounded bg-gray-100 px-1 py-1 text-xs sm:px-2 sm:text-sm"
|
|
||||||
>https://git-scm.com/downloads/win</code
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>点击 "Download for Windows" 下载安装包</li>
|
|
||||||
<li>
|
|
||||||
运行下载的
|
|
||||||
<code class="rounded bg-gray-100 px-1 py-1 text-xs sm:px-2 sm:text-sm">.exe</code>
|
|
||||||
安装文件
|
|
||||||
</li>
|
|
||||||
<li>在安装过程中保持默认设置,直接点击 "Next" 完成安装</li>
|
|
||||||
</ol>
|
|
||||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
|
||||||
<h6 class="mb-2 text-sm font-medium text-green-800 sm:text-base">安装完成后</h6>
|
|
||||||
<ul class="space-y-1 text-xs text-green-700 sm:text-sm">
|
|
||||||
<li>• 在任意文件夹右键可以看到 "Git Bash Here" 选项</li>
|
|
||||||
<li>• 也可以从开始菜单启动 "Git Bash"</li>
|
|
||||||
<li>• 只需要在 Git Bash 中运行 npm install 命令</li>
|
|
||||||
<li>• 后续的环境变量设置和使用都在 PowerShell/CMD 中</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 验证安装 -->
|
|
||||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
|
||||||
<h6 class="mb-2 text-sm font-medium text-green-800 sm:text-base">验证 Git Bash 安装</h6>
|
|
||||||
<p class="mb-2 text-xs text-green-700 sm:mb-3 sm:text-sm">
|
|
||||||
打开 Git Bash,输入以下命令验证:
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
|
||||||
>
|
|
||||||
<div class="whitespace-nowrap text-gray-300">git --version</div>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-xs text-green-700 sm:text-sm">如果显示 Git 版本号,说明安装成功!</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 第三步:安装 Claude Code -->
|
|
||||||
<div class="mb-4 sm:mb-10 sm:mb-6">
|
|
||||||
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl">
|
|
||||||
<span
|
|
||||||
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-purple-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
|
|
||||||
>3</span
|
|
||||||
>
|
|
||||||
安装 Claude Code
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="mb-4 rounded-xl border border-purple-100 bg-gradient-to-r from-purple-50 to-pink-50 p-4 sm:mb-6 sm:p-6"
|
|
||||||
>
|
|
||||||
<h5
|
|
||||||
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg"
|
|
||||||
>
|
|
||||||
<i class="fas fa-download mr-2 text-purple-600" />
|
|
||||||
安装 Claude Code
|
安装 Claude Code
|
||||||
</h5>
|
</h5>
|
||||||
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base">
|
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base">
|
||||||
打开 Git Bash(重要:不要使用 PowerShell),运行以下命令:
|
打开 PowerShell 或 CMD,运行以下命令:
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
class="mb-4 overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-green-400 sm:p-4 sm:text-sm"
|
class="mb-4 overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-green-400 sm:p-4 sm:text-sm"
|
||||||
>
|
>
|
||||||
<div class="mb-2"># 在 Git Bash 中全局安装 Claude Code</div>
|
<div class="mb-2"># 全局安装 Claude Code</div>
|
||||||
<div class="whitespace-nowrap text-gray-300">
|
<div class="whitespace-nowrap text-gray-300">
|
||||||
npm install -g @anthropic-ai/claude-code
|
npm install -g @anthropic-ai/claude-code
|
||||||
</div>
|
</div>
|
||||||
@@ -214,11 +148,11 @@
|
|||||||
这个命令会从 npm 官方仓库下载并安装最新版本的 Claude Code。
|
这个命令会从 npm 官方仓库下载并安装最新版本的 Claude Code。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-4 rounded-lg border border-yellow-200 bg-yellow-50 p-3 sm:p-4">
|
<div class="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3 sm:p-4">
|
||||||
<h6 class="mb-2 text-sm font-medium text-yellow-800 sm:text-base">重要提醒</h6>
|
<h6 class="mb-2 text-sm font-medium text-blue-800 sm:text-base">提示</h6>
|
||||||
<ul class="space-y-1 text-xs text-yellow-700 sm:text-sm">
|
<ul class="space-y-1 text-xs text-blue-700 sm:text-sm">
|
||||||
<li>• 必须在 Git Bash 中运行,不要在 PowerShell 中运行</li>
|
<li>• 建议使用 PowerShell 而不是 CMD,功能更强大</li>
|
||||||
<li>• 如果遇到权限问题,可以尝试在 Git Bash 中使用 sudo 命令</li>
|
<li>• 如果遇到权限问题,以管理员身份运行 PowerShell</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,23 +172,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 第四步:设置环境变量 -->
|
<!-- 第三步:设置环境变量 -->
|
||||||
<div class="mb-6 sm:mb-10">
|
<div class="mb-6 sm:mb-10">
|
||||||
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl">
|
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl">
|
||||||
<span
|
<span
|
||||||
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-orange-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
|
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-purple-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
|
||||||
>4</span
|
>3</span
|
||||||
>
|
>
|
||||||
设置环境变量
|
设置环境变量
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="mb-4 rounded-xl border border-orange-100 bg-gradient-to-r from-orange-50 to-yellow-50 p-4 sm:mb-6 sm:p-6"
|
class="mb-4 rounded-xl border border-purple-100 bg-gradient-to-r from-purple-50 to-pink-50 p-4 sm:mb-6 sm:p-6"
|
||||||
>
|
>
|
||||||
<h5
|
<h5
|
||||||
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg"
|
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg"
|
||||||
>
|
>
|
||||||
<i class="fas fa-cog mr-2 text-orange-600" />
|
<i class="fas fa-cog mr-2 text-purple-600" />
|
||||||
配置 Claude Code 环境变量
|
配置 Claude Code 环境变量
|
||||||
</h5>
|
</h5>
|
||||||
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base">
|
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base">
|
||||||
@@ -262,9 +196,9 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="rounded-lg border border-orange-200 bg-white p-3 sm:p-4">
|
<div class="rounded-lg border border-purple-200 bg-white p-3 sm:p-4">
|
||||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">
|
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">
|
||||||
方法一:PowerShell 临时设置(推荐)
|
方法一:PowerShell 临时设置(当前会话)
|
||||||
</h6>
|
</h6>
|
||||||
<p class="mb-3 text-sm text-gray-600">在 PowerShell 中运行以下命令:</p>
|
<p class="mb-3 text-sm text-gray-600">在 PowerShell 中运行以下命令:</p>
|
||||||
<div
|
<div
|
||||||
@@ -282,26 +216,44 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border border-orange-200 bg-white p-3 sm:p-4">
|
<div class="rounded-lg border border-purple-200 bg-white p-3 sm:p-4">
|
||||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">
|
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">
|
||||||
方法二:系统环境变量(永久设置)
|
方法二:PowerShell 永久设置(用户级)
|
||||||
</h6>
|
</h6>
|
||||||
<ol class="list-inside list-decimal space-y-1 text-xs text-gray-600 sm:text-sm">
|
<p class="mb-3 text-sm text-gray-600">
|
||||||
<li>右键"此电脑" → "属性" → "高级系统设置"</li>
|
在 PowerShell 中运行以下命令设置用户级环境变量:
|
||||||
<li>点击"环境变量"按钮</li>
|
</p>
|
||||||
<li>在"用户变量"或"系统变量"中点击"新建"</li>
|
<div
|
||||||
<li>添加以下两个变量:</li>
|
class="mb-3 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||||
</ol>
|
>
|
||||||
<div class="mt-3 space-y-2">
|
<div class="mb-2"># 设置用户级环境变量(永久生效)</div>
|
||||||
<div class="rounded bg-gray-100 p-2 text-sm">
|
<div class="whitespace-nowrap text-gray-300">
|
||||||
<strong>变量名:</strong> ANTHROPIC_BASE_URL<br />
|
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_BASE_URL", "{{
|
||||||
<strong>变量值:</strong> <span class="font-mono">{{ currentBaseUrl }}</span>
|
currentBaseUrl
|
||||||
|
}}", [System.EnvironmentVariableTarget]::User)
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded bg-gray-100 p-2 text-sm">
|
<div class="whitespace-nowrap text-gray-300">
|
||||||
<strong>变量名:</strong> ANTHROPIC_AUTH_TOKEN<br />
|
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_AUTH_TOKEN",
|
||||||
<strong>变量值:</strong> <span class="font-mono">你的API密钥</span>
|
"你的API密钥", [System.EnvironmentVariableTarget]::User)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mb-3 text-sm text-gray-600">查看已设置的环境变量:</p>
|
||||||
|
<div
|
||||||
|
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||||
|
>
|
||||||
|
<div class="mb-2"># 查看用户级环境变量</div>
|
||||||
|
<div class="whitespace-nowrap text-gray-300">
|
||||||
|
[System.Environment]::GetEnvironmentVariable("ANTHROPIC_BASE_URL",
|
||||||
|
[System.EnvironmentVariableTarget]::User)
|
||||||
|
</div>
|
||||||
|
<div class="whitespace-nowrap text-gray-300">
|
||||||
|
[System.Environment]::GetEnvironmentVariable("ANTHROPIC_AUTH_TOKEN",
|
||||||
|
[System.EnvironmentVariableTarget]::User)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-blue-700">
|
||||||
|
💡 设置后需要重新打开 PowerShell 窗口才能生效。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -389,23 +341,30 @@
|
|||||||
|
|
||||||
<div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4">
|
<div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4">
|
||||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">
|
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">
|
||||||
系统环境变量(永久设置)
|
PowerShell 永久设置(用户级)
|
||||||
</h6>
|
</h6>
|
||||||
<p class="mb-3 text-sm text-gray-600">在系统环境变量中添加:</p>
|
<p class="mb-3 text-sm text-gray-600">在 PowerShell 中运行以下命令:</p>
|
||||||
<div class="space-y-2">
|
<div
|
||||||
<div class="rounded bg-gray-100 p-2 text-sm">
|
class="mb-3 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||||
<strong>变量名:</strong> CODE_ASSIST_ENDPOINT<br />
|
>
|
||||||
<strong>变量值:</strong> <span class="font-mono">{{ geminiBaseUrl }}</span>
|
<div class="mb-2"># 设置用户级环境变量(永久生效)</div>
|
||||||
|
<div class="whitespace-nowrap text-gray-300">
|
||||||
|
[System.Environment]::SetEnvironmentVariable("CODE_ASSIST_ENDPOINT", "{{
|
||||||
|
geminiBaseUrl
|
||||||
|
}}", [System.EnvironmentVariableTarget]::User)
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded bg-gray-100 p-2 text-sm">
|
<div class="whitespace-nowrap text-gray-300">
|
||||||
<strong>变量名:</strong> GOOGLE_CLOUD_ACCESS_TOKEN<br />
|
[System.Environment]::SetEnvironmentVariable("GOOGLE_CLOUD_ACCESS_TOKEN",
|
||||||
<strong>变量值:</strong> <span class="font-mono">你的API密钥</span>
|
"你的API密钥", [System.EnvironmentVariableTarget]::User)
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded bg-gray-100 p-2 text-sm">
|
<div class="whitespace-nowrap text-gray-300">
|
||||||
<strong>变量名:</strong> GOOGLE_GENAI_USE_GCA<br />
|
[System.Environment]::SetEnvironmentVariable("GOOGLE_GENAI_USE_GCA", "true",
|
||||||
<strong>变量值:</strong> <span class="font-mono">true</span>
|
[System.EnvironmentVariableTarget]::User)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-blue-700">
|
||||||
|
💡 设置后需要重新打开 PowerShell 窗口才能生效。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||||
@@ -425,17 +384,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 第五步:开始使用 -->
|
<!-- 第四步:开始使用 -->
|
||||||
<div class="mb-6 sm:mb-8">
|
<div class="mb-6 sm:mb-8">
|
||||||
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl">
|
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl">
|
||||||
<span
|
<span
|
||||||
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-yellow-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
|
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-orange-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
|
||||||
>5</span
|
>4</span
|
||||||
>
|
>
|
||||||
开始使用 Claude Code
|
开始使用 Claude Code
|
||||||
</h4>
|
</h4>
|
||||||
<div
|
<div
|
||||||
class="rounded-xl border border-yellow-100 bg-gradient-to-r from-yellow-50 to-amber-50 p-4 sm:p-6"
|
class="rounded-xl border border-orange-100 bg-gradient-to-r from-orange-50 to-yellow-50 p-4 sm:p-6"
|
||||||
>
|
>
|
||||||
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base">
|
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base">
|
||||||
现在你可以开始使用 Claude Code 了!
|
现在你可以开始使用 Claude Code 了!
|
||||||
|
|||||||
Reference in New Issue
Block a user