mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:38:14 +00:00
feat: 支持Dark Mode
This commit is contained in:
58
CLAUDE.md
58
CLAUDE.md
@@ -11,12 +11,14 @@ Claude Relay Service 是一个功能完整的 AI API 中转服务,支持 Claud
|
||||
## 核心架构
|
||||
|
||||
### 关键架构概念
|
||||
|
||||
- **代理认证流**: 客户端用自建API Key → 验证 → 获取Claude账户OAuth token → 转发到Anthropic
|
||||
- **Token管理**: 自动监控OAuth token过期并刷新,支持10秒提前刷新策略
|
||||
- **代理支持**: 每个Claude账户支持独立代理配置,OAuth token交换也通过代理进行
|
||||
- **数据加密**: 敏感数据(refreshToken, accessToken)使用AES加密存储在Redis
|
||||
|
||||
### 主要服务组件
|
||||
|
||||
- **claudeRelayService.js**: 核心代理服务,处理请求转发和流式响应
|
||||
- **claudeAccountService.js**: Claude账户管理,OAuth token刷新和账户选择
|
||||
- **geminiAccountService.js**: Gemini账户管理,Google OAuth token刷新和账户选择
|
||||
@@ -24,7 +26,8 @@ Claude Relay Service 是一个功能完整的 AI API 中转服务,支持 Claud
|
||||
- **oauthHelper.js**: OAuth工具,PKCE流程实现和代理支持
|
||||
|
||||
### 认证和代理流程
|
||||
1. 客户端使用自建API Key(cr_前缀格式)发送请求
|
||||
|
||||
1. 客户端使用自建API Key(cr\_前缀格式)发送请求
|
||||
2. authenticateApiKey中间件验证API Key有效性和速率限制
|
||||
3. claudeAccountService自动选择可用Claude账户
|
||||
4. 检查OAuth access token有效性,过期则自动刷新(使用代理)
|
||||
@@ -33,6 +36,7 @@ Claude Relay Service 是一个功能完整的 AI API 中转服务,支持 Claud
|
||||
7. 流式或非流式返回响应,记录使用统计
|
||||
|
||||
### OAuth集成
|
||||
|
||||
- **PKCE流程**: 完整的OAuth 2.0 PKCE实现,支持代理
|
||||
- **自动刷新**: 智能token过期检测和自动刷新机制
|
||||
- **代理支持**: OAuth授权和token交换全程支持代理配置
|
||||
@@ -41,7 +45,8 @@ Claude Relay Service 是一个功能完整的 AI API 中转服务,支持 Claud
|
||||
## 常用命令
|
||||
|
||||
### 基本开发命令
|
||||
```bash
|
||||
|
||||
````bash
|
||||
# 安装依赖和初始化
|
||||
npm install
|
||||
npm run setup # 生成配置和管理员凭据
|
||||
@@ -76,37 +81,43 @@ npm run service:stop # 停止服务
|
||||
cp config/config.example.js config/config.js
|
||||
cp .env.example .env
|
||||
npm run setup # 自动生成密钥并创建管理员账户
|
||||
```
|
||||
````
|
||||
|
||||
## Web界面功能
|
||||
|
||||
### OAuth账户添加流程
|
||||
|
||||
1. **基本信息和代理设置**: 配置账户名称、描述和代理参数
|
||||
2. **OAuth授权**:
|
||||
2. **OAuth授权**:
|
||||
- 生成授权URL → 用户打开链接并登录Claude Code账号
|
||||
- 授权后会显示Authorization Code → 复制并粘贴到输入框
|
||||
- 系统自动交换token并创建账户
|
||||
|
||||
### 核心管理功能
|
||||
|
||||
- **实时仪表板**: 系统统计、账户状态、使用量监控
|
||||
- **API Key管理**: 创建、配额设置、使用统计查看
|
||||
- **Claude账户管理**: OAuth账户添加、代理配置、状态监控
|
||||
- **系统日志**: 实时日志查看,多级别过滤
|
||||
- **主题系统**: 支持明亮/暗黑模式切换,自动保存用户偏好设置
|
||||
|
||||
## 重要端点
|
||||
|
||||
### API转发端点
|
||||
|
||||
- `POST /api/v1/messages` - 主要消息处理端点(支持流式)
|
||||
- `GET /api/v1/models` - 模型列表(兼容性)
|
||||
- `GET /api/v1/usage` - 使用统计查询
|
||||
- `GET /api/v1/key-info` - API Key信息
|
||||
|
||||
### OAuth管理端点
|
||||
|
||||
- `POST /admin/claude-accounts/generate-auth-url` - 生成OAuth授权URL(含代理)
|
||||
- `POST /admin/claude-accounts/exchange-code` - 交换authorization code
|
||||
- `POST /admin/claude-accounts` - 创建OAuth账户
|
||||
|
||||
### 系统端点
|
||||
|
||||
- `GET /health` - 健康检查
|
||||
- `GET /web` - Web管理界面
|
||||
- `GET /admin/dashboard` - 系统概览数据
|
||||
@@ -114,22 +125,26 @@ npm run setup # 自动生成密钥并创建管理员账户
|
||||
## 故障排除
|
||||
|
||||
### OAuth相关问题
|
||||
|
||||
1. **代理配置错误**: 检查代理设置是否正确,OAuth token交换也需要代理
|
||||
2. **授权码无效**: 确保复制了完整的Authorization Code,没有遗漏字符
|
||||
3. **Token刷新失败**: 检查refreshToken有效性和代理配置
|
||||
|
||||
### Gemini Token刷新问题
|
||||
|
||||
1. **刷新失败**: 确保 refresh_token 有效且未过期
|
||||
2. **错误日志**: 查看 `logs/token-refresh-error.log` 获取详细错误信息
|
||||
3. **测试脚本**: 运行 `node scripts/test-gemini-refresh.js` 测试 token 刷新
|
||||
|
||||
### 常见开发问题
|
||||
|
||||
1. **Redis连接失败**: 确认Redis服务运行,检查连接配置
|
||||
2. **管理员登录失败**: 检查init.json同步到Redis,运行npm run setup
|
||||
3. **API Key格式错误**: 确保使用cr_前缀格式
|
||||
3. **API Key格式错误**: 确保使用cr\_前缀格式
|
||||
4. **代理连接问题**: 验证SOCKS5/HTTP代理配置和认证信息
|
||||
|
||||
### 调试工具
|
||||
|
||||
- **日志系统**: Winston结构化日志,支持不同级别
|
||||
- **CLI工具**: 命令行状态查看和管理
|
||||
- **Web界面**: 实时日志查看和系统监控
|
||||
@@ -138,19 +153,35 @@ npm run setup # 自动生成密钥并创建管理员账户
|
||||
## 开发最佳实践
|
||||
|
||||
### 代码格式化要求
|
||||
|
||||
- **必须使用 Prettier 格式化所有代码**
|
||||
- 后端代码(src/):运行 `npx prettier --write <file>` 格式化
|
||||
- 前端代码(web/admin-spa/):已安装 `prettier-plugin-tailwindcss`,运行 `npx prettier --write <file>` 格式化
|
||||
- 提交前检查格式:`npx prettier --check <file>`
|
||||
- 格式化所有文件:`npm run format`(如果配置了此脚本)
|
||||
|
||||
### 前端开发特殊要求
|
||||
|
||||
- **响应式设计**: 必须兼容不同设备尺寸(手机、平板、桌面),使用 Tailwind CSS 响应式前缀(sm:、md:、lg:、xl:)
|
||||
- **暗黑模式兼容**: 项目已集成完整的暗黑模式支持,所有新增/修改的UI组件都必须同时兼容明亮模式和暗黑模式
|
||||
- 使用 Tailwind CSS 的 `dark:` 前缀为暗黑模式提供样式
|
||||
- 文本颜色:`text-gray-700 dark:text-gray-200`
|
||||
- 背景颜色:`bg-white dark:bg-gray-800`
|
||||
- 边框颜色:`border-gray-200 dark:border-gray-700`
|
||||
- 状态颜色保持一致:`text-blue-500`、`text-green-600`、`text-red-500` 等
|
||||
- **主题切换**: 使用 `stores/theme.js` 中的 `useThemeStore()` 来实现主题切换功能
|
||||
- **玻璃态效果**: 保持现有的玻璃态设计风格,在暗黑模式下调整透明度和背景色
|
||||
- **图标和交互**: 确保所有图标、按钮、交互元素在两种模式下都清晰可见且易于操作
|
||||
|
||||
### 代码修改原则
|
||||
|
||||
- 对现有文件进行修改时,首先检查代码库的现有模式和风格
|
||||
- 尽可能重用现有的服务和工具函数,避免重复代码
|
||||
- 遵循项目现有的错误处理和日志记录模式
|
||||
- 敏感数据必须使用加密存储(参考 claudeAccountService.js 中的加密实现)
|
||||
|
||||
### 测试和质量保证
|
||||
|
||||
- 运行 `npm run lint` 进行代码风格检查(使用 ESLint)
|
||||
- 运行 `npm test` 执行测试套件(Jest + SuperTest 配置)
|
||||
- 在修改核心服务后,使用 CLI 工具验证功能:`npm run cli status`
|
||||
@@ -158,20 +189,26 @@ npm run setup # 自动生成密钥并创建管理员账户
|
||||
- 注意:当前项目缺少实际测试文件,建议补充单元测试和集成测试
|
||||
|
||||
### 开发工作流
|
||||
|
||||
- **功能开发**: 始终从理解现有代码开始,重用已有的服务和模式
|
||||
- **调试流程**: 使用 Winston 日志 + Web 界面实时日志查看 + CLI 状态工具
|
||||
- **代码审查**: 关注安全性(加密存储)、性能(异步处理)、错误处理
|
||||
- **部署前检查**: 运行 lint → 测试 CLI 功能 → 检查日志 → Docker 构建
|
||||
|
||||
### 常见文件位置
|
||||
|
||||
- 核心服务逻辑:`src/services/` 目录
|
||||
- 路由处理:`src/routes/` 目录
|
||||
- 路由处理:`src/routes/` 目录
|
||||
- 中间件:`src/middleware/` 目录
|
||||
- 配置管理:`config/config.js`
|
||||
- Redis 模型:`src/models/redis.js`
|
||||
- 工具函数:`src/utils/` 目录
|
||||
- 前端主题管理:`web/admin-spa/src/stores/theme.js`
|
||||
- 前端组件:`web/admin-spa/src/components/` 目录
|
||||
- 前端页面:`web/admin-spa/src/views/` 目录
|
||||
|
||||
### 重要架构决策
|
||||
|
||||
- 所有敏感数据(OAuth token、refreshToken)都使用 AES 加密存储在 Redis
|
||||
- 每个 Claude 账户支持独立的代理配置,包括 SOCKS5 和 HTTP 代理
|
||||
- API Key 使用哈希存储,支持 `cr_` 前缀格式
|
||||
@@ -179,6 +216,7 @@ npm run setup # 自动生成密钥并创建管理员账户
|
||||
- 支持流式和非流式响应,客户端断开时自动清理资源
|
||||
|
||||
### 核心数据流和性能优化
|
||||
|
||||
- **哈希映射优化**: API Key 验证从 O(n) 优化到 O(1) 查找
|
||||
- **智能 Usage 捕获**: 从 SSE 流中解析真实的 token 使用数据
|
||||
- **多维度统计**: 支持按时间、模型、用户的实时使用统计
|
||||
@@ -186,6 +224,7 @@ npm run setup # 自动生成密钥并创建管理员账户
|
||||
- **原子操作**: Redis 管道操作确保数据一致性
|
||||
|
||||
### 安全和容错机制
|
||||
|
||||
- **多层加密**: API Key 哈希 + OAuth Token AES 加密
|
||||
- **零信任验证**: 每个请求都需要完整的认证链
|
||||
- **优雅降级**: Redis 连接失败时的回退机制
|
||||
@@ -195,6 +234,7 @@ npm run setup # 自动生成密钥并创建管理员账户
|
||||
## 项目特定注意事项
|
||||
|
||||
### Redis 数据结构
|
||||
|
||||
- **API Keys**: `api_key:{id}` (详细信息) + `api_key_hash:{hash}` (快速查找)
|
||||
- **Claude 账户**: `claude_account:{id}` (加密的 OAuth 数据)
|
||||
- **管理员**: `admin:{id}` + `admin_username:{username}` (用户名映射)
|
||||
@@ -203,12 +243,14 @@ npm run setup # 自动生成密钥并创建管理员账户
|
||||
- **系统信息**: `system_info` (系统状态缓存)
|
||||
|
||||
### 流式响应处理
|
||||
|
||||
- 支持 SSE (Server-Sent Events) 流式传输
|
||||
- 自动从流中解析 usage 数据并记录
|
||||
- 客户端断开时通过 AbortController 清理资源
|
||||
- 错误时发送适当的 SSE 错误事件
|
||||
|
||||
### CLI 工具使用示例
|
||||
|
||||
```bash
|
||||
# 创建新的 API Key
|
||||
npm run cli keys create -- --name "MyApp" --limit 1000
|
||||
@@ -224,8 +266,10 @@ npm run cli accounts refresh <accountId>
|
||||
npm run cli admin create -- --username admin2
|
||||
npm run cli admin reset-password -- --username admin
|
||||
```
|
||||
|
||||
# important-instruction-reminders
|
||||
|
||||
Do what has been asked; nothing more, nothing less.
|
||||
NEVER create files unless they're absolutely necessary for achieving your goal.
|
||||
ALWAYS prefer editing an existing file to creating a new one.
|
||||
NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
|
||||
NEVER proactively create documentation files (\*.md) or README files. Only create documentation files if explicitly requested by the User.
|
||||
|
||||
2
web/admin-spa/package-lock.json
generated
2
web/admin-spa/package-lock.json
generated
@@ -3655,7 +3655,7 @@
|
||||
},
|
||||
"node_modules/prettier-plugin-tailwindcss": {
|
||||
"version": "0.6.14",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz",
|
||||
"resolved": "https://registry.npmmirror.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz",
|
||||
"integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
|
||||
@@ -11,14 +11,22 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import ToastNotification from '@/components/common/ToastNotification.vue'
|
||||
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const themeStore = useThemeStore()
|
||||
const toastRef = ref()
|
||||
const confirmRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化主题
|
||||
themeStore.initTheme()
|
||||
|
||||
// 监听系统主题变化
|
||||
themeStore.watchSystemTheme()
|
||||
|
||||
// 检查本地存储的认证状态
|
||||
authStore.checkAuth()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* 从原始 style.css 复制的全局样式 */
|
||||
:root {
|
||||
/* 亮色模式 */
|
||||
--primary-color: #667eea;
|
||||
--secondary-color: #764ba2;
|
||||
--accent-color: #f093fb;
|
||||
@@ -8,31 +9,62 @@
|
||||
--error-color: #ef4444;
|
||||
--surface-color: rgba(255, 255, 255, 0.95);
|
||||
--glass-color: rgba(255, 255, 255, 0.1);
|
||||
--glass-strong-color: rgba(255, 255, 255, 0.95);
|
||||
--text-primary: #1f2937;
|
||||
--text-secondary: #6b7280;
|
||||
--border-color: rgba(255, 255, 255, 0.2);
|
||||
--bg-gradient-start: #667eea;
|
||||
--bg-gradient-mid: #764ba2;
|
||||
--bg-gradient-end: #f093fb;
|
||||
--input-bg: rgba(255, 255, 255, 0.9);
|
||||
--input-border: rgba(255, 255, 255, 0.3);
|
||||
--modal-bg: rgba(0, 0, 0, 0.4);
|
||||
--table-bg: rgba(255, 255, 255, 0.95);
|
||||
--table-hover: rgba(102, 126, 234, 0.05);
|
||||
}
|
||||
|
||||
/* 通用transition - 仅应用于特定元素 */
|
||||
body,
|
||||
div,
|
||||
.dark {
|
||||
/* 暗黑模式 */
|
||||
--primary-color: #818cf8;
|
||||
--secondary-color: #a78bfa;
|
||||
--accent-color: #c084fc;
|
||||
--success-color: #10b981;
|
||||
--warning-color: #f59e0b;
|
||||
--error-color: #ef4444;
|
||||
--surface-color: rgba(31, 41, 55, 0.95);
|
||||
--glass-color: rgba(0, 0, 0, 0.2);
|
||||
--glass-strong-color: rgba(31, 41, 55, 0.95);
|
||||
--text-primary: #f3f4f6;
|
||||
--text-secondary: #9ca3af;
|
||||
--border-color: rgba(75, 85, 99, 0.3);
|
||||
--bg-gradient-start: #1f2937;
|
||||
--bg-gradient-mid: #374151;
|
||||
--bg-gradient-end: #4b5563;
|
||||
--input-bg: rgba(31, 41, 55, 0.9);
|
||||
--input-border: rgba(75, 85, 99, 0.5);
|
||||
--modal-bg: rgba(0, 0, 0, 0.6);
|
||||
--table-bg: rgba(31, 41, 55, 0.95);
|
||||
--table-hover: rgba(129, 140, 248, 0.1);
|
||||
}
|
||||
|
||||
/* 优化后的transition - 避免布局跳动 */
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
table,
|
||||
tr,
|
||||
td,
|
||||
th,
|
||||
span,
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
textarea {
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
border-color 0.3s ease,
|
||||
box-shadow 0.3s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* 颜色和背景过渡 */
|
||||
.transition-colors {
|
||||
transition:
|
||||
color 0.3s ease,
|
||||
background-color 0.3s ease,
|
||||
border-color 0.3s ease;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -45,14 +77,18 @@ body {
|
||||
sans-serif;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-color) 0%,
|
||||
var(--secondary-color) 50%,
|
||||
var(--accent-color) 100%
|
||||
var(--bg-gradient-start) 0%,
|
||||
var(--bg-gradient-mid) 50%,
|
||||
var(--bg-gradient-end) 100%
|
||||
);
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
color: var(--text-primary);
|
||||
transition:
|
||||
background 0.3s ease,
|
||||
color 0.3s ease;
|
||||
}
|
||||
|
||||
body::before {
|
||||
@@ -73,21 +109,43 @@ body::before {
|
||||
.glass {
|
||||
background: var(--glass-color);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.04),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .glass {
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.25),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.glass-strong {
|
||||
background: var(--surface-color);
|
||||
background: var(--glass-strong-color);
|
||||
backdrop-filter: blur(25px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
-webkit-backdrop-filter: blur(25px);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .glass-strong {
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.02),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
@@ -201,13 +259,18 @@ body::before {
|
||||
|
||||
/* 表单输入框样式 */
|
||||
.form-input {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
background: var(--input-bg);
|
||||
border: 2px solid var(--input-border);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
color: var(--text-primary);
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
border-color 0.3s ease,
|
||||
box-shadow 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
@@ -217,17 +280,35 @@ body::before {
|
||||
0 0 0 3px rgba(102, 126, 234, 0.1),
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.dark .form-input:focus {
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(129, 140, 248, 0.2),
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.2);
|
||||
background: rgba(17, 24, 39, 0.95);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface-color);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .card {
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.25),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card::before {
|
||||
@@ -241,13 +322,19 @@ body::before {
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.8) 100%);
|
||||
background: linear-gradient(135deg, var(--surface-color) 0%, var(--glass-strong-color) 100%);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
transition:
|
||||
transform 0.3s ease,
|
||||
box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .stat-card {
|
||||
background: linear-gradient(135deg, rgba(31, 41, 55, 0.95) 0%, rgba(17, 24, 39, 0.8) 100%);
|
||||
}
|
||||
|
||||
.stat-card::before {
|
||||
@@ -364,13 +451,18 @@ body::before {
|
||||
}
|
||||
|
||||
.form-input {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
background: var(--input-bg);
|
||||
border: 2px solid var(--input-border);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
color: var(--text-primary);
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
border-color 0.3s ease,
|
||||
box-shadow 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
@@ -380,39 +472,71 @@ body::before {
|
||||
0 0 0 3px rgba(102, 126, 234, 0.1),
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.dark .form-input:focus {
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(129, 140, 248, 0.2),
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.2);
|
||||
background: rgba(17, 24, 39, 0.95);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
background: var(--table-bg);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .table-container {
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.25),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
transition: all 0.2s ease;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
background: var(--table-hover);
|
||||
transform: scale(1.005);
|
||||
}
|
||||
|
||||
.modal {
|
||||
backdrop-filter: blur(8px);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
background: var(--modal-bg);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
background: white;
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid rgba(229, 231, 235, 0.8);
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .modal-content {
|
||||
background: #1f2937;
|
||||
border: 1px solid rgba(75, 85, 99, 0.5);
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.6),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
@@ -517,6 +641,10 @@ body::before {
|
||||
scrollbar-color: rgba(102, 126, 234, 0.3) rgba(102, 126, 234, 0.05);
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar {
|
||||
scrollbar-color: rgba(129, 140, 248, 0.3) rgba(129, 140, 248, 0.05);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@@ -527,20 +655,36 @@ body::before {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(129, 140, 248, 0.05);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.4) 0%, rgba(118, 75, 162, 0.4) 100%);
|
||||
border-radius: 10px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(135deg, rgba(129, 140, 248, 0.4) 0%, rgba(167, 139, 250, 0.4) 100%);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.6) 0%, rgba(118, 75, 162, 0.6) 100%);
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(135deg, rgba(129, 140, 248, 0.6) 0%, rgba(167, 139, 250, 0.6) 100%);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:active {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.8) 0%, rgba(118, 75, 162, 0.8) 100%);
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb:active {
|
||||
background: linear-gradient(135deg, rgba(129, 140, 248, 0.8) 0%, rgba(167, 139, 250, 0.8) 100%);
|
||||
}
|
||||
|
||||
/* 弹窗滚动内容样式 */
|
||||
.modal-scroll-content {
|
||||
max-height: calc(90vh - 160px);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,9 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Claude OAuth流程 -->
|
||||
<div v-if="platform === 'claude'">
|
||||
<div class="rounded-lg border border-blue-200 bg-blue-50 p-6">
|
||||
<div
|
||||
class="rounded-lg border border-blue-200 bg-blue-50 p-6 dark:border-blue-700 dark:bg-blue-900/30"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-blue-500"
|
||||
@@ -10,12 +12,16 @@
|
||||
<i class="fas fa-link text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="mb-3 font-semibold text-blue-900">Claude 账户授权</h4>
|
||||
<p class="mb-4 text-sm text-blue-800">请按照以下步骤完成 Claude 账户的授权:</p>
|
||||
<h4 class="mb-3 font-semibold text-blue-900 dark:text-blue-200">Claude 账户授权</h4>
|
||||
<p class="mb-4 text-sm text-blue-800 dark:text-blue-300">
|
||||
请按照以下步骤完成 Claude 账户的授权:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- 步骤1: 生成授权链接 -->
|
||||
<div class="rounded-lg border border-blue-300 bg-white/80 p-4">
|
||||
<div
|
||||
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-blue-600 text-xs font-bold text-white"
|
||||
@@ -23,7 +29,9 @@
|
||||
1
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="mb-2 font-medium text-blue-900">点击下方按钮生成授权链接</p>
|
||||
<p class="mb-2 font-medium text-blue-900 dark:text-blue-200">
|
||||
点击下方按钮生成授权链接
|
||||
</p>
|
||||
<button
|
||||
v-if="!authUrl"
|
||||
class="btn btn-primary px-4 py-2 text-sm"
|
||||
@@ -37,13 +45,13 @@
|
||||
<div v-else class="space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
class="form-input flex-1 bg-gray-50 font-mono text-xs"
|
||||
class="form-input flex-1 bg-gray-50 font-mono text-xs dark:bg-gray-700"
|
||||
readonly
|
||||
type="text"
|
||||
:value="authUrl"
|
||||
/>
|
||||
<button
|
||||
class="rounded-lg bg-gray-100 px-3 py-2 transition-colors hover:bg-gray-200"
|
||||
class="rounded-lg bg-gray-100 px-3 py-2 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
title="复制链接"
|
||||
@click="copyAuthUrl"
|
||||
>
|
||||
@@ -62,7 +70,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 步骤2: 访问链接并授权 -->
|
||||
<div class="rounded-lg border border-blue-300 bg-white/80 p-4">
|
||||
<div
|
||||
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-blue-600 text-xs font-bold text-white"
|
||||
@@ -70,12 +80,16 @@
|
||||
2
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="mb-2 font-medium text-blue-900">在浏览器中打开链接并完成授权</p>
|
||||
<p class="mb-2 text-sm text-blue-700">
|
||||
<p class="mb-2 font-medium text-blue-900 dark:text-blue-200">
|
||||
在浏览器中打开链接并完成授权
|
||||
</p>
|
||||
<p class="mb-2 text-sm text-blue-700 dark:text-blue-300">
|
||||
请在新标签页中打开授权链接,登录您的 Claude 账户并授权。
|
||||
</p>
|
||||
<div class="rounded border border-yellow-300 bg-yellow-50 p-3">
|
||||
<p class="text-xs text-yellow-800">
|
||||
<div
|
||||
class="rounded border border-yellow-300 bg-yellow-50 p-3 dark:border-yellow-700 dark:bg-yellow-900/30"
|
||||
>
|
||||
<p class="text-xs text-yellow-800 dark:text-yellow-300">
|
||||
<i class="fas fa-exclamation-triangle mr-1" />
|
||||
<strong>注意:</strong
|
||||
>如果您设置了代理,请确保浏览器也使用相同的代理访问授权页面。
|
||||
@@ -86,7 +100,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 步骤3: 输入授权码 -->
|
||||
<div class="rounded-lg border border-blue-300 bg-white/80 p-4">
|
||||
<div
|
||||
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-blue-600 text-xs font-bold text-white"
|
||||
@@ -94,14 +110,18 @@
|
||||
3
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="mb-2 font-medium text-blue-900">输入 Authorization Code</p>
|
||||
<p class="mb-3 text-sm text-blue-700">
|
||||
<p class="mb-2 font-medium text-blue-900 dark:text-blue-200">
|
||||
输入 Authorization Code
|
||||
</p>
|
||||
<p class="mb-3 text-sm text-blue-700 dark:text-blue-300">
|
||||
授权完成后,页面会显示一个
|
||||
<strong>Authorization Code</strong>,请将其复制并粘贴到下方输入框:
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">
|
||||
<label
|
||||
class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-key mr-2 text-blue-500" />Authorization Code
|
||||
</label>
|
||||
<textarea
|
||||
@@ -111,7 +131,7 @@
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-info-circle mr-1" />
|
||||
请粘贴从Claude页面复制的Authorization Code
|
||||
</p>
|
||||
@@ -127,7 +147,9 @@
|
||||
|
||||
<!-- Gemini OAuth流程 -->
|
||||
<div v-else-if="platform === 'gemini'">
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-6">
|
||||
<div
|
||||
class="rounded-lg border border-green-200 bg-green-50 p-6 dark:border-green-700 dark:bg-green-900/30"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-green-500"
|
||||
@@ -135,12 +157,16 @@
|
||||
<i class="fas fa-robot text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="mb-3 font-semibold text-green-900">Gemini 账户授权</h4>
|
||||
<p class="mb-4 text-sm text-green-800">请按照以下步骤完成 Gemini 账户的授权:</p>
|
||||
<h4 class="mb-3 font-semibold text-green-900 dark:text-green-200">Gemini 账户授权</h4>
|
||||
<p class="mb-4 text-sm text-green-800 dark:text-green-300">
|
||||
请按照以下步骤完成 Gemini 账户的授权:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- 步骤1: 生成授权链接 -->
|
||||
<div class="rounded-lg border border-green-300 bg-white/80 p-4">
|
||||
<div
|
||||
class="rounded-lg border border-green-300 bg-white/80 p-4 dark:border-green-600 dark:bg-gray-800/80"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-600 text-xs font-bold text-white"
|
||||
@@ -148,7 +174,9 @@
|
||||
1
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="mb-2 font-medium text-green-900">点击下方按钮生成授权链接</p>
|
||||
<p class="mb-2 font-medium text-green-900 dark:text-green-200">
|
||||
点击下方按钮生成授权链接
|
||||
</p>
|
||||
<button
|
||||
v-if="!authUrl"
|
||||
class="btn btn-primary px-4 py-2 text-sm"
|
||||
@@ -162,13 +190,13 @@
|
||||
<div v-else class="space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
class="form-input flex-1 bg-gray-50 font-mono text-xs"
|
||||
class="form-input flex-1 bg-gray-50 font-mono text-xs dark:bg-gray-700"
|
||||
readonly
|
||||
type="text"
|
||||
:value="authUrl"
|
||||
/>
|
||||
<button
|
||||
class="rounded-lg bg-gray-100 px-3 py-2 transition-colors hover:bg-gray-200"
|
||||
class="rounded-lg bg-gray-100 px-3 py-2 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
title="复制链接"
|
||||
@click="copyAuthUrl"
|
||||
>
|
||||
@@ -187,7 +215,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 步骤2: 操作说明 -->
|
||||
<div class="rounded-lg border border-green-300 bg-white/80 p-4">
|
||||
<div
|
||||
class="rounded-lg border border-green-300 bg-white/80 p-4 dark:border-green-600 dark:bg-gray-800/80"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-600 text-xs font-bold text-white"
|
||||
@@ -195,12 +225,16 @@
|
||||
2
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="mb-2 font-medium text-blue-900">在浏览器中打开链接并完成授权</p>
|
||||
<p class="mb-2 text-sm text-blue-700">
|
||||
<p class="mb-2 font-medium text-green-900 dark:text-green-200">
|
||||
在浏览器中打开链接并完成授权
|
||||
</p>
|
||||
<p class="mb-2 text-sm text-green-700 dark:text-green-300">
|
||||
请在新标签页中打开授权链接,登录您的 Gemini 账户并授权。
|
||||
</p>
|
||||
<div class="rounded border border-yellow-300 bg-yellow-50 p-3">
|
||||
<p class="text-xs text-yellow-800">
|
||||
<div
|
||||
class="rounded border border-yellow-300 bg-yellow-50 p-3 dark:border-yellow-700 dark:bg-yellow-900/30"
|
||||
>
|
||||
<p class="text-xs text-yellow-800 dark:text-yellow-300">
|
||||
<i class="fas fa-exclamation-triangle mr-1" />
|
||||
<strong>注意:</strong
|
||||
>如果您设置了代理,请确保浏览器也使用相同的代理访问授权页面。
|
||||
@@ -211,7 +245,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 步骤3: 输入授权码 -->
|
||||
<div class="rounded-lg border border-green-300 bg-white/80 p-4">
|
||||
<div
|
||||
class="rounded-lg border border-green-300 bg-white/80 p-4 dark:border-green-600 dark:bg-gray-800/80"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-green-600 text-xs font-bold text-white"
|
||||
@@ -219,13 +255,17 @@
|
||||
3
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="mb-2 font-medium text-green-900">输入 Authorization Code</p>
|
||||
<p class="mb-3 text-sm text-green-700">
|
||||
<p class="mb-2 font-medium text-green-900 dark:text-green-200">
|
||||
输入 Authorization Code
|
||||
</p>
|
||||
<p class="mb-3 text-sm text-green-700 dark:text-green-300">
|
||||
授权完成后,页面会显示一个 Authorization Code,请将其复制并粘贴到下方输入框:
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">
|
||||
<label
|
||||
class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-key mr-2 text-green-500" />Authorization Code
|
||||
</label>
|
||||
<textarea
|
||||
@@ -236,7 +276,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 space-y-1">
|
||||
<p class="text-xs text-gray-600">
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<i class="fas fa-check-circle mr-1 text-green-500" />
|
||||
请粘贴从Gemini页面复制的Authorization Code
|
||||
</p>
|
||||
@@ -253,7 +293,9 @@
|
||||
|
||||
<!-- OpenAI OAuth流程 -->
|
||||
<div v-else-if="platform === 'openai'">
|
||||
<div class="rounded-lg border border-orange-200 bg-orange-50 p-6">
|
||||
<div
|
||||
class="rounded-lg border border-orange-200 bg-orange-50 p-6 dark:border-orange-700 dark:bg-orange-900/30"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-orange-500"
|
||||
@@ -261,12 +303,16 @@
|
||||
<i class="fas fa-brain text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="mb-3 font-semibold text-orange-900">OpenAI 账户授权</h4>
|
||||
<p class="mb-4 text-sm text-orange-800">请按照以下步骤完成 OpenAI 账户的授权:</p>
|
||||
<h4 class="mb-3 font-semibold text-orange-900 dark:text-orange-200">OpenAI 账户授权</h4>
|
||||
<p class="mb-4 text-sm text-orange-800 dark:text-orange-300">
|
||||
请按照以下步骤完成 OpenAI 账户的授权:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- 步骤1: 生成授权链接 -->
|
||||
<div class="rounded-lg border border-orange-300 bg-white/80 p-4">
|
||||
<div
|
||||
class="rounded-lg border border-orange-300 bg-white/80 p-4 dark:border-orange-600 dark:bg-gray-800/80"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-orange-600 text-xs font-bold text-white"
|
||||
@@ -274,7 +320,9 @@
|
||||
1
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="mb-2 font-medium text-orange-900">点击下方按钮生成授权链接</p>
|
||||
<p class="mb-2 font-medium text-orange-900 dark:text-orange-200">
|
||||
点击下方按钮生成授权链接
|
||||
</p>
|
||||
<button
|
||||
v-if="!authUrl"
|
||||
class="btn btn-primary px-4 py-2 text-sm"
|
||||
@@ -288,13 +336,13 @@
|
||||
<div v-else class="space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
class="form-input flex-1 bg-gray-50 font-mono text-xs"
|
||||
class="form-input flex-1 bg-gray-50 font-mono text-xs dark:bg-gray-700"
|
||||
readonly
|
||||
type="text"
|
||||
:value="authUrl"
|
||||
/>
|
||||
<button
|
||||
class="rounded-lg bg-gray-100 px-3 py-2 transition-colors hover:bg-gray-200"
|
||||
class="rounded-lg bg-gray-100 px-3 py-2 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
title="复制链接"
|
||||
@click="copyAuthUrl"
|
||||
>
|
||||
@@ -313,7 +361,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 步骤2: 访问链接并授权 -->
|
||||
<div class="rounded-lg border border-orange-300 bg-white/80 p-4">
|
||||
<div
|
||||
class="rounded-lg border border-orange-300 bg-white/80 p-4 dark:border-orange-600 dark:bg-gray-800/80"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-orange-600 text-xs font-bold text-white"
|
||||
@@ -321,23 +371,29 @@
|
||||
2
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="mb-2 font-medium text-orange-900">在浏览器中打开链接并完成授权</p>
|
||||
<p class="mb-2 text-sm text-orange-700">
|
||||
<p class="mb-2 font-medium text-orange-900 dark:text-orange-200">
|
||||
在浏览器中打开链接并完成授权
|
||||
</p>
|
||||
<p class="mb-2 text-sm text-orange-700 dark:text-orange-300">
|
||||
请在新标签页中打开授权链接,登录您的 OpenAI 账户并授权。
|
||||
</p>
|
||||
<div class="mb-3 rounded border border-amber-300 bg-amber-50 p-3">
|
||||
<p class="text-xs text-amber-800">
|
||||
<div
|
||||
class="mb-3 rounded border border-amber-300 bg-amber-50 p-3 dark:border-amber-700 dark:bg-amber-900/30"
|
||||
>
|
||||
<p class="text-xs text-amber-800 dark:text-amber-300">
|
||||
<i class="fas fa-clock mr-1" />
|
||||
<strong>重要提示:</strong>授权后页面可能会加载较长时间,请耐心等待。
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-amber-700">
|
||||
<p class="mt-2 text-xs text-amber-700 dark:text-amber-400">
|
||||
当浏览器地址栏变为
|
||||
<strong class="font-mono">http://localhost:1455/...</strong>
|
||||
开头时,表示授权已完成。
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded border border-yellow-300 bg-yellow-50 p-3">
|
||||
<p class="text-xs text-yellow-800">
|
||||
<div
|
||||
class="rounded border border-yellow-300 bg-yellow-50 p-3 dark:border-yellow-700 dark:bg-yellow-900/30"
|
||||
>
|
||||
<p class="text-xs text-yellow-800 dark:text-yellow-300">
|
||||
<i class="fas fa-exclamation-triangle mr-1" />
|
||||
<strong>注意:</strong
|
||||
>如果您设置了代理,请确保浏览器也使用相同的代理访问授权页面。
|
||||
@@ -348,7 +404,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 步骤3: 输入授权码 -->
|
||||
<div class="rounded-lg border border-orange-300 bg-white/80 p-4">
|
||||
<div
|
||||
class="rounded-lg border border-orange-300 bg-white/80 p-4 dark:border-orange-600 dark:bg-gray-800/80"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-orange-600 text-xs font-bold text-white"
|
||||
@@ -356,14 +414,18 @@
|
||||
3
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="mb-2 font-medium text-orange-900">输入授权链接或 Code</p>
|
||||
<p class="mb-3 text-sm text-orange-700">
|
||||
<p class="mb-2 font-medium text-orange-900 dark:text-orange-200">
|
||||
输入授权链接或 Code
|
||||
</p>
|
||||
<p class="mb-3 text-sm text-orange-700 dark:text-orange-300">
|
||||
授权完成后,当页面地址变为
|
||||
<strong class="font-mono">http://localhost:1455/...</strong> 时:
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">
|
||||
<label
|
||||
class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-link mr-2 text-orange-500" />授权链接或 Code
|
||||
</label>
|
||||
<textarea
|
||||
@@ -373,13 +435,15 @@
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
<div class="rounded border border-blue-300 bg-blue-50 p-2">
|
||||
<p class="text-xs text-blue-700">
|
||||
<div
|
||||
class="rounded border border-blue-300 bg-blue-50 p-2 dark:border-blue-700 dark:bg-blue-900/30"
|
||||
>
|
||||
<p class="text-xs text-blue-700 dark:text-blue-300">
|
||||
<i class="fas fa-lightbulb mr-1" />
|
||||
<strong>提示:</strong>您可以直接复制整个链接或仅复制 code
|
||||
参数值,系统会自动识别。
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-blue-600">
|
||||
<p class="mt-1 text-xs text-blue-600 dark:text-blue-400">
|
||||
• 完整链接示例:<span class="font-mono"
|
||||
>http://localhost:1455/auth/callback?code=ac_4hm8...</span
|
||||
>
|
||||
@@ -402,7 +466,7 @@
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
class="flex-1 rounded-xl bg-gray-100 px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-200"
|
||||
class="flex-1 rounded-xl bg-gray-100 px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
type="button"
|
||||
@click="$emit('back')"
|
||||
>
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-semibold text-gray-700">代理设置 (可选)</h4>
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">代理设置 (可选)</h4>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="proxy.enabled"
|
||||
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700">启用代理</span>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">启用代理</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="proxy.enabled" class="space-y-4 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<div
|
||||
v-if="proxy.enabled"
|
||||
class="space-y-4 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-600 dark:bg-gray-800"
|
||||
>
|
||||
<div class="mb-3 flex items-start gap-3">
|
||||
<div class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-gray-500">
|
||||
<i class="fas fa-server text-sm text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-gray-700">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||
配置代理以访问受限的网络资源。支持 SOCKS5 和 HTTP 代理。
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
请确保代理服务器稳定可用,否则会影响账户的正常使用。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">代理类型</label>
|
||||
<select v-model="proxy.type" class="form-input w-full">
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>代理类型</label
|
||||
>
|
||||
<select
|
||||
v-model="proxy.type"
|
||||
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
>
|
||||
<option value="socks5">SOCKS5</option>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
@@ -38,19 +46,23 @@
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">主机地址</label>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>主机地址</label
|
||||
>
|
||||
<input
|
||||
v-model="proxy.host"
|
||||
class="form-input w-full"
|
||||
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="例如: 192.168.1.100"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">端口</label>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>端口</label
|
||||
>
|
||||
<input
|
||||
v-model="proxy.port"
|
||||
class="form-input w-full"
|
||||
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="例如: 1080"
|
||||
type="number"
|
||||
/>
|
||||
@@ -65,32 +77,39 @@
|
||||
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 text-gray-700" for="proxyAuth">
|
||||
<label
|
||||
class="ml-2 cursor-pointer text-sm text-gray-700 dark:text-gray-300"
|
||||
for="proxyAuth"
|
||||
>
|
||||
需要身份验证
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="showAuth" class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">用户名</label>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>用户名</label
|
||||
>
|
||||
<input
|
||||
v-model="proxy.username"
|
||||
class="form-input w-full"
|
||||
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="代理用户名"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">密码</label>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>密码</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="proxy.password"
|
||||
class="form-input w-full pr-10"
|
||||
class="form-input w-full pr-10 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="代理密码"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
/>
|
||||
<button
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-400"
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
>
|
||||
@@ -101,8 +120,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<p class="text-xs text-blue-700">
|
||||
<div
|
||||
class="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-700 dark:bg-blue-900/30"
|
||||
>
|
||||
<p class="text-xs text-blue-700 dark:text-blue-300">
|
||||
<i class="fas fa-info-circle mr-1" />
|
||||
<strong>提示:</strong
|
||||
>代理设置将用于所有与此账户相关的API请求。请确保代理服务器支持HTTPS流量转发。
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
>
|
||||
<i class="fas fa-key text-sm text-white sm:text-base" />
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-gray-900 sm:text-xl">创建新的 API Key</h3>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
|
||||
创建新的 API Key
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
class="p-1 text-gray-400 transition-colors hover:text-gray-600"
|
||||
class="p-1 text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<i class="fas fa-times text-lg sm:text-xl" />
|
||||
@@ -25,7 +27,7 @@
|
||||
>
|
||||
<!-- 创建类型选择 -->
|
||||
<div
|
||||
class="rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-3 sm:p-4"
|
||||
class="rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-3 dark:border-blue-700 dark:from-blue-900/20 dark:to-indigo-900/20 sm:p-4"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
@@ -33,18 +35,21 @@
|
||||
form.createType === 'batch' ? 'mb-3' : ''
|
||||
]"
|
||||
>
|
||||
<label class="flex h-full items-center text-xs font-semibold text-gray-700 sm:text-sm"
|
||||
<label
|
||||
class="flex h-full items-center text-xs font-semibold text-gray-700 dark:text-gray-300 sm:text-sm"
|
||||
>创建类型</label
|
||||
>
|
||||
<div class="flex items-center gap-3 sm:gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="form.createType"
|
||||
class="mr-1.5 text-blue-600 sm:mr-2"
|
||||
class="mr-1.5 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 sm:mr-2"
|
||||
type="radio"
|
||||
value="single"
|
||||
/>
|
||||
<span class="flex items-center text-xs text-gray-700 sm:text-sm">
|
||||
<span
|
||||
class="flex items-center text-xs text-gray-700 dark:text-gray-300 sm:text-sm"
|
||||
>
|
||||
<i class="fas fa-key mr-1 text-xs" />
|
||||
单个创建
|
||||
</span>
|
||||
@@ -52,11 +57,13 @@
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="form.createType"
|
||||
class="mr-1.5 text-blue-600 sm:mr-2"
|
||||
class="mr-1.5 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 sm:mr-2"
|
||||
type="radio"
|
||||
value="batch"
|
||||
/>
|
||||
<span class="flex items-center text-xs text-gray-700 sm:text-sm">
|
||||
<span
|
||||
class="flex items-center text-xs text-gray-700 dark:text-gray-300 sm:text-sm"
|
||||
>
|
||||
<i class="fas fa-layer-group mr-1 text-xs" />
|
||||
批量创建
|
||||
</span>
|
||||
@@ -68,22 +75,26 @@
|
||||
<div v-if="form.createType === 'batch'" class="mt-3">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-1">
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600">创建数量</label>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
>创建数量</label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model.number="form.batchCount"
|
||||
class="form-input w-full text-sm"
|
||||
class="form-input w-full text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
max="500"
|
||||
min="2"
|
||||
placeholder="输入数量 (2-500)"
|
||||
required
|
||||
type="number"
|
||||
/>
|
||||
<div class="whitespace-nowrap text-xs text-gray-500">最大支持 500 个</div>
|
||||
<div class="whitespace-nowrap text-xs text-gray-500 dark:text-gray-400">
|
||||
最大支持 500 个
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 flex items-start text-xs text-amber-600">
|
||||
<p class="mt-2 flex items-start text-xs text-amber-600 dark:text-amber-400">
|
||||
<i class="fas fa-info-circle mr-1 mt-0.5 flex-shrink-0" />
|
||||
<span
|
||||
>批量创建时,每个 Key 的名称会自动添加序号后缀,例如:{{
|
||||
@@ -95,12 +106,13 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1.5 block text-xs font-semibold text-gray-700 sm:mb-2 sm:text-sm"
|
||||
<label
|
||||
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-2 sm:text-sm"
|
||||
>名称 <span class="text-red-500">*</span></label
|
||||
>
|
||||
<input
|
||||
v-model="form.name"
|
||||
class="form-input w-full text-sm"
|
||||
class="form-input w-full text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
:class="{ 'border-red-500': errors.name }"
|
||||
:placeholder="
|
||||
form.createType === 'batch'
|
||||
@@ -111,27 +123,31 @@
|
||||
type="text"
|
||||
@input="errors.name = ''"
|
||||
/>
|
||||
<p v-if="errors.name" class="mt-1 text-xs text-red-500">
|
||||
<p v-if="errors.name" class="mt-1 text-xs text-red-500 dark:text-red-400">
|
||||
{{ errors.name }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">标签</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>标签</label
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<!-- 已选择的标签 -->
|
||||
<div v-if="form.tags.length > 0">
|
||||
<div class="mb-2 text-xs font-medium text-gray-600">已选择的标签:</div>
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
已选择的标签:
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="(tag, index) in form.tags"
|
||||
:key="'selected-' + index"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-800"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
|
||||
>
|
||||
{{ tag }}
|
||||
<button
|
||||
class="ml-1 hover:text-blue-900"
|
||||
class="ml-1 hover:text-blue-900 dark:hover:text-blue-300"
|
||||
type="button"
|
||||
@click="removeTag(index)"
|
||||
>
|
||||
@@ -143,16 +159,18 @@
|
||||
|
||||
<!-- 可选择的已有标签 -->
|
||||
<div v-if="unselectedTags.length > 0">
|
||||
<div class="mb-2 text-xs font-medium text-gray-600">点击选择已有标签:</div>
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
点击选择已有标签:
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="tag in unselectedTags"
|
||||
:key="'available-' + tag"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-blue-100 hover:text-blue-700"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-blue-100 hover:text-blue-700 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-blue-900/30 dark:hover:text-blue-400"
|
||||
type="button"
|
||||
@click="selectTag(tag)"
|
||||
>
|
||||
<i class="fas fa-tag text-xs text-gray-500" />
|
||||
<i class="fas fa-tag text-xs text-gray-500 dark:text-gray-400" />
|
||||
{{ tag }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -160,11 +178,13 @@
|
||||
|
||||
<!-- 创建新标签 -->
|
||||
<div>
|
||||
<div class="mb-2 text-xs font-medium text-gray-600">创建新标签:</div>
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
创建新标签:
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="newTag"
|
||||
class="form-input flex-1"
|
||||
class="form-input flex-1 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="输入新标签名称"
|
||||
type="text"
|
||||
@keypress.enter.prevent="addTag"
|
||||
@@ -179,65 +199,79 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500">用于标记不同团队或用途,方便筛选管理</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
用于标记不同团队或用途,方便筛选管理
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 速率限制设置 -->
|
||||
<div class="rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<div
|
||||
class="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-700 dark:bg-blue-900/20"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded bg-blue-500"
|
||||
>
|
||||
<i class="fas fa-tachometer-alt text-xs text-white" />
|
||||
</div>
|
||||
<h4 class="text-sm font-semibold text-gray-800">速率限制设置 (可选)</h4>
|
||||
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
速率限制设置 (可选)
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700"
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>时间窗口 (分钟)</label
|
||||
>
|
||||
<input
|
||||
v-model="form.rateLimitWindow"
|
||||
class="form-input w-full text-sm"
|
||||
class="form-input w-full text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500">时间段单位</p>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">时间段单位</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700">请求次数限制</label>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>请求次数限制</label
|
||||
>
|
||||
<input
|
||||
v-model="form.rateLimitRequests"
|
||||
class="form-input w-full text-sm"
|
||||
class="form-input w-full text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500">窗口内最大请求</p>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">窗口内最大请求</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700">Token 限制</label>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>Token 限制</label
|
||||
>
|
||||
<input
|
||||
v-model="form.tokenLimit"
|
||||
class="form-input w-full text-sm"
|
||||
class="form-input w-full text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="无限制"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500">窗口内最大Token</p>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
窗口内最大Token
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例说明 -->
|
||||
<div class="rounded-lg bg-blue-100 p-2">
|
||||
<h5 class="mb-1 text-xs font-semibold text-blue-800">💡 使用示例</h5>
|
||||
<div class="space-y-0.5 text-xs text-blue-700">
|
||||
<div class="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
|
||||
<h5 class="mb-1 text-xs font-semibold text-blue-800 dark:text-blue-400">
|
||||
💡 使用示例
|
||||
</h5>
|
||||
<div class="space-y-0.5 text-xs text-blue-700 dark:text-blue-300">
|
||||
<div>
|
||||
<strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求
|
||||
</div>
|
||||
@@ -254,34 +288,34 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700"
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>每日费用限制 (美元)</label
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200"
|
||||
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
type="button"
|
||||
@click="form.dailyCostLimit = '50'"
|
||||
>
|
||||
$50
|
||||
</button>
|
||||
<button
|
||||
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200"
|
||||
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
type="button"
|
||||
@click="form.dailyCostLimit = '100'"
|
||||
>
|
||||
$100
|
||||
</button>
|
||||
<button
|
||||
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200"
|
||||
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
type="button"
|
||||
@click="form.dailyCostLimit = '200'"
|
||||
>
|
||||
$200
|
||||
</button>
|
||||
<button
|
||||
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200"
|
||||
class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
type="button"
|
||||
@click="form.dailyCostLimit = ''"
|
||||
>
|
||||
@@ -290,47 +324,53 @@
|
||||
</div>
|
||||
<input
|
||||
v-model="form.dailyCostLimit"
|
||||
class="form-input w-full"
|
||||
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
<p class="text-xs text-gray-500">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">并发限制 (可选)</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>并发限制 (可选)</label
|
||||
>
|
||||
<input
|
||||
v-model="form.concurrencyLimit"
|
||||
class="form-input w-full"
|
||||
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
type="number"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
设置此 API Key 可同时处理的最大请求数,0 或留空表示无限制
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">备注 (可选)</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>备注 (可选)</label
|
||||
>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
class="form-input w-full resize-none text-sm"
|
||||
class="form-input w-full resize-none text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="描述此 API Key 的用途..."
|
||||
rows="2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">有效期限</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>有效期限</label
|
||||
>
|
||||
<select
|
||||
v-model="form.expireDuration"
|
||||
class="form-input w-full"
|
||||
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
@change="updateExpireAt"
|
||||
>
|
||||
<option value="">永不过期</option>
|
||||
@@ -345,45 +385,71 @@
|
||||
<div v-if="form.expireDuration === 'custom'" class="mt-3">
|
||||
<input
|
||||
v-model="form.customExpireDate"
|
||||
class="form-input w-full"
|
||||
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
:min="minDateTime"
|
||||
type="datetime-local"
|
||||
@change="updateCustomExpireAt"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="form.expiresAt" class="mt-2 text-xs text-gray-500">
|
||||
<p v-if="form.expiresAt" class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
将于 {{ formatExpireDate(form.expiresAt) }} 过期
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">服务权限</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>服务权限</label
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="all" />
|
||||
<span class="text-sm text-gray-700">全部服务</span>
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="radio"
|
||||
value="all"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">全部服务</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="claude" />
|
||||
<span class="text-sm text-gray-700">仅 Claude</span>
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="radio"
|
||||
value="claude"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 Claude</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="gemini" />
|
||||
<span class="text-sm text-gray-700">仅 Gemini</span>
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="radio"
|
||||
value="gemini"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 Gemini</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="openai" />
|
||||
<span class="text-sm text-gray-700">仅 OpenAI</span>
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="radio"
|
||||
value="openai"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 OpenAI</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">控制此 API Key 可以访问哪些服务</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
控制此 API Key 可以访问哪些服务
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<label class="text-sm font-semibold text-gray-700">专属账号绑定 (可选)</label>
|
||||
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>专属账号绑定 (可选)</label
|
||||
>
|
||||
<button
|
||||
class="flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-800 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-800 disabled:cursor-not-allowed disabled:opacity-50 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
:disabled="accountsLoading"
|
||||
title="刷新账号列表"
|
||||
type="button"
|
||||
@@ -401,7 +467,9 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600">Claude 专属账号</label>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Claude 专属账号</label
|
||||
>
|
||||
<AccountSelector
|
||||
v-model="form.claudeAccountId"
|
||||
:accounts="localAccounts.claude"
|
||||
@@ -413,7 +481,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600">Gemini 专属账号</label>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Gemini 专属账号</label
|
||||
>
|
||||
<AccountSelector
|
||||
v-model="form.geminiAccountId"
|
||||
:accounts="localAccounts.gemini"
|
||||
@@ -425,7 +495,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600">OpenAI 专属账号</label>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>OpenAI 专属账号</label
|
||||
>
|
||||
<AccountSelector
|
||||
v-model="form.openaiAccountId"
|
||||
:accounts="localAccounts.openai"
|
||||
@@ -437,7 +509,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600">Bedrock 专属账号</label>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Bedrock 专属账号</label
|
||||
>
|
||||
<AccountSelector
|
||||
v-model="form.bedrockAccountId"
|
||||
:accounts="localAccounts.bedrock"
|
||||
@@ -449,7 +523,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
选择专属账号后,此API Key将只使用该账号,不选择则使用共享账号池
|
||||
</p>
|
||||
</div>
|
||||
@@ -463,7 +537,7 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700"
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
for="enableModelRestriction"
|
||||
>
|
||||
启用模型限制
|
||||
@@ -549,7 +623,7 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700"
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
for="enableClientRestriction"
|
||||
>
|
||||
启用客户端限制
|
||||
@@ -558,10 +632,12 @@
|
||||
|
||||
<div
|
||||
v-if="form.enableClientRestriction"
|
||||
class="rounded-lg border border-green-200 bg-green-50 p-3"
|
||||
class="rounded-lg border border-green-200 bg-green-50 p-3 dark:border-green-700 dark:bg-green-900/20"
|
||||
>
|
||||
<div>
|
||||
<label class="mb-2 block text-xs font-medium text-gray-700">允许的客户端</label>
|
||||
<label class="mb-2 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>允许的客户端</label
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
|
||||
<input
|
||||
@@ -572,8 +648,12 @@
|
||||
:value="client.id"
|
||||
/>
|
||||
<label class="ml-2 flex-1 cursor-pointer" :for="`client_${client.id}`">
|
||||
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
|
||||
<span class="block text-xs text-gray-500">{{ client.description }}</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
||||
client.name
|
||||
}}</span>
|
||||
<span class="block text-xs text-gray-500 dark:text-gray-400">{{
|
||||
client.description
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -583,7 +663,7 @@
|
||||
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button
|
||||
class="flex-1 rounded-lg bg-gray-100 px-4 py-2.5 text-sm font-semibold text-gray-700 transition-colors hover:bg-gray-200"
|
||||
class="flex-1 rounded-lg bg-gray-100 px-4 py-2.5 text-sm font-semibold text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
>
|
||||
<i class="fas fa-edit text-sm text-white sm:text-base" />
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-gray-900 sm:text-xl">编辑 API Key</h3>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
|
||||
编辑 API Key
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
class="p-1 text-gray-400 transition-colors hover:text-gray-600"
|
||||
class="p-1 text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<i class="fas fa-times text-lg sm:text-xl" />
|
||||
@@ -26,36 +28,40 @@
|
||||
@submit.prevent="updateApiKey"
|
||||
>
|
||||
<div>
|
||||
<label class="mb-1.5 block text-xs font-semibold text-gray-700 sm:mb-3 sm:text-sm"
|
||||
<label
|
||||
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
|
||||
>名称</label
|
||||
>
|
||||
<input
|
||||
class="form-input w-full cursor-not-allowed bg-gray-100 text-sm"
|
||||
class="form-input w-full cursor-not-allowed bg-gray-100 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
||||
disabled
|
||||
type="text"
|
||||
:value="form.name"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 sm:mt-2">名称不可修改</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 sm:mt-2">名称不可修改</p>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-xs font-semibold text-gray-700 sm:mb-3 sm:text-sm"
|
||||
<label
|
||||
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
|
||||
>标签</label
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<!-- 已选择的标签 -->
|
||||
<div v-if="form.tags.length > 0">
|
||||
<div class="mb-2 text-xs font-medium text-gray-600">已选择的标签:</div>
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
已选择的标签:
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="(tag, index) in form.tags"
|
||||
:key="'selected-' + index"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-800"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
|
||||
>
|
||||
{{ tag }}
|
||||
<button
|
||||
class="ml-1 hover:text-blue-900"
|
||||
class="ml-1 hover:text-blue-900 dark:hover:text-blue-300"
|
||||
type="button"
|
||||
@click="removeTag(index)"
|
||||
>
|
||||
@@ -67,16 +73,18 @@
|
||||
|
||||
<!-- 可选择的已有标签 -->
|
||||
<div v-if="unselectedTags.length > 0">
|
||||
<div class="mb-2 text-xs font-medium text-gray-600">点击选择已有标签:</div>
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
点击选择已有标签:
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="tag in unselectedTags"
|
||||
:key="'available-' + tag"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-blue-100 hover:text-blue-700"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-blue-100 hover:text-blue-700 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-blue-900/30 dark:hover:text-blue-400"
|
||||
type="button"
|
||||
@click="selectTag(tag)"
|
||||
>
|
||||
<i class="fas fa-tag text-xs text-gray-500" />
|
||||
<i class="fas fa-tag text-xs text-gray-500 dark:text-gray-400" />
|
||||
{{ tag }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -84,11 +92,13 @@
|
||||
|
||||
<!-- 创建新标签 -->
|
||||
<div>
|
||||
<div class="mb-2 text-xs font-medium text-gray-600">创建新标签:</div>
|
||||
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
创建新标签:
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="newTag"
|
||||
class="form-input flex-1"
|
||||
class="form-input flex-1 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="输入新标签名称"
|
||||
type="text"
|
||||
@keypress.enter.prevent="addTag"
|
||||
@@ -103,65 +113,79 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500">用于标记不同团队或用途,方便筛选管理</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
用于标记不同团队或用途,方便筛选管理
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 速率限制设置 -->
|
||||
<div class="rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<div
|
||||
class="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-700 dark:bg-blue-900/20"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<div
|
||||
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded bg-blue-500"
|
||||
>
|
||||
<i class="fas fa-tachometer-alt text-xs text-white" />
|
||||
</div>
|
||||
<h4 class="text-sm font-semibold text-gray-800">速率限制设置 (可选)</h4>
|
||||
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
速率限制设置 (可选)
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700"
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>时间窗口 (分钟)</label
|
||||
>
|
||||
<input
|
||||
v-model="form.rateLimitWindow"
|
||||
class="form-input w-full text-sm"
|
||||
class="form-input w-full text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500">时间段单位</p>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">时间段单位</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700">请求次数限制</label>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>请求次数限制</label
|
||||
>
|
||||
<input
|
||||
v-model="form.rateLimitRequests"
|
||||
class="form-input w-full text-sm"
|
||||
class="form-input w-full text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="1"
|
||||
placeholder="无限制"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500">窗口内最大请求</p>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">窗口内最大请求</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700">Token 限制</label>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300"
|
||||
>Token 限制</label
|
||||
>
|
||||
<input
|
||||
v-model="form.tokenLimit"
|
||||
class="form-input w-full text-sm"
|
||||
class="form-input w-full text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="无限制"
|
||||
type="number"
|
||||
/>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500">窗口内最大Token</p>
|
||||
<p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
窗口内最大Token
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例说明 -->
|
||||
<div class="rounded-lg bg-blue-100 p-2">
|
||||
<h5 class="mb-1 text-xs font-semibold text-blue-800">💡 使用示例</h5>
|
||||
<div class="space-y-0.5 text-xs text-blue-700">
|
||||
<div class="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
|
||||
<h5 class="mb-1 text-xs font-semibold text-blue-800 dark:text-blue-400">
|
||||
💡 使用示例
|
||||
</h5>
|
||||
<div class="space-y-0.5 text-xs text-blue-700 dark:text-blue-300">
|
||||
<div>
|
||||
<strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求
|
||||
</div>
|
||||
@@ -178,34 +202,34 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700"
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>每日费用限制 (美元)</label
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200"
|
||||
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
type="button"
|
||||
@click="form.dailyCostLimit = '50'"
|
||||
>
|
||||
$50
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200"
|
||||
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
type="button"
|
||||
@click="form.dailyCostLimit = '100'"
|
||||
>
|
||||
$100
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200"
|
||||
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
type="button"
|
||||
@click="form.dailyCostLimit = '200'"
|
||||
>
|
||||
$200
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200"
|
||||
class="rounded-lg bg-gray-100 px-3 py-1 text-sm font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
type="button"
|
||||
@click="form.dailyCostLimit = ''"
|
||||
>
|
||||
@@ -214,28 +238,32 @@
|
||||
</div>
|
||||
<input
|
||||
v-model="form.dailyCostLimit"
|
||||
class="form-input w-full"
|
||||
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
<p class="text-xs text-gray-500">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制
|
||||
</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 dark:text-gray-300"
|
||||
>并发限制</label
|
||||
>
|
||||
<input
|
||||
v-model="form.concurrencyLimit"
|
||||
class="form-input w-full"
|
||||
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
min="0"
|
||||
placeholder="0 表示无限制"
|
||||
type="number"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-gray-500">设置此 API Key 可同时处理的最大请求数</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
设置此 API Key 可同时处理的最大请求数
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 激活账号 -->
|
||||
@@ -244,49 +272,75 @@
|
||||
<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"
|
||||
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700"
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
for="editIsActive"
|
||||
>
|
||||
激活账号
|
||||
</label>
|
||||
</div>
|
||||
<p class="mb-4 text-xs text-gray-500">
|
||||
<p class="mb-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
取消勾选将禁用此 API Key,暂停所有请求,客户端返回 401 错误
|
||||
</p>
|
||||
</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 dark:text-gray-300"
|
||||
>服务权限</label
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="all" />
|
||||
<span class="text-sm text-gray-700">全部服务</span>
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="radio"
|
||||
value="all"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">全部服务</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="claude" />
|
||||
<span class="text-sm text-gray-700">仅 Claude</span>
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="radio"
|
||||
value="claude"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 Claude</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="gemini" />
|
||||
<span class="text-sm text-gray-700">仅 Gemini</span>
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="radio"
|
||||
value="gemini"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 Gemini</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center">
|
||||
<input v-model="form.permissions" class="mr-2" type="radio" value="openai" />
|
||||
<span class="text-sm text-gray-700">仅 OpenAI</span>
|
||||
<input
|
||||
v-model="form.permissions"
|
||||
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="radio"
|
||||
value="openai"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">仅 OpenAI</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">控制此 API Key 可以访问哪些服务</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
控制此 API Key 可以访问哪些服务
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="text-sm font-semibold text-gray-700">专属账号绑定</label>
|
||||
<label class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>专属账号绑定</label
|
||||
>
|
||||
<button
|
||||
class="flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-800 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-800 disabled:cursor-not-allowed disabled:opacity-50 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
:disabled="accountsLoading"
|
||||
title="刷新账号列表"
|
||||
type="button"
|
||||
@@ -304,7 +358,9 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600">Claude 专属账号</label>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Claude 专属账号</label
|
||||
>
|
||||
<AccountSelector
|
||||
v-model="form.claudeAccountId"
|
||||
:accounts="localAccounts.claude"
|
||||
@@ -316,7 +372,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600">Gemini 专属账号</label>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Gemini 专属账号</label
|
||||
>
|
||||
<AccountSelector
|
||||
v-model="form.geminiAccountId"
|
||||
:accounts="localAccounts.gemini"
|
||||
@@ -328,7 +386,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600">OpenAI 专属账号</label>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>OpenAI 专属账号</label
|
||||
>
|
||||
<AccountSelector
|
||||
v-model="form.openaiAccountId"
|
||||
:accounts="localAccounts.openai"
|
||||
@@ -340,7 +400,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600">Bedrock 专属账号</label>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>Bedrock 专属账号</label
|
||||
>
|
||||
<AccountSelector
|
||||
v-model="form.bedrockAccountId"
|
||||
:accounts="localAccounts.bedrock"
|
||||
@@ -352,7 +414,9 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">修改绑定账号将影响此API Key的请求路由</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
修改绑定账号将影响此API Key的请求路由
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -360,11 +424,11 @@
|
||||
<input
|
||||
id="editEnableModelRestriction"
|
||||
v-model="form.enableModelRestriction"
|
||||
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500"
|
||||
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700"
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
for="editEnableModelRestriction"
|
||||
>
|
||||
启用模型限制
|
||||
@@ -373,25 +437,30 @@
|
||||
|
||||
<div v-if="form.enableModelRestriction" class="space-y-3">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-600">限制的模型列表</label>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>限制的模型列表</label
|
||||
>
|
||||
<div
|
||||
class="mb-3 flex min-h-[32px] flex-wrap gap-2 rounded-lg border border-gray-200 bg-gray-50 p-2"
|
||||
class="mb-3 flex min-h-[32px] flex-wrap gap-2 rounded-lg border border-gray-200 bg-gray-50 p-2 dark:border-gray-600 dark:bg-gray-700"
|
||||
>
|
||||
<span
|
||||
v-for="(model, index) in form.restrictedModels"
|
||||
:key="index"
|
||||
class="inline-flex items-center rounded-full bg-red-100 px-3 py-1 text-sm text-red-800"
|
||||
class="inline-flex items-center rounded-full bg-red-100 px-3 py-1 text-sm text-red-800 dark:bg-red-900/30 dark:text-red-400"
|
||||
>
|
||||
{{ model }}
|
||||
<button
|
||||
class="ml-2 text-red-600 hover:text-red-800"
|
||||
class="ml-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
|
||||
type="button"
|
||||
@click="removeRestrictedModel(index)"
|
||||
>
|
||||
<i class="fas fa-times text-xs" />
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="form.restrictedModels.length === 0" class="text-sm text-gray-400">
|
||||
<span
|
||||
v-if="form.restrictedModels.length === 0"
|
||||
class="text-sm text-gray-400 dark:text-gray-500"
|
||||
>
|
||||
暂无限制的模型
|
||||
</span>
|
||||
</div>
|
||||
@@ -401,7 +470,7 @@
|
||||
<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"
|
||||
class="flex-shrink-0 rounded-lg bg-gray-100 px-3 py-1 text-xs text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 sm:text-sm"
|
||||
type="button"
|
||||
@click="quickAddRestrictedModel(model)"
|
||||
>
|
||||
@@ -409,7 +478,7 @@
|
||||
</button>
|
||||
<span
|
||||
v-if="availableQuickModels.length === 0"
|
||||
class="text-sm italic text-gray-400"
|
||||
class="text-sm italic text-gray-400 dark:text-gray-500"
|
||||
>
|
||||
所有常用模型已在限制列表中
|
||||
</span>
|
||||
@@ -419,7 +488,7 @@
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="form.modelInput"
|
||||
class="form-input flex-1"
|
||||
class="form-input flex-1 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="输入模型名称,按回车添加"
|
||||
type="text"
|
||||
@keydown.enter.prevent="addRestrictedModel"
|
||||
@@ -433,7 +502,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
设置此API Key无法访问的模型,例如:claude-opus-4-20250514
|
||||
</p>
|
||||
</div>
|
||||
@@ -446,11 +515,11 @@
|
||||
<input
|
||||
id="editEnableClientRestriction"
|
||||
v-model="form.enableClientRestriction"
|
||||
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500"
|
||||
class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700"
|
||||
class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
for="editEnableClientRestriction"
|
||||
>
|
||||
启用客户端限制
|
||||
@@ -459,8 +528,12 @@
|
||||
|
||||
<div v-if="form.enableClientRestriction" class="space-y-3">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-600">允许的客户端</label>
|
||||
<p class="mb-3 text-xs text-gray-500">勾选允许使用此API Key的客户端</p>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>允许的客户端</label
|
||||
>
|
||||
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
勾选允许使用此API Key的客户端
|
||||
</p>
|
||||
<div class="space-y-2">
|
||||
<div v-for="client in supportedClients" :key="client.id" class="flex items-start">
|
||||
<input
|
||||
@@ -471,8 +544,12 @@
|
||||
:value="client.id"
|
||||
/>
|
||||
<label class="ml-2 flex-1 cursor-pointer" :for="`edit_client_${client.id}`">
|
||||
<span class="text-sm font-medium text-gray-700">{{ client.name }}</span>
|
||||
<span class="block text-xs text-gray-500">{{ client.description }}</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{
|
||||
client.name
|
||||
}}</span>
|
||||
<span class="block text-xs text-gray-500 dark:text-gray-400">{{
|
||||
client.description
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -482,7 +559,7 @@
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
class="flex-1 rounded-xl bg-gray-100 px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-200"
|
||||
class="flex-1 rounded-xl bg-gray-100 px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="show" class="modal fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<!-- 背景遮罩 -->
|
||||
<div
|
||||
class="fixed inset-0 bg-gray-900 bg-opacity-50 backdrop-blur-sm"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
|
||||
<!-- 模态框内容 -->
|
||||
<div class="modal-content mx-auto w-full max-w-lg p-8">
|
||||
<div class="modal-content relative mx-auto w-full max-w-lg p-8">
|
||||
<!-- 头部 -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -12,14 +18,14 @@
|
||||
<i class="fas fa-clock text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900">修改过期时间</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">修改过期时间</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
为 "{{ apiKey.name || 'API Key' }}" 设置新的过期时间
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 transition-colors hover:text-gray-600"
|
||||
class="text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<i class="fas fa-times text-xl" />
|
||||
@@ -29,12 +35,14 @@
|
||||
<div class="space-y-6">
|
||||
<!-- 当前状态显示 -->
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-gradient-to-r from-gray-50 to-gray-100 p-4"
|
||||
class="rounded-lg border border-gray-200 bg-gradient-to-r from-gray-50 to-gray-100 p-4 dark:border-gray-600 dark:from-gray-700 dark:to-gray-800"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="mb-1 text-xs font-medium text-gray-600">当前过期时间</p>
|
||||
<p class="text-sm font-semibold text-gray-800">
|
||||
<p class="mb-1 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
当前过期时间
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
<template v-if="apiKey.expiresAt">
|
||||
{{ formatExpireDate(apiKey.expiresAt) }}
|
||||
<span
|
||||
@@ -51,7 +59,9 @@
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-white shadow-sm">
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-lg bg-white shadow-sm dark:bg-gray-700"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'fas fa-hourglass-half text-lg',
|
||||
@@ -66,7 +76,9 @@
|
||||
|
||||
<!-- 快捷选项 -->
|
||||
<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 dark:text-gray-300"
|
||||
>选择新的期限</label
|
||||
>
|
||||
<div class="mb-3 grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="option in quickOptions"
|
||||
@@ -75,7 +87,7 @@
|
||||
'rounded-lg px-3 py-2 text-sm font-medium transition-all',
|
||||
localForm.expireDuration === option.value
|
||||
? 'bg-blue-500 text-white shadow-md'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
]"
|
||||
@click="selectQuickOption(option.value)"
|
||||
>
|
||||
@@ -86,7 +98,7 @@
|
||||
'rounded-lg px-3 py-2 text-sm font-medium transition-all',
|
||||
localForm.expireDuration === 'custom'
|
||||
? 'bg-blue-500 text-white shadow-md'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
]"
|
||||
@click="selectQuickOption('custom')"
|
||||
>
|
||||
@@ -98,29 +110,33 @@
|
||||
|
||||
<!-- 自定义日期选择 -->
|
||||
<div v-if="localForm.expireDuration === 'custom'" class="animate-fadeIn">
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">选择日期和时间</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>选择日期和时间</label
|
||||
>
|
||||
<input
|
||||
v-model="localForm.customExpireDate"
|
||||
class="form-input w-full"
|
||||
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
:min="minDateTime"
|
||||
type="datetime-local"
|
||||
@change="updateCustomExpiryPreview"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-gray-500">选择一个未来的日期和时间作为过期时间</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
选择一个未来的日期和时间作为过期时间
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 预览新的过期时间 -->
|
||||
<div
|
||||
v-if="localForm.expiresAt !== apiKey.expiresAt"
|
||||
class="rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-4"
|
||||
class="rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-4 dark:border-blue-700 dark:from-blue-900/20 dark:to-indigo-900/20"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="mb-1 text-xs font-medium text-blue-700">
|
||||
<p class="mb-1 text-xs font-medium text-blue-700 dark:text-blue-400">
|
||||
<i class="fas fa-arrow-right mr-1" />
|
||||
新的过期时间
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-blue-900">
|
||||
<p class="text-sm font-semibold text-blue-900 dark:text-blue-200">
|
||||
<template v-if="localForm.expiresAt">
|
||||
{{ formatExpireDate(localForm.expiresAt) }}
|
||||
<span
|
||||
@@ -137,7 +153,9 @@
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-white shadow-sm">
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-lg bg-white shadow-sm dark:bg-gray-700"
|
||||
>
|
||||
<i class="fas fa-check text-lg text-green-500" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,7 +164,7 @@
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button
|
||||
class="flex-1 rounded-lg bg-gray-100 px-4 py-2.5 font-semibold text-gray-700 transition-colors hover:bg-gray-200"
|
||||
class="flex-1 rounded-lg bg-gray-100 px-4 py-2.5 font-semibold text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
<i class="fas fa-check text-lg text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900">API Key 创建成功</h3>
|
||||
<p class="text-sm text-gray-600">请妥善保存您的 API Key</p>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">API Key 创建成功</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">请妥善保存您的 API Key</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 transition-colors hover:text-gray-600"
|
||||
class="text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
title="直接关闭(不推荐)"
|
||||
@click="handleDirectClose"
|
||||
>
|
||||
@@ -26,16 +26,18 @@
|
||||
</div>
|
||||
|
||||
<!-- 警告提示 -->
|
||||
<div class="mb-6 border-l-4 border-amber-400 bg-amber-50 p-4">
|
||||
<div
|
||||
class="mb-6 border-l-4 border-amber-400 bg-amber-50 p-4 dark:border-amber-500 dark:bg-amber-900/20"
|
||||
>
|
||||
<div class="flex items-start">
|
||||
<div
|
||||
class="mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-lg bg-amber-400"
|
||||
class="mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-lg bg-amber-400 dark:bg-amber-500"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle text-sm text-white" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h5 class="mb-1 font-semibold text-amber-900">重要提醒</h5>
|
||||
<p class="text-sm text-amber-800">
|
||||
<h5 class="mb-1 font-semibold text-amber-900 dark:text-amber-400">重要提醒</h5>
|
||||
<p class="text-sm text-amber-800 dark:text-amber-300">
|
||||
这是您唯一能看到完整 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API
|
||||
Key。请立即复制并妥善保存。
|
||||
</p>
|
||||
@@ -46,30 +48,42 @@
|
||||
<!-- API Key 信息 -->
|
||||
<div class="mb-6 space-y-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">API Key 名称</label>
|
||||
<div class="rounded-lg border bg-gray-50 p-3">
|
||||
<span class="font-medium text-gray-900">{{ apiKey.name }}</span>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>API Key 名称</label
|
||||
>
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-600 dark:bg-gray-800"
|
||||
>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ apiKey.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="apiKey.description">
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">备注</label>
|
||||
<div class="rounded-lg border bg-gray-50 p-3">
|
||||
<span class="text-gray-700">{{ apiKey.description || '无描述' }}</span>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>备注</label
|
||||
>
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-600 dark:bg-gray-800"
|
||||
>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{
|
||||
apiKey.description || '无描述'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700">API Key</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>API Key</label
|
||||
>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="flex min-h-[60px] items-center break-all rounded-lg border bg-gray-900 p-4 pr-14 font-mono text-sm text-white"
|
||||
class="flex min-h-[60px] items-center break-all rounded-lg border border-gray-700 bg-gray-900 p-4 pr-14 font-mono text-sm text-white dark:border-gray-600 dark:bg-gray-900"
|
||||
>
|
||||
{{ getDisplayedApiKey() }}
|
||||
</div>
|
||||
<div class="absolute right-3 top-3">
|
||||
<button
|
||||
class="btn-icon-sm bg-gray-700 hover:bg-gray-800"
|
||||
class="btn-icon-sm bg-gray-700 hover:bg-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
:title="showFullKey ? '隐藏API Key' : '显示完整API Key'"
|
||||
type="button"
|
||||
@click="toggleKeyVisibility"
|
||||
@@ -78,7 +92,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
点击眼睛图标切换显示模式,使用下方按钮复制完整 API Key
|
||||
</p>
|
||||
</div>
|
||||
@@ -94,7 +108,7 @@
|
||||
复制 API Key
|
||||
</button>
|
||||
<button
|
||||
class="rounded-xl border border-gray-300 bg-gray-200 px-6 py-3 font-semibold text-gray-800 transition-colors hover:bg-gray-300"
|
||||
class="rounded-xl border border-gray-300 bg-gray-200 px-6 py-3 font-semibold text-gray-800 transition-colors hover:bg-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||
@click="handleClose"
|
||||
>
|
||||
我已保存
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
>
|
||||
<i class="fas fa-chart-line text-sm text-white sm:text-base" />
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-gray-900 sm:text-xl">
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
|
||||
使用统计详情 - {{ apiKey.name }}
|
||||
</h3>
|
||||
</div>
|
||||
@@ -31,64 +31,68 @@
|
||||
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<!-- 请求统计卡片 -->
|
||||
<div
|
||||
class="rounded-lg border border-blue-200 bg-gradient-to-br from-blue-50 to-blue-100 p-4"
|
||||
class="rounded-lg border border-blue-200 bg-gradient-to-br from-blue-50 to-blue-100 p-4 dark:border-blue-700 dark:from-blue-900/20 dark:to-blue-800/20"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">总请求数</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">总请求数</span>
|
||||
<i class="fas fa-paper-plane text-blue-500" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900">
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(totalRequests) }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-600">
|
||||
<div class="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
今日: {{ formatNumber(dailyRequests) }} 次
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token统计卡片 -->
|
||||
<div
|
||||
class="rounded-lg border border-green-200 bg-gradient-to-br from-green-50 to-green-100 p-4"
|
||||
class="rounded-lg border border-green-200 bg-gradient-to-br from-green-50 to-green-100 p-4 dark:border-green-700 dark:from-green-900/20 dark:to-green-800/20"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">总Token数</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">总Token数</span>
|
||||
<i class="fas fa-coins text-green-500" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900">
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{{ formatTokenCount(totalTokens) }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-600">
|
||||
<div class="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
今日: {{ formatTokenCount(dailyTokens) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 费用统计卡片 -->
|
||||
<div
|
||||
class="rounded-lg border border-yellow-200 bg-gradient-to-br from-yellow-50 to-yellow-100 p-4"
|
||||
class="rounded-lg border border-yellow-200 bg-gradient-to-br from-yellow-50 to-yellow-100 p-4 dark:border-yellow-700 dark:from-yellow-900/20 dark:to-yellow-800/20"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">总费用</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">总费用</span>
|
||||
<i class="fas fa-dollar-sign text-yellow-600" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900">${{ totalCost.toFixed(4) }}</div>
|
||||
<div class="mt-1 text-xs text-gray-600">今日: ${{ dailyCost.toFixed(4) }}</div>
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
${{ totalCost.toFixed(4) }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
今日: ${{ dailyCost.toFixed(4) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 平均统计卡片 -->
|
||||
<div
|
||||
class="rounded-lg border border-purple-200 bg-gradient-to-br from-purple-50 to-purple-100 p-4"
|
||||
class="rounded-lg border border-purple-200 bg-gradient-to-br from-purple-50 to-purple-100 p-4 dark:border-purple-700 dark:from-purple-900/20 dark:to-purple-800/20"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">平均速率</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">平均速率</span>
|
||||
<i class="fas fa-tachometer-alt text-purple-500" />
|
||||
</div>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">RPM:</span>
|
||||
<span class="font-semibold">{{ rpm }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">RPM:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-gray-100">{{ rpm }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">TPM:</span>
|
||||
<span class="font-semibold">{{ tpm }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">TPM:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-gray-100">{{ tpm }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,33 +100,35 @@
|
||||
|
||||
<!-- Token详细分布 -->
|
||||
<div class="mb-6">
|
||||
<h4 class="mb-3 flex items-center text-sm font-semibold text-gray-700">
|
||||
<h4
|
||||
class="mb-3 flex items-center text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-chart-pie mr-2 text-indigo-500" />
|
||||
Token 使用分布
|
||||
</h4>
|
||||
<div class="space-y-3 rounded-lg bg-gray-50 p-4">
|
||||
<div class="space-y-3 rounded-lg bg-gray-50 p-4 dark:bg-gray-700/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-arrow-down mr-2 text-green-500" />
|
||||
<span class="text-sm text-gray-600">输入 Token</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">输入 Token</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900">
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ formatTokenCount(inputTokens) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-arrow-up mr-2 text-blue-500" />
|
||||
<span class="text-sm text-gray-600">输出 Token</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">输出 Token</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900">
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ formatTokenCount(outputTokens) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="cacheCreateTokens > 0" class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-save mr-2 text-purple-500" />
|
||||
<span class="text-sm text-gray-600">缓存创建 Token</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">缓存创建 Token</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-purple-600">
|
||||
{{ formatTokenCount(cacheCreateTokens) }}
|
||||
@@ -131,7 +137,7 @@
|
||||
<div v-if="cacheReadTokens > 0" class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-download mr-2 text-purple-500" />
|
||||
<span class="text-sm text-gray-600">缓存读取 Token</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">缓存读取 Token</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-purple-600">
|
||||
{{ formatTokenCount(cacheReadTokens) }}
|
||||
@@ -142,19 +148,21 @@
|
||||
|
||||
<!-- 限制信息 -->
|
||||
<div v-if="hasLimits" class="mb-6">
|
||||
<h4 class="mb-3 flex items-center text-sm font-semibold text-gray-700">
|
||||
<h4
|
||||
class="mb-3 flex items-center text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-shield-alt mr-2 text-red-500" />
|
||||
限制设置
|
||||
</h4>
|
||||
<div class="space-y-3 rounded-lg bg-gray-50 p-4">
|
||||
<div class="space-y-3 rounded-lg bg-gray-50 p-4 dark:bg-gray-700/50">
|
||||
<div v-if="apiKey.dailyCostLimit > 0" class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">每日费用限制</span>
|
||||
<span class="font-semibold text-gray-900">
|
||||
<span class="text-gray-600 dark:text-gray-400">每日费用限制</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-gray-100">
|
||||
${{ apiKey.dailyCostLimit.toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-2 w-full rounded-full bg-gray-200">
|
||||
<div class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-600">
|
||||
<div
|
||||
class="h-2 rounded-full transition-all duration-300"
|
||||
:class="
|
||||
@@ -167,7 +175,7 @@
|
||||
:style="{ width: Math.min(dailyCostPercentage, 100) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-right text-xs text-gray-500">
|
||||
<div class="text-right text-xs text-gray-500 dark:text-gray-400">
|
||||
已使用 {{ dailyCostPercentage.toFixed(1) }}%
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,14 +184,14 @@
|
||||
v-if="apiKey.concurrencyLimit > 0"
|
||||
class="flex items-center justify-between text-sm"
|
||||
>
|
||||
<span class="text-gray-600">并发限制</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">并发限制</span>
|
||||
<span class="font-semibold text-purple-600">
|
||||
{{ apiKey.currentConcurrency || 0 }} / {{ apiKey.concurrencyLimit }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="apiKey.rateLimitWindow > 0" class="space-y-2">
|
||||
<h5 class="text-sm font-medium text-gray-700">
|
||||
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<i class="fas fa-clock mr-1 text-blue-500" />
|
||||
时间窗口限制
|
||||
</h5>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="api-input-wide-card glass-strong mb-8 rounded-3xl p-6 shadow-xl">
|
||||
<div class="api-input-wide-card mb-8 rounded-3xl p-6 shadow-xl">
|
||||
<!-- 标题区域 -->
|
||||
<div class="wide-card-title mb-6 text-center">
|
||||
<h2 class="mb-2 text-2xl font-bold">
|
||||
<h2 class="mb-2 text-2xl font-bold text-gray-900 dark:text-white">
|
||||
<i class="fas fa-chart-line mr-3" />
|
||||
使用统计查询
|
||||
</h2>
|
||||
<p class="text-base text-gray-600">查询您的 API Key 使用情况和统计数据</p>
|
||||
<p class="text-base text-gray-600 dark:text-gray-300">查询您的 API Key 使用情况和统计数据</p>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="api-input-grid grid grid-cols-1 lg:grid-cols-4">
|
||||
<!-- API Key 输入 -->
|
||||
<div class="lg:col-span-3">
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
<i class="fas fa-key mr-2" />
|
||||
输入您的 API Key
|
||||
</label>
|
||||
@@ -30,7 +30,9 @@
|
||||
|
||||
<!-- 查询按钮 -->
|
||||
<div class="lg:col-span-1">
|
||||
<label class="mb-2 hidden text-sm font-medium text-gray-700 lg:block"> </label>
|
||||
<label class="mb-2 hidden text-sm font-medium text-gray-700 dark:text-gray-200 lg:block">
|
||||
|
||||
</label>
|
||||
<button
|
||||
class="btn btn-primary btn-query flex h-full w-full items-center justify-center gap-2"
|
||||
:disabled="loading || !apiKey.trim()"
|
||||
@@ -62,11 +64,11 @@ const { queryStats } = apiStatsStore
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 宽卡片样式 */
|
||||
/* 宽卡片样式 - 使用CSS变量 */
|
||||
.api-input-wide-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
background: var(--surface-color);
|
||||
backdrop-filter: blur(25px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05),
|
||||
@@ -74,6 +76,14 @@ const { queryStats } = apiStatsStore
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* 暗夜模式宽卡片样式 */
|
||||
:global(.dark) .api-input-wide-card {
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.6),
|
||||
0 0 0 1px rgba(75, 85, 99, 0.2),
|
||||
inset 0 1px 0 rgba(107, 114, 128, 0.15);
|
||||
}
|
||||
|
||||
.api-input-wide-card:hover {
|
||||
box-shadow:
|
||||
0 32px 64px -12px rgba(0, 0, 0, 0.35),
|
||||
@@ -82,6 +92,13 @@ const { queryStats } = apiStatsStore
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
:global(.dark) .api-input-wide-card:hover {
|
||||
box-shadow:
|
||||
0 32px 64px -12px rgba(0, 0, 0, 0.7),
|
||||
0 0 0 1px rgba(75, 85, 99, 0.25),
|
||||
inset 0 1px 0 rgba(107, 114, 128, 0.3) !important;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.wide-card-title h2 {
|
||||
color: #1f2937;
|
||||
@@ -89,11 +106,21 @@ const { queryStats } = apiStatsStore
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:global(.dark) .wide-card-title h2 {
|
||||
color: #f9fafb;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.wide-card-title p {
|
||||
color: #4b5563;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
:global(.dark) .wide-card-title p {
|
||||
color: #d1d5db;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.wide-card-title .fas.fa-chart-line {
|
||||
color: #3b82f6;
|
||||
text-shadow: 0 1px 2px rgba(59, 130, 246, 0.2);
|
||||
@@ -105,23 +132,32 @@ const { queryStats } = apiStatsStore
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
/* 输入框样式 - 使用CSS变量 */
|
||||
.wide-card-input {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 2px solid rgba(255, 255, 255, 0.4);
|
||||
background: var(--input-bg);
|
||||
border: 2px solid var(--input-border);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
color: #1f2937;
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:global(.dark) .wide-card-input {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.wide-card-input::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
:global(.dark) .wide-card-input::placeholder {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.wide-card-input:focus {
|
||||
outline: none;
|
||||
border-color: #60a5fa;
|
||||
@@ -129,6 +165,16 @@ const { queryStats } = apiStatsStore
|
||||
0 0 0 3px rgba(96, 165, 250, 0.2),
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
background: white;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
:global(.dark) .wide-card-input:focus {
|
||||
border-color: #60a5fa;
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(96, 165, 250, 0.15),
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.4);
|
||||
background: rgba(31, 41, 55, 0.95);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
@@ -172,8 +218,8 @@ const { queryStats } = apiStatsStore
|
||||
|
||||
/* 安全提示样式 */
|
||||
.security-notice {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
@@ -182,9 +228,22 @@ const { queryStats } = apiStatsStore
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.dark) .security-notice {
|
||||
background: rgba(31, 41, 55, 0.8) !important;
|
||||
border: 1px solid rgba(75, 85, 99, 0.5) !important;
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
.security-notice:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
:global(.dark) .security-notice:hover {
|
||||
background: rgba(31, 41, 55, 0.9) !important;
|
||||
border-color: rgba(75, 85, 99, 0.6) !important;
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
.security-notice .fas.fa-shield-alt {
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<div>
|
||||
<!-- 限制配置 -->
|
||||
<div class="card p-4 md:p-6">
|
||||
<h3 class="mb-3 flex items-center text-lg font-bold text-gray-900 md:mb-4 md:text-xl">
|
||||
<h3
|
||||
class="mb-3 flex items-center text-lg font-bold text-gray-900 dark:text-gray-100 md:mb-4 md:text-xl"
|
||||
>
|
||||
<i class="fas fa-shield-alt mr-2 text-sm text-red-500 md:mr-3 md:text-base" />
|
||||
限制配置
|
||||
</h3>
|
||||
@@ -10,8 +12,10 @@
|
||||
<!-- 每日费用限制 -->
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-600 md:text-base">每日费用限制</span>
|
||||
<span class="text-xs text-gray-500 md:text-sm">
|
||||
<span class="text-sm font-medium text-gray-600 dark:text-gray-400 md:text-base"
|
||||
>每日费用限制</span
|
||||
>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 md:text-sm">
|
||||
<span v-if="statsData.limits.dailyCostLimit > 0">
|
||||
${{ statsData.limits.currentDailyCost.toFixed(4) }} / ${{
|
||||
statsData.limits.dailyCostLimit.toFixed(2)
|
||||
@@ -24,7 +28,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="statsData.limits.dailyCostLimit > 0"
|
||||
class="h-2 w-full rounded-full bg-gray-200"
|
||||
class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700"
|
||||
>
|
||||
<div
|
||||
class="h-2 rounded-full transition-all duration-300"
|
||||
@@ -58,16 +62,16 @@
|
||||
:window-start-time="statsData.limits.windowStartTime"
|
||||
/>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-500">
|
||||
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-info-circle mr-1" />
|
||||
请求次数和Token使用量为"或"的关系,任一达到限制即触发限流
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他限制信息 -->
|
||||
<div class="space-y-2 border-t border-gray-100 pt-2">
|
||||
<div class="space-y-2 border-t border-gray-100 pt-2 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 md:text-base">并发限制</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">并发限制</span>
|
||||
<span class="text-sm font-medium text-gray-900 md:text-base">
|
||||
<span v-if="statsData.limits.concurrencyLimit > 0">
|
||||
{{ statsData.limits.concurrencyLimit }}
|
||||
@@ -78,7 +82,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 md:text-base">模型限制</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">模型限制</span>
|
||||
<span class="text-sm font-medium text-gray-900 md:text-base">
|
||||
<span
|
||||
v-if="
|
||||
@@ -97,7 +101,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 md:text-base">客户端限制</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">客户端限制</span>
|
||||
<span class="text-sm font-medium text-gray-900 md:text-base">
|
||||
<span
|
||||
v-if="
|
||||
@@ -129,7 +133,9 @@
|
||||
"
|
||||
class="card mt-4 p-4 md:mt-6 md:p-6"
|
||||
>
|
||||
<h3 class="mb-3 flex items-center text-lg font-bold text-gray-900 md:mb-4 md:text-xl">
|
||||
<h3
|
||||
class="mb-3 flex items-center text-lg font-bold text-gray-900 dark:text-gray-100 md:mb-4 md:text-xl"
|
||||
>
|
||||
<i class="fas fa-list-alt mr-2 text-sm text-amber-500 md:mr-3 md:text-base" />
|
||||
详细限制信息
|
||||
</h3>
|
||||
@@ -141,9 +147,11 @@
|
||||
statsData.restrictions.enableModelRestriction &&
|
||||
statsData.restrictions.restrictedModels.length > 0
|
||||
"
|
||||
class="rounded-lg border border-amber-200 bg-amber-50 p-3 md:p-4"
|
||||
class="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-900/20 md:p-4"
|
||||
>
|
||||
<h4 class="mb-2 flex items-center text-sm font-bold text-amber-800 md:mb-3 md:text-base">
|
||||
<h4
|
||||
class="mb-2 flex items-center text-sm font-bold text-amber-800 dark:text-amber-300 md:mb-3 md:text-base"
|
||||
>
|
||||
<i class="fas fa-robot mr-1 text-xs md:mr-2 md:text-sm" />
|
||||
受限模型列表
|
||||
</h4>
|
||||
@@ -151,13 +159,13 @@
|
||||
<div
|
||||
v-for="model in statsData.restrictions.restrictedModels"
|
||||
:key="model"
|
||||
class="rounded border border-amber-200 bg-white px-2 py-1 text-xs md:px-3 md:py-2 md:text-sm"
|
||||
class="rounded border border-amber-200 bg-white px-2 py-1 text-xs dark:border-amber-700 dark:bg-gray-800 md:px-3 md:py-2 md:text-sm"
|
||||
>
|
||||
<i class="fas fa-ban mr-1 text-xs text-red-500 md:mr-2" />
|
||||
<span class="break-all text-gray-800">{{ model }}</span>
|
||||
<span class="break-all text-gray-800 dark:text-gray-200">{{ model }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-amber-700 md:mt-3">
|
||||
<p class="mt-2 text-xs text-amber-700 dark:text-amber-400 md:mt-3">
|
||||
<i class="fas fa-info-circle mr-1" />
|
||||
此 API Key 不能访问以上列出的模型
|
||||
</p>
|
||||
@@ -169,9 +177,11 @@
|
||||
statsData.restrictions.enableClientRestriction &&
|
||||
statsData.restrictions.allowedClients.length > 0
|
||||
"
|
||||
class="rounded-lg border border-blue-200 bg-blue-50 p-3 md:p-4"
|
||||
class="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20 md:p-4"
|
||||
>
|
||||
<h4 class="mb-2 flex items-center text-sm font-bold text-blue-800 md:mb-3 md:text-base">
|
||||
<h4
|
||||
class="mb-2 flex items-center text-sm font-bold text-blue-800 dark:text-blue-300 md:mb-3 md:text-base"
|
||||
>
|
||||
<i class="fas fa-desktop mr-1 text-xs md:mr-2 md:text-sm" />
|
||||
允许的客户端
|
||||
</h4>
|
||||
@@ -179,13 +189,13 @@
|
||||
<div
|
||||
v-for="client in statsData.restrictions.allowedClients"
|
||||
:key="client"
|
||||
class="rounded border border-blue-200 bg-white px-2 py-1 text-xs md:px-3 md:py-2 md:text-sm"
|
||||
class="rounded border border-blue-200 bg-white px-2 py-1 text-xs dark:border-blue-700 dark:bg-gray-800 md:px-3 md:py-2 md:text-sm"
|
||||
>
|
||||
<i class="fas fa-check mr-1 text-xs text-green-500 md:mr-2" />
|
||||
<span class="break-all text-gray-800">{{ client }}</span>
|
||||
<span class="break-all text-gray-800 dark:text-gray-200">{{ client }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-blue-700 md:mt-3">
|
||||
<p class="mt-2 text-xs text-blue-700 dark:text-blue-400 md:mt-3">
|
||||
<i class="fas fa-info-circle mr-1" />
|
||||
此 API Key 只能被以上列出的客户端使用
|
||||
</p>
|
||||
@@ -222,11 +232,11 @@ const getDailyCostProgressColor = () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 卡片样式 */
|
||||
/* 卡片样式 - 使用CSS变量 */
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
background: var(--surface-color);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
@@ -251,4 +261,10 @@ const getDailyCostProgressColor = () => {
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.15),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
:global(.dark) .card:hover {
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.5),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<div class="card p-4 md:p-6">
|
||||
<div class="mb-4 md:mb-6">
|
||||
<h3
|
||||
class="flex flex-col text-lg font-bold text-gray-900 sm:flex-row sm:items-center md:text-xl"
|
||||
class="flex flex-col text-lg font-bold text-gray-900 dark:text-gray-100 sm:flex-row sm:items-center md:text-xl"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-robot mr-2 text-sm text-indigo-500 md:mr-3 md:text-base" />
|
||||
模型使用统计
|
||||
</span>
|
||||
<span class="text-xs font-normal text-gray-600 sm:ml-2 md:text-sm"
|
||||
<span class="text-xs font-normal text-gray-600 dark:text-gray-400 sm:ml-2 md:text-sm"
|
||||
>({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span
|
||||
>
|
||||
</h3>
|
||||
@@ -16,8 +16,10 @@
|
||||
|
||||
<!-- 模型统计加载状态 -->
|
||||
<div v-if="modelStatsLoading" class="py-6 text-center md:py-8">
|
||||
<i class="fas fa-spinner loading-spinner mb-2 text-xl text-gray-600 md:text-2xl" />
|
||||
<p class="text-sm text-gray-600 md:text-base">加载模型统计数据中...</p>
|
||||
<i
|
||||
class="fas fa-spinner loading-spinner mb-2 text-xl text-gray-600 dark:text-gray-400 md:text-2xl"
|
||||
/>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 md:text-base">加载模型统计数据中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 模型统计数据 -->
|
||||
@@ -25,41 +27,43 @@
|
||||
<div v-for="(model, index) in modelStats" :key="index" class="model-usage-item">
|
||||
<div class="mb-2 flex items-start justify-between md:mb-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<h4 class="break-all text-base font-bold text-gray-900 md:text-lg">
|
||||
<h4 class="break-all text-base font-bold text-gray-900 dark:text-gray-100 md:text-lg">
|
||||
{{ model.model }}
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 md:text-sm">{{ model.requests }} 次请求</p>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">
|
||||
{{ model.requests }} 次请求
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-3 flex-shrink-0 text-right">
|
||||
<div class="text-base font-bold text-green-600 md:text-lg">
|
||||
{{ model.formatted?.total || '$0.000000' }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 md:text-sm">总费用</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">总费用</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 text-xs md:grid-cols-4 md:gap-3 md:text-sm">
|
||||
<div class="rounded bg-gray-50 p-2">
|
||||
<div class="text-gray-600">输入 Token</div>
|
||||
<div class="font-medium text-gray-900">
|
||||
<div class="rounded bg-gray-50 p-2 dark:bg-gray-700">
|
||||
<div class="text-gray-600 dark:text-gray-400">输入 Token</div>
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(model.inputTokens) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded bg-gray-50 p-2">
|
||||
<div class="text-gray-600">输出 Token</div>
|
||||
<div class="font-medium text-gray-900">
|
||||
<div class="rounded bg-gray-50 p-2 dark:bg-gray-700">
|
||||
<div class="text-gray-600 dark:text-gray-400">输出 Token</div>
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(model.outputTokens) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded bg-gray-50 p-2">
|
||||
<div class="text-gray-600">缓存创建</div>
|
||||
<div class="font-medium text-gray-900">
|
||||
<div class="rounded bg-gray-50 p-2 dark:bg-gray-700">
|
||||
<div class="text-gray-600 dark:text-gray-400">缓存创建</div>
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(model.cacheCreateTokens) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded bg-gray-50 p-2">
|
||||
<div class="text-gray-600">缓存读取</div>
|
||||
<div class="font-medium text-gray-900">
|
||||
<div class="rounded bg-gray-50 p-2 dark:bg-gray-700">
|
||||
<div class="text-gray-600 dark:text-gray-400">缓存读取</div>
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(model.cacheReadTokens) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,7 +72,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 无模型数据 -->
|
||||
<div v-else class="py-6 text-center text-gray-500 md:py-8">
|
||||
<div v-else class="py-6 text-center text-gray-500 dark:text-gray-400 md:py-8">
|
||||
<i class="fas fa-chart-pie mb-3 text-2xl md:text-3xl" />
|
||||
<p class="text-sm md:text-base">
|
||||
暂无{{ statsPeriod === 'daily' ? '今日' : '本月' }}模型使用数据
|
||||
@@ -104,11 +108,11 @@ const formatNumber = (num) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 卡片样式 */
|
||||
/* 卡片样式 - 使用CSS变量 */
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
background: var(--surface-color);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
@@ -134,10 +138,16 @@ const formatNumber = (num) => {
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 模型使用项样式 */
|
||||
:global(.dark) .card:hover {
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.5),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
/* 模型使用项样式 - 使用CSS变量 */
|
||||
.model-usage-item {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
transition: all 0.3s ease;
|
||||
@@ -169,6 +179,13 @@ const formatNumber = (num) => {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
:global(.dark) .model-usage-item:hover {
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.4),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.25);
|
||||
border-color: rgba(75, 85, 99, 0.6);
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading-spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
|
||||
@@ -2,19 +2,22 @@
|
||||
<div class="mb-6 grid grid-cols-1 gap-4 md:mb-8 md:gap-6 lg:grid-cols-2">
|
||||
<!-- API Key 基本信息 -->
|
||||
<div class="card p-4 md:p-6">
|
||||
<h3 class="mb-3 flex items-center text-lg font-bold text-gray-900 md:mb-4 md:text-xl">
|
||||
<h3
|
||||
class="mb-3 flex items-center text-lg font-bold text-gray-900 dark:text-gray-100 md:mb-4 md:text-xl"
|
||||
>
|
||||
<i class="fas fa-info-circle mr-2 text-sm text-blue-500 md:mr-3 md:text-base" />
|
||||
API Key 信息
|
||||
</h3>
|
||||
<div class="space-y-2 md:space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 md:text-base">名称</span>
|
||||
<span class="break-all text-sm font-medium text-gray-900 md:text-base">{{
|
||||
statsData.name
|
||||
}}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">名称</span>
|
||||
<span
|
||||
class="break-all text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base"
|
||||
>{{ statsData.name }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 md:text-base">状态</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">状态</span>
|
||||
<span
|
||||
class="text-sm font-medium md:text-base"
|
||||
:class="statsData.isActive ? 'text-green-600' : 'text-red-600'"
|
||||
@@ -27,19 +30,22 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 md:text-base">权限</span>
|
||||
<span class="text-sm font-medium text-gray-900 md:text-base">{{
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">权限</span>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">{{
|
||||
formatPermissions(statsData.permissions)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600 md:text-base">创建时间</span>
|
||||
<span class="break-all text-xs font-medium text-gray-900 md:text-base">{{
|
||||
formatDate(statsData.createdAt)
|
||||
}}</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 md:text-base">创建时间</span>
|
||||
<span
|
||||
class="break-all text-xs font-medium text-gray-900 dark:text-gray-100 md:text-base"
|
||||
>{{ formatDate(statsData.createdAt) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-start justify-between">
|
||||
<span class="mt-1 flex-shrink-0 text-sm text-gray-600 md:text-base">过期时间</span>
|
||||
<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="isApiKeyExpired(statsData.expiresAt)"
|
||||
@@ -55,11 +61,14 @@
|
||||
<i class="fas fa-clock mr-1 text-xs md:text-sm" />
|
||||
{{ formatExpireDate(statsData.expiresAt) }}
|
||||
</div>
|
||||
<div v-else class="break-all text-xs font-medium text-gray-900 md:text-base">
|
||||
<div
|
||||
v-else
|
||||
class="break-all text-xs font-medium text-gray-900 dark:text-gray-100 md:text-base"
|
||||
>
|
||||
{{ formatExpireDate(statsData.expiresAt) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-sm font-medium text-gray-400 md:text-base">
|
||||
<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" />
|
||||
永不过期
|
||||
</div>
|
||||
@@ -70,13 +79,13 @@
|
||||
<!-- 使用统计概览 -->
|
||||
<div class="card p-4 md:p-6">
|
||||
<h3
|
||||
class="mb-3 flex flex-col text-lg font-bold text-gray-900 sm:flex-row sm:items-center md:mb-4 md:text-xl"
|
||||
class="mb-3 flex flex-col text-lg font-bold text-gray-900 dark:text-gray-100 sm:flex-row sm:items-center md:mb-4 md:text-xl"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-chart-bar mr-2 text-sm text-green-500 md:mr-3 md:text-base" />
|
||||
使用统计概览
|
||||
</span>
|
||||
<span class="text-xs font-normal text-gray-600 sm:ml-2 md:text-sm"
|
||||
<span class="text-xs font-normal text-gray-600 dark:text-gray-400 sm:ml-2 md:text-sm"
|
||||
>({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span
|
||||
>
|
||||
</h3>
|
||||
@@ -85,7 +94,7 @@
|
||||
<div class="text-lg font-bold text-green-600 md:text-3xl">
|
||||
{{ formatNumber(currentPeriodData.requests) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 md:text-sm">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">
|
||||
{{ statsPeriod === 'daily' ? '今日' : '本月' }}请求数
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,7 +102,7 @@
|
||||
<div class="text-lg font-bold text-blue-600 md:text-3xl">
|
||||
{{ formatNumber(currentPeriodData.allTokens) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 md:text-sm">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">
|
||||
{{ statsPeriod === 'daily' ? '今日' : '本月' }}Token数
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,7 +110,7 @@
|
||||
<div class="text-lg font-bold text-purple-600 md:text-3xl">
|
||||
{{ currentPeriodData.formattedCost || '$0.000000' }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 md:text-sm">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">
|
||||
{{ statsPeriod === 'daily' ? '今日' : '本月' }}费用
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,7 +118,7 @@
|
||||
<div class="text-lg font-bold text-yellow-600 md:text-3xl">
|
||||
{{ formatNumber(currentPeriodData.inputTokens) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 md:text-sm">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400 md:text-sm">
|
||||
{{ statsPeriod === 'daily' ? '今日' : '本月' }}输入Token
|
||||
</div>
|
||||
</div>
|
||||
@@ -197,11 +206,11 @@ const formatPermissions = (permissions) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 卡片样式 */
|
||||
/* 卡片样式 - 使用CSS变量 */
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
background: var(--surface-color);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
@@ -227,11 +236,17 @@ const formatPermissions = (permissions) => {
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 统计卡片样式 */
|
||||
:global(.dark) .card:hover {
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.5),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
/* 统计卡片样式 - 使用CSS变量 */
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.8) 100%);
|
||||
background: linear-gradient(135deg, var(--surface-color) 0%, var(--glass-strong-color) 100%);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -264,6 +279,12 @@ const formatPermissions = (permissions) => {
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
:global(.dark) .stat-card:hover {
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.4),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.stat-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
<template>
|
||||
<div class="card p-4 md:p-6">
|
||||
<h3
|
||||
class="mb-3 flex flex-col text-lg font-bold text-gray-900 sm:flex-row sm:items-center md:mb-4 md:text-xl"
|
||||
class="mb-3 flex flex-col text-lg font-bold text-gray-900 dark:text-gray-100 sm:flex-row sm:items-center md:mb-4 md:text-xl"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-coins mr-2 text-sm text-yellow-500 md:mr-3 md:text-base" />
|
||||
Token 使用分布
|
||||
</span>
|
||||
<span class="text-xs font-normal text-gray-600 sm:ml-2 md:text-sm"
|
||||
<span class="text-xs font-normal text-gray-600 dark:text-gray-400 sm:ml-2 md:text-sm"
|
||||
>({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span
|
||||
>
|
||||
</h3>
|
||||
<div class="space-y-2 md:space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center text-sm text-gray-600 md:text-base">
|
||||
<span class="flex items-center text-sm text-gray-600 dark:text-gray-400 md:text-base">
|
||||
<i class="fas fa-arrow-right mr-1 text-xs text-green-500 md:mr-2 md:text-sm" />
|
||||
输入 Token
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-900 md:text-base">{{
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">{{
|
||||
formatNumber(currentPeriodData.inputTokens)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center text-sm text-gray-600 md:text-base">
|
||||
<span class="flex items-center text-sm text-gray-600 dark:text-gray-400 md:text-base">
|
||||
<i class="fas fa-arrow-left mr-1 text-xs text-blue-500 md:mr-2 md:text-sm" />
|
||||
输出 Token
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-900 md:text-base">{{
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">{{
|
||||
formatNumber(currentPeriodData.outputTokens)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center text-sm text-gray-600 md:text-base">
|
||||
<span class="flex items-center text-sm text-gray-600 dark:text-gray-400 md:text-base">
|
||||
<i class="fas fa-save mr-1 text-xs text-purple-500 md:mr-2 md:text-sm" />
|
||||
缓存创建 Token
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-900 md:text-base">{{
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">{{
|
||||
formatNumber(currentPeriodData.cacheCreateTokens)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center text-sm text-gray-600 md:text-base">
|
||||
<span class="flex items-center text-sm text-gray-600 dark:text-gray-400 md:text-base">
|
||||
<i class="fas fa-download mr-1 text-xs text-orange-500 md:mr-2 md:text-sm" />
|
||||
缓存读取 Token
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-900 md:text-base">{{
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">{{
|
||||
formatNumber(currentPeriodData.cacheReadTokens)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 border-t border-gray-200 pt-3 md:mt-4 md:pt-4">
|
||||
<div class="flex items-center justify-between font-bold text-gray-900">
|
||||
<div class="mt-3 border-t border-gray-200 pt-3 dark:border-gray-700 md:mt-4 md:pt-4">
|
||||
<div class="flex items-center justify-between font-bold text-gray-900 dark:text-gray-100">
|
||||
<span class="text-sm md:text-base"
|
||||
>{{ statsPeriod === 'daily' ? '今日' : '本月' }}总计</span
|
||||
>
|
||||
@@ -87,11 +87,11 @@ const formatNumber = (num) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 卡片样式 */
|
||||
/* 卡片样式 - 使用CSS变量 */
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
background: var(--surface-color);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
@@ -116,4 +116,10 @@ const formatNumber = (num) => {
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.15),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
:global(.dark) .card:hover {
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.5),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
<div ref="triggerRef" class="relative">
|
||||
<!-- 选择器主体 -->
|
||||
<div
|
||||
class="form-input flex w-full cursor-pointer items-center justify-between"
|
||||
class="form-input flex w-full cursor-pointer items-center justify-between dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
:class="{ 'opacity-50': disabled }"
|
||||
@click="!disabled && toggleDropdown()"
|
||||
>
|
||||
<span :class="modelValue ? 'text-gray-900' : 'text-gray-500'">{{ selectedLabel }}</span>
|
||||
<span
|
||||
:class="
|
||||
modelValue ? 'text-gray-900 dark:text-gray-200' : 'text-gray-500 dark:text-gray-400'
|
||||
"
|
||||
>{{ selectedLabel }}</span
|
||||
>
|
||||
<i
|
||||
class="fas fa-chevron-down text-gray-400 transition-transform duration-200"
|
||||
class="fas fa-chevron-down text-gray-400 transition-transform duration-200 dark:text-gray-500"
|
||||
:class="{ 'rotate-180': showDropdown }"
|
||||
/>
|
||||
</div>
|
||||
@@ -26,27 +31,27 @@
|
||||
<div
|
||||
v-if="showDropdown"
|
||||
ref="dropdownRef"
|
||||
class="absolute z-50 flex flex-col rounded-lg border border-gray-200 bg-white shadow-lg"
|
||||
class="absolute z-50 flex flex-col rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800"
|
||||
:style="dropdownStyle"
|
||||
>
|
||||
<!-- 搜索框 -->
|
||||
<div class="flex-shrink-0 border-b border-gray-200 p-3">
|
||||
<div class="flex-shrink-0 border-b border-gray-200 p-3 dark:border-gray-600">
|
||||
<div class="relative">
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="searchQuery"
|
||||
class="form-input w-full text-sm"
|
||||
class="form-input w-full text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="搜索账号名称..."
|
||||
style="padding-left: 40px; padding-right: 36px"
|
||||
type="text"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
<i
|
||||
class="fas fa-search pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-gray-400"
|
||||
class="fas fa-search pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-gray-400 dark:text-gray-500"
|
||||
/>
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-400"
|
||||
type="button"
|
||||
@click="clearSearch"
|
||||
>
|
||||
@@ -59,59 +64,67 @@
|
||||
<div class="custom-scrollbar flex-1 overflow-y-auto">
|
||||
<!-- 默认选项 -->
|
||||
<div
|
||||
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50"
|
||||
:class="{ 'bg-blue-50': !modelValue }"
|
||||
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
:class="{ 'bg-blue-50 dark:bg-blue-900/20': !modelValue }"
|
||||
@click="selectAccount(null)"
|
||||
>
|
||||
<span class="text-gray-700">{{ defaultOptionText }}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ defaultOptionText }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 分组选项 -->
|
||||
<div v-if="filteredGroups.length > 0">
|
||||
<div class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500">调度分组</div>
|
||||
<div
|
||||
class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500 dark:bg-gray-700 dark:text-gray-400"
|
||||
>
|
||||
调度分组
|
||||
</div>
|
||||
<div
|
||||
v-for="group in filteredGroups"
|
||||
:key="`group:${group.id}`"
|
||||
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50"
|
||||
:class="{ 'bg-blue-50': modelValue === `group:${group.id}` }"
|
||||
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
:class="{ 'bg-blue-50 dark:bg-blue-900/20': modelValue === `group:${group.id}` }"
|
||||
@click="selectAccount(`group:${group.id}`)"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-700">{{ group.name }}</span>
|
||||
<span class="text-xs text-gray-500">{{ group.memberCount || 0 }} 个成员</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ group.name }}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400"
|
||||
>{{ group.memberCount || 0 }} 个成员</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OAuth 账号 -->
|
||||
<div v-if="filteredOAuthAccounts.length > 0">
|
||||
<div class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500">
|
||||
<div
|
||||
class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500 dark:bg-gray-700 dark:text-gray-400"
|
||||
>
|
||||
{{ platform === 'claude' ? 'Claude OAuth 专属账号' : 'OAuth 专属账号' }}
|
||||
</div>
|
||||
<div
|
||||
v-for="account in filteredOAuthAccounts"
|
||||
:key="account.id"
|
||||
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50"
|
||||
:class="{ 'bg-blue-50': modelValue === account.id }"
|
||||
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
:class="{ 'bg-blue-50 dark:bg-blue-900/20': modelValue === account.id }"
|
||||
@click="selectAccount(account.id)"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<span class="text-gray-700">{{ account.name }}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ account.name }}</span>
|
||||
<span
|
||||
class="ml-2 rounded-full px-2 py-0.5 text-xs"
|
||||
:class="
|
||||
account.isActive
|
||||
? 'bg-green-100 text-green-700'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
||||
: account.status === 'unauthorized'
|
||||
? 'bg-orange-100 text-orange-700'
|
||||
: 'bg-red-100 text-red-700'
|
||||
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||||
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||
"
|
||||
>
|
||||
{{ getAccountStatusText(account) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400">
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ formatDate(account.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -120,33 +133,37 @@
|
||||
|
||||
<!-- Console 账号(仅 Claude) -->
|
||||
<div v-if="platform === 'claude' && filteredConsoleAccounts.length > 0">
|
||||
<div class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500">
|
||||
<div
|
||||
class="bg-gray-50 px-4 py-2 text-xs font-semibold text-gray-500 dark:bg-gray-700 dark:text-gray-400"
|
||||
>
|
||||
Claude Console 专属账号
|
||||
</div>
|
||||
<div
|
||||
v-for="account in filteredConsoleAccounts"
|
||||
:key="account.id"
|
||||
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50"
|
||||
:class="{ 'bg-blue-50': modelValue === `console:${account.id}` }"
|
||||
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
:class="{
|
||||
'bg-blue-50 dark:bg-blue-900/20': modelValue === `console:${account.id}`
|
||||
}"
|
||||
@click="selectAccount(`console:${account.id}`)"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<span class="text-gray-700">{{ account.name }}</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ account.name }}</span>
|
||||
<span
|
||||
class="ml-2 rounded-full px-2 py-0.5 text-xs"
|
||||
:class="
|
||||
account.isActive
|
||||
? 'bg-green-100 text-green-700'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
||||
: account.status === 'unauthorized'
|
||||
? 'bg-orange-100 text-orange-700'
|
||||
: 'bg-red-100 text-red-700'
|
||||
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||||
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||
"
|
||||
>
|
||||
{{ getAccountStatusText(account) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400">
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ formatDate(account.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -154,7 +171,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 无搜索结果 -->
|
||||
<div v-if="searchQuery && !hasResults" class="px-4 py-8 text-center text-gray-500">
|
||||
<div
|
||||
v-if="searchQuery && !hasResults"
|
||||
class="px-4 py-8 text-center text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<i class="fas fa-search mb-2 text-2xl" />
|
||||
<p class="text-sm">没有找到匹配的账号</p>
|
||||
</div>
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
<i class="fas fa-exclamation-triangle text-lg text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="mb-2 text-lg font-semibold text-gray-900">
|
||||
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div class="whitespace-pre-line leading-relaxed text-gray-600">
|
||||
<div class="whitespace-pre-line leading-relaxed text-gray-700 dark:text-gray-400">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<button
|
||||
class="btn bg-gray-100 px-6 py-3 text-gray-700 hover:bg-gray-200"
|
||||
class="btn 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="isProcessing"
|
||||
@click="handleCancel"
|
||||
>
|
||||
@@ -141,6 +141,10 @@ defineExpose({
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
:global(.dark) .modal {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
@@ -150,6 +154,12 @@ defineExpose({
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:global(.dark) .modal-content {
|
||||
background: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
box-shadow: 0 20px 64px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2;
|
||||
}
|
||||
@@ -197,12 +207,24 @@ defineExpose({
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:global(.dark) .modal-content::-webkit-scrollbar-track {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:global(.dark) .modal-content::-webkit-scrollbar-thumb {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
:global(.dark) .modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #6b7280;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="show" class="modal fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div class="modal-content mx-auto w-full max-w-md p-6">
|
||||
<div
|
||||
class="modal-content mx-auto w-full max-w-md rounded-2xl bg-white p-6 shadow-xl dark:bg-gray-800"
|
||||
>
|
||||
<div class="mb-6 flex items-start gap-4">
|
||||
<div
|
||||
class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-yellow-400 to-yellow-500"
|
||||
@@ -9,10 +11,10 @@
|
||||
<i class="fas fa-exclamation text-xl text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="mb-2 text-lg font-bold text-gray-900">
|
||||
<h3 class="mb-2 text-lg font-bold text-gray-900 dark:text-white">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p class="whitespace-pre-line text-sm leading-relaxed text-gray-600">
|
||||
<p class="whitespace-pre-line text-sm leading-relaxed text-gray-700 dark:text-gray-300">
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -20,7 +22,7 @@
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
class="flex-1 rounded-xl bg-gray-100 px-4 py-2.5 font-medium text-gray-700 transition-colors hover:bg-gray-200"
|
||||
class="flex-1 rounded-xl bg-gray-100 px-4 py-2.5 font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||
@click="$emit('cancel')"
|
||||
>
|
||||
{{ cancelText }}
|
||||
@@ -63,3 +65,15 @@ defineProps({
|
||||
|
||||
defineEmits(['confirm', 'cancel'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
:global(.dark) .modal {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,17 +3,19 @@
|
||||
<!-- 触发器 -->
|
||||
<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="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 dark:border-gray-600 dark:bg-gray-800"
|
||||
: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">
|
||||
<span
|
||||
class="select-none whitespace-nowrap text-sm font-medium text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
{{ selectedLabel || placeholder }}
|
||||
</span>
|
||||
<i
|
||||
:class="[
|
||||
'fas fa-chevron-down ml-auto text-xs text-gray-400 transition-transform duration-200',
|
||||
'fas fa-chevron-down ml-auto text-xs text-gray-400 transition-transform duration-200 dark:text-gray-500',
|
||||
isOpen && 'rotate-180'
|
||||
]"
|
||||
></i>
|
||||
@@ -32,7 +34,7 @@
|
||||
<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"
|
||||
class="fixed z-[9999] min-w-max overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800"
|
||||
:style="dropdownStyle"
|
||||
>
|
||||
<div class="max-h-60 overflow-y-auto py-1">
|
||||
@@ -42,8 +44,8 @@
|
||||
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'
|
||||
? 'bg-blue-50 font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
: 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||
]"
|
||||
@click="selectOption(option)"
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Logo区域 -->
|
||||
<div
|
||||
class="flex h-12 w-12 flex-shrink-0 items-center justify-center overflow-hidden rounded-xl border border-gray-300/30 bg-gradient-to-br from-blue-500/20 to-purple-500/20 backdrop-blur-sm"
|
||||
class="flex h-12 w-12 flex-shrink-0 items-center justify-center overflow-hidden rounded-xl border border-gray-300/30 bg-gradient-to-br from-blue-500/20 to-purple-500/20 backdrop-blur-sm dark:border-gray-600/30 dark:from-blue-600/20 dark:to-purple-600/20"
|
||||
>
|
||||
<template v-if="!loading">
|
||||
<img
|
||||
@@ -12,9 +12,9 @@
|
||||
:src="logoSrc"
|
||||
@error="handleLogoError"
|
||||
/>
|
||||
<i v-else class="fas fa-cloud text-xl text-gray-700" />
|
||||
<i v-else class="fas fa-cloud text-xl text-gray-700 dark:text-gray-300" />
|
||||
</template>
|
||||
<div v-else class="h-8 w-8 animate-pulse rounded bg-gray-300/50" />
|
||||
<div v-else class="h-8 w-8 animate-pulse rounded bg-gray-300/50 dark:bg-gray-600/50" />
|
||||
</div>
|
||||
|
||||
<!-- 标题区域 -->
|
||||
@@ -25,11 +25,14 @@
|
||||
{{ title }}
|
||||
</h1>
|
||||
</template>
|
||||
<div v-else-if="loading" class="h-8 w-64 animate-pulse rounded bg-gray-300/50" />
|
||||
<div
|
||||
v-else-if="loading"
|
||||
class="h-8 w-64 animate-pulse rounded bg-gray-300/50 dark:bg-gray-600/50"
|
||||
/>
|
||||
<!-- 插槽用于版本信息等额外内容 -->
|
||||
<slot name="after-title" />
|
||||
</div>
|
||||
<p v-if="subtitle" class="mt-0.5 text-sm leading-tight text-gray-600">
|
||||
<p v-if="subtitle" class="mt-0.5 text-sm leading-tight text-gray-600 dark:text-gray-400">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
<div class="stat-card">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<p class="mb-1 text-xs font-medium text-gray-600 sm:text-sm">
|
||||
<p class="mb-1 text-xs font-medium text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
{{ title }}
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-gray-800 sm:text-3xl">
|
||||
<p class="text-2xl font-bold text-gray-800 dark:text-gray-100 sm:text-3xl">
|
||||
{{ value }}
|
||||
</p>
|
||||
<p v-if="subtitle" class="mt-1.5 text-xs text-gray-500 sm:mt-2 sm:text-sm">
|
||||
<p
|
||||
v-if="subtitle"
|
||||
class="mt-1.5 text-xs text-gray-500 dark:text-gray-400 sm:mt-2 sm:text-sm"
|
||||
>
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
573
web/admin-spa/src/components/common/ThemeToggle.vue
Normal file
573
web/admin-spa/src/components/common/ThemeToggle.vue
Normal file
@@ -0,0 +1,573 @@
|
||||
<template>
|
||||
<div class="theme-toggle-container">
|
||||
<!-- 紧凑模式:仅显示图标按钮 -->
|
||||
<button
|
||||
v-if="mode === 'compact'"
|
||||
class="theme-toggle-button"
|
||||
:title="themeTooltip"
|
||||
@click="handleCycleTheme"
|
||||
>
|
||||
<transition mode="out-in" name="fade">
|
||||
<i v-if="themeStore.themeMode === 'light'" key="sun" class="fas fa-sun" />
|
||||
<i v-else-if="themeStore.themeMode === 'dark'" key="moon" class="fas fa-moon" />
|
||||
<i v-else key="auto" class="fas fa-circle-half-stroke" />
|
||||
</transition>
|
||||
</button>
|
||||
|
||||
<!-- 下拉菜单模式 - 改为创意切换开关 -->
|
||||
<div v-else-if="mode === 'dropdown'" class="theme-switch-wrapper">
|
||||
<button
|
||||
class="theme-switch"
|
||||
:class="{
|
||||
'is-dark': themeStore.themeMode === 'dark',
|
||||
'is-auto': themeStore.themeMode === 'auto'
|
||||
}"
|
||||
:title="themeTooltip"
|
||||
@click="handleCycleTheme"
|
||||
>
|
||||
<!-- 背景装饰 -->
|
||||
<div class="switch-bg">
|
||||
<div class="stars">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="clouds">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 切换滑块 -->
|
||||
<div class="switch-handle">
|
||||
<div class="handle-icon">
|
||||
<i v-if="themeStore.themeMode === 'light'" class="fas fa-sun" />
|
||||
<i v-else-if="themeStore.themeMode === 'dark'" class="fas fa-moon" />
|
||||
<i v-else class="fas fa-circle-half-stroke" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 分段按钮模式 -->
|
||||
<div v-else-if="mode === 'segmented'" class="theme-segmented">
|
||||
<button
|
||||
v-for="option in themeOptions"
|
||||
:key="option.value"
|
||||
class="theme-segment"
|
||||
:class="{ active: themeStore.themeMode === option.value }"
|
||||
:title="option.label"
|
||||
@click="selectTheme(option.value)"
|
||||
>
|
||||
<i :class="option.icon" />
|
||||
<span v-if="showLabel" class="ml-1 hidden sm:inline">{{ option.shortLabel }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
|
||||
// Props
|
||||
defineProps({
|
||||
// 显示模式:compact(紧凑)、dropdown(下拉)、segmented(分段)
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'compact',
|
||||
validator: (value) => ['compact', 'dropdown', 'segmented'].includes(value)
|
||||
},
|
||||
// 是否显示文字标签
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Store
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
// 主题选项配置
|
||||
const themeOptions = [
|
||||
{
|
||||
value: 'light',
|
||||
label: '浅色模式',
|
||||
shortLabel: '浅色',
|
||||
icon: 'fas fa-sun'
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: '深色模式',
|
||||
shortLabel: '深色',
|
||||
icon: 'fas fa-moon'
|
||||
},
|
||||
{
|
||||
value: 'auto',
|
||||
label: '跟随系统',
|
||||
shortLabel: '自动',
|
||||
icon: 'fas fa-circle-half-stroke'
|
||||
}
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const themeTooltip = computed(() => {
|
||||
const current = themeOptions.find((opt) => opt.value === themeStore.themeMode)
|
||||
return current ? `点击切换主题 - ${current.label}` : '切换主题'
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleCycleTheme = () => {
|
||||
themeStore.cycleThemeMode()
|
||||
}
|
||||
|
||||
const selectTheme = (mode) => {
|
||||
themeStore.setThemeMode(mode)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 容器样式 */
|
||||
.theme-toggle-container {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 基础按钮样式 - 更简洁优雅 */
|
||||
.theme-toggle-button {
|
||||
@apply flex items-center justify-center;
|
||||
@apply h-9 w-9 rounded-full;
|
||||
@apply bg-white/80 dark:bg-gray-800/80;
|
||||
@apply hover:bg-white/90 dark:hover:bg-gray-700/90;
|
||||
@apply text-gray-600 dark:text-gray-300;
|
||||
@apply border border-gray-200/50 dark:border-gray-600/50;
|
||||
@apply transition-all duration-200 ease-out;
|
||||
@apply shadow-md backdrop-blur-sm hover:shadow-lg;
|
||||
@apply hover:scale-110 active:scale-95;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 添加优雅的光环效果 */
|
||||
.theme-toggle-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
border-radius: inherit;
|
||||
background: conic-gradient(
|
||||
from 180deg at 50% 50%,
|
||||
rgba(59, 130, 246, 0.2),
|
||||
rgba(147, 51, 234, 0.2),
|
||||
rgba(59, 130, 246, 0.2)
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
animation: rotate 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-toggle-button:hover::before {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 图标样式优化 - 更生动 */
|
||||
.theme-toggle-button i {
|
||||
@apply text-base;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
|
||||
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.theme-toggle-button:hover i {
|
||||
transform: rotate(180deg) scale(1.1);
|
||||
}
|
||||
|
||||
/* 不同主题的图标颜色 */
|
||||
.theme-toggle-button i.fa-sun {
|
||||
@apply text-amber-500;
|
||||
}
|
||||
|
||||
.theme-toggle-button i.fa-moon {
|
||||
@apply text-indigo-500;
|
||||
}
|
||||
|
||||
.theme-toggle-button i.fa-circle-half-stroke {
|
||||
background: linear-gradient(90deg, #60a5fa 0%, #2563eb 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* 创意切换开关样式 */
|
||||
.theme-switch-wrapper {
|
||||
@apply inline-flex items-center;
|
||||
}
|
||||
|
||||
.theme-switch {
|
||||
@apply relative;
|
||||
width: 76px;
|
||||
height: 38px;
|
||||
border-radius: 50px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 4px 15px rgba(102, 126, 234, 0.3),
|
||||
inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.theme-switch:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(102, 126, 234, 0.4),
|
||||
inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.theme-switch:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 深色模式样式 */
|
||||
.theme-switch.is-dark {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
||||
border-color: rgba(148, 163, 184, 0.2);
|
||||
box-shadow:
|
||||
0 4px 15px rgba(0, 0, 0, 0.5),
|
||||
inset 0 1px 2px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.theme-switch.is-dark:hover {
|
||||
box-shadow:
|
||||
0 6px 20px rgba(0, 0, 0, 0.6),
|
||||
inset 0 1px 2px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* 自动模式样式 - 静态蓝紫渐变设计(优化版) */
|
||||
.theme-switch.is-auto {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#c4b5fd 0%,
|
||||
/* 更柔和的起始:淡紫 */ #a78bfa 15%,
|
||||
/* 浅紫 */ #818cf8 40%,
|
||||
/* 紫蓝 */ #6366f1 60%,
|
||||
/* 靛蓝 */ #4f46e5 85%,
|
||||
/* 深蓝紫 */ #4338ca 100% /* 更深的结束:深紫 */
|
||||
);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-size: 120% 120%;
|
||||
background-position: center;
|
||||
box-shadow:
|
||||
0 4px 15px rgba(139, 92, 246, 0.25),
|
||||
inset 0 1px 3px rgba(0, 0, 0, 0.1),
|
||||
inset 0 -1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 自动模式的分割线效果 */
|
||||
.theme-switch.is-auto::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 10%;
|
||||
bottom: 10%;
|
||||
width: 1px;
|
||||
background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
transform: translateX(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 背景装饰 */
|
||||
.switch-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 星星装饰(深色模式) */
|
||||
.stars {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.theme-switch.is-dark .stars {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.stars span {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 2px white;
|
||||
animation: twinkle 3s infinite;
|
||||
}
|
||||
|
||||
.stars span:nth-child(1) {
|
||||
top: 25%;
|
||||
left: 20%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.stars span:nth-child(2) {
|
||||
top: 40%;
|
||||
left: 40%;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.stars span:nth-child(3) {
|
||||
top: 60%;
|
||||
left: 25%;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 云朵装饰(浅色模式) */
|
||||
.clouds {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.theme-switch:not(.is-dark):not(.is-auto) .clouds {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.clouds span {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.clouds span:nth-child(1) {
|
||||
width: 20px;
|
||||
height: 8px;
|
||||
top: 40%;
|
||||
left: 15%;
|
||||
animation: float 4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.clouds span:nth-child(2) {
|
||||
width: 15px;
|
||||
height: 6px;
|
||||
top: 60%;
|
||||
left: 35%;
|
||||
animation: float 4s infinite ease-in-out;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 切换滑块 */
|
||||
.switch-handle {
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.2),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(0);
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
/* 深色模式滑块位置 */
|
||||
.theme-switch.is-dark .switch-handle {
|
||||
transform: translateY(-50%) translateX(38px);
|
||||
background: linear-gradient(135deg, #1e293b 0%, #475569 100%);
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 自动模式滑块位置 - 玻璃态设计 */
|
||||
.theme-switch.is-auto .switch-handle {
|
||||
transform: translateY(-50%) translateX(19px);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.1),
|
||||
inset 0 0 8px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 滑块图标 */
|
||||
.handle-icon {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.handle-icon i {
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.handle-icon .fa-sun {
|
||||
color: #f59e0b;
|
||||
filter: drop-shadow(0 0 3px rgba(245, 158, 11, 0.5));
|
||||
}
|
||||
|
||||
.handle-icon .fa-moon {
|
||||
color: #fbbf24;
|
||||
filter: drop-shadow(0 0 3px rgba(251, 191, 36, 0.5));
|
||||
}
|
||||
|
||||
.handle-icon .fa-circle-half-stroke {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
filter: drop-shadow(0 0 4px rgba(167, 139, 250, 0.5));
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* 滑块悬停动画 */
|
||||
.theme-switch:hover .switch-handle {
|
||||
animation: bounce 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(-50%) translateX(var(--handle-x, 0));
|
||||
}
|
||||
50% {
|
||||
transform: translateY(calc(-50% - 3px)) translateX(var(--handle-x, 0));
|
||||
}
|
||||
}
|
||||
|
||||
.theme-switch.is-dark:hover .switch-handle {
|
||||
--handle-x: 38px;
|
||||
}
|
||||
|
||||
.theme-switch.is-auto:hover .switch-handle {
|
||||
--handle-x: 19px;
|
||||
}
|
||||
|
||||
/* 分段按钮样式 - 更现代 */
|
||||
.theme-segmented {
|
||||
@apply inline-flex;
|
||||
@apply bg-gray-100 dark:bg-gray-800;
|
||||
@apply rounded-full p-1;
|
||||
@apply border border-gray-200 dark:border-gray-700;
|
||||
@apply shadow-sm;
|
||||
}
|
||||
|
||||
.theme-segment {
|
||||
@apply px-3 py-1.5;
|
||||
@apply text-xs font-medium;
|
||||
@apply text-gray-500 dark:text-gray-400;
|
||||
@apply transition-all duration-200;
|
||||
@apply rounded-full;
|
||||
@apply flex items-center gap-1;
|
||||
@apply cursor-pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.theme-segment:hover {
|
||||
@apply text-gray-700 dark:text-gray-300;
|
||||
@apply bg-white/30 dark:bg-gray-600/30;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.theme-segment.active {
|
||||
@apply bg-white dark:bg-gray-700;
|
||||
@apply text-gray-900 dark:text-white;
|
||||
@apply shadow-sm;
|
||||
}
|
||||
|
||||
.theme-segment i {
|
||||
@apply text-xs;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* 过渡动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dropdown-enter-active {
|
||||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.dropdown-leave-active {
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 1, 1);
|
||||
}
|
||||
|
||||
.dropdown-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
}
|
||||
|
||||
.dropdown-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px) scale(0.98);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 640px) {
|
||||
.theme-dropdown {
|
||||
@apply left-0 right-auto;
|
||||
}
|
||||
|
||||
.theme-segment span {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -162,6 +162,12 @@ defineExpose({
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
:global(.dark) .toast {
|
||||
background: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.toast-show {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
@@ -227,6 +233,11 @@ defineExpose({
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
:global(.dark) .toast-close:hover {
|
||||
background: #374151;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.toast-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -256,14 +267,26 @@ defineExpose({
|
||||
background: #d1fae5;
|
||||
}
|
||||
|
||||
:global(.dark) .toast-success .toast-icon {
|
||||
background: #064e3b;
|
||||
}
|
||||
|
||||
.toast-success .toast-title {
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
:global(.dark) .toast-success .toast-title {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.toast-success .toast-message {
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
:global(.dark) .toast-success .toast-message {
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
.toast-success .toast-progress {
|
||||
background: #10b981;
|
||||
}
|
||||
@@ -278,14 +301,26 @@ defineExpose({
|
||||
background: #fee2e2;
|
||||
}
|
||||
|
||||
:global(.dark) .toast-error .toast-icon {
|
||||
background: #7f1d1d;
|
||||
}
|
||||
|
||||
.toast-error .toast-title {
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
:global(.dark) .toast-error .toast-title {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.toast-error .toast-message {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
:global(.dark) .toast-error .toast-message {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.toast-error .toast-progress {
|
||||
background: #ef4444;
|
||||
}
|
||||
@@ -300,14 +335,26 @@ defineExpose({
|
||||
background: #fef3c7;
|
||||
}
|
||||
|
||||
:global(.dark) .toast-warning .toast-icon {
|
||||
background: #78350f;
|
||||
}
|
||||
|
||||
.toast-warning .toast-title {
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
:global(.dark) .toast-warning .toast-title {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.toast-warning .toast-message {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
:global(.dark) .toast-warning .toast-message {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.toast-warning .toast-progress {
|
||||
background: #f59e0b;
|
||||
}
|
||||
@@ -322,14 +369,26 @@ defineExpose({
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
:global(.dark) .toast-info .toast-icon {
|
||||
background: #1e3a8a;
|
||||
}
|
||||
|
||||
.toast-info .toast-title {
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
:global(.dark) .toast-info .toast-title {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.toast-info .toast-message {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
:global(.dark) .toast-info .toast-message {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.toast-info .toast-progress {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
:logo-src="oemSettings.siteIconData || oemSettings.siteIcon"
|
||||
subtitle="管理后台"
|
||||
:title="oemSettings.siteName"
|
||||
title-class="text-white"
|
||||
title-class="text-white dark:text-gray-100"
|
||||
>
|
||||
<template #after-title>
|
||||
<!-- 版本信息 -->
|
||||
<div class="flex items-center gap-1 sm:gap-2">
|
||||
<span class="font-mono text-xs text-gray-400 sm:text-sm"
|
||||
<span class="font-mono text-xs text-gray-400 dark:text-gray-500 sm:text-sm"
|
||||
>v{{ versionInfo.current || '...' }}</span
|
||||
>
|
||||
<!-- 更新提示 -->
|
||||
@@ -36,95 +36,112 @@
|
||||
</template>
|
||||
</LogoTitle>
|
||||
</div>
|
||||
<!-- 用户菜单 -->
|
||||
<div class="user-menu-container relative">
|
||||
<button
|
||||
class="btn btn-primary relative flex items-center gap-1 px-3 py-2 text-sm sm:gap-2 sm:px-4 sm:py-3 sm:text-base"
|
||||
@click="userMenuOpen = !userMenuOpen"
|
||||
>
|
||||
<i class="fas fa-user-circle" />
|
||||
<span class="hidden sm:inline">{{ currentUser.username || 'Admin' }}</span>
|
||||
<i
|
||||
class="fas fa-chevron-down text-xs transition-transform duration-200"
|
||||
:class="{ 'rotate-180': userMenuOpen }"
|
||||
/>
|
||||
</button>
|
||||
<!-- 主题切换和用户菜单 -->
|
||||
<div class="flex items-center gap-2 sm:gap-4">
|
||||
<!-- 主题切换按钮 -->
|
||||
<div class="flex items-center">
|
||||
<ThemeToggle mode="dropdown" />
|
||||
</div>
|
||||
|
||||
<!-- 悬浮菜单 -->
|
||||
<!-- 分隔线 -->
|
||||
<div
|
||||
v-if="userMenuOpen"
|
||||
class="user-menu-dropdown absolute right-0 top-full mt-2 w-48 rounded-xl border border-gray-200 bg-white py-2 shadow-xl sm:w-56"
|
||||
style="z-index: 999999"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 版本信息 -->
|
||||
<div class="border-b border-gray-100 px-4 py-3">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-500">当前版本</span>
|
||||
<span class="font-mono text-gray-700">v{{ versionInfo.current || '...' }}</span>
|
||||
</div>
|
||||
<div v-if="versionInfo.hasUpdate" class="mt-2">
|
||||
<div class="mb-2 flex items-center justify-between text-sm">
|
||||
<span class="font-medium text-green-600">
|
||||
<i class="fas fa-arrow-up mr-1" />有新版本
|
||||
</span>
|
||||
<span class="font-mono text-green-600">v{{ versionInfo.latest }}</span>
|
||||
class="h-8 w-px bg-gradient-to-b from-transparent via-gray-300 to-transparent opacity-50 dark:via-gray-600"
|
||||
/>
|
||||
|
||||
<!-- 用户菜单 -->
|
||||
<div class="user-menu-container relative">
|
||||
<button
|
||||
class="user-menu-button flex items-center gap-2 rounded-2xl bg-gradient-to-r from-blue-500 to-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-lg transition-all duration-200 hover:scale-105 hover:shadow-xl active:scale-95 sm:px-4 sm:py-2.5"
|
||||
@click="userMenuOpen = !userMenuOpen"
|
||||
>
|
||||
<i class="fas fa-user-circle text-sm sm:text-base" />
|
||||
<span class="hidden sm:inline">{{ currentUser.username || 'Admin' }}</span>
|
||||
<i
|
||||
class="fas fa-chevron-down ml-1 text-xs transition-transform duration-200"
|
||||
:class="{ 'rotate-180': userMenuOpen }"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- 悬浮菜单 -->
|
||||
<div
|
||||
v-if="userMenuOpen"
|
||||
class="user-menu-dropdown absolute right-0 top-full mt-2 w-48 rounded-xl border border-gray-200 bg-white py-2 shadow-xl dark:border-gray-700 dark:bg-gray-800 sm:w-56"
|
||||
style="z-index: 999999"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 版本信息 -->
|
||||
<div class="border-b border-gray-100 px-4 py-3 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">当前版本</span>
|
||||
<span class="font-mono text-gray-700 dark:text-gray-300"
|
||||
>v{{ versionInfo.current || '...' }}</span
|
||||
>
|
||||
</div>
|
||||
<a
|
||||
class="block w-full rounded-lg bg-green-500 px-3 py-1.5 text-center text-sm text-white transition-colors hover:bg-green-600"
|
||||
:href="versionInfo.releaseInfo?.htmlUrl || '#'"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="fas fa-external-link-alt mr-1" />查看更新
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="versionInfo.checkingUpdate"
|
||||
class="mt-2 text-center text-xs text-gray-500"
|
||||
>
|
||||
<i class="fas fa-spinner fa-spin mr-1" />检查更新中...
|
||||
</div>
|
||||
<div v-else class="mt-2 text-center">
|
||||
<!-- 已是最新版提醒 -->
|
||||
<transition mode="out-in" name="fade">
|
||||
<div
|
||||
v-if="versionInfo.noUpdateMessage"
|
||||
key="message"
|
||||
class="inline-block rounded-lg border border-green-200 bg-green-100 px-3 py-1.5"
|
||||
>
|
||||
<p class="text-xs font-medium text-green-700">
|
||||
<i class="fas fa-check-circle mr-1" />当前已是最新版本
|
||||
</p>
|
||||
<div v-if="versionInfo.hasUpdate" class="mt-2">
|
||||
<div class="mb-2 flex items-center justify-between text-sm">
|
||||
<span class="font-medium text-green-600 dark:text-green-400">
|
||||
<i class="fas fa-arrow-up mr-1" />有新版本
|
||||
</span>
|
||||
<span class="font-mono text-green-600 dark:text-green-400"
|
||||
>v{{ versionInfo.latest }}</span
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
key="button"
|
||||
class="text-xs text-blue-500 transition-colors hover:text-blue-700"
|
||||
@click="checkForUpdates()"
|
||||
<a
|
||||
class="block w-full rounded-lg bg-green-500 px-3 py-1.5 text-center text-sm text-white transition-colors hover:bg-green-600"
|
||||
:href="versionInfo.releaseInfo?.htmlUrl || '#'"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="fas fa-sync-alt mr-1" />检查更新
|
||||
</button>
|
||||
</transition>
|
||||
<i class="fas fa-external-link-alt mr-1" />查看更新
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="versionInfo.checkingUpdate"
|
||||
class="mt-2 text-center text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<i class="fas fa-spinner fa-spin mr-1" />检查更新中...
|
||||
</div>
|
||||
<div v-else class="mt-2 text-center">
|
||||
<!-- 已是最新版提醒 -->
|
||||
<transition mode="out-in" name="fade">
|
||||
<div
|
||||
v-if="versionInfo.noUpdateMessage"
|
||||
key="message"
|
||||
class="inline-block rounded-lg border border-green-200 bg-green-100 px-3 py-1.5 dark:border-green-800 dark:bg-green-900/30"
|
||||
>
|
||||
<p class="text-xs font-medium text-green-700 dark:text-green-400">
|
||||
<i class="fas fa-check-circle mr-1" />当前已是最新版本
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
key="button"
|
||||
class="text-xs text-blue-500 transition-colors hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
@click="checkForUpdates()"
|
||||
>
|
||||
<i class="fas fa-sync-alt mr-1" />检查更新
|
||||
</button>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-left text-gray-700 transition-colors hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
@click="openChangePasswordModal"
|
||||
>
|
||||
<i class="fas fa-key text-blue-500" />
|
||||
<span>修改账户信息</span>
|
||||
</button>
|
||||
|
||||
<hr class="my-2 border-gray-200 dark:border-gray-700" />
|
||||
|
||||
<button
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-left text-gray-700 transition-colors hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
@click="logout"
|
||||
>
|
||||
<i class="fas fa-sign-out-alt text-red-500" />
|
||||
<span>退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-left text-gray-700 transition-colors hover:bg-gray-50"
|
||||
@click="openChangePasswordModal"
|
||||
>
|
||||
<i class="fas fa-key text-blue-500" />
|
||||
<span>修改账户信息</span>
|
||||
</button>
|
||||
|
||||
<hr class="my-2 border-gray-200" />
|
||||
|
||||
<button
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-left text-gray-700 transition-colors hover:bg-gray-50"
|
||||
@click="logout"
|
||||
>
|
||||
<i class="fas fa-sign-out-alt text-red-500" />
|
||||
<span>退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,10 +160,10 @@
|
||||
>
|
||||
<i class="fas fa-key text-white" />
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900">修改账户信息</h3>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">修改账户信息</h3>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 transition-colors hover:text-gray-600"
|
||||
class="text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-gray-300"
|
||||
@click="closeChangePasswordModal"
|
||||
>
|
||||
<i class="fas fa-times text-xl" />
|
||||
@@ -158,29 +175,37 @@
|
||||
@submit.prevent="changePassword"
|
||||
>
|
||||
<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 dark:text-gray-300"
|
||||
>当前用户名</label
|
||||
>
|
||||
<input
|
||||
class="form-input w-full cursor-not-allowed bg-gray-100"
|
||||
class="form-input w-full cursor-not-allowed bg-gray-100 dark:bg-gray-700 dark:text-gray-300"
|
||||
disabled
|
||||
type="text"
|
||||
:value="currentUser.username || 'Admin'"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-gray-500">当前用户名,输入新用户名以修改</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
当前用户名,输入新用户名以修改
|
||||
</p>
|
||||
</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 dark:text-gray-300"
|
||||
>新用户名</label
|
||||
>
|
||||
<input
|
||||
v-model="changePasswordForm.newUsername"
|
||||
class="form-input w-full"
|
||||
placeholder="输入新用户名(留空保持不变)"
|
||||
type="text"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-gray-500">留空表示不修改用户名</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">留空表示不修改用户名</p>
|
||||
</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 dark:text-gray-300"
|
||||
>当前密码</label
|
||||
>
|
||||
<input
|
||||
v-model="changePasswordForm.currentPassword"
|
||||
class="form-input w-full"
|
||||
@@ -191,7 +216,9 @@
|
||||
</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 dark:text-gray-300"
|
||||
>新密码</label
|
||||
>
|
||||
<input
|
||||
v-model="changePasswordForm.newPassword"
|
||||
class="form-input w-full"
|
||||
@@ -199,11 +226,13 @@
|
||||
required
|
||||
type="password"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-gray-500">密码长度至少8位</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">密码长度至少8位</p>
|
||||
</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 dark:text-gray-300"
|
||||
>确认新密码</label
|
||||
>
|
||||
<input
|
||||
v-model="changePasswordForm.confirmPassword"
|
||||
class="form-input w-full"
|
||||
@@ -215,7 +244,7 @@
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
class="flex-1 rounded-xl bg-gray-100 px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-200"
|
||||
class="flex-1 rounded-xl bg-gray-100 px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
type="button"
|
||||
@click="closeChangePasswordModal"
|
||||
>
|
||||
@@ -243,6 +272,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import { apiClient } from '@/config/api'
|
||||
import LogoTitle from '@/components/common/LogoTitle.vue'
|
||||
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
@@ -430,9 +460,44 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 用户菜单按钮样式 */
|
||||
.user-menu-button {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
/* 添加光泽效果 */
|
||||
.user-menu-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.user-menu-button:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
/* 用户菜单样式优化 */
|
||||
.user-menu-dropdown {
|
||||
margin-top: 8px;
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* fade过渡动画 */
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="mb-4 sm:mb-6">
|
||||
<!-- 移动端下拉选择器 -->
|
||||
<div class="block rounded-xl bg-white/10 p-2 backdrop-blur-sm sm:hidden">
|
||||
<div class="block rounded-xl bg-white/10 p-2 backdrop-blur-sm dark:bg-gray-800/20 sm:hidden">
|
||||
<select
|
||||
class="focus:ring-primary-color w-full rounded-lg bg-white/90 px-4 py-3 font-semibold text-gray-700 focus:outline-none focus:ring-2"
|
||||
class="focus:ring-primary-color w-full rounded-lg bg-white/90 px-4 py-3 font-semibold text-gray-700 focus:outline-none focus:ring-2 dark:bg-gray-800/90 dark:text-gray-200 dark:focus:ring-indigo-400"
|
||||
:value="activeTab"
|
||||
@change="$emit('tab-change', $event.target.value)"
|
||||
>
|
||||
@@ -14,13 +14,17 @@
|
||||
</div>
|
||||
|
||||
<!-- 桌面端标签栏 -->
|
||||
<div class="hidden flex-wrap gap-2 rounded-2xl bg-white/10 p-2 backdrop-blur-sm sm:flex">
|
||||
<div
|
||||
class="hidden flex-wrap gap-2 rounded-2xl bg-white/10 p-2 backdrop-blur-sm dark:bg-gray-800/20 sm:flex"
|
||||
>
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:class="[
|
||||
'tab-btn flex-1 px-3 py-2 text-xs font-semibold transition-all duration-300 sm:px-4 sm:py-3 sm:text-sm md:px-6',
|
||||
activeTab === tab.key ? 'active' : 'text-gray-700 hover:bg-white/10 hover:text-gray-900'
|
||||
activeTab === tab.key
|
||||
? 'active'
|
||||
: 'text-gray-700 hover:bg-white/10 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-700/30 dark:hover:text-gray-100'
|
||||
]"
|
||||
@click="$emit('tab-change', tab.key)"
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './assets/styles/main.css'
|
||||
|
||||
149
web/admin-spa/src/stores/theme.js
Normal file
149
web/admin-spa/src/stores/theme.js
Normal file
@@ -0,0 +1,149 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
// 主题模式枚举
|
||||
export const ThemeMode = {
|
||||
LIGHT: 'light',
|
||||
DARK: 'dark',
|
||||
AUTO: 'auto'
|
||||
}
|
||||
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
// 状态 - 支持三种模式:light, dark, auto
|
||||
const themeMode = ref(ThemeMode.AUTO)
|
||||
const systemPrefersDark = ref(false)
|
||||
|
||||
// 计算属性 - 实际的暗黑模式状态
|
||||
const isDarkMode = computed(() => {
|
||||
if (themeMode.value === ThemeMode.DARK) {
|
||||
return true
|
||||
} else if (themeMode.value === ThemeMode.LIGHT) {
|
||||
return false
|
||||
} else {
|
||||
// auto 模式,跟随系统
|
||||
return systemPrefersDark.value
|
||||
}
|
||||
})
|
||||
|
||||
// 计算属性 - 当前实际使用的主题
|
||||
const currentTheme = computed(() => {
|
||||
return isDarkMode.value ? ThemeMode.DARK : ThemeMode.LIGHT
|
||||
})
|
||||
|
||||
// 初始化主题
|
||||
const initTheme = () => {
|
||||
// 检测系统主题偏好
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
systemPrefersDark.value = mediaQuery.matches
|
||||
|
||||
// 从 localStorage 读取保存的主题模式
|
||||
const savedMode = localStorage.getItem('themeMode')
|
||||
|
||||
if (savedMode && Object.values(ThemeMode).includes(savedMode)) {
|
||||
themeMode.value = savedMode
|
||||
} else {
|
||||
// 默认使用 auto 模式
|
||||
themeMode.value = ThemeMode.AUTO
|
||||
}
|
||||
|
||||
// 应用主题
|
||||
applyTheme()
|
||||
|
||||
// 开始监听系统主题变化
|
||||
watchSystemTheme()
|
||||
}
|
||||
|
||||
// 应用主题到 DOM
|
||||
const applyTheme = () => {
|
||||
const root = document.documentElement
|
||||
|
||||
if (isDarkMode.value) {
|
||||
root.classList.add('dark')
|
||||
} else {
|
||||
root.classList.remove('dark')
|
||||
}
|
||||
}
|
||||
|
||||
// 设置主题模式
|
||||
const setThemeMode = (mode) => {
|
||||
if (Object.values(ThemeMode).includes(mode)) {
|
||||
themeMode.value = mode
|
||||
}
|
||||
}
|
||||
|
||||
// 循环切换主题模式
|
||||
const cycleThemeMode = () => {
|
||||
const modes = [ThemeMode.LIGHT, ThemeMode.DARK, ThemeMode.AUTO]
|
||||
const currentIndex = modes.indexOf(themeMode.value)
|
||||
const nextIndex = (currentIndex + 1) % modes.length
|
||||
themeMode.value = modes[nextIndex]
|
||||
}
|
||||
|
||||
// 监听主题模式变化,自动保存到 localStorage 并应用
|
||||
watch(themeMode, (newMode) => {
|
||||
localStorage.setItem('themeMode', newMode)
|
||||
applyTheme()
|
||||
})
|
||||
|
||||
// 监听系统主题偏好变化
|
||||
watch(systemPrefersDark, () => {
|
||||
// 只有在 auto 模式下才需要重新应用主题
|
||||
if (themeMode.value === ThemeMode.AUTO) {
|
||||
applyTheme()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听系统主题变化
|
||||
const watchSystemTheme = () => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
|
||||
const handleChange = (e) => {
|
||||
systemPrefersDark.value = e.matches
|
||||
}
|
||||
|
||||
// 初始检测
|
||||
systemPrefersDark.value = mediaQuery.matches
|
||||
|
||||
// 添加监听器
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', handleChange)
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容旧版 API
|
||||
const toggleTheme = () => {
|
||||
cycleThemeMode()
|
||||
}
|
||||
|
||||
const setTheme = (theme) => {
|
||||
if (theme === 'dark') {
|
||||
setThemeMode(ThemeMode.DARK)
|
||||
} else if (theme === 'light') {
|
||||
setThemeMode(ThemeMode.LIGHT)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
themeMode,
|
||||
isDarkMode,
|
||||
currentTheme,
|
||||
systemPrefersDark,
|
||||
|
||||
// Constants
|
||||
ThemeMode,
|
||||
|
||||
// Actions
|
||||
initTheme,
|
||||
setThemeMode,
|
||||
cycleThemeMode,
|
||||
watchSystemTheme,
|
||||
|
||||
// 兼容旧版 API
|
||||
toggleTheme,
|
||||
setTheme
|
||||
}
|
||||
})
|
||||
@@ -3,8 +3,12 @@
|
||||
<div class="card p-4 sm:p-6">
|
||||
<div class="mb-4 flex flex-col gap-4 sm:mb-6">
|
||||
<div>
|
||||
<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>
|
||||
<h3 class="mb-1 text-lg font-bold text-gray-900 dark:text-gray-100 sm:mb-2 sm:text-xl">
|
||||
账户管理
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">
|
||||
管理您的 Claude 和 Gemini 账户及代理配置
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<!-- 筛选器组 -->
|
||||
@@ -62,7 +66,7 @@
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
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"
|
||||
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 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-gray-500 sm:w-auto"
|
||||
:disabled="accountsLoading"
|
||||
@click.ctrl.exact="loadAccounts(true)"
|
||||
@click.exact="loadAccounts(false)"
|
||||
@@ -96,26 +100,26 @@
|
||||
|
||||
<div v-if="accountsLoading" class="py-12 text-center">
|
||||
<div class="loading-spinner mx-auto mb-4" />
|
||||
<p class="text-gray-500">正在加载账户...</p>
|
||||
<p class="text-gray-500 dark:text-gray-400">正在加载账户...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="sortedAccounts.length === 0" class="py-12 text-center">
|
||||
<div
|
||||
class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-gray-100"
|
||||
class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-gray-100 dark:bg-gray-700"
|
||||
>
|
||||
<i class="fas fa-user-circle text-xl text-gray-400" />
|
||||
</div>
|
||||
<p class="text-lg text-gray-500">暂无账户</p>
|
||||
<p class="mt-2 text-sm text-gray-400">点击上方按钮添加您的第一个账户</p>
|
||||
<p class="text-lg text-gray-500 dark:text-gray-400">暂无账户</p>
|
||||
<p class="mt-2 text-sm text-gray-400 dark:text-gray-500">点击上方按钮添加您的第一个账户</p>
|
||||
</div>
|
||||
|
||||
<!-- 桌面端表格视图 -->
|
||||
<div v-else class="table-container hidden md:block">
|
||||
<table class="w-full table-fixed">
|
||||
<thead class="bg-gray-50/80 backdrop-blur-sm">
|
||||
<thead class="bg-gray-50/80 backdrop-blur-sm dark:bg-gray-700/80">
|
||||
<tr>
|
||||
<th
|
||||
class="w-[22%] min-w-[180px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100"
|
||||
class="w-[22%] min-w-[180px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
@click="sortAccounts('name')"
|
||||
>
|
||||
名称
|
||||
@@ -130,7 +134,7 @@
|
||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||
</th>
|
||||
<th
|
||||
class="w-[15%] min-w-[120px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100"
|
||||
class="w-[15%] min-w-[120px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
@click="sortAccounts('platform')"
|
||||
>
|
||||
平台/类型
|
||||
@@ -145,7 +149,7 @@
|
||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||
</th>
|
||||
<th
|
||||
class="w-[12%] min-w-[100px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100"
|
||||
class="w-[12%] min-w-[100px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
@click="sortAccounts('status')"
|
||||
>
|
||||
状态
|
||||
@@ -160,7 +164,7 @@
|
||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||
</th>
|
||||
<th
|
||||
class="w-[8%] min-w-[80px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100"
|
||||
class="w-[8%] min-w-[80px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
@click="sortAccounts('priority')"
|
||||
>
|
||||
优先级
|
||||
@@ -175,33 +179,33 @@
|
||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||
</th>
|
||||
<th
|
||||
class="w-[10%] min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||
class="w-[10%] min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
代理
|
||||
</th>
|
||||
<th
|
||||
class="w-[10%] min-w-[90px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||
class="w-[10%] min-w-[90px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
今日使用
|
||||
</th>
|
||||
<th
|
||||
class="w-[10%] min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||
class="w-[10%] min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
会话窗口
|
||||
</th>
|
||||
<th
|
||||
class="w-[8%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||
class="w-[8%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
最后使用
|
||||
</th>
|
||||
<th
|
||||
class="w-[15%] min-w-[180px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||
class="w-[15%] min-w-[180px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200/50">
|
||||
<tbody class="divide-y divide-gray-200/50 dark:divide-gray-600/50">
|
||||
<tr v-for="account in sortedAccounts" :key="account.id" class="table-row">
|
||||
<td class="px-3 py-4">
|
||||
<div class="flex items-center">
|
||||
@@ -213,7 +217,7 @@
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="truncate text-sm font-semibold text-gray-900"
|
||||
class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100"
|
||||
:title="account.name"
|
||||
>
|
||||
{{ account.name }}
|
||||
@@ -238,13 +242,16 @@
|
||||
</span>
|
||||
<span
|
||||
v-if="account.groupInfo"
|
||||
class="ml-1 inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600"
|
||||
class="ml-1 inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-400"
|
||||
:title="`所属分组: ${account.groupInfo.name}`"
|
||||
>
|
||||
<i class="fas fa-folder mr-1" />{{ account.groupInfo.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="truncate text-xs text-gray-500" :title="account.id">
|
||||
<div
|
||||
class="truncate text-xs text-gray-500 dark:text-gray-400"
|
||||
:title="account.id"
|
||||
>
|
||||
{{ account.id }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,12 +383,15 @@
|
||||
</span>
|
||||
<span
|
||||
v-if="account.status === 'blocked' && account.errorMessage"
|
||||
class="mt-1 max-w-xs truncate text-xs text-gray-500"
|
||||
class="mt-1 max-w-xs truncate text-xs text-gray-500 dark:text-gray-400"
|
||||
:title="account.errorMessage"
|
||||
>
|
||||
{{ account.errorMessage }}
|
||||
</span>
|
||||
<span v-if="account.accountType === 'dedicated'" class="text-xs text-gray-500">
|
||||
<span
|
||||
v-if="account.accountType === 'dedicated'"
|
||||
class="text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
绑定: {{ account.boundApiKeysCount || 0 }} 个API Key
|
||||
</span>
|
||||
</div>
|
||||
@@ -403,7 +413,7 @@
|
||||
:style="{ width: 101 - (account.priority || 50) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<span class="min-w-[20px] text-xs font-medium text-gray-700">
|
||||
<span class="min-w-[20px] text-xs font-medium text-gray-700 dark:text-gray-200">
|
||||
{{ account.priority || 50 }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -425,19 +435,19 @@
|
||||
<div v-if="account.usage && account.usage.daily" class="space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2 w-2 rounded-full bg-green-500" />
|
||||
<span class="text-sm font-medium text-gray-900"
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>{{ account.usage.daily.requests || 0 }} 次</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2 w-2 rounded-full bg-blue-500" />
|
||||
<span class="text-xs text-gray-600"
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300"
|
||||
>{{ formatNumber(account.usage.daily.allTokens || 0) }} tokens</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="account.usage.averages && account.usage.averages.rpm > 0"
|
||||
class="text-xs text-gray-500"
|
||||
class="text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
平均 {{ account.usage.averages.rpm.toFixed(2) }} RPM
|
||||
</div>
|
||||
@@ -460,11 +470,11 @@
|
||||
:style="{ width: account.sessionWindow.progress + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<span class="min-w-[32px] text-xs font-medium text-gray-700">
|
||||
<span class="min-w-[32px] text-xs font-medium text-gray-700 dark:text-gray-200">
|
||||
{{ account.sessionWindow.progress }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-300">
|
||||
<div>
|
||||
{{
|
||||
formatSessionWindow(
|
||||
@@ -488,7 +498,7 @@
|
||||
<span class="text-xs">N/A</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-600">
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ formatLastUsed(account.lastUsedAt) }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm font-medium">
|
||||
@@ -590,9 +600,11 @@
|
||||
{{ account.name || account.email }}
|
||||
</h4>
|
||||
<div class="mt-0.5 flex items-center gap-2">
|
||||
<span class="text-xs text-gray-500">{{ account.platform }}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
account.platform
|
||||
}}</span>
|
||||
<span class="text-xs text-gray-400">|</span>
|
||||
<span class="text-xs text-gray-500">{{ account.type }}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ account.type }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -612,20 +624,20 @@
|
||||
<!-- 使用统计 -->
|
||||
<div class="mb-3 grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">今日使用</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">今日使用</p>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(account.usage?.daily?.requests || 0) }} 次
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-gray-500">
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ formatNumber(account.usage?.daily?.allTokens || 0) }} tokens
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">总使用量</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">总使用量</p>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(account.usage?.total?.requests || 0) }} 次
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs text-gray-500">
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ formatNumber(account.usage?.total?.allTokens || 0) }} tokens
|
||||
</p>
|
||||
</div>
|
||||
@@ -640,22 +652,22 @@
|
||||
account.sessionWindow &&
|
||||
account.sessionWindow.hasActiveWindow
|
||||
"
|
||||
class="space-y-1.5 rounded-lg bg-gray-50 p-2"
|
||||
class="space-y-1.5 rounded-lg bg-gray-50 p-2 dark:bg-gray-700"
|
||||
>
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="font-medium text-gray-600">会话窗口</span>
|
||||
<span class="font-medium text-gray-700">
|
||||
<span class="font-medium text-gray-600 dark:text-gray-300">会话窗口</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-200">
|
||||
{{ account.sessionWindow.progress }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-2 w-full overflow-hidden rounded-full bg-gray-200">
|
||||
<div class="h-2 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-600">
|
||||
<div
|
||||
class="h-full bg-gradient-to-r from-blue-500 to-indigo-600 transition-all duration-300"
|
||||
:style="{ width: account.sessionWindow.progress + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500">
|
||||
<span class="text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
formatSessionWindow(
|
||||
account.sessionWindow.windowStart,
|
||||
@@ -675,8 +687,8 @@
|
||||
|
||||
<!-- 最后使用时间 -->
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500">最后使用</span>
|
||||
<span class="text-gray-700">
|
||||
<span class="text-gray-500 dark:text-gray-400">最后使用</span>
|
||||
<span class="text-gray-700 dark:text-gray-200">
|
||||
{{ account.lastUsedAt ? formatRelativeTime(account.lastUsedAt) : '从未使用' }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -686,16 +698,16 @@
|
||||
v-if="account.proxyConfig && account.proxyConfig.type !== 'none'"
|
||||
class="flex items-center justify-between text-xs"
|
||||
>
|
||||
<span class="text-gray-500">代理</span>
|
||||
<span class="text-gray-700">
|
||||
<span class="text-gray-500 dark:text-gray-400">代理</span>
|
||||
<span class="text-gray-700 dark:text-gray-200">
|
||||
{{ account.proxyConfig.type.toUpperCase() }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 调度优先级 -->
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500">优先级</span>
|
||||
<span class="font-medium text-gray-700">
|
||||
<span class="text-gray-500 dark:text-gray-400">优先级</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-200">
|
||||
{{ account.priority || 50 }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
<div class="card p-4 sm:p-6">
|
||||
<div class="mb-4 flex flex-col gap-4 sm:mb-6">
|
||||
<div>
|
||||
<h3 class="mb-1 text-lg font-bold text-gray-900 sm:mb-2 sm:text-xl">API Keys 管理</h3>
|
||||
<p class="text-sm text-gray-600 sm:text-base">管理和监控您的 API 密钥</p>
|
||||
<h3 class="mb-1 text-lg font-bold text-gray-900 dark:text-gray-100 sm:mb-2 sm:text-xl">
|
||||
API Keys 管理
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">
|
||||
管理和监控您的 API 密钥
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<!-- 筛选器组 -->
|
||||
@@ -55,7 +59,7 @@
|
||||
<div class="relative flex items-center">
|
||||
<input
|
||||
v-model="searchKeyword"
|
||||
class="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 pl-9 text-sm text-gray-700 placeholder-gray-400 shadow-sm transition-all duration-200 hover:border-gray-300 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20"
|
||||
class="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 pl-9 text-sm text-gray-700 placeholder-gray-400 shadow-sm transition-all duration-200 hover:border-gray-300 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:placeholder-gray-500 dark:hover:border-gray-500"
|
||||
placeholder="搜索名称..."
|
||||
type="text"
|
||||
@input="currentPage = 1"
|
||||
@@ -63,7 +67,7 @@
|
||||
<i class="fas fa-search absolute left-3 text-sm text-cyan-500" />
|
||||
<button
|
||||
v-if="searchKeyword"
|
||||
class="absolute right-2 flex h-5 w-5 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
||||
class="absolute right-2 flex h-5 w-5 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<i class="fas fa-times text-xs" />
|
||||
@@ -73,7 +77,7 @@
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<button
|
||||
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"
|
||||
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 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:border-gray-500 sm:w-auto"
|
||||
:disabled="apiKeysLoading"
|
||||
@click="loadApiKeys()"
|
||||
>
|
||||
@@ -131,7 +135,7 @@
|
||||
<!-- 桌面端表格视图 -->
|
||||
<div v-else class="table-container hidden md:block">
|
||||
<table class="w-full table-fixed">
|
||||
<thead class="bg-gray-50/80 backdrop-blur-sm">
|
||||
<thead class="bg-gray-50/80 backdrop-blur-sm dark:bg-gray-700/80">
|
||||
<tr>
|
||||
<th class="w-[50px] px-3 py-4 text-left">
|
||||
<div class="flex items-center">
|
||||
@@ -145,7 +149,7 @@
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="w-[25%] min-w-[200px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100"
|
||||
class="w-[25%] min-w-[200px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
@click="sortApiKeys('name')"
|
||||
>
|
||||
名称
|
||||
@@ -160,12 +164,12 @@
|
||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||
</th>
|
||||
<th
|
||||
class="w-[10%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||
class="w-[10%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
标签
|
||||
</th>
|
||||
<th
|
||||
class="w-[8%] min-w-[70px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100"
|
||||
class="w-[8%] min-w-[70px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
@click="sortApiKeys('status')"
|
||||
>
|
||||
状态
|
||||
@@ -180,11 +184,11 @@
|
||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||
</th>
|
||||
<th
|
||||
class="w-[17%] min-w-[140px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||
class="w-[17%] min-w-[140px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
使用统计
|
||||
<span
|
||||
class="cursor-pointer rounded px-2 py-1 hover:bg-gray-100"
|
||||
class="cursor-pointer rounded px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-600"
|
||||
@click="sortApiKeys('cost')"
|
||||
>
|
||||
(费用
|
||||
@@ -200,7 +204,7 @@
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
class="w-[10%] min-w-[90px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100"
|
||||
class="w-[10%] min-w-[90px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
@click="sortApiKeys('createdAt')"
|
||||
>
|
||||
创建时间
|
||||
@@ -215,7 +219,7 @@
|
||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||
</th>
|
||||
<th
|
||||
class="w-[10%] min-w-[90px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100"
|
||||
class="w-[10%] min-w-[90px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
@click="sortApiKeys('expiresAt')"
|
||||
>
|
||||
过期时间
|
||||
@@ -230,13 +234,13 @@
|
||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||
</th>
|
||||
<th
|
||||
class="w-[20%] min-w-[180px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700"
|
||||
class="w-[20%] min-w-[180px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200/50">
|
||||
<tbody class="divide-y divide-gray-200/50 dark:divide-gray-600/50">
|
||||
<template v-for="key in paginatedApiKeys" :key="key.id">
|
||||
<!-- API Key 主行 -->
|
||||
<tr class="table-row">
|
||||
@@ -259,10 +263,16 @@
|
||||
<i class="fas fa-key text-xs text-white" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm font-semibold text-gray-900" :title="key.name">
|
||||
<div
|
||||
class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100"
|
||||
:title="key.name"
|
||||
>
|
||||
{{ key.name }}
|
||||
</div>
|
||||
<div class="truncate text-xs text-gray-500" :title="key.id">
|
||||
<div
|
||||
class="truncate text-xs text-gray-500 dark:text-gray-400"
|
||||
:title="key.id"
|
||||
>
|
||||
{{ key.id }}
|
||||
</div>
|
||||
<!-- 账户绑定信息 -->
|
||||
@@ -278,7 +288,7 @@
|
||||
<i class="fas fa-brain mr-1 text-[10px]" />
|
||||
Claude
|
||||
</span>
|
||||
<span class="truncate text-gray-600">
|
||||
<span class="truncate text-gray-600 dark:text-gray-400">
|
||||
{{ getClaudeBindingInfo(key) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -290,7 +300,7 @@
|
||||
<i class="fas fa-robot mr-1 text-[10px]" />
|
||||
Gemini
|
||||
</span>
|
||||
<span class="truncate text-gray-600">
|
||||
<span class="truncate text-gray-600 dark:text-gray-400">
|
||||
{{ getGeminiBindingInfo(key) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -302,7 +312,7 @@
|
||||
<i class="fa-openai mr-1 text-[10px]" />
|
||||
OpenAI
|
||||
</span>
|
||||
<span class="truncate text-gray-600">
|
||||
<span class="truncate text-gray-600 dark:text-gray-400">
|
||||
{{ getOpenAIBindingInfo(key) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -358,20 +368,20 @@
|
||||
<!-- 今日使用统计 -->
|
||||
<div class="mb-2">
|
||||
<div class="mb-1 flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">今日请求</span>
|
||||
<span class="font-semibold text-gray-900"
|
||||
<span class="text-gray-600 dark:text-gray-400">今日请求</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-gray-100"
|
||||
>{{ formatNumber(key.usage?.daily?.requests || 0) }}次</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">今日费用</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">今日费用</span>
|
||||
<span class="font-semibold text-green-600"
|
||||
>${{ (key.dailyCost || 0).toFixed(4) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">最后使用</span>
|
||||
<span class="font-medium text-gray-700">{{
|
||||
<span class="text-gray-600 dark:text-gray-400">最后使用</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{
|
||||
formatLastUsed(key.lastUsedAt)
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -380,8 +390,8 @@
|
||||
<!-- 每日费用限制进度条 -->
|
||||
<div v-if="key.dailyCostLimit > 0" class="space-y-1">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500">费用限额</span>
|
||||
<span class="text-gray-700">
|
||||
<span class="text-gray-500 dark:text-gray-400">费用限额</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
${{ (key.dailyCost || 0).toFixed(2) }} / ${{
|
||||
key.dailyCostLimit.toFixed(2)
|
||||
}}
|
||||
@@ -414,7 +424,7 @@
|
||||
<!-- 查看详情按钮 -->
|
||||
<div class="pt-1">
|
||||
<button
|
||||
class="flex w-full items-center justify-center gap-1 rounded py-1 text-xs font-medium text-blue-600 transition-colors hover:bg-blue-50 hover:text-blue-800"
|
||||
class="flex w-full items-center justify-center gap-1 rounded py-1 text-xs font-medium text-blue-600 transition-colors hover:bg-blue-50 hover:text-blue-800 dark:hover:bg-blue-900/20 dark:hover:text-blue-400"
|
||||
@click="showUsageDetails(key)"
|
||||
>
|
||||
<i class="fas fa-chart-line" />
|
||||
@@ -540,7 +550,7 @@
|
||||
|
||||
<!-- 模型统计展开区域 -->
|
||||
<tr v-if="key && key.id && expandedApiKeys[key.id]">
|
||||
<td class="bg-gray-50 px-3 py-4" colspan="8">
|
||||
<td class="bg-gray-50 px-3 py-4 dark:bg-gray-700" colspan="8">
|
||||
<div v-if="!apiKeyModelStats[key.id]" class="py-4 text-center">
|
||||
<div class="loading-spinner mx-auto" />
|
||||
<p class="mt-2 text-sm text-gray-500">加载模型统计...</p>
|
||||
@@ -548,14 +558,16 @@
|
||||
<div class="space-y-4">
|
||||
<!-- 通用的标题和时间筛选器,无论是否有数据都显示 -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h5 class="flex items-center text-sm font-semibold text-gray-700">
|
||||
<h5
|
||||
class="flex items-center text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-chart-pie mr-2 text-indigo-500" />
|
||||
模型使用分布
|
||||
</h5>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0"
|
||||
class="rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-500"
|
||||
class="rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-500 dark:bg-gray-700 dark:text-gray-400"
|
||||
>
|
||||
{{ apiKeyModelStats[key.id].length }} 个模型
|
||||
</span>
|
||||
@@ -563,7 +575,7 @@
|
||||
<!-- API Keys日期筛选器 -->
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- 快捷日期选择 -->
|
||||
<div class="flex gap-1 rounded bg-gray-100 p-1">
|
||||
<div class="flex gap-1 rounded bg-gray-100 p-1 dark:bg-gray-700">
|
||||
<button
|
||||
v-for="option in getApiKeyDateFilter(key.id).presetOptions"
|
||||
:key="option.value"
|
||||
@@ -571,8 +583,8 @@
|
||||
'rounded px-2 py-1 text-xs font-medium transition-colors',
|
||||
getApiKeyDateFilter(key.id).preset === option.value &&
|
||||
getApiKeyDateFilter(key.id).type === 'preset'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
||||
]"
|
||||
@click="setApiKeyDateFilterPreset(option.value, key.id)"
|
||||
>
|
||||
@@ -630,14 +642,16 @@
|
||||
<div
|
||||
v-for="stat in apiKeyModelStats[key.id]"
|
||||
:key="stat.model"
|
||||
class="rounded-xl border border-gray-200 bg-gradient-to-br from-white to-gray-50 p-4 transition-all duration-200 hover:border-indigo-300 hover:shadow-lg"
|
||||
class="rounded-xl border border-gray-200 bg-gradient-to-br from-white to-gray-50 p-4 transition-all duration-200 hover:border-indigo-300 hover:shadow-lg dark:border-gray-600 dark:from-gray-800 dark:to-gray-700 dark:hover:border-indigo-500"
|
||||
>
|
||||
<div class="mb-3 flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<span class="mb-1 block text-sm font-semibold text-gray-800">{{
|
||||
stat.model
|
||||
}}</span>
|
||||
<span class="rounded-full bg-blue-50 px-2 py-1 text-xs text-gray-500"
|
||||
<span
|
||||
class="mb-1 block text-sm font-semibold text-gray-800 dark:text-gray-200"
|
||||
>{{ stat.model }}</span
|
||||
>
|
||||
<span
|
||||
class="rounded-full bg-blue-50 px-2 py-1 text-xs text-gray-500 dark:bg-blue-900/30 dark:text-gray-400"
|
||||
>{{ stat.requests }} 次请求</span
|
||||
>
|
||||
</div>
|
||||
@@ -645,16 +659,16 @@
|
||||
|
||||
<div class="mb-3 space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="flex items-center text-gray-600">
|
||||
<span class="flex items-center text-gray-600 dark:text-gray-400">
|
||||
<i class="fas fa-coins mr-1 text-xs text-yellow-500" />
|
||||
总Token:
|
||||
</span>
|
||||
<span class="font-semibold text-gray-900">{{
|
||||
<span class="font-semibold text-gray-900 dark:text-gray-100">{{
|
||||
formatTokenCount(stat.allTokens)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="flex items-center text-gray-600">
|
||||
<span class="flex items-center text-gray-600 dark:text-gray-400">
|
||||
<i class="fas fa-dollar-sign mr-1 text-xs text-green-500" />
|
||||
费用:
|
||||
</span>
|
||||
@@ -662,8 +676,10 @@
|
||||
calculateModelCost(stat)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="mt-2 border-t border-gray-100 pt-2">
|
||||
<div class="flex items-center justify-between text-xs text-gray-500">
|
||||
<div class="mt-2 border-t border-gray-100 pt-2 dark:border-gray-600">
|
||||
<div
|
||||
class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-arrow-down mr-1 text-green-500" />
|
||||
输入:
|
||||
@@ -672,7 +688,9 @@
|
||||
formatTokenCount(stat.inputTokens)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs text-gray-500">
|
||||
<div
|
||||
class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<i class="fas fa-arrow-up mr-1 text-blue-500" />
|
||||
输出:
|
||||
@@ -737,23 +755,25 @@
|
||||
<!-- 总计统计,仅在有数据时显示 -->
|
||||
<div
|
||||
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0"
|
||||
class="mt-4 rounded-lg border border-indigo-100 bg-gradient-to-r from-indigo-50 to-purple-50 p-3"
|
||||
class="mt-4 rounded-lg border border-indigo-100 bg-gradient-to-r from-indigo-50 to-purple-50 p-3 dark:border-indigo-700 dark:from-indigo-900/20 dark:to-purple-900/20"
|
||||
>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="flex items-center font-semibold text-gray-700">
|
||||
<span
|
||||
class="flex items-center font-semibold text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-calculator mr-2 text-indigo-500" />
|
||||
总计统计
|
||||
</span>
|
||||
<div class="flex gap-4 text-xs">
|
||||
<span class="text-gray-600">
|
||||
<span class="text-gray-600 dark:text-gray-400">
|
||||
总请求:
|
||||
<span class="font-semibold text-gray-800">{{
|
||||
<span class="font-semibold text-gray-800 dark:text-gray-200">{{
|
||||
apiKeyModelStats[key.id].reduce((sum, stat) => sum + stat.requests, 0)
|
||||
}}</span>
|
||||
</span>
|
||||
<span class="text-gray-600">
|
||||
<span class="text-gray-600 dark:text-gray-400">
|
||||
总Token:
|
||||
<span class="font-semibold text-gray-800">{{
|
||||
<span class="font-semibold text-gray-800 dark:text-gray-200">{{
|
||||
formatTokenCount(
|
||||
apiKeyModelStats[key.id].reduce(
|
||||
(sum, stat) => sum + stat.allTokens,
|
||||
@@ -796,10 +816,10 @@
|
||||
<i class="fas fa-key text-sm text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-900">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ key.name }}
|
||||
</h4>
|
||||
<p class="mt-0.5 text-xs text-gray-500">
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ key.id }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -889,21 +909,21 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ formatNumber(key.usage?.daily?.requests || 0) }} 次
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">请求</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">请求</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-green-600">
|
||||
${{ (key.dailyCost || 0).toFixed(4) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">费用</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">费用</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<span class="text-xs text-gray-600">最后使用</span>
|
||||
<span class="text-xs font-medium text-gray-700">{{
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">最后使用</span>
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
formatLastUsed(key.lastUsedAt)
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -1042,28 +1062,28 @@
|
||||
class="mt-4 flex flex-col items-center justify-between gap-4 sm:mt-6 sm:flex-row"
|
||||
>
|
||||
<div class="flex w-full flex-col items-center gap-3 sm:w-auto sm:flex-row">
|
||||
<span class="text-xs text-gray-600 sm:text-sm">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
共 {{ sortedApiKeys.length }} 条记录
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-gray-600 sm:text-sm">每页显示</span>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">每页显示</span>
|
||||
<select
|
||||
v-model="pageSize"
|
||||
class="rounded-md border border-gray-200 bg-white px-2 py-1 text-xs text-gray-700 transition-colors hover:border-gray-300 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 sm:text-sm"
|
||||
class="rounded-md border border-gray-200 bg-white px-2 py-1 text-xs text-gray-700 transition-colors hover:border-gray-300 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-gray-500 sm:text-sm"
|
||||
@change="currentPage = 1"
|
||||
>
|
||||
<option v-for="size in pageSizeOptions" :key="size" :value="size">
|
||||
{{ size }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="text-xs text-gray-600 sm:text-sm">条</span>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">条</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 上一页 -->
|
||||
<button
|
||||
class="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 sm:py-1 sm:text-sm"
|
||||
class="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 sm:py-1 sm:text-sm"
|
||||
:disabled="currentPage === 1"
|
||||
@click="currentPage--"
|
||||
>
|
||||
@@ -1075,12 +1095,16 @@
|
||||
<!-- 第一页 -->
|
||||
<button
|
||||
v-if="currentPage > 3"
|
||||
class="hidden rounded-md border border-gray-300 bg-white px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-50 sm:block"
|
||||
class="hidden rounded-md border border-gray-300 bg-white px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 sm:block"
|
||||
@click="currentPage = 1"
|
||||
>
|
||||
1
|
||||
</button>
|
||||
<span v-if="currentPage > 4" class="hidden px-2 text-gray-500 sm:inline">...</span>
|
||||
<span
|
||||
v-if="currentPage > 4"
|
||||
class="hidden px-2 text-gray-500 dark:text-gray-400 sm:inline"
|
||||
>...</span
|
||||
>
|
||||
|
||||
<!-- 中间页码 -->
|
||||
<button
|
||||
@@ -1090,7 +1114,7 @@
|
||||
'rounded-md px-2 py-1 text-xs font-medium sm:px-3 sm:text-sm',
|
||||
page === currentPage
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
|
||||
: 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||
]"
|
||||
@click="currentPage = page"
|
||||
>
|
||||
@@ -1098,12 +1122,14 @@
|
||||
</button>
|
||||
|
||||
<!-- 最后一页 -->
|
||||
<span v-if="currentPage < totalPages - 3" class="hidden px-2 text-gray-500 sm:inline"
|
||||
<span
|
||||
v-if="currentPage < totalPages - 3"
|
||||
class="hidden px-2 text-gray-500 dark:text-gray-400 sm:inline"
|
||||
>...</span
|
||||
>
|
||||
<button
|
||||
v-if="totalPages > 1 && currentPage < totalPages - 2"
|
||||
class="hidden rounded-md border border-gray-300 bg-white px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-50 sm:block"
|
||||
class="hidden rounded-md border border-gray-300 bg-white px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 sm:block"
|
||||
@click="currentPage = totalPages"
|
||||
>
|
||||
{{ totalPages }}
|
||||
@@ -1112,7 +1138,7 @@
|
||||
|
||||
<!-- 下一页 -->
|
||||
<button
|
||||
class="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 sm:py-1 sm:text-sm"
|
||||
class="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 sm:py-1 sm:text-sm"
|
||||
:disabled="currentPage === totalPages || totalPages === 0"
|
||||
@click="currentPage++"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="gradient-bg min-h-screen p-4 md:p-6">
|
||||
<div class="min-h-screen p-4 md:p-6" :class="isDarkMode ? 'gradient-bg-dark' : 'gradient-bg'">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="glass-strong mb-6 rounded-3xl p-4 shadow-xl md:mb-8 md:p-6">
|
||||
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
||||
@@ -9,13 +9,24 @@
|
||||
:subtitle="currentTab === 'stats' ? 'API Key 使用统计' : '使用教程'"
|
||||
:title="oemSettings.siteName"
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2 md:gap-4">
|
||||
<!-- 主题切换按钮 -->
|
||||
<div class="flex items-center">
|
||||
<ThemeToggle mode="dropdown" />
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div
|
||||
class="h-8 w-px bg-gradient-to-b from-transparent via-gray-300 to-transparent opacity-50 dark:via-gray-600"
|
||||
/>
|
||||
|
||||
<!-- 管理后台按钮 -->
|
||||
<router-link
|
||||
class="admin-button flex items-center gap-2 rounded-xl px-3 py-2 text-white transition-all duration-300 md:px-4 md:py-2"
|
||||
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"
|
||||
>
|
||||
<i class="fas fa-cog text-sm" />
|
||||
<span class="text-xs font-medium md:text-sm">管理后台</span>
|
||||
<i class="fas fa-shield-alt text-sm md:text-base" />
|
||||
<span class="text-xs font-semibold tracking-wide md:text-sm">管理后台</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,7 +64,7 @@
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="error" class="mb-6 md:mb-8">
|
||||
<div
|
||||
class="rounded-xl border border-red-500/30 bg-red-500/20 p-3 text-sm text-red-800 backdrop-blur-sm md:p-4 md:text-base"
|
||||
class="rounded-xl border border-red-500/30 bg-red-500/20 p-3 text-sm text-red-800 backdrop-blur-sm dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-200 md:p-4 md:text-base"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle mr-2" />
|
||||
{{ error }}
|
||||
@@ -64,13 +75,15 @@
|
||||
<div v-if="statsData" class="fade-in">
|
||||
<div class="glass-strong rounded-3xl p-4 shadow-xl md:p-6">
|
||||
<!-- 时间范围选择器 -->
|
||||
<div class="mb-4 border-b border-gray-200 pb-4 md:mb-6 md:pb-6">
|
||||
<div class="mb-4 border-b border-gray-200 pb-4 dark:border-gray-700 md:mb-6 md:pb-6">
|
||||
<div
|
||||
class="flex flex-col items-start justify-between gap-3 md:flex-row md:items-center md:gap-4"
|
||||
>
|
||||
<div class="flex items-center gap-2 md:gap-3">
|
||||
<i class="fas fa-clock text-base text-blue-500 md:text-lg" />
|
||||
<span class="text-base font-medium text-gray-700 md:text-lg">统计时间范围</span>
|
||||
<span class="text-base font-medium text-gray-700 dark:text-gray-200 md:text-lg"
|
||||
>统计时间范围</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex w-full gap-2 md:w-auto">
|
||||
<button
|
||||
@@ -120,11 +133,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useApiStatsStore } from '@/stores/apistats'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import LogoTitle from '@/components/common/LogoTitle.vue'
|
||||
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
||||
import ApiKeyInput from '@/components/apistats/ApiKeyInput.vue'
|
||||
import StatsOverview from '@/components/apistats/StatsOverview.vue'
|
||||
import TokenDistribution from '@/components/apistats/TokenDistribution.vue'
|
||||
@@ -134,10 +149,14 @@ import TutorialView from './TutorialView.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const apiStatsStore = useApiStatsStore()
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
// 当前标签页
|
||||
const currentTab = ref('stats')
|
||||
|
||||
// 主题相关
|
||||
const isDarkMode = computed(() => themeStore.isDarkMode)
|
||||
|
||||
const {
|
||||
apiKey,
|
||||
apiId,
|
||||
@@ -172,6 +191,9 @@ const handleKeyDown = (event) => {
|
||||
onMounted(() => {
|
||||
console.log('API Stats Page loaded')
|
||||
|
||||
// 初始化主题(因为该页面不在 MainLayout 内)
|
||||
themeStore.initTheme()
|
||||
|
||||
// 加载 OEM 设置
|
||||
loadOemSettings()
|
||||
|
||||
@@ -217,6 +239,14 @@ watch(apiKey, (newValue) => {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 暗色模式的渐变背景 */
|
||||
.gradient-bg-dark {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #334155 50%, #475569 100%);
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gradient-bg::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
@@ -232,11 +262,27 @@ watch(apiKey, (newValue) => {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* 玻璃态效果 */
|
||||
/* 暗色模式的背景覆盖 */
|
||||
.gradient-bg-dark::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, rgba(100, 116, 139, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(71, 85, 105, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(30, 41, 59, 0.1) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* 玻璃态效果 - 使用CSS变量 */
|
||||
.glass-strong {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
background: var(--glass-strong-color);
|
||||
backdrop-filter: blur(25px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05),
|
||||
@@ -245,6 +291,14 @@ watch(apiKey, (newValue) => {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 暗色模式的玻璃态效果 */
|
||||
:global(.dark) .glass-strong {
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.7),
|
||||
0 0 0 1px rgba(55, 65, 81, 0.3),
|
||||
inset 0 1px 0 rgba(75, 85, 99, 0.2);
|
||||
}
|
||||
|
||||
/* 标题渐变 */
|
||||
.header-title {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
@@ -255,38 +309,76 @@ watch(apiKey, (newValue) => {
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
/* 管理后台按钮 */
|
||||
.admin-button {
|
||||
/* 管理后台按钮 - 精致版本 */
|
||||
.admin-button-refined {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(102, 126, 234, 0.3),
|
||||
0 2px 4px -1px rgba(102, 126, 234, 0.1);
|
||||
0 4px 12px rgba(102, 126, 234, 0.25),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-button::before {
|
||||
/* 暗色模式下的管理后台按钮 */
|
||||
:global(.dark) .admin-button-refined {
|
||||
background: rgba(55, 65, 81, 0.8);
|
||||
border: 1px solid rgba(107, 114, 128, 0.4);
|
||||
color: #f3f4f6;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.admin-button-refined::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.admin-button:hover {
|
||||
transform: translateY(-2px);
|
||||
.admin-button-refined:hover {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(102, 126, 234, 0.4),
|
||||
0 4px 6px -2px rgba(102, 126, 234, 0.15);
|
||||
0 8px 20px rgba(118, 75, 162, 0.35),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.admin-button:hover::before {
|
||||
left: 100%;
|
||||
.admin-button-refined:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 暗色模式下的悬停效果 */
|
||||
:global(.dark) .admin-button-refined:hover {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-color: rgba(147, 51, 234, 0.4);
|
||||
box-shadow:
|
||||
0 8px 20px rgba(102, 126, 234, 0.3),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.admin-button-refined:active {
|
||||
transform: translateY(-1px) scale(1);
|
||||
}
|
||||
|
||||
/* 确保图标和文字在所有模式下都清晰可见 */
|
||||
.admin-button-refined i,
|
||||
.admin-button-refined span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 时间范围按钮 */
|
||||
@@ -312,12 +404,26 @@ watch(apiKey, (newValue) => {
|
||||
|
||||
.period-btn:not(.active) {
|
||||
color: #374151;
|
||||
background: transparent;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border: 1px solid rgba(229, 231, 235, 0.5);
|
||||
}
|
||||
|
||||
:global(html.dark) .period-btn:not(.active) {
|
||||
color: #e5e7eb;
|
||||
background: rgba(55, 65, 81, 0.4);
|
||||
border: 1px solid rgba(75, 85, 99, 0.5);
|
||||
}
|
||||
|
||||
.period-btn:not(.active):hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
color: #1f2937;
|
||||
border-color: rgba(209, 213, 219, 0.8);
|
||||
}
|
||||
|
||||
:global(html.dark) .period-btn:not(.active):hover {
|
||||
background: rgba(75, 85, 99, 0.6);
|
||||
color: #ffffff;
|
||||
border-color: rgba(107, 114, 128, 0.8);
|
||||
}
|
||||
|
||||
/* Tab 胶囊按钮样式 */
|
||||
@@ -339,6 +445,11 @@ watch(apiKey, (newValue) => {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 暗夜模式下的Tab按钮基础样式 */
|
||||
:global(html.dark) .tab-pill-button {
|
||||
color: rgba(209, 213, 219, 0.8);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tab-pill-button {
|
||||
padding: 0.625rem 1.25rem;
|
||||
@@ -351,6 +462,11 @@ watch(apiKey, (newValue) => {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
:global(html.dark) .tab-pill-button:hover {
|
||||
color: #f3f4f6;
|
||||
background: rgba(100, 116, 139, 0.2);
|
||||
}
|
||||
|
||||
.tab-pill-button.active {
|
||||
background: white;
|
||||
color: #764ba2;
|
||||
@@ -359,6 +475,14 @@ watch(apiKey, (newValue) => {
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
:global(html.dark) .tab-pill-button.active {
|
||||
background: rgba(71, 85, 105, 0.9);
|
||||
color: #f3f4f6;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.3),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.tab-pill-button i {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,15 @@
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 sm:text-sm">总API Keys</p>
|
||||
<p class="text-2xl font-bold text-gray-900 sm:text-3xl">
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
总API Keys
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100 sm:text-3xl">
|
||||
{{ dashboardData.totalApiKeys }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500">活跃: {{ dashboardData.activeApiKeys || 0 }}</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
活跃: {{ dashboardData.activeApiKeys || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-blue-500 to-blue-600">
|
||||
<i class="fas fa-key" />
|
||||
@@ -22,9 +26,11 @@
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 sm:text-sm">服务账户</p>
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
服务账户
|
||||
</p>
|
||||
<div class="flex flex-wrap items-baseline gap-x-2">
|
||||
<p class="text-2xl font-bold text-gray-900 sm:text-3xl">
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100 sm:text-3xl">
|
||||
{{ dashboardData.totalAccounts }}
|
||||
</p>
|
||||
<!-- 各平台账户数量展示 -->
|
||||
@@ -39,7 +45,7 @@
|
||||
:title="`Claude: ${dashboardData.accountsByPlatform.claude.total} 个 (正常: ${dashboardData.accountsByPlatform.claude.normal})`"
|
||||
>
|
||||
<i class="fas fa-brain text-xs text-indigo-600" />
|
||||
<span class="text-xs font-medium text-gray-700">{{
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
dashboardData.accountsByPlatform.claude.total
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -53,7 +59,7 @@
|
||||
:title="`Console: ${dashboardData.accountsByPlatform['claude-console'].total} 个 (正常: ${dashboardData.accountsByPlatform['claude-console'].normal})`"
|
||||
>
|
||||
<i class="fas fa-terminal text-xs text-purple-600" />
|
||||
<span class="text-xs font-medium text-gray-700">{{
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
dashboardData.accountsByPlatform['claude-console'].total
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -67,7 +73,7 @@
|
||||
:title="`Gemini: ${dashboardData.accountsByPlatform.gemini.total} 个 (正常: ${dashboardData.accountsByPlatform.gemini.normal})`"
|
||||
>
|
||||
<i class="fas fa-robot text-xs text-yellow-600" />
|
||||
<span class="text-xs font-medium text-gray-700">{{
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
dashboardData.accountsByPlatform.gemini.total
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -81,7 +87,7 @@
|
||||
:title="`Bedrock: ${dashboardData.accountsByPlatform.bedrock.total} 个 (正常: ${dashboardData.accountsByPlatform.bedrock.normal})`"
|
||||
>
|
||||
<i class="fab fa-aws text-xs text-orange-600" />
|
||||
<span class="text-xs font-medium text-gray-700">{{
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
dashboardData.accountsByPlatform.bedrock.total
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -95,18 +101,21 @@
|
||||
:title="`OpenAI: ${dashboardData.accountsByPlatform.openai.total} 个 (正常: ${dashboardData.accountsByPlatform.openai.normal})`"
|
||||
>
|
||||
<i class="fas fa-openai text-xs text-gray-100" />
|
||||
<span class="text-xs font-medium text-gray-700">{{
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
|
||||
dashboardData.accountsByPlatform.openai.total
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
正常: {{ dashboardData.normalAccounts || 0 }}
|
||||
<span v-if="dashboardData.abnormalAccounts > 0" class="text-red-600">
|
||||
| 异常: {{ dashboardData.abnormalAccounts }}
|
||||
</span>
|
||||
<span v-if="dashboardData.pausedAccounts > 0" class="text-gray-600">
|
||||
<span
|
||||
v-if="dashboardData.pausedAccounts > 0"
|
||||
class="text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
| 停止调度: {{ dashboardData.pausedAccounts }}
|
||||
</span>
|
||||
<span v-if="dashboardData.rateLimitedAccounts > 0" class="text-yellow-600">
|
||||
@@ -123,11 +132,13 @@
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 sm:text-sm">今日请求</p>
|
||||
<p class="text-2xl font-bold text-gray-900 sm:text-3xl">
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
今日请求
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100 sm:text-3xl">
|
||||
{{ dashboardData.todayRequests }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
总请求: {{ formatNumber(dashboardData.totalRequests || 0) }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -140,11 +151,15 @@
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 sm:text-sm">系统状态</p>
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
系统状态
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-green-600 sm:text-3xl">
|
||||
{{ dashboardData.systemStatus }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500">运行时间: {{ formattedUptime }}</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
运行时间: {{ formattedUptime }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-yellow-500 to-orange-500">
|
||||
<i class="fas fa-heartbeat" />
|
||||
@@ -160,7 +175,9 @@
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="mr-8 flex-1">
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 sm:text-sm">今日Token</p>
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
今日Token
|
||||
</p>
|
||||
<div class="mb-2 flex flex-wrap items-baseline gap-2">
|
||||
<p class="text-xl font-bold text-blue-600 sm:text-2xl md:text-3xl">
|
||||
{{
|
||||
@@ -176,7 +193,7 @@
|
||||
>/ {{ costsData.todayCosts.formatted.totalCost }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<div class="flex flex-wrap items-center justify-between gap-x-4">
|
||||
<span
|
||||
>输入:
|
||||
@@ -214,7 +231,9 @@
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="mr-8 flex-1">
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 sm:text-sm">总Token消耗</p>
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
总Token消耗
|
||||
</p>
|
||||
<div class="mb-2 flex flex-wrap items-baseline gap-2">
|
||||
<p class="text-xl font-bold text-emerald-600 sm:text-2xl md:text-3xl">
|
||||
{{
|
||||
@@ -230,7 +249,7 @@
|
||||
>/ {{ costsData.totalCosts.formatted.totalCost }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<div class="flex flex-wrap items-center justify-between gap-x-4">
|
||||
<span
|
||||
>输入:
|
||||
@@ -268,14 +287,14 @@
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 sm:text-sm">
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
实时RPM
|
||||
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span>
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-orange-600 sm:text-3xl">
|
||||
{{ dashboardData.realtimeRPM || 0 }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
每分钟请求数
|
||||
<span v-if="dashboardData.isHistoricalMetrics" class="text-yellow-600">
|
||||
<i class="fas fa-exclamation-circle" /> 历史数据
|
||||
@@ -291,14 +310,14 @@
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 sm:text-sm">
|
||||
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
实时TPM
|
||||
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span>
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-rose-600 sm:text-3xl">
|
||||
{{ formatNumber(dashboardData.realtimeTPM || 0) }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
每分钟Token数
|
||||
<span v-if="dashboardData.isHistoricalMetrics" class="text-yellow-600">
|
||||
<i class="fas fa-exclamation-circle" /> 历史数据
|
||||
@@ -315,18 +334,22 @@
|
||||
<!-- 模型消费统计 -->
|
||||
<div class="mb-8">
|
||||
<div class="mb-4 flex flex-col gap-4 sm:mb-6">
|
||||
<h3 class="text-lg font-bold text-gray-900 sm:text-xl">模型使用分布与Token使用趋势</h3>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
|
||||
模型使用分布与Token使用趋势
|
||||
</h3>
|
||||
<div class="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-end">
|
||||
<!-- 快捷日期选择 -->
|
||||
<div class="flex flex-shrink-0 gap-1 overflow-x-auto rounded-lg bg-gray-100 p-1">
|
||||
<div
|
||||
class="flex flex-shrink-0 gap-1 overflow-x-auto rounded-lg bg-gray-100 p-1 dark:bg-gray-700"
|
||||
>
|
||||
<button
|
||||
v-for="option in dateFilter.presetOptions"
|
||||
:key="option.value"
|
||||
:class="[
|
||||
'rounded-md px-3 py-1 text-sm font-medium transition-colors',
|
||||
dateFilter.preset === option.value && dateFilter.type === 'preset'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100'
|
||||
]"
|
||||
@click="setDateFilterPreset(option.value)"
|
||||
>
|
||||
@@ -335,13 +358,13 @@
|
||||
</div>
|
||||
|
||||
<!-- 粒度切换按钮 -->
|
||||
<div class="flex gap-1 rounded-lg bg-gray-100 p-1">
|
||||
<div class="flex gap-1 rounded-lg bg-gray-100 p-1 dark:bg-gray-700">
|
||||
<button
|
||||
:class="[
|
||||
'rounded-md px-3 py-1 text-sm font-medium transition-colors',
|
||||
trendGranularity === 'day'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100'
|
||||
]"
|
||||
@click="setTrendGranularity('day')"
|
||||
>
|
||||
@@ -351,8 +374,8 @@
|
||||
:class="[
|
||||
'rounded-md px-3 py-1 text-sm font-medium transition-colors',
|
||||
trendGranularity === 'hour'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100'
|
||||
]"
|
||||
@click="setTrendGranularity('hour')"
|
||||
>
|
||||
@@ -385,17 +408,17 @@
|
||||
<!-- 刷新控制 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 自动刷新控制 -->
|
||||
<div class="flex items-center rounded-lg bg-gray-100 px-3 py-1">
|
||||
<div class="flex items-center rounded-lg bg-gray-100 px-3 py-1 dark:bg-gray-700">
|
||||
<label class="relative inline-flex cursor-pointer items-center">
|
||||
<input v-model="autoRefreshEnabled" class="peer sr-only" type="checkbox" />
|
||||
<!-- 更小的开关 -->
|
||||
<div
|
||||
class="peer relative h-5 w-9 rounded-full bg-gray-300 transition-all duration-200 after:absolute after:left-[2px] after:top-0.5 after:h-4 after:w-4 after:rounded-full after:bg-white after:shadow-sm after:transition-transform after:duration-200 after:content-[''] peer-checked:bg-blue-500 peer-checked:after:translate-x-4 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300"
|
||||
class="peer relative h-5 w-9 rounded-full bg-gray-300 transition-all duration-200 after:absolute after:left-[2px] after:top-0.5 after:h-4 after:w-4 after:rounded-full after:bg-white after:shadow-sm after:transition-transform after:duration-200 after:content-[''] peer-checked:bg-blue-500 peer-checked:after:translate-x-4 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-300 dark:bg-gray-600 dark:after:bg-gray-300 dark:peer-focus:ring-blue-600"
|
||||
/>
|
||||
<span
|
||||
class="ml-2.5 flex select-none items-center gap-1 text-sm font-medium text-gray-600"
|
||||
class="ml-2.5 flex select-none items-center gap-1 text-sm font-medium text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
<i class="fas fa-redo-alt text-xs text-gray-500" />
|
||||
<i class="fas fa-redo-alt text-xs text-gray-500 dark:text-gray-400" />
|
||||
<span>自动刷新</span>
|
||||
<span
|
||||
v-if="autoRefreshEnabled"
|
||||
@@ -410,7 +433,7 @@
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<button
|
||||
class="flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1 text-sm font-medium text-blue-600 shadow-sm transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 sm:gap-2"
|
||||
class="flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1 text-sm font-medium text-blue-600 shadow-sm transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-700 sm:gap-2"
|
||||
:disabled="isRefreshing"
|
||||
title="立即刷新数据"
|
||||
@click="refreshAllData()"
|
||||
@@ -425,7 +448,9 @@
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<!-- 饼图 -->
|
||||
<div class="card p-4 sm:p-6">
|
||||
<h4 class="mb-4 text-base font-semibold text-gray-800 sm:text-lg">Token使用分布</h4>
|
||||
<h4 class="mb-4 text-base font-semibold text-gray-800 dark:text-gray-200 sm:text-lg">
|
||||
Token使用分布
|
||||
</h4>
|
||||
<div class="relative" style="height: 250px">
|
||||
<canvas ref="modelUsageChart" />
|
||||
</div>
|
||||
@@ -433,48 +458,62 @@
|
||||
|
||||
<!-- 详细数据表格 -->
|
||||
<div class="card p-4 sm:p-6">
|
||||
<h4 class="mb-4 text-base font-semibold text-gray-800 sm:text-lg">详细统计数据</h4>
|
||||
<h4 class="mb-4 text-base font-semibold text-gray-800 dark:text-gray-200 sm:text-lg">
|
||||
详细统计数据
|
||||
</h4>
|
||||
<div v-if="dashboardModelStats.length === 0" class="py-8 text-center">
|
||||
<p class="text-sm text-gray-500 sm:text-base">暂无模型使用数据</p>
|
||||
</div>
|
||||
<div v-else class="max-h-[250px] overflow-auto sm:max-h-[300px]">
|
||||
<table class="min-w-full">
|
||||
<thead class="sticky top-0 bg-gray-50">
|
||||
<thead class="sticky top-0 bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-2 py-2 text-left text-xs font-medium text-gray-700 sm:px-4">
|
||||
<th
|
||||
class="px-2 py-2 text-left text-xs font-medium text-gray-700 dark:text-gray-300 sm:px-4"
|
||||
>
|
||||
模型
|
||||
</th>
|
||||
<th
|
||||
class="hidden px-2 py-2 text-right text-xs font-medium text-gray-700 sm:table-cell sm:px-4"
|
||||
class="hidden px-2 py-2 text-right text-xs font-medium text-gray-700 dark:text-gray-300 sm:table-cell sm:px-4"
|
||||
>
|
||||
请求数
|
||||
</th>
|
||||
<th class="px-2 py-2 text-right text-xs font-medium text-gray-700 sm:px-4">
|
||||
<th
|
||||
class="px-2 py-2 text-right text-xs font-medium text-gray-700 dark:text-gray-300 sm:px-4"
|
||||
>
|
||||
总Token
|
||||
</th>
|
||||
<th class="px-2 py-2 text-right text-xs font-medium text-gray-700 sm:px-4">
|
||||
<th
|
||||
class="px-2 py-2 text-right text-xs font-medium text-gray-700 dark:text-gray-300 sm:px-4"
|
||||
>
|
||||
费用
|
||||
</th>
|
||||
<th
|
||||
class="hidden px-2 py-2 text-right text-xs font-medium text-gray-700 sm:table-cell sm:px-4"
|
||||
class="hidden px-2 py-2 text-right text-xs font-medium text-gray-700 dark:text-gray-300 sm:table-cell sm:px-4"
|
||||
>
|
||||
占比
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tr v-for="stat in dashboardModelStats" :key="stat.model" class="hover:bg-gray-50">
|
||||
<td class="px-2 py-2 text-xs text-gray-900 sm:px-4 sm:text-sm">
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-600">
|
||||
<tr
|
||||
v-for="stat in dashboardModelStats"
|
||||
:key="stat.model"
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
<td class="px-2 py-2 text-xs text-gray-900 dark:text-gray-100 sm:px-4 sm:text-sm">
|
||||
<span class="block max-w-[100px] truncate sm:max-w-none" :title="stat.model">
|
||||
{{ stat.model }}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
class="hidden px-2 py-2 text-right text-xs text-gray-600 sm:table-cell sm:px-4 sm:text-sm"
|
||||
class="hidden px-2 py-2 text-right text-xs text-gray-600 dark:text-gray-400 sm:table-cell sm:px-4 sm:text-sm"
|
||||
>
|
||||
{{ formatNumber(stat.requests) }}
|
||||
</td>
|
||||
<td class="px-2 py-2 text-right text-xs text-gray-600 sm:px-4 sm:text-sm">
|
||||
<td
|
||||
class="px-2 py-2 text-right text-xs text-gray-600 dark:text-gray-400 sm:px-4 sm:text-sm"
|
||||
>
|
||||
{{ formatNumber(stat.allTokens) }}
|
||||
</td>
|
||||
<td
|
||||
@@ -486,7 +525,7 @@
|
||||
class="hidden px-2 py-2 text-right text-xs font-medium sm:table-cell sm:px-4 sm:text-sm"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800"
|
||||
class="inline-flex items-center rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"
|
||||
>
|
||||
{{ calculatePercentage(stat.allTokens, dashboardModelStats) }}%
|
||||
</span>
|
||||
@@ -512,15 +551,17 @@
|
||||
<div class="mb-4 sm:mb-6 md:mb-8">
|
||||
<div class="card p-4 sm:p-6">
|
||||
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h3 class="text-base font-semibold text-gray-900 sm:text-lg">API Keys 使用趋势</h3>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100 sm:text-lg">
|
||||
API Keys 使用趋势
|
||||
</h3>
|
||||
<!-- 维度切换按钮 -->
|
||||
<div class="flex gap-1 rounded-lg bg-gray-100 p-1">
|
||||
<div class="flex gap-1 rounded-lg bg-gray-100 p-1 dark:bg-gray-700">
|
||||
<button
|
||||
:class="[
|
||||
'rounded-md px-2 py-1 text-xs font-medium transition-colors sm:px-3 sm:text-sm',
|
||||
apiKeysTrendMetric === 'requests'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100'
|
||||
]"
|
||||
@click="((apiKeysTrendMetric = 'requests'), updateApiKeysUsageTrendChart())"
|
||||
>
|
||||
@@ -531,8 +572,8 @@
|
||||
:class="[
|
||||
'rounded-md px-2 py-1 text-xs font-medium transition-colors sm:px-3 sm:text-sm',
|
||||
apiKeysTrendMetric === 'tokens'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100'
|
||||
]"
|
||||
@click="((apiKeysTrendMetric = 'tokens'), updateApiKeysUsageTrendChart())"
|
||||
>
|
||||
@@ -541,7 +582,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 text-xs text-gray-600 sm:text-sm">
|
||||
<div class="mb-4 text-xs text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
<span v-if="apiKeysTrendData.totalApiKeys > 10">
|
||||
共 {{ apiKeysTrendData.totalApiKeys }} 个 API Key,显示使用量前 10 个
|
||||
</span>
|
||||
@@ -556,12 +597,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useDashboardStore } from '@/stores/dashboard'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import Chart from 'chart.js/auto'
|
||||
|
||||
const dashboardStore = useDashboardStore()
|
||||
const themeStore = useThemeStore()
|
||||
const { isDarkMode } = storeToRefs(themeStore)
|
||||
|
||||
const {
|
||||
dashboardData,
|
||||
costsData,
|
||||
@@ -607,6 +652,13 @@ const isRefreshing = ref(false)
|
||||
// return `${refreshCountdown.value}秒后刷新`
|
||||
// })
|
||||
|
||||
// 图表颜色配置(根据主题动态调整)
|
||||
const chartColors = computed(() => ({
|
||||
text: isDarkMode.value ? '#e5e7eb' : '#374151',
|
||||
grid: isDarkMode.value ? 'rgba(75, 85, 99, 0.3)' : 'rgba(0, 0, 0, 0.1)',
|
||||
legend: isDarkMode.value ? '#e5e7eb' : '#374151'
|
||||
}))
|
||||
|
||||
// 格式化数字
|
||||
function formatNumber(num) {
|
||||
if (num >= 1000000) {
|
||||
@@ -670,7 +722,8 @@ function createModelUsageChart() {
|
||||
usePointStyle: true,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
},
|
||||
color: chartColors.value.legend
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
@@ -800,10 +853,14 @@ function createUsageTrendChart() {
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
color: chartColors.value.text
|
||||
},
|
||||
legend: {
|
||||
position: 'top'
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: chartColors.value.legend
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
@@ -858,7 +915,14 @@ function createUsageTrendChart() {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: trendGranularity.value === 'hour' ? '时间' : '日期'
|
||||
text: trendGranularity === 'hour' ? '时间' : '日期',
|
||||
color: chartColors.value.text
|
||||
},
|
||||
ticks: {
|
||||
color: chartColors.value.text
|
||||
},
|
||||
grid: {
|
||||
color: chartColors.value.grid
|
||||
}
|
||||
},
|
||||
y: {
|
||||
@@ -867,12 +931,17 @@ function createUsageTrendChart() {
|
||||
position: 'left',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Token数量'
|
||||
text: 'Token数量',
|
||||
color: chartColors.value.text
|
||||
},
|
||||
ticks: {
|
||||
callback: function (value) {
|
||||
return formatNumber(value)
|
||||
}
|
||||
},
|
||||
color: chartColors.value.text
|
||||
},
|
||||
grid: {
|
||||
color: chartColors.value.grid
|
||||
}
|
||||
},
|
||||
y1: {
|
||||
@@ -881,7 +950,8 @@ function createUsageTrendChart() {
|
||||
position: 'right',
|
||||
title: {
|
||||
display: true,
|
||||
text: '请求数'
|
||||
text: '请求数',
|
||||
color: chartColors.value.text
|
||||
},
|
||||
grid: {
|
||||
drawOnChartArea: false
|
||||
@@ -889,7 +959,8 @@ function createUsageTrendChart() {
|
||||
ticks: {
|
||||
callback: function (value) {
|
||||
return value.toLocaleString()
|
||||
}
|
||||
},
|
||||
color: chartColors.value.text
|
||||
}
|
||||
},
|
||||
y2: {
|
||||
@@ -911,7 +982,7 @@ function createApiKeysUsageTrendChart() {
|
||||
}
|
||||
|
||||
const data = apiKeysTrendData.value.data || []
|
||||
const metric = apiKeysTrendMetric.value
|
||||
const metric = apiKeysTrendMetric
|
||||
|
||||
// 颜色数组
|
||||
const colors = [
|
||||
@@ -998,7 +1069,8 @@ function createApiKeysUsageTrendChart() {
|
||||
usePointStyle: true,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
},
|
||||
color: chartColors.value.legend
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
@@ -1032,7 +1104,7 @@ function createApiKeysUsageTrendChart() {
|
||||
else if (rank === 2) rankIcon = '🥈 '
|
||||
else if (rank === 3) rankIcon = '🥉 '
|
||||
|
||||
if (apiKeysTrendMetric.value === 'tokens') {
|
||||
if (apiKeysTrendMetric === 'tokens') {
|
||||
// 格式化token显示
|
||||
let formattedValue = ''
|
||||
if (value >= 1000000) {
|
||||
@@ -1062,19 +1134,31 @@ function createApiKeysUsageTrendChart() {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: trendGranularity.value === 'hour' ? '时间' : '日期'
|
||||
text: trendGranularity === 'hour' ? '时间' : '日期',
|
||||
color: chartColors.value.text
|
||||
},
|
||||
ticks: {
|
||||
color: chartColors.value.text
|
||||
},
|
||||
grid: {
|
||||
color: chartColors.value.grid
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: apiKeysTrendMetric.value === 'tokens' ? 'Token 数量' : '请求次数'
|
||||
text: apiKeysTrendMetric === 'tokens' ? 'Token 数量' : '请求次数',
|
||||
color: chartColors.value.text
|
||||
},
|
||||
ticks: {
|
||||
callback: function (value) {
|
||||
return formatNumber(value)
|
||||
}
|
||||
},
|
||||
color: chartColors.value.text
|
||||
},
|
||||
grid: {
|
||||
color: chartColors.value.grid
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1084,7 +1168,7 @@ function createApiKeysUsageTrendChart() {
|
||||
|
||||
// 更新API Keys使用趋势图
|
||||
async function updateApiKeysUsageTrendChart() {
|
||||
await loadApiKeysTrend(apiKeysTrendMetric.value)
|
||||
await loadApiKeysTrend(apiKeysTrendMetric)
|
||||
await nextTick()
|
||||
createApiKeysUsageTrendChart()
|
||||
}
|
||||
@@ -1179,6 +1263,15 @@ watch(autoRefreshEnabled, (newVal) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 监听主题变化,重新创建图表
|
||||
watch(isDarkMode, () => {
|
||||
nextTick(() => {
|
||||
createModelUsageChart()
|
||||
createUsageTrendChart()
|
||||
createApiKeysUsageTrendChart()
|
||||
})
|
||||
})
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
// 加载所有数据
|
||||
@@ -1208,19 +1301,8 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义日期选择器样式 */
|
||||
.custom-date-picker :deep(.el-input__inner) {
|
||||
@apply border-gray-300 bg-white focus:border-blue-500 focus:ring-blue-500;
|
||||
font-size: 13px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.custom-date-picker :deep(.el-range-separator) {
|
||||
@apply text-gray-500;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.custom-date-picker :deep(.el-range-input) {
|
||||
/* 日期选择器基本样式调整 - 让Element Plus官方暗黑模式生效 */
|
||||
.custom-date-picker {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<div class="flex min-h-screen items-center justify-center p-4 sm:p-6">
|
||||
<!-- 主题切换按钮 - 固定在右上角 -->
|
||||
<div class="fixed right-4 top-4 z-50">
|
||||
<ThemeToggle mode="dropdown" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="glass-strong w-full max-w-md rounded-xl p-6 shadow-2xl sm:rounded-2xl sm:p-8 md:rounded-3xl md:p-10"
|
||||
>
|
||||
@@ -29,12 +34,14 @@
|
||||
v-else-if="oemLoading"
|
||||
class="mx-auto mb-2 h-8 w-48 animate-pulse rounded bg-gray-300/50 sm:h-9 sm:w-64"
|
||||
/>
|
||||
<p class="text-base text-gray-600 sm:text-lg">管理后台</p>
|
||||
<p class="text-base text-gray-600 dark:text-gray-400 sm:text-lg">管理后台</p>
|
||||
</div>
|
||||
|
||||
<form class="space-y-4 sm:space-y-6" @submit.prevent="handleLogin">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-900 sm:mb-3">用户名</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
|
||||
>用户名</label
|
||||
>
|
||||
<input
|
||||
v-model="loginForm.username"
|
||||
class="form-input w-full"
|
||||
@@ -45,7 +52,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-900 sm:mb-3">密码</label>
|
||||
<label class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
|
||||
>密码</label
|
||||
>
|
||||
<input
|
||||
v-model="loginForm.password"
|
||||
class="form-input w-full"
|
||||
@@ -68,7 +77,7 @@
|
||||
|
||||
<div
|
||||
v-if="authStore.loginError"
|
||||
class="mt-4 rounded-lg border border-red-500/30 bg-red-500/20 p-3 text-center text-xs text-red-800 backdrop-blur-sm sm:mt-6 sm:rounded-xl sm:p-4 sm:text-sm"
|
||||
class="mt-4 rounded-lg border border-red-500/30 bg-red-500/20 p-3 text-center text-xs text-red-800 backdrop-blur-sm dark:text-red-400 sm:mt-6 sm:rounded-xl sm:p-4 sm:text-sm"
|
||||
>
|
||||
<i class="fas fa-exclamation-triangle mr-2" />{{ authStore.loginError }}
|
||||
</div>
|
||||
@@ -79,8 +88,11 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const themeStore = useThemeStore()
|
||||
const oemLoading = computed(() => authStore.oemLoading)
|
||||
|
||||
const loginForm = ref({
|
||||
@@ -89,6 +101,8 @@ const loginForm = ref({
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化主题
|
||||
themeStore.initTheme()
|
||||
// 加载OEM设置
|
||||
authStore.loadOemSettings()
|
||||
})
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
<div class="settings-container">
|
||||
<div class="card p-4 sm:p-6">
|
||||
<div class="mb-4 sm:mb-6">
|
||||
<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">自定义网站名称和图标</p>
|
||||
<h3 class="mb-1 text-lg font-bold text-gray-900 dark:text-gray-100 sm:mb-2 sm:text-xl">
|
||||
其他设置
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">自定义网站名称和图标</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="py-12 text-center">
|
||||
<div class="loading-spinner mx-auto mb-4" />
|
||||
<p class="text-gray-500">正在加载设置...</p>
|
||||
<p class="text-gray-500 dark:text-gray-400">正在加载设置...</p>
|
||||
</div>
|
||||
|
||||
<!-- 桌面端表格视图 -->
|
||||
<div v-else class="table-container hidden sm:block">
|
||||
<table class="min-w-full">
|
||||
<tbody class="divide-y divide-gray-200/50">
|
||||
<tbody class="divide-y divide-gray-200/50 dark:divide-gray-600/50">
|
||||
<!-- 网站名称 -->
|
||||
<tr class="table-row">
|
||||
<td class="w-48 whitespace-nowrap px-6 py-4">
|
||||
@@ -25,20 +27,24 @@
|
||||
<i class="fas fa-font text-xs text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-900">网站名称</div>
|
||||
<div class="text-xs text-gray-500">品牌标识</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">
|
||||
<input
|
||||
v-model="oemSettings.siteName"
|
||||
class="form-input w-full max-w-md"
|
||||
class="form-input w-full max-w-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
maxlength="100"
|
||||
placeholder="Claude Relay Service"
|
||||
type="text"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">将显示在浏览器标题和页面头部</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
将显示在浏览器标题和页面头部
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -52,8 +58,10 @@
|
||||
<i class="fas fa-image text-xs text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-900">网站图标</div>
|
||||
<div class="text-xs text-gray-500">Favicon</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">Favicon</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -62,7 +70,7 @@
|
||||
<!-- 图标预览 -->
|
||||
<div
|
||||
v-if="oemSettings.siteIconData || oemSettings.siteIcon"
|
||||
class="inline-flex items-center gap-3 rounded-lg bg-gray-50 p-3"
|
||||
class="inline-flex items-center gap-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-700"
|
||||
>
|
||||
<img
|
||||
alt="图标预览"
|
||||
@@ -70,7 +78,7 @@
|
||||
:src="oemSettings.siteIconData || oemSettings.siteIcon"
|
||||
@error="handleIconError"
|
||||
/>
|
||||
<span class="text-sm text-gray-600">当前图标</span>
|
||||
<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"
|
||||
@@ -92,7 +100,7 @@
|
||||
<i class="fas fa-upload mr-2" />
|
||||
上传图标
|
||||
</button>
|
||||
<span class="ml-3 text-xs text-gray-500"
|
||||
<span class="ml-3 text-xs text-gray-500 dark:text-gray-400"
|
||||
>支持 .ico, .png, .jpg, .svg 格式,最大 350KB</span
|
||||
>
|
||||
</div>
|
||||
@@ -117,7 +125,7 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn bg-gray-100 px-6 py-3 text-gray-700 hover:bg-gray-200"
|
||||
class="btn 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"
|
||||
>
|
||||
@@ -126,7 +134,10 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="oemSettings.updatedAt" class="text-sm text-gray-500">
|
||||
<div
|
||||
v-if="oemSettings.updatedAt"
|
||||
class="text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<i class="fas fa-clock mr-1" />
|
||||
最后更新:{{ formatDateTime(oemSettings.updatedAt) }}
|
||||
</div>
|
||||
@@ -140,7 +151,7 @@
|
||||
<!-- 移动端卡片视图 -->
|
||||
<div v-if="!loading" class="space-y-4 sm:hidden">
|
||||
<!-- 网站名称设置卡片 -->
|
||||
<div class="rounded-lg bg-gray-50 p-4">
|
||||
<div class="rounded-lg bg-gray-50 p-4 dark:bg-gray-700">
|
||||
<div class="mb-3 flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-blue-600"
|
||||
@@ -148,24 +159,24 @@
|
||||
<i class="fas fa-font text-sm text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-900">网站名称</h4>
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">网站名称</h4>
|
||||
<p class="text-xs text-gray-500">品牌标识</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<input
|
||||
v-model="oemSettings.siteName"
|
||||
class="form-input w-full text-sm"
|
||||
class="form-input w-full text-sm dark:border-gray-500 dark:bg-gray-600 dark:text-gray-200"
|
||||
maxlength="100"
|
||||
placeholder="Claude Relay Service"
|
||||
type="text"
|
||||
/>
|
||||
<p class="text-xs text-gray-500">将显示在浏览器标题和页面头部</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">将显示在浏览器标题和页面头部</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 网站图标设置卡片 -->
|
||||
<div class="rounded-lg bg-gray-50 p-4">
|
||||
<div class="rounded-lg bg-gray-50 p-4 dark:bg-gray-700">
|
||||
<div class="mb-3 flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-purple-500 to-purple-600"
|
||||
@@ -173,15 +184,15 @@
|
||||
<i class="fas fa-image text-sm text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-900">网站图标</h4>
|
||||
<p class="text-xs text-gray-500">Favicon</p>
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">网站图标</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Favicon</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<!-- 图标预览 -->
|
||||
<div
|
||||
v-if="oemSettings.siteIconData || oemSettings.siteIcon"
|
||||
class="inline-flex w-full items-center gap-3 rounded-lg border border-gray-200 bg-white p-3"
|
||||
class="inline-flex w-full items-center gap-3 rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-600 dark:bg-gray-800"
|
||||
>
|
||||
<img
|
||||
alt="图标预览"
|
||||
@@ -189,7 +200,7 @@
|
||||
:src="oemSettings.siteIconData || oemSettings.siteIcon"
|
||||
@error="handleIconError"
|
||||
/>
|
||||
<span class="flex-1 text-sm text-gray-600">当前图标</span>
|
||||
<span class="flex-1 text-sm text-gray-600 dark:text-gray-400">当前图标</span>
|
||||
<button
|
||||
class="rounded-lg px-3 py-1 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
|
||||
@click="removeIcon"
|
||||
@@ -208,21 +219,23 @@
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
class="flex w-full items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-50"
|
||||
class="flex w-full items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
@click="$refs.iconFileInput.click()"
|
||||
>
|
||||
<i class="fas fa-upload" />
|
||||
上传图标
|
||||
</button>
|
||||
<div class="text-xs text-gray-500">或者输入图标URL:</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">或者输入图标URL:</div>
|
||||
<input
|
||||
v-model="oemSettings.siteIcon"
|
||||
class="form-input w-full text-sm"
|
||||
class="form-input w-full text-sm dark:border-gray-500 dark:bg-gray-600 dark:text-gray-200"
|
||||
placeholder="https://example.com/icon.png"
|
||||
type="url"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">支持 PNG、JPEG、GIF 格式,建议使用正方形图片</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
支持 PNG、JPEG、GIF 格式,建议使用正方形图片
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -239,7 +252,7 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn w-full bg-gray-100 py-3 text-sm text-gray-700 hover:bg-gray-200"
|
||||
class="btn w-full bg-gray-100 py-3 text-sm text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
:disabled="saving"
|
||||
@click="resetOemSettings"
|
||||
>
|
||||
@@ -247,7 +260,10 @@
|
||||
重置为默认
|
||||
</button>
|
||||
|
||||
<div v-if="oemSettings.updatedAt" class="text-center text-xs text-gray-500">
|
||||
<div
|
||||
v-if="oemSettings.updatedAt"
|
||||
class="text-center text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<i class="fas fa-clock mr-1" />
|
||||
最后更新:{{ formatDateTime(oemSettings.updatedAt) }}
|
||||
</div>
|
||||
@@ -365,12 +381,22 @@ const formatDateTime = settingsStore.formatDateTime
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
:root.dark .card {
|
||||
background: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
:root.dark .table-container {
|
||||
border: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
@@ -379,6 +405,10 @@ const formatDateTime = settingsStore.formatDateTime
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
:root.dark .table-row:hover {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
@apply w-full rounded-lg border border-gray-300 px-4 py-2 transition-all duration-200 focus:border-transparent focus:ring-2 focus:ring-blue-500;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
<template>
|
||||
<div class="card p-3 sm:p-6">
|
||||
<div class="mb-4 sm:mb-8">
|
||||
<h3 class="mb-3 flex items-center text-xl font-bold text-gray-900 sm:mb-4 sm:text-2xl">
|
||||
<h3
|
||||
class="mb-3 flex items-center text-xl font-bold text-gray-900 dark:text-gray-100 sm:mb-4 sm:text-2xl"
|
||||
>
|
||||
<i class="fas fa-graduation-cap mr-2 text-blue-600 sm:mr-3" />
|
||||
Claude Code 使用教程
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 sm:text-lg">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-lg">
|
||||
跟着这个教程,你可以轻松在自己的电脑上安装并使用 Claude Code。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 系统选择标签 -->
|
||||
<div class="mb-4 sm:mb-8">
|
||||
<div class="flex flex-wrap gap-1 rounded-xl bg-gray-100 p-1 sm:gap-2 sm:p-2">
|
||||
<div class="flex flex-wrap gap-1 rounded-xl bg-gray-100 p-1 dark:bg-gray-700 sm:gap-2 sm:p-2">
|
||||
<button
|
||||
v-for="system in tutorialSystems"
|
||||
:key="system.key"
|
||||
:class="[
|
||||
'flex flex-1 items-center justify-center gap-1 rounded-lg px-3 py-2 text-xs font-semibold transition-all duration-300 sm:gap-2 sm:px-6 sm:py-3 sm:text-sm',
|
||||
activeTutorialSystem === system.key
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:bg-white/50 hover:text-gray-900'
|
||||
? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
|
||||
: 'text-gray-600 hover:bg-white/50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200'
|
||||
]"
|
||||
@click="activeTutorialSystem = system.key"
|
||||
>
|
||||
@@ -34,14 +36,16 @@
|
||||
<div v-if="activeTutorialSystem === 'windows'" class="tutorial-content">
|
||||
<!-- 第一步:安装 Node.js -->
|
||||
<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 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<span
|
||||
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-blue-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
|
||||
>1</span
|
||||
>
|
||||
安装 Node.js 环境
|
||||
</h4>
|
||||
<p class="mb-4 text-sm text-gray-600 sm:mb-4 sm:mb-6 sm:text-base">
|
||||
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400 sm:mb-4 sm:mb-6 sm:text-base">
|
||||
Claude Code 需要 Node.js 环境才能运行。
|
||||
</p>
|
||||
|
||||
@@ -49,34 +53,42 @@
|
||||
class="mb-4 rounded-xl border border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-4 sm:mb-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"
|
||||
class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fab fa-windows mr-2 text-blue-600" />
|
||||
Windows 安装方法
|
||||
</h5>
|
||||
<div class="mb-3 sm:mb-4">
|
||||
<p class="mb-2 text-sm text-gray-700 sm:mb-3 sm:text-base">方法一:官网下载(推荐)</p>
|
||||
<p class="mb-2 text-sm text-gray-700 dark:text-gray-600 sm:mb-3 sm:text-base">
|
||||
方法一:官网下载(推荐)
|
||||
</p>
|
||||
<ol
|
||||
class="ml-2 list-inside list-decimal space-y-1 text-xs text-gray-600 sm:ml-4 sm:space-y-2 sm:text-sm"
|
||||
class="ml-2 list-inside list-decimal space-y-1 text-xs text-gray-600 dark: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"
|
||||
<code
|
||||
class="rounded bg-gray-100 px-1 py-1 text-xs dark:bg-gray-700 sm:px-2 sm:text-sm"
|
||||
>https://nodejs.org/</code
|
||||
>
|
||||
</li>
|
||||
<li>点击 "LTS" 版本进行下载(推荐长期支持版本)</li>
|
||||
<li>
|
||||
下载完成后双击
|
||||
<code class="rounded bg-gray-100 px-1 py-1 text-xs sm:px-2 sm:text-sm">.msi</code>
|
||||
<code
|
||||
class="rounded bg-gray-100 px-1 py-1 text-xs dark:bg-gray-700 sm:px-2 sm:text-sm"
|
||||
>.msi</code
|
||||
>
|
||||
文件
|
||||
</li>
|
||||
<li>按照安装向导完成安装,保持默认设置即可</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="mb-3 sm:mb-4">
|
||||
<p class="mb-2 text-sm text-gray-700 sm:mb-3 sm:text-base">方法二:使用包管理器</p>
|
||||
<p class="mb-2 text-xs text-gray-600 sm:text-sm">
|
||||
<p class="mb-2 text-sm text-gray-700 dark:text-gray-600 sm:mb-3 sm:text-base">
|
||||
方法二:使用包管理器
|
||||
</p>
|
||||
<p class="mb-2 text-xs text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
如果你安装了 Chocolatey 或 Scoop,可以使用命令行安装:
|
||||
</p>
|
||||
<div
|
||||
@@ -116,7 +128,9 @@
|
||||
|
||||
<!-- 第二步:安装 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">
|
||||
<h4
|
||||
class="mb-3 flex items-center text-lg font-semibold text-gray-800 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<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"
|
||||
>2</span
|
||||
@@ -128,12 +142,12 @@
|
||||
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"
|
||||
>
|
||||
<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 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fas fa-download mr-2 text-green-600" />
|
||||
安装 Claude Code
|
||||
</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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
打开 PowerShell 或 CMD,运行以下命令:
|
||||
</p>
|
||||
<div
|
||||
@@ -144,7 +158,7 @@
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
这个命令会从 npm 官方仓库下载并安装最新版本的 Claude Code。
|
||||
</p>
|
||||
|
||||
@@ -159,7 +173,7 @@
|
||||
|
||||
<!-- 验证安装 -->
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-green-800">验证 Claude Code 安装</h6>
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">验证 Claude Code 安装</h6>
|
||||
<p class="mb-3 text-sm text-green-700">安装完成后,输入以下命令检查是否安装成功:</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"
|
||||
@@ -174,7 +188,9 @@
|
||||
|
||||
<!-- 第三步:设置环境变量 -->
|
||||
<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 dark:text-gray-300 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
|
||||
@@ -186,18 +202,20 @@
|
||||
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"
|
||||
class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fas fa-cog mr-2 text-purple-600" />
|
||||
配置 Claude Code 环境变量
|
||||
</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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
为了让 Claude Code 连接到你的中转服务,需要设置两个环境变量:
|
||||
</p>
|
||||
|
||||
<div class="space-y-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">
|
||||
<div
|
||||
class="rounded-lg border border-purple-200 bg-white p-3 dark:border-purple-700 dark:bg-gray-800 sm:p-4"
|
||||
>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
方法一:PowerShell 临时设置(当前会话)
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">在 PowerShell 中运行以下命令:</p>
|
||||
@@ -216,8 +234,10 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div
|
||||
class="rounded-lg border border-purple-200 bg-white p-3 dark:border-purple-700 dark:bg-gray-800 sm:p-4"
|
||||
>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
方法二:PowerShell 永久设置(用户级)
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">
|
||||
@@ -260,14 +280,14 @@
|
||||
|
||||
<!-- 验证环境变量设置 -->
|
||||
<div class="mt-6 rounded-lg border border-blue-200 bg-blue-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-blue-800">验证环境变量设置</h6>
|
||||
<h6 class="mb-2 font-medium text-blue-800 dark:text-blue-300">验证环境变量设置</h6>
|
||||
<p class="mb-3 text-sm text-blue-700">
|
||||
设置完环境变量后,可以通过以下命令验证是否设置成功:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<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 dark:text-gray-600 sm:text-base">
|
||||
在 PowerShell 中验证:
|
||||
</h6>
|
||||
<div
|
||||
@@ -279,7 +299,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">在 CMD 中验证:</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
在 CMD 中验证:
|
||||
</h6>
|
||||
<div
|
||||
class="space-y-1 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
>
|
||||
@@ -293,7 +315,7 @@
|
||||
<p class="text-sm text-blue-700">
|
||||
<strong>预期输出示例:</strong>
|
||||
</p>
|
||||
<div class="rounded bg-gray-100 p-2 font-mono text-sm">
|
||||
<div class="rounded bg-gray-100 p-2 font-mono text-sm dark:bg-gray-700">
|
||||
<div>{{ currentBaseUrl }}</div>
|
||||
<div>cr_xxxxxxxxxxxxxxxxxx</div>
|
||||
</div>
|
||||
@@ -306,18 +328,18 @@
|
||||
<!-- Gemini CLI 环境变量设置 -->
|
||||
<div class="mt-8">
|
||||
<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 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fas fa-robot mr-2 text-green-600" />
|
||||
配置 Gemini CLI 环境变量
|
||||
</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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
如果你使用 Gemini CLI,需要设置以下环境变量:
|
||||
</p>
|
||||
|
||||
<div class="space-y-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 dark:text-gray-600 sm:text-base">
|
||||
PowerShell 设置方法
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">在 PowerShell 中运行以下命令:</p>
|
||||
@@ -340,7 +362,7 @@
|
||||
</div>
|
||||
|
||||
<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 dark:text-gray-600 sm:text-base">
|
||||
PowerShell 永久设置(用户级)
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">在 PowerShell 中运行以下命令:</p>
|
||||
@@ -368,7 +390,9 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-green-800">验证 Gemini CLI 环境变量</h6>
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
|
||||
验证 Gemini CLI 环境变量
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-green-700">在 PowerShell 中验证:</p>
|
||||
<div
|
||||
class="space-y-1 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
@@ -386,18 +410,18 @@
|
||||
<!-- Codex 环境变量设置 -->
|
||||
<div class="mt-8">
|
||||
<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 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fas fa-code mr-2 text-indigo-600" />
|
||||
配置 Codex 环境变量
|
||||
</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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
如果你使用支持 OpenAI API 的工具(如 Codex),需要设置以下环境变量:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg border border-indigo-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 dark:text-gray-600 sm:text-base">
|
||||
PowerShell 设置方法
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">在 PowerShell 中运行以下命令:</p>
|
||||
@@ -417,7 +441,7 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-indigo-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 dark:text-gray-600 sm:text-base">
|
||||
PowerShell 永久设置(用户级)
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">在 PowerShell 中运行以下命令:</p>
|
||||
@@ -456,7 +480,9 @@
|
||||
|
||||
<!-- 第四步:开始使用 -->
|
||||
<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 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<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"
|
||||
>4</span
|
||||
@@ -466,13 +492,15 @@
|
||||
<div
|
||||
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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
现在你可以开始使用 Claude Code 了!
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">启动 Claude Code</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
启动 Claude Code
|
||||
</h6>
|
||||
<div
|
||||
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
>
|
||||
@@ -481,7 +509,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">在特定项目中使用</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
在特定项目中使用
|
||||
</h6>
|
||||
<div
|
||||
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
>
|
||||
@@ -497,7 +527,9 @@
|
||||
|
||||
<!-- Windows 故障排除 -->
|
||||
<div class="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 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<i class="fas fa-wrench mr-2 text-red-600 sm:mr-3" />
|
||||
Windows 常见问题解决
|
||||
</h4>
|
||||
@@ -567,7 +599,9 @@
|
||||
<div v-else-if="activeTutorialSystem === 'macos'" class="tutorial-content">
|
||||
<!-- 第一步:安装 Node.js -->
|
||||
<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 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<span
|
||||
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-blue-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
|
||||
>1</span
|
||||
@@ -580,14 +614,14 @@
|
||||
class="mb-4 rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-slate-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"
|
||||
class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fab fa-apple mr-2 text-gray-700" />
|
||||
macOS 安装方法
|
||||
</h5>
|
||||
<div class="mb-4">
|
||||
<p class="mb-3 text-gray-700">方法一:使用 Homebrew(推荐)</p>
|
||||
<p class="mb-2 text-xs text-gray-600 sm:text-sm">
|
||||
<p class="mb-2 text-xs text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
如果你已经安装了 Homebrew,使用它安装 Node.js 会更方便:
|
||||
</p>
|
||||
<div
|
||||
@@ -602,18 +636,22 @@
|
||||
<div class="mb-4">
|
||||
<p class="mb-3 text-gray-700">方法二:官网下载</p>
|
||||
<ol
|
||||
class="ml-2 list-inside list-decimal space-y-1 text-xs text-gray-600 sm:ml-4 sm:space-y-2 sm:text-sm"
|
||||
class="ml-2 list-inside list-decimal space-y-1 text-xs text-gray-600 dark:text-gray-400 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"
|
||||
<code
|
||||
class="rounded bg-gray-100 px-1 py-1 text-xs dark:bg-gray-700 sm:px-2 sm:text-sm"
|
||||
>https://nodejs.org/</code
|
||||
>
|
||||
</li>
|
||||
<li>下载适合 macOS 的 LTS 版本</li>
|
||||
<li>
|
||||
打开下载的
|
||||
<code class="rounded bg-gray-100 px-1 py-1 text-xs sm:px-2 sm:text-sm">.pkg</code>
|
||||
<code
|
||||
class="rounded bg-gray-100 px-1 py-1 text-xs dark:bg-gray-700 sm:px-2 sm:text-sm"
|
||||
>.pkg</code
|
||||
>
|
||||
文件
|
||||
</li>
|
||||
<li>按照安装程序指引完成安装</li>
|
||||
@@ -634,7 +672,7 @@
|
||||
|
||||
<!-- 验证安装 -->
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-green-800">验证安装是否成功</h6>
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">验证安装是否成功</h6>
|
||||
<p class="mb-3 text-sm text-green-700">安装完成后,打开 Terminal,输入以下命令:</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"
|
||||
@@ -648,7 +686,9 @@
|
||||
|
||||
<!-- 第二步:安装 Claude Code -->
|
||||
<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 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<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"
|
||||
>2</span
|
||||
@@ -660,12 +700,12 @@
|
||||
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"
|
||||
class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fas fa-download mr-2 text-purple-600" />
|
||||
安装 Claude Code
|
||||
</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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
打开 Terminal,运行以下命令:
|
||||
</p>
|
||||
<div
|
||||
@@ -688,7 +728,7 @@
|
||||
|
||||
<!-- 验证安装 -->
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-green-800">验证 Claude Code 安装</h6>
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">验证 Claude Code 安装</h6>
|
||||
<p class="mb-3 text-sm text-green-700">安装完成后,输入以下命令检查是否安装成功:</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"
|
||||
@@ -703,7 +743,9 @@
|
||||
|
||||
<!-- 第三步:设置环境变量 -->
|
||||
<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 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<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"
|
||||
>3</span
|
||||
@@ -715,18 +757,18 @@
|
||||
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"
|
||||
>
|
||||
<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 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fas fa-cog mr-2 text-orange-600" />
|
||||
配置 Claude Code 环境变量
|
||||
</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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
为了让 Claude Code 连接到你的中转服务,需要设置两个环境变量:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg border border-orange-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 dark:text-gray-600 sm:text-base">
|
||||
方法一:临时设置(当前会话)
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">在 Terminal 中运行以下命令:</p>
|
||||
@@ -746,7 +788,9 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-orange-200 bg-white p-3 sm:p-4">
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">方法二:永久设置</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
方法二:永久设置
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">
|
||||
编辑你的 shell 配置文件(根据你使用的 shell):
|
||||
</p>
|
||||
@@ -781,18 +825,20 @@
|
||||
<!-- Gemini CLI 环境变量设置 -->
|
||||
<div class="mt-8">
|
||||
<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 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fas fa-robot mr-2 text-green-600" />
|
||||
配置 Gemini CLI 环境变量
|
||||
</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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
如果你使用 Gemini CLI,需要设置以下环境变量:
|
||||
</p>
|
||||
|
||||
<div class="space-y-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">Terminal 设置方法</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
Terminal 设置方法
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">在 Terminal 中运行以下命令:</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"
|
||||
@@ -813,7 +859,9 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
永久设置方法
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">添加到你的 shell 配置文件:</p>
|
||||
<div
|
||||
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"
|
||||
@@ -848,7 +896,9 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-green-800">验证 Gemini CLI 环境变量</h6>
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
|
||||
验证 Gemini CLI 环境变量
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-green-700">在 Terminal 中验证:</p>
|
||||
<div
|
||||
class="space-y-1 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
@@ -864,18 +914,20 @@
|
||||
<!-- Codex 环境变量设置 -->
|
||||
<div class="mt-8">
|
||||
<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 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fas fa-code mr-2 text-indigo-600" />
|
||||
配置 Codex 环境变量
|
||||
</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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
如果你使用支持 OpenAI API 的工具(如 Codex),需要设置以下环境变量:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">Terminal 设置方法</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
Terminal 设置方法
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">在 Terminal 中运行以下命令:</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"
|
||||
@@ -893,7 +945,9 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">永久设置方法</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
永久设置方法
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">添加到你的 shell 配置文件:</p>
|
||||
<div
|
||||
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"
|
||||
@@ -937,7 +991,9 @@
|
||||
|
||||
<!-- 第四步:开始使用 -->
|
||||
<div class="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 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<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"
|
||||
>4</span
|
||||
@@ -947,13 +1003,15 @@
|
||||
<div
|
||||
class="rounded-xl border border-yellow-100 bg-gradient-to-r from-yellow-50 to-amber-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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
现在你可以开始使用 Claude Code 了!
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">启动 Claude Code</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
启动 Claude Code
|
||||
</h6>
|
||||
<div
|
||||
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
>
|
||||
@@ -962,7 +1020,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">在特定项目中使用</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
在特定项目中使用
|
||||
</h6>
|
||||
<div
|
||||
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
>
|
||||
@@ -978,7 +1038,9 @@
|
||||
|
||||
<!-- macOS 故障排除 -->
|
||||
<div class="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 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<i class="fas fa-wrench mr-2 text-red-600 sm:mr-3" />
|
||||
macOS 常见问题解决
|
||||
</h4>
|
||||
@@ -1054,7 +1116,9 @@
|
||||
<div v-else-if="activeTutorialSystem === 'linux'" class="tutorial-content">
|
||||
<!-- 第一步:安装 Node.js -->
|
||||
<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 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<span
|
||||
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-blue-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
|
||||
>1</span
|
||||
@@ -1067,7 +1131,7 @@
|
||||
class="mb-4 rounded-xl border border-orange-100 bg-gradient-to-r from-orange-50 to-red-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"
|
||||
class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fab fa-ubuntu mr-2 text-orange-600" />
|
||||
Linux 安装方法
|
||||
@@ -1087,7 +1151,7 @@
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<p class="mb-3 text-gray-700">方法二:使用系统包管理器</p>
|
||||
<p class="mb-2 text-xs text-gray-600 sm:text-sm">
|
||||
<p class="mb-2 text-xs text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
虽然版本可能不是最新的,但对于基本使用已经足够:
|
||||
</p>
|
||||
<div
|
||||
@@ -1112,7 +1176,7 @@
|
||||
|
||||
<!-- 验证安装 -->
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-green-800">验证安装是否成功</h6>
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">验证安装是否成功</h6>
|
||||
<p class="mb-3 text-sm text-green-700">安装完成后,打开终端,输入以下命令:</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"
|
||||
@@ -1126,7 +1190,9 @@
|
||||
|
||||
<!-- 第二步:安装 Claude Code -->
|
||||
<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 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<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"
|
||||
>2</span
|
||||
@@ -1138,12 +1204,14 @@
|
||||
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"
|
||||
class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fas fa-download mr-2 text-purple-600" />
|
||||
安装 Claude Code
|
||||
</h5>
|
||||
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base">打开终端,运行以下命令:</p>
|
||||
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
打开终端,运行以下命令:
|
||||
</p>
|
||||
<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"
|
||||
>
|
||||
@@ -1164,7 +1232,7 @@
|
||||
|
||||
<!-- 验证安装 -->
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-green-800">验证 Claude Code 安装</h6>
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">验证 Claude Code 安装</h6>
|
||||
<p class="mb-3 text-sm text-green-700">安装完成后,输入以下命令检查是否安装成功:</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"
|
||||
@@ -1179,7 +1247,9 @@
|
||||
|
||||
<!-- 第三步:设置环境变量 -->
|
||||
<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 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<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"
|
||||
>3</span
|
||||
@@ -1191,18 +1261,18 @@
|
||||
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"
|
||||
>
|
||||
<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 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fas fa-cog mr-2 text-orange-600" />
|
||||
配置 Claude Code 环境变量
|
||||
</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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
为了让 Claude Code 连接到你的中转服务,需要设置两个环境变量:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg border border-orange-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 dark:text-gray-600 sm:text-base">
|
||||
方法一:临时设置(当前会话)
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">在终端中运行以下命令:</p>
|
||||
@@ -1222,7 +1292,9 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-orange-200 bg-white p-3 sm:p-4">
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">方法二:永久设置</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
方法二:永久设置
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">编辑你的 shell 配置文件:</p>
|
||||
<div
|
||||
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"
|
||||
@@ -1255,18 +1327,20 @@
|
||||
<!-- Gemini CLI 环境变量设置 -->
|
||||
<div class="mt-8">
|
||||
<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 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fas fa-robot mr-2 text-green-600" />
|
||||
配置 Gemini CLI 环境变量
|
||||
</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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
如果你使用 Gemini CLI,需要设置以下环境变量:
|
||||
</p>
|
||||
|
||||
<div class="space-y-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>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
终端设置方法
|
||||
</h6>
|
||||
<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"
|
||||
@@ -1287,7 +1361,9 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
永久设置方法
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">添加到你的 shell 配置文件:</p>
|
||||
<div
|
||||
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"
|
||||
@@ -1322,7 +1398,9 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
|
||||
<h6 class="mb-2 font-medium text-green-800">验证 Gemini CLI 环境变量</h6>
|
||||
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
|
||||
验证 Gemini CLI 环境变量
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-green-700">在终端中验证:</p>
|
||||
<div
|
||||
class="space-y-1 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
@@ -1338,18 +1416,20 @@
|
||||
<!-- Codex 环境变量设置 -->
|
||||
<div class="mt-8">
|
||||
<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 dark:text-gray-600 sm:mb-3 sm:text-lg"
|
||||
>
|
||||
<i class="fas fa-code mr-2 text-indigo-600" />
|
||||
配置 Codex 环境变量
|
||||
</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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
如果你使用支持 OpenAI API 的工具(如 Codex),需要设置以下环境变量:
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">终端设置方法</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
终端设置方法
|
||||
</h6>
|
||||
<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"
|
||||
@@ -1367,7 +1447,9 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">永久设置方法</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
永久设置方法
|
||||
</h6>
|
||||
<p class="mb-3 text-sm text-gray-600">添加到你的 shell 配置文件:</p>
|
||||
<div
|
||||
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"
|
||||
@@ -1411,7 +1493,9 @@
|
||||
|
||||
<!-- 第四步:开始使用 -->
|
||||
<div class="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 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<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"
|
||||
>4</span
|
||||
@@ -1421,13 +1505,15 @@
|
||||
<div
|
||||
class="rounded-xl border border-yellow-100 bg-gradient-to-r from-yellow-50 to-amber-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 dark:text-gray-300 sm:mb-4 sm:text-base">
|
||||
现在你可以开始使用 Claude Code 了!
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">启动 Claude Code</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
启动 Claude Code
|
||||
</h6>
|
||||
<div
|
||||
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
>
|
||||
@@ -1436,7 +1522,9 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">在特定项目中使用</h6>
|
||||
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
|
||||
在特定项目中使用
|
||||
</h6>
|
||||
<div
|
||||
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
|
||||
>
|
||||
@@ -1452,7 +1540,9 @@
|
||||
|
||||
<!-- Linux 故障排除 -->
|
||||
<div class="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 dark:text-gray-300 sm:mb-4 sm:text-xl"
|
||||
>
|
||||
<i class="fas fa-wrench mr-2 text-red-600 sm:mr-3" />
|
||||
Linux 常见问题解决
|
||||
</h4>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
animation: {
|
||||
|
||||
Reference in New Issue
Block a user