feat: 支持Dark Mode

This commit is contained in:
shaw
2025-08-22 22:09:38 +08:00
parent 8328b6ddac
commit d2f0ac37a9
37 changed files with 3226 additions and 1155 deletions

View File

@@ -11,12 +11,14 @@ Claude Relay Service 是一个功能完整的 AI API 中转服务,支持 Claud
## 核心架构 ## 核心架构
### 关键架构概念 ### 关键架构概念
- **代理认证流**: 客户端用自建API Key → 验证 → 获取Claude账户OAuth token → 转发到Anthropic - **代理认证流**: 客户端用自建API Key → 验证 → 获取Claude账户OAuth token → 转发到Anthropic
- **Token管理**: 自动监控OAuth token过期并刷新支持10秒提前刷新策略 - **Token管理**: 自动监控OAuth token过期并刷新支持10秒提前刷新策略
- **代理支持**: 每个Claude账户支持独立代理配置OAuth token交换也通过代理进行 - **代理支持**: 每个Claude账户支持独立代理配置OAuth token交换也通过代理进行
- **数据加密**: 敏感数据refreshToken, accessToken使用AES加密存储在Redis - **数据加密**: 敏感数据refreshToken, accessToken使用AES加密存储在Redis
### 主要服务组件 ### 主要服务组件
- **claudeRelayService.js**: 核心代理服务,处理请求转发和流式响应 - **claudeRelayService.js**: 核心代理服务,处理请求转发和流式响应
- **claudeAccountService.js**: Claude账户管理OAuth token刷新和账户选择 - **claudeAccountService.js**: Claude账户管理OAuth token刷新和账户选择
- **geminiAccountService.js**: Gemini账户管理Google OAuth token刷新和账户选择 - **geminiAccountService.js**: Gemini账户管理Google OAuth token刷新和账户选择
@@ -24,7 +26,8 @@ Claude Relay Service 是一个功能完整的 AI API 中转服务,支持 Claud
- **oauthHelper.js**: OAuth工具PKCE流程实现和代理支持 - **oauthHelper.js**: OAuth工具PKCE流程实现和代理支持
### 认证和代理流程 ### 认证和代理流程
1. 客户端使用自建API Keycr_前缀格式发送请求
1. 客户端使用自建API Keycr\_前缀格式发送请求
2. authenticateApiKey中间件验证API Key有效性和速率限制 2. authenticateApiKey中间件验证API Key有效性和速率限制
3. claudeAccountService自动选择可用Claude账户 3. claudeAccountService自动选择可用Claude账户
4. 检查OAuth access token有效性过期则自动刷新使用代理 4. 检查OAuth access token有效性过期则自动刷新使用代理
@@ -33,6 +36,7 @@ Claude Relay Service 是一个功能完整的 AI API 中转服务,支持 Claud
7. 流式或非流式返回响应,记录使用统计 7. 流式或非流式返回响应,记录使用统计
### OAuth集成 ### OAuth集成
- **PKCE流程**: 完整的OAuth 2.0 PKCE实现支持代理 - **PKCE流程**: 完整的OAuth 2.0 PKCE实现支持代理
- **自动刷新**: 智能token过期检测和自动刷新机制 - **自动刷新**: 智能token过期检测和自动刷新机制
- **代理支持**: OAuth授权和token交换全程支持代理配置 - **代理支持**: OAuth授权和token交换全程支持代理配置
@@ -41,7 +45,8 @@ Claude Relay Service 是一个功能完整的 AI API 中转服务,支持 Claud
## 常用命令 ## 常用命令
### 基本开发命令 ### 基本开发命令
```bash
````bash
# 安装依赖和初始化 # 安装依赖和初始化
npm install npm install
npm run setup # 生成配置和管理员凭据 npm run setup # 生成配置和管理员凭据
@@ -76,11 +81,12 @@ npm run service:stop # 停止服务
cp config/config.example.js config/config.js cp config/config.example.js config/config.js
cp .env.example .env cp .env.example .env
npm run setup # 自动生成密钥并创建管理员账户 npm run setup # 自动生成密钥并创建管理员账户
``` ````
## Web界面功能 ## Web界面功能
### OAuth账户添加流程 ### OAuth账户添加流程
1. **基本信息和代理设置**: 配置账户名称、描述和代理参数 1. **基本信息和代理设置**: 配置账户名称、描述和代理参数
2. **OAuth授权**: 2. **OAuth授权**:
- 生成授权URL → 用户打开链接并登录Claude Code账号 - 生成授权URL → 用户打开链接并登录Claude Code账号
@@ -88,25 +94,30 @@ npm run setup # 自动生成密钥并创建管理员账户
- 系统自动交换token并创建账户 - 系统自动交换token并创建账户
### 核心管理功能 ### 核心管理功能
- **实时仪表板**: 系统统计、账户状态、使用量监控 - **实时仪表板**: 系统统计、账户状态、使用量监控
- **API Key管理**: 创建、配额设置、使用统计查看 - **API Key管理**: 创建、配额设置、使用统计查看
- **Claude账户管理**: OAuth账户添加、代理配置、状态监控 - **Claude账户管理**: OAuth账户添加、代理配置、状态监控
- **系统日志**: 实时日志查看,多级别过滤 - **系统日志**: 实时日志查看,多级别过滤
- **主题系统**: 支持明亮/暗黑模式切换,自动保存用户偏好设置
## 重要端点 ## 重要端点
### API转发端点 ### API转发端点
- `POST /api/v1/messages` - 主要消息处理端点(支持流式) - `POST /api/v1/messages` - 主要消息处理端点(支持流式)
- `GET /api/v1/models` - 模型列表(兼容性) - `GET /api/v1/models` - 模型列表(兼容性)
- `GET /api/v1/usage` - 使用统计查询 - `GET /api/v1/usage` - 使用统计查询
- `GET /api/v1/key-info` - API Key信息 - `GET /api/v1/key-info` - API Key信息
### OAuth管理端点 ### OAuth管理端点
- `POST /admin/claude-accounts/generate-auth-url` - 生成OAuth授权URL含代理 - `POST /admin/claude-accounts/generate-auth-url` - 生成OAuth授权URL含代理
- `POST /admin/claude-accounts/exchange-code` - 交换authorization code - `POST /admin/claude-accounts/exchange-code` - 交换authorization code
- `POST /admin/claude-accounts` - 创建OAuth账户 - `POST /admin/claude-accounts` - 创建OAuth账户
### 系统端点 ### 系统端点
- `GET /health` - 健康检查 - `GET /health` - 健康检查
- `GET /web` - Web管理界面 - `GET /web` - Web管理界面
- `GET /admin/dashboard` - 系统概览数据 - `GET /admin/dashboard` - 系统概览数据
@@ -114,22 +125,26 @@ npm run setup # 自动生成密钥并创建管理员账户
## 故障排除 ## 故障排除
### OAuth相关问题 ### OAuth相关问题
1. **代理配置错误**: 检查代理设置是否正确OAuth token交换也需要代理 1. **代理配置错误**: 检查代理设置是否正确OAuth token交换也需要代理
2. **授权码无效**: 确保复制了完整的Authorization Code没有遗漏字符 2. **授权码无效**: 确保复制了完整的Authorization Code没有遗漏字符
3. **Token刷新失败**: 检查refreshToken有效性和代理配置 3. **Token刷新失败**: 检查refreshToken有效性和代理配置
### Gemini Token刷新问题 ### Gemini Token刷新问题
1. **刷新失败**: 确保 refresh_token 有效且未过期 1. **刷新失败**: 确保 refresh_token 有效且未过期
2. **错误日志**: 查看 `logs/token-refresh-error.log` 获取详细错误信息 2. **错误日志**: 查看 `logs/token-refresh-error.log` 获取详细错误信息
3. **测试脚本**: 运行 `node scripts/test-gemini-refresh.js` 测试 token 刷新 3. **测试脚本**: 运行 `node scripts/test-gemini-refresh.js` 测试 token 刷新
### 常见开发问题 ### 常见开发问题
1. **Redis连接失败**: 确认Redis服务运行检查连接配置 1. **Redis连接失败**: 确认Redis服务运行检查连接配置
2. **管理员登录失败**: 检查init.json同步到Redis运行npm run setup 2. **管理员登录失败**: 检查init.json同步到Redis运行npm run setup
3. **API Key格式错误**: 确保使用cr_前缀格式 3. **API Key格式错误**: 确保使用cr\_前缀格式
4. **代理连接问题**: 验证SOCKS5/HTTP代理配置和认证信息 4. **代理连接问题**: 验证SOCKS5/HTTP代理配置和认证信息
### 调试工具 ### 调试工具
- **日志系统**: Winston结构化日志支持不同级别 - **日志系统**: Winston结构化日志支持不同级别
- **CLI工具**: 命令行状态查看和管理 - **CLI工具**: 命令行状态查看和管理
- **Web界面**: 实时日志查看和系统监控 - **Web界面**: 实时日志查看和系统监控
@@ -138,19 +153,35 @@ npm run setup # 自动生成密钥并创建管理员账户
## 开发最佳实践 ## 开发最佳实践
### 代码格式化要求 ### 代码格式化要求
- **必须使用 Prettier 格式化所有代码** - **必须使用 Prettier 格式化所有代码**
- 后端代码src/):运行 `npx prettier --write <file>` 格式化 - 后端代码src/):运行 `npx prettier --write <file>` 格式化
- 前端代码web/admin-spa/):已安装 `prettier-plugin-tailwindcss`,运行 `npx prettier --write <file>` 格式化 - 前端代码web/admin-spa/):已安装 `prettier-plugin-tailwindcss`,运行 `npx prettier --write <file>` 格式化
- 提交前检查格式:`npx prettier --check <file>` - 提交前检查格式:`npx prettier --check <file>`
- 格式化所有文件:`npm run format`(如果配置了此脚本) - 格式化所有文件:`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 中的加密实现) - 敏感数据必须使用加密存储(参考 claudeAccountService.js 中的加密实现)
### 测试和质量保证 ### 测试和质量保证
- 运行 `npm run lint` 进行代码风格检查(使用 ESLint - 运行 `npm run lint` 进行代码风格检查(使用 ESLint
- 运行 `npm test` 执行测试套件Jest + SuperTest 配置) - 运行 `npm test` 执行测试套件Jest + SuperTest 配置)
- 在修改核心服务后,使用 CLI 工具验证功能:`npm run cli status` - 在修改核心服务后,使用 CLI 工具验证功能:`npm run cli status`
@@ -158,20 +189,26 @@ npm run setup # 自动生成密钥并创建管理员账户
- 注意:当前项目缺少实际测试文件,建议补充单元测试和集成测试 - 注意:当前项目缺少实际测试文件,建议补充单元测试和集成测试
### 开发工作流 ### 开发工作流
- **功能开发**: 始终从理解现有代码开始,重用已有的服务和模式 - **功能开发**: 始终从理解现有代码开始,重用已有的服务和模式
- **调试流程**: 使用 Winston 日志 + Web 界面实时日志查看 + CLI 状态工具 - **调试流程**: 使用 Winston 日志 + Web 界面实时日志查看 + CLI 状态工具
- **代码审查**: 关注安全性(加密存储)、性能(异步处理)、错误处理 - **代码审查**: 关注安全性(加密存储)、性能(异步处理)、错误处理
- **部署前检查**: 运行 lint → 测试 CLI 功能 → 检查日志 → Docker 构建 - **部署前检查**: 运行 lint → 测试 CLI 功能 → 检查日志 → Docker 构建
### 常见文件位置 ### 常见文件位置
- 核心服务逻辑:`src/services/` 目录 - 核心服务逻辑:`src/services/` 目录
- 路由处理:`src/routes/` 目录 - 路由处理:`src/routes/` 目录
- 中间件:`src/middleware/` 目录 - 中间件:`src/middleware/` 目录
- 配置管理:`config/config.js` - 配置管理:`config/config.js`
- Redis 模型:`src/models/redis.js` - Redis 模型:`src/models/redis.js`
- 工具函数:`src/utils/` 目录 - 工具函数:`src/utils/` 目录
- 前端主题管理:`web/admin-spa/src/stores/theme.js`
- 前端组件:`web/admin-spa/src/components/` 目录
- 前端页面:`web/admin-spa/src/views/` 目录
### 重要架构决策 ### 重要架构决策
- 所有敏感数据OAuth token、refreshToken都使用 AES 加密存储在 Redis - 所有敏感数据OAuth token、refreshToken都使用 AES 加密存储在 Redis
- 每个 Claude 账户支持独立的代理配置,包括 SOCKS5 和 HTTP 代理 - 每个 Claude 账户支持独立的代理配置,包括 SOCKS5 和 HTTP 代理
- API Key 使用哈希存储,支持 `cr_` 前缀格式 - API Key 使用哈希存储,支持 `cr_` 前缀格式
@@ -179,6 +216,7 @@ npm run setup # 自动生成密钥并创建管理员账户
- 支持流式和非流式响应,客户端断开时自动清理资源 - 支持流式和非流式响应,客户端断开时自动清理资源
### 核心数据流和性能优化 ### 核心数据流和性能优化
- **哈希映射优化**: API Key 验证从 O(n) 优化到 O(1) 查找 - **哈希映射优化**: API Key 验证从 O(n) 优化到 O(1) 查找
- **智能 Usage 捕获**: 从 SSE 流中解析真实的 token 使用数据 - **智能 Usage 捕获**: 从 SSE 流中解析真实的 token 使用数据
- **多维度统计**: 支持按时间、模型、用户的实时使用统计 - **多维度统计**: 支持按时间、模型、用户的实时使用统计
@@ -186,6 +224,7 @@ npm run setup # 自动生成密钥并创建管理员账户
- **原子操作**: Redis 管道操作确保数据一致性 - **原子操作**: Redis 管道操作确保数据一致性
### 安全和容错机制 ### 安全和容错机制
- **多层加密**: API Key 哈希 + OAuth Token AES 加密 - **多层加密**: API Key 哈希 + OAuth Token AES 加密
- **零信任验证**: 每个请求都需要完整的认证链 - **零信任验证**: 每个请求都需要完整的认证链
- **优雅降级**: Redis 连接失败时的回退机制 - **优雅降级**: Redis 连接失败时的回退机制
@@ -195,6 +234,7 @@ npm run setup # 自动生成密钥并创建管理员账户
## 项目特定注意事项 ## 项目特定注意事项
### Redis 数据结构 ### Redis 数据结构
- **API Keys**: `api_key:{id}` (详细信息) + `api_key_hash:{hash}` (快速查找) - **API Keys**: `api_key:{id}` (详细信息) + `api_key_hash:{hash}` (快速查找)
- **Claude 账户**: `claude_account:{id}` (加密的 OAuth 数据) - **Claude 账户**: `claude_account:{id}` (加密的 OAuth 数据)
- **管理员**: `admin:{id}` + `admin_username:{username}` (用户名映射) - **管理员**: `admin:{id}` + `admin_username:{username}` (用户名映射)
@@ -203,12 +243,14 @@ npm run setup # 自动生成密钥并创建管理员账户
- **系统信息**: `system_info` (系统状态缓存) - **系统信息**: `system_info` (系统状态缓存)
### 流式响应处理 ### 流式响应处理
- 支持 SSE (Server-Sent Events) 流式传输 - 支持 SSE (Server-Sent Events) 流式传输
- 自动从流中解析 usage 数据并记录 - 自动从流中解析 usage 数据并记录
- 客户端断开时通过 AbortController 清理资源 - 客户端断开时通过 AbortController 清理资源
- 错误时发送适当的 SSE 错误事件 - 错误时发送适当的 SSE 错误事件
### CLI 工具使用示例 ### CLI 工具使用示例
```bash ```bash
# 创建新的 API Key # 创建新的 API Key
npm run cli keys create -- --name "MyApp" --limit 1000 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 create -- --username admin2
npm run cli admin reset-password -- --username admin npm run cli admin reset-password -- --username admin
``` ```
# important-instruction-reminders # important-instruction-reminders
Do what has been asked; nothing more, nothing less. Do what has been asked; nothing more, nothing less.
NEVER create files unless they're absolutely necessary for achieving your goal. NEVER create files unless they're absolutely necessary for achieving your goal.
ALWAYS prefer editing an existing file to creating a new one. 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.

View File

@@ -3655,7 +3655,7 @@
}, },
"node_modules/prettier-plugin-tailwindcss": { "node_modules/prettier-plugin-tailwindcss": {
"version": "0.6.14", "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==", "integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",

View File

@@ -11,14 +11,22 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useThemeStore } from '@/stores/theme'
import ToastNotification from '@/components/common/ToastNotification.vue' import ToastNotification from '@/components/common/ToastNotification.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue' import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
const authStore = useAuthStore() const authStore = useAuthStore()
const themeStore = useThemeStore()
const toastRef = ref() const toastRef = ref()
const confirmRef = ref() const confirmRef = ref()
onMounted(() => { onMounted(() => {
// 初始化主题
themeStore.initTheme()
// 监听系统主题变化
themeStore.watchSystemTheme()
// 检查本地存储的认证状态 // 检查本地存储的认证状态
authStore.checkAuth() authStore.checkAuth()

View File

@@ -1,5 +1,6 @@
/* 从原始 style.css 复制的全局样式 */ /* 从原始 style.css 复制的全局样式 */
:root { :root {
/* 亮色模式 */
--primary-color: #667eea; --primary-color: #667eea;
--secondary-color: #764ba2; --secondary-color: #764ba2;
--accent-color: #f093fb; --accent-color: #f093fb;
@@ -8,31 +9,62 @@
--error-color: #ef4444; --error-color: #ef4444;
--surface-color: rgba(255, 255, 255, 0.95); --surface-color: rgba(255, 255, 255, 0.95);
--glass-color: rgba(255, 255, 255, 0.1); --glass-color: rgba(255, 255, 255, 0.1);
--glass-strong-color: rgba(255, 255, 255, 0.95);
--text-primary: #1f2937; --text-primary: #1f2937;
--text-secondary: #6b7280; --text-secondary: #6b7280;
--border-color: rgba(255, 255, 255, 0.2); --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 - 仅应用于特定元素 */ .dark {
body, /* 暗黑模式 */
div, --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, button,
input, input,
select, select,
textarea, textarea {
table, transition:
tr, background-color 0.3s ease,
td, border-color 0.3s ease,
th, box-shadow 0.3s ease,
span, transform 0.2s ease;
p, }
h1,
h2, /* 颜色和背景过渡 */
h3, .transition-colors {
h4, transition:
h5, color 0.3s ease,
h6 { background-color 0.3s ease,
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); border-color 0.3s ease;
} }
body { body {
@@ -45,14 +77,18 @@ body {
sans-serif; sans-serif;
background: linear-gradient( background: linear-gradient(
135deg, 135deg,
var(--primary-color) 0%, var(--bg-gradient-start) 0%,
var(--secondary-color) 50%, var(--bg-gradient-mid) 50%,
var(--accent-color) 100% var(--bg-gradient-end) 100%
); );
background-attachment: fixed; background-attachment: fixed;
min-height: 100vh; min-height: 100vh;
margin: 0; margin: 0;
overflow-x: hidden; overflow-x: hidden;
color: var(--text-primary);
transition:
background 0.3s ease,
color 0.3s ease;
} }
body::before { body::before {
@@ -73,21 +109,43 @@ body::before {
.glass { .glass {
background: var(--glass-color); background: var(--glass-color);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
box-shadow: box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04), 0 10px 10px -5px rgba(0, 0, 0, 0.04),
inset 0 1px 0 rgba(255, 255, 255, 0.1); 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 { .glass-strong {
background: var(--surface-color); background: var(--glass-strong-color);
backdrop-filter: blur(25px); 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: box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(255, 255, 255, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.1); 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 { .tab-btn {
@@ -201,13 +259,18 @@ body::before {
/* 表单输入框样式 */ /* 表单输入框样式 */
.form-input { .form-input {
background: rgba(255, 255, 255, 0.9); background: var(--input-bg);
border: 2px solid rgba(255, 255, 255, 0.3); border: 2px solid var(--input-border);
border-radius: 12px; border-radius: 12px;
padding: 16px; padding: 16px;
font-size: 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); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
} }
.form-input:focus { .form-input:focus {
@@ -217,17 +280,35 @@ body::before {
0 0 0 3px rgba(102, 126, 234, 0.1), 0 0 0 3px rgba(102, 126, 234, 0.1),
0 10px 15px -3px rgba(0, 0, 0, 0.1); 0 10px 15px -3px rgba(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 0.95); 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 { .card {
background: var(--surface-color); background: var(--surface-color);
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid var(--border-color);
box-shadow: box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05); 0 4px 6px -2px rgba(0, 0, 0, 0.05);
overflow: hidden; overflow: hidden;
position: relative; 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 { .card::before {
@@ -241,13 +322,19 @@ body::before {
} }
.stat-card { .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-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid var(--border-color);
padding: 24px; padding: 24px;
position: relative; position: relative;
overflow: hidden; 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 { .stat-card::before {
@@ -364,13 +451,18 @@ body::before {
} }
.form-input { .form-input {
background: rgba(255, 255, 255, 0.9); background: var(--input-bg);
border: 2px solid rgba(255, 255, 255, 0.3); border: 2px solid var(--input-border);
border-radius: 12px; border-radius: 12px;
padding: 16px; padding: 16px;
font-size: 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); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
} }
.form-input:focus { .form-input:focus {
@@ -380,39 +472,71 @@ body::before {
0 0 0 3px rgba(102, 126, 234, 0.1), 0 0 0 3px rgba(102, 126, 234, 0.1),
0 10px 15px -3px rgba(0, 0, 0, 0.1); 0 10px 15px -3px rgba(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 0.95); 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 { .table-container {
background: rgba(255, 255, 255, 0.95); background: var(--table-bg);
border-radius: 16px; border-radius: 16px;
overflow: hidden; overflow: hidden;
box-shadow: box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05); 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 { .table-row {
transition: all 0.2s ease; transition:
background-color 0.2s ease,
transform 0.2s ease;
} }
.table-row:hover { .table-row:hover {
background: rgba(102, 126, 234, 0.05); background: var(--table-hover);
transform: scale(1.005); transform: scale(1.005);
} }
.modal { .modal {
backdrop-filter: blur(8px); 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 { .modal-content {
background: rgba(255, 255, 255, 0.95); background: white;
border-radius: 24px; border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid rgba(229, 231, 235, 0.8);
box-shadow: box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.05); 0 0 0 1px rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px); 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 { .header-title {
@@ -517,6 +641,10 @@ body::before {
scrollbar-color: rgba(102, 126, 234, 0.3) rgba(102, 126, 234, 0.05); 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 { .custom-scrollbar::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px; height: 8px;
@@ -527,20 +655,36 @@ body::before {
border-radius: 10px; border-radius: 10px;
} }
.dark .custom-scrollbar::-webkit-scrollbar-track {
background: rgba(129, 140, 248, 0.05);
}
.custom-scrollbar::-webkit-scrollbar-thumb { .custom-scrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.4) 0%, rgba(118, 75, 162, 0.4) 100%); background: linear-gradient(135deg, rgba(102, 126, 234, 0.4) 0%, rgba(118, 75, 162, 0.4) 100%);
border-radius: 10px; border-radius: 10px;
transition: background 0.3s ease; 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 { .custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.6) 0%, rgba(118, 75, 162, 0.6) 100%); 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 { .custom-scrollbar::-webkit-scrollbar-thumb:active {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.8) 0%, rgba(118, 75, 162, 0.8) 100%); 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 { .modal-scroll-content {
max-height: calc(90vh - 160px); max-height: calc(90vh - 160px);

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,9 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- Claude OAuth流程 --> <!-- Claude OAuth流程 -->
<div v-if="platform === 'claude'"> <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 items-start gap-4">
<div <div
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-blue-500" 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" /> <i class="fas fa-link text-white" />
</div> </div>
<div class="flex-1"> <div class="flex-1">
<h4 class="mb-3 font-semibold text-blue-900">Claude 账户授权</h4> <h4 class="mb-3 font-semibold text-blue-900 dark:text-blue-200">Claude 账户授权</h4>
<p class="mb-4 text-sm text-blue-800">请按照以下步骤完成 Claude 账户的授权</p> <p class="mb-4 text-sm text-blue-800 dark:text-blue-300">
请按照以下步骤完成 Claude 账户的授权
</p>
<div class="space-y-4"> <div class="space-y-4">
<!-- 步骤1: 生成授权链接 --> <!-- 步骤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 items-start gap-3">
<div <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" 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 1
</div> </div>
<div class="flex-1"> <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 <button
v-if="!authUrl" v-if="!authUrl"
class="btn btn-primary px-4 py-2 text-sm" class="btn btn-primary px-4 py-2 text-sm"
@@ -37,13 +45,13 @@
<div v-else class="space-y-3"> <div v-else class="space-y-3">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input <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 readonly
type="text" type="text"
:value="authUrl" :value="authUrl"
/> />
<button <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="复制链接" title="复制链接"
@click="copyAuthUrl" @click="copyAuthUrl"
> >
@@ -62,7 +70,9 @@
</div> </div>
<!-- 步骤2: 访问链接并授权 --> <!-- 步骤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 items-start gap-3">
<div <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" 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 2
</div> </div>
<div class="flex-1"> <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 class="mb-2 text-sm text-blue-700"> 在浏览器中打开链接并完成授权
</p>
<p class="mb-2 text-sm text-blue-700 dark:text-blue-300">
请在新标签页中打开授权链接登录您的 Claude 账户并授权 请在新标签页中打开授权链接登录您的 Claude 账户并授权
</p> </p>
<div class="rounded border border-yellow-300 bg-yellow-50 p-3"> <div
<p class="text-xs text-yellow-800"> 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" /> <i class="fas fa-exclamation-triangle mr-1" />
<strong>注意</strong <strong>注意</strong
>如果您设置了代理请确保浏览器也使用相同的代理访问授权页面 >如果您设置了代理请确保浏览器也使用相同的代理访问授权页面
@@ -86,7 +100,9 @@
</div> </div>
<!-- 步骤3: 输入授权码 --> <!-- 步骤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 items-start gap-3">
<div <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" 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 3
</div> </div>
<div class="flex-1"> <div class="flex-1">
<p class="mb-2 font-medium text-blue-900">输入 Authorization Code</p> <p class="mb-2 font-medium text-blue-900 dark:text-blue-200">
<p class="mb-3 text-sm text-blue-700"> 输入 Authorization Code
</p>
<p class="mb-3 text-sm text-blue-700 dark:text-blue-300">
授权完成后页面会显示一个 授权完成后页面会显示一个
<strong>Authorization Code</strong>请将其复制并粘贴到下方输入框 <strong>Authorization Code</strong>请将其复制并粘贴到下方输入框
</p> </p>
<div class="space-y-3"> <div class="space-y-3">
<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"
>
<i class="fas fa-key mr-2 text-blue-500" />Authorization Code <i class="fas fa-key mr-2 text-blue-500" />Authorization Code
</label> </label>
<textarea <textarea
@@ -111,7 +131,7 @@
rows="3" rows="3"
/> />
</div> </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" /> <i class="fas fa-info-circle mr-1" />
请粘贴从Claude页面复制的Authorization Code 请粘贴从Claude页面复制的Authorization Code
</p> </p>
@@ -127,7 +147,9 @@
<!-- Gemini OAuth流程 --> <!-- Gemini OAuth流程 -->
<div v-else-if="platform === 'gemini'"> <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 items-start gap-4">
<div <div
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-green-500" 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" /> <i class="fas fa-robot text-white" />
</div> </div>
<div class="flex-1"> <div class="flex-1">
<h4 class="mb-3 font-semibold text-green-900">Gemini 账户授权</h4> <h4 class="mb-3 font-semibold text-green-900 dark:text-green-200">Gemini 账户授权</h4>
<p class="mb-4 text-sm text-green-800">请按照以下步骤完成 Gemini 账户的授权</p> <p class="mb-4 text-sm text-green-800 dark:text-green-300">
请按照以下步骤完成 Gemini 账户的授权
</p>
<div class="space-y-4"> <div class="space-y-4">
<!-- 步骤1: 生成授权链接 --> <!-- 步骤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 items-start gap-3">
<div <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" 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 1
</div> </div>
<div class="flex-1"> <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 <button
v-if="!authUrl" v-if="!authUrl"
class="btn btn-primary px-4 py-2 text-sm" class="btn btn-primary px-4 py-2 text-sm"
@@ -162,13 +190,13 @@
<div v-else class="space-y-3"> <div v-else class="space-y-3">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input <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 readonly
type="text" type="text"
:value="authUrl" :value="authUrl"
/> />
<button <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="复制链接" title="复制链接"
@click="copyAuthUrl" @click="copyAuthUrl"
> >
@@ -187,7 +215,9 @@
</div> </div>
<!-- 步骤2: 操作说明 --> <!-- 步骤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 items-start gap-3">
<div <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" 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 2
</div> </div>
<div class="flex-1"> <div class="flex-1">
<p class="mb-2 font-medium text-blue-900">在浏览器中打开链接并完成授权</p> <p class="mb-2 font-medium text-green-900 dark:text-green-200">
<p class="mb-2 text-sm text-blue-700"> 在浏览器中打开链接并完成授权
</p>
<p class="mb-2 text-sm text-green-700 dark:text-green-300">
请在新标签页中打开授权链接登录您的 Gemini 账户并授权 请在新标签页中打开授权链接登录您的 Gemini 账户并授权
</p> </p>
<div class="rounded border border-yellow-300 bg-yellow-50 p-3"> <div
<p class="text-xs text-yellow-800"> 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" /> <i class="fas fa-exclamation-triangle mr-1" />
<strong>注意</strong <strong>注意</strong
>如果您设置了代理请确保浏览器也使用相同的代理访问授权页面 >如果您设置了代理请确保浏览器也使用相同的代理访问授权页面
@@ -211,7 +245,9 @@
</div> </div>
<!-- 步骤3: 输入授权码 --> <!-- 步骤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 items-start gap-3">
<div <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" 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 3
</div> </div>
<div class="flex-1"> <div class="flex-1">
<p class="mb-2 font-medium text-green-900">输入 Authorization Code</p> <p class="mb-2 font-medium text-green-900 dark:text-green-200">
<p class="mb-3 text-sm text-green-700"> 输入 Authorization Code
</p>
<p class="mb-3 text-sm text-green-700 dark:text-green-300">
授权完成后页面会显示一个 Authorization Code请将其复制并粘贴到下方输入框 授权完成后页面会显示一个 Authorization Code请将其复制并粘贴到下方输入框
</p> </p>
<div class="space-y-3"> <div class="space-y-3">
<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"
>
<i class="fas fa-key mr-2 text-green-500" />Authorization Code <i class="fas fa-key mr-2 text-green-500" />Authorization Code
</label> </label>
<textarea <textarea
@@ -236,7 +276,7 @@
/> />
</div> </div>
<div class="mt-2 space-y-1"> <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" /> <i class="fas fa-check-circle mr-1 text-green-500" />
请粘贴从Gemini页面复制的Authorization Code 请粘贴从Gemini页面复制的Authorization Code
</p> </p>
@@ -253,7 +293,9 @@
<!-- OpenAI OAuth流程 --> <!-- OpenAI OAuth流程 -->
<div v-else-if="platform === 'openai'"> <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 items-start gap-4">
<div <div
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-orange-500" 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" /> <i class="fas fa-brain text-white" />
</div> </div>
<div class="flex-1"> <div class="flex-1">
<h4 class="mb-3 font-semibold text-orange-900">OpenAI 账户授权</h4> <h4 class="mb-3 font-semibold text-orange-900 dark:text-orange-200">OpenAI 账户授权</h4>
<p class="mb-4 text-sm text-orange-800">请按照以下步骤完成 OpenAI 账户的授权</p> <p class="mb-4 text-sm text-orange-800 dark:text-orange-300">
请按照以下步骤完成 OpenAI 账户的授权
</p>
<div class="space-y-4"> <div class="space-y-4">
<!-- 步骤1: 生成授权链接 --> <!-- 步骤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 items-start gap-3">
<div <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" 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 1
</div> </div>
<div class="flex-1"> <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 <button
v-if="!authUrl" v-if="!authUrl"
class="btn btn-primary px-4 py-2 text-sm" class="btn btn-primary px-4 py-2 text-sm"
@@ -288,13 +336,13 @@
<div v-else class="space-y-3"> <div v-else class="space-y-3">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input <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 readonly
type="text" type="text"
:value="authUrl" :value="authUrl"
/> />
<button <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="复制链接" title="复制链接"
@click="copyAuthUrl" @click="copyAuthUrl"
> >
@@ -313,7 +361,9 @@
</div> </div>
<!-- 步骤2: 访问链接并授权 --> <!-- 步骤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 items-start gap-3">
<div <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" 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 2
</div> </div>
<div class="flex-1"> <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 class="mb-2 text-sm text-orange-700"> 在浏览器中打开链接并完成授权
</p>
<p class="mb-2 text-sm text-orange-700 dark:text-orange-300">
请在新标签页中打开授权链接登录您的 OpenAI 账户并授权 请在新标签页中打开授权链接登录您的 OpenAI 账户并授权
</p> </p>
<div class="mb-3 rounded border border-amber-300 bg-amber-50 p-3"> <div
<p class="text-xs text-amber-800"> 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" /> <i class="fas fa-clock mr-1" />
<strong>重要提示</strong>授权后页面可能会加载较长时间请耐心等待 <strong>重要提示</strong>授权后页面可能会加载较长时间请耐心等待
</p> </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> <strong class="font-mono">http://localhost:1455/...</strong>
开头时表示授权已完成 开头时表示授权已完成
</p> </p>
</div> </div>
<div class="rounded border border-yellow-300 bg-yellow-50 p-3"> <div
<p class="text-xs text-yellow-800"> 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" /> <i class="fas fa-exclamation-triangle mr-1" />
<strong>注意</strong <strong>注意</strong
>如果您设置了代理请确保浏览器也使用相同的代理访问授权页面 >如果您设置了代理请确保浏览器也使用相同的代理访问授权页面
@@ -348,7 +404,9 @@
</div> </div>
<!-- 步骤3: 输入授权码 --> <!-- 步骤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 items-start gap-3">
<div <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" 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 3
</div> </div>
<div class="flex-1"> <div class="flex-1">
<p class="mb-2 font-medium text-orange-900">输入授权链接或 Code</p> <p class="mb-2 font-medium text-orange-900 dark:text-orange-200">
<p class="mb-3 text-sm text-orange-700"> 输入授权链接或 Code
</p>
<p class="mb-3 text-sm text-orange-700 dark:text-orange-300">
授权完成后当页面地址变为 授权完成后当页面地址变为
<strong class="font-mono">http://localhost:1455/...</strong> 时: <strong class="font-mono">http://localhost:1455/...</strong> 时:
</p> </p>
<div class="space-y-3"> <div class="space-y-3">
<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"
>
<i class="fas fa-link mr-2 text-orange-500" />授权链接或 Code <i class="fas fa-link mr-2 text-orange-500" />授权链接或 Code
</label> </label>
<textarea <textarea
@@ -373,13 +435,15 @@
rows="3" rows="3"
/> />
</div> </div>
<div class="rounded border border-blue-300 bg-blue-50 p-2"> <div
<p class="text-xs text-blue-700"> 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" /> <i class="fas fa-lightbulb mr-1" />
<strong>提示</strong>您可以直接复制整个链接或仅复制 code <strong>提示</strong>您可以直接复制整个链接或仅复制 code
参数值系统会自动识别 参数值系统会自动识别
</p> </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" 完整链接示例<span class="font-mono"
>http://localhost:1455/auth/callback?code=ac_4hm8...</span >http://localhost:1455/auth/callback?code=ac_4hm8...</span
> >
@@ -402,7 +466,7 @@
<div class="flex gap-3 pt-4"> <div class="flex gap-3 pt-4">
<button <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" type="button"
@click="$emit('back')" @click="$emit('back')"
> >

View File

@@ -1,35 +1,43 @@
<template> <template>
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-center justify-between"> <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"> <label class="flex cursor-pointer items-center">
<input <input
v-model="proxy.enabled" v-model="proxy.enabled"
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"
type="checkbox" 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> </label>
</div> </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="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"> <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" /> <i class="fas fa-server text-sm text-white" />
</div> </div>
<div class="flex-1"> <div class="flex-1">
<p class="text-sm text-gray-700"> <p class="text-sm text-gray-700 dark:text-gray-300">
配置代理以访问受限的网络资源支持 SOCKS5 HTTP 代理 配置代理以访问受限的网络资源支持 SOCKS5 HTTP 代理
</p> </p>
<p class="mt-1 text-xs text-gray-500"> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
请确保代理服务器稳定可用否则会影响账户的正常使用 请确保代理服务器稳定可用否则会影响账户的正常使用
</p> </p>
</div> </div>
</div> </div>
<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"
<select v-model="proxy.type" class="form-input w-full"> >代理类型</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="socks5">SOCKS5</option>
<option value="http">HTTP</option> <option value="http">HTTP</option>
<option value="https">HTTPS</option> <option value="https">HTTPS</option>
@@ -38,19 +46,23 @@
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<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 <input
v-model="proxy.host" 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" placeholder="例如: 192.168.1.100"
type="text" type="text"
/> />
</div> </div>
<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 <input
v-model="proxy.port" 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" placeholder="例如: 1080"
type="number" 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" class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500"
type="checkbox" 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> </label>
</div> </div>
<div v-if="showAuth" class="grid grid-cols-2 gap-4"> <div v-if="showAuth" class="grid grid-cols-2 gap-4">
<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 <input
v-model="proxy.username" 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="代理用户名" placeholder="代理用户名"
type="text" type="text"
/> />
</div> </div>
<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"> <div class="relative">
<input <input
v-model="proxy.password" 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="代理密码" placeholder="代理密码"
:type="showPassword ? 'text' : 'password'" :type="showPassword ? 'text' : 'password'"
/> />
<button <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" type="button"
@click="showPassword = !showPassword" @click="showPassword = !showPassword"
> >
@@ -101,8 +120,10 @@
</div> </div>
</div> </div>
<div class="rounded-lg border border-blue-200 bg-blue-50 p-3"> <div
<p class="text-xs text-blue-700"> 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" /> <i class="fas fa-info-circle mr-1" />
<strong>提示</strong <strong>提示</strong
>代理设置将用于所有与此账户相关的API请求请确保代理服务器支持HTTPS流量转发 >代理设置将用于所有与此账户相关的API请求请确保代理服务器支持HTTPS流量转发

View File

@@ -9,10 +9,12 @@
> >
<i class="fas fa-key text-sm text-white sm:text-base" /> <i class="fas fa-key text-sm text-white sm:text-base" />
</div> </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> </div>
<button <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')" @click="$emit('close')"
> >
<i class="fas fa-times text-lg sm:text-xl" /> <i class="fas fa-times text-lg sm:text-xl" />
@@ -25,7 +27,7 @@
> >
<!-- 创建类型选择 --> <!-- 创建类型选择 -->
<div <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 <div
:class="[ :class="[
@@ -33,18 +35,21 @@
form.createType === 'batch' ? 'mb-3' : '' 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 >创建类型</label
> >
<div class="flex items-center gap-3 sm:gap-4"> <div class="flex items-center gap-3 sm:gap-4">
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input <input
v-model="form.createType" 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" type="radio"
value="single" 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" /> <i class="fas fa-key mr-1 text-xs" />
单个创建 单个创建
</span> </span>
@@ -52,11 +57,13 @@
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input <input
v-model="form.createType" 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" type="radio"
value="batch" 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" /> <i class="fas fa-layer-group mr-1 text-xs" />
批量创建 批量创建
</span> </span>
@@ -68,22 +75,26 @@
<div v-if="form.createType === 'batch'" class="mt-3"> <div v-if="form.createType === 'batch'" class="mt-3">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="flex-1"> <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"> <div class="flex items-center gap-2">
<input <input
v-model.number="form.batchCount" 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" max="500"
min="2" min="2"
placeholder="输入数量 (2-500)" placeholder="输入数量 (2-500)"
required required
type="number" 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> </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" /> <i class="fas fa-info-circle mr-1 mt-0.5 flex-shrink-0" />
<span <span
>批量创建时每个 Key 的名称会自动添加序号后缀例如{{ >批量创建时每个 Key 的名称会自动添加序号后缀例如{{
@@ -95,12 +106,13 @@
</div> </div>
<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 >名称 <span class="text-red-500">*</span></label
> >
<input <input
v-model="form.name" 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 }" :class="{ 'border-red-500': errors.name }"
:placeholder=" :placeholder="
form.createType === 'batch' form.createType === 'batch'
@@ -111,27 +123,31 @@
type="text" type="text"
@input="errors.name = ''" @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 }} {{ errors.name }}
</p> </p>
</div> </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
>
<div class="space-y-4"> <div class="space-y-4">
<!-- 已选择的标签 --> <!-- 已选择的标签 -->
<div v-if="form.tags.length > 0"> <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"> <div class="flex flex-wrap gap-2">
<span <span
v-for="(tag, index) in form.tags" v-for="(tag, index) in form.tags"
:key="'selected-' + index" :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 }} {{ tag }}
<button <button
class="ml-1 hover:text-blue-900" class="ml-1 hover:text-blue-900 dark:hover:text-blue-300"
type="button" type="button"
@click="removeTag(index)" @click="removeTag(index)"
> >
@@ -143,16 +159,18 @@
<!-- 可选择的已有标签 --> <!-- 可选择的已有标签 -->
<div v-if="unselectedTags.length > 0"> <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"> <div class="flex flex-wrap gap-2">
<button <button
v-for="tag in unselectedTags" v-for="tag in unselectedTags"
:key="'available-' + tag" :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" type="button"
@click="selectTag(tag)" @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 }} {{ tag }}
</button> </button>
</div> </div>
@@ -160,11 +178,13 @@
<!-- 创建新标签 --> <!-- 创建新标签 -->
<div> <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"> <div class="flex gap-2">
<input <input
v-model="newTag" 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="输入新标签名称" placeholder="输入新标签名称"
type="text" type="text"
@keypress.enter.prevent="addTag" @keypress.enter.prevent="addTag"
@@ -179,65 +199,79 @@
</div> </div>
</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> </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="mb-2 flex items-center gap-2">
<div <div
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded bg-blue-500" 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" /> <i class="fas fa-tachometer-alt text-xs text-white" />
</div> </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>
<div class="space-y-2"> <div class="space-y-2">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
<div> <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 >时间窗口 (分钟)</label
> >
<input <input
v-model="form.rateLimitWindow" 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" min="1"
placeholder="无限制" placeholder="无限制"
type="number" 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>
<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 <input
v-model="form.rateLimitRequests" 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" min="1"
placeholder="无限制" placeholder="无限制"
type="number" 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>
<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 <input
v-model="form.tokenLimit" 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="无限制" placeholder="无限制"
type="number" 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> </div>
<!-- 示例说明 --> <!-- 示例说明 -->
<div class="rounded-lg bg-blue-100 p-2"> <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">💡 使用示例</h5> <h5 class="mb-1 text-xs font-semibold text-blue-800 dark:text-blue-400">
<div class="space-y-0.5 text-xs text-blue-700"> 💡 使用示例
</h5>
<div class="space-y-0.5 text-xs text-blue-700 dark:text-blue-300">
<div> <div>
<strong>示例1:</strong> 时间窗口=60请求次数=1000 每60分钟最多1000次请求 <strong>示例1:</strong> 时间窗口=60请求次数=1000 每60分钟最多1000次请求
</div> </div>
@@ -254,34 +288,34 @@
</div> </div>
<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 >每日费用限制 (美元)</label
> >
<div class="space-y-2"> <div class="space-y-2">
<div class="flex gap-2"> <div class="flex gap-2">
<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" type="button"
@click="form.dailyCostLimit = '50'" @click="form.dailyCostLimit = '50'"
> >
$50 $50
</button> </button>
<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" type="button"
@click="form.dailyCostLimit = '100'" @click="form.dailyCostLimit = '100'"
> >
$100 $100
</button> </button>
<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" type="button"
@click="form.dailyCostLimit = '200'" @click="form.dailyCostLimit = '200'"
> >
$200 $200
</button> </button>
<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" type="button"
@click="form.dailyCostLimit = ''" @click="form.dailyCostLimit = ''"
> >
@@ -290,47 +324,53 @@
</div> </div>
<input <input
v-model="form.dailyCostLimit" 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" min="0"
placeholder="0 表示无限制" placeholder="0 表示无限制"
step="0.01" step="0.01"
type="number" type="number"
/> />
<p class="text-xs text-gray-500"> <p class="text-xs text-gray-500 dark:text-gray-400">
设置此 API Key 每日的费用限制超过限制将拒绝请求0 或留空表示无限制 设置此 API Key 每日的费用限制超过限制将拒绝请求0 或留空表示无限制
</p> </p>
</div> </div>
</div> </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 <input
v-model="form.concurrencyLimit" 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" min="0"
placeholder="0 表示无限制" placeholder="0 表示无限制"
type="number" 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 或留空表示无限制 设置此 API Key 可同时处理的最大请求数0 或留空表示无限制
</p> </p>
</div> </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
>
<textarea <textarea
v-model="form.description" 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 的用途..." placeholder="描述此 API Key 的用途..."
rows="2" rows="2"
/> />
</div> </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
>
<select <select
v-model="form.expireDuration" 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" @change="updateExpireAt"
> >
<option value="">永不过期</option> <option value="">永不过期</option>
@@ -345,45 +385,71 @@
<div v-if="form.expireDuration === 'custom'" class="mt-3"> <div v-if="form.expireDuration === 'custom'" class="mt-3">
<input <input
v-model="form.customExpireDate" 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" :min="minDateTime"
type="datetime-local" type="datetime-local"
@change="updateCustomExpireAt" @change="updateCustomExpireAt"
/> />
</div> </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) }} 过期 将于 {{ formatExpireDate(form.expiresAt) }} 过期
</p> </p>
</div> </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
>
<div class="flex gap-4"> <div class="flex gap-4">
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="all" /> <input
<span class="text-sm text-gray-700">全部服务</span> 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>
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="claude" /> <input
<span class="text-sm text-gray-700"> Claude</span> 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>
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="gemini" /> <input
<span class="text-sm text-gray-700"> Gemini</span> 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>
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="openai" /> <input
<span class="text-sm text-gray-700"> OpenAI</span> 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> </label>
</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>
<div> <div>
<div class="mb-2 flex items-center justify-between"> <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 <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" :disabled="accountsLoading"
title="刷新账号列表" title="刷新账号列表"
type="button" type="button"
@@ -401,7 +467,9 @@
</div> </div>
<div class="grid grid-cols-1 gap-3"> <div class="grid grid-cols-1 gap-3">
<div> <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 <AccountSelector
v-model="form.claudeAccountId" v-model="form.claudeAccountId"
:accounts="localAccounts.claude" :accounts="localAccounts.claude"
@@ -413,7 +481,9 @@
/> />
</div> </div>
<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 <AccountSelector
v-model="form.geminiAccountId" v-model="form.geminiAccountId"
:accounts="localAccounts.gemini" :accounts="localAccounts.gemini"
@@ -425,7 +495,9 @@
/> />
</div> </div>
<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 <AccountSelector
v-model="form.openaiAccountId" v-model="form.openaiAccountId"
:accounts="localAccounts.openai" :accounts="localAccounts.openai"
@@ -437,7 +509,9 @@
/> />
</div> </div>
<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 <AccountSelector
v-model="form.bedrockAccountId" v-model="form.bedrockAccountId"
:accounts="localAccounts.bedrock" :accounts="localAccounts.bedrock"
@@ -449,7 +523,7 @@
/> />
</div> </div>
</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将只使用该账号不选择则使用共享账号池 选择专属账号后此API Key将只使用该账号不选择则使用共享账号池
</p> </p>
</div> </div>
@@ -463,7 +537,7 @@
type="checkbox" type="checkbox"
/> />
<label <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" for="enableModelRestriction"
> >
启用模型限制 启用模型限制
@@ -549,7 +623,7 @@
type="checkbox" type="checkbox"
/> />
<label <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" for="enableClientRestriction"
> >
启用客户端限制 启用客户端限制
@@ -558,10 +632,12 @@
<div <div
v-if="form.enableClientRestriction" 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> <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 class="space-y-1">
<div v-for="client in supportedClients" :key="client.id" class="flex items-start"> <div v-for="client in supportedClients" :key="client.id" class="flex items-start">
<input <input
@@ -572,8 +648,12 @@
:value="client.id" :value="client.id"
/> />
<label class="ml-2 flex-1 cursor-pointer" :for="`client_${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="text-sm font-medium text-gray-700 dark:text-gray-300">{{
<span class="block text-xs text-gray-500">{{ client.description }}</span> client.name
}}</span>
<span class="block text-xs text-gray-500 dark:text-gray-400">{{
client.description
}}</span>
</label> </label>
</div> </div>
</div> </div>
@@ -583,7 +663,7 @@
<div class="flex gap-3 pt-2"> <div class="flex gap-3 pt-2">
<button <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" type="button"
@click="$emit('close')" @click="$emit('close')"
> >

View File

@@ -11,10 +11,12 @@
> >
<i class="fas fa-edit text-sm text-white sm:text-base" /> <i class="fas fa-edit text-sm text-white sm:text-base" />
</div> </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> </div>
<button <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')" @click="$emit('close')"
> >
<i class="fas fa-times text-lg sm:text-xl" /> <i class="fas fa-times text-lg sm:text-xl" />
@@ -26,36 +28,40 @@
@submit.prevent="updateApiKey" @submit.prevent="updateApiKey"
> >
<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 >名称</label
> >
<input <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 disabled
type="text" type="text"
:value="form.name" :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>
<!-- 标签 --> <!-- 标签 -->
<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 >标签</label
> >
<div class="space-y-4"> <div class="space-y-4">
<!-- 已选择的标签 --> <!-- 已选择的标签 -->
<div v-if="form.tags.length > 0"> <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"> <div class="flex flex-wrap gap-2">
<span <span
v-for="(tag, index) in form.tags" v-for="(tag, index) in form.tags"
:key="'selected-' + index" :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 }} {{ tag }}
<button <button
class="ml-1 hover:text-blue-900" class="ml-1 hover:text-blue-900 dark:hover:text-blue-300"
type="button" type="button"
@click="removeTag(index)" @click="removeTag(index)"
> >
@@ -67,16 +73,18 @@
<!-- 可选择的已有标签 --> <!-- 可选择的已有标签 -->
<div v-if="unselectedTags.length > 0"> <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"> <div class="flex flex-wrap gap-2">
<button <button
v-for="tag in unselectedTags" v-for="tag in unselectedTags"
:key="'available-' + tag" :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" type="button"
@click="selectTag(tag)" @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 }} {{ tag }}
</button> </button>
</div> </div>
@@ -84,11 +92,13 @@
<!-- 创建新标签 --> <!-- 创建新标签 -->
<div> <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"> <div class="flex gap-2">
<input <input
v-model="newTag" 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="输入新标签名称" placeholder="输入新标签名称"
type="text" type="text"
@keypress.enter.prevent="addTag" @keypress.enter.prevent="addTag"
@@ -103,65 +113,79 @@
</div> </div>
</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> </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="mb-2 flex items-center gap-2">
<div <div
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded bg-blue-500" 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" /> <i class="fas fa-tachometer-alt text-xs text-white" />
</div> </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>
<div class="space-y-2"> <div class="space-y-2">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
<div> <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 >时间窗口 (分钟)</label
> >
<input <input
v-model="form.rateLimitWindow" 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" min="1"
placeholder="无限制" placeholder="无限制"
type="number" 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>
<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 <input
v-model="form.rateLimitRequests" 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" min="1"
placeholder="无限制" placeholder="无限制"
type="number" 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>
<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 <input
v-model="form.tokenLimit" 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="无限制" placeholder="无限制"
type="number" 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> </div>
<!-- 示例说明 --> <!-- 示例说明 -->
<div class="rounded-lg bg-blue-100 p-2"> <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">💡 使用示例</h5> <h5 class="mb-1 text-xs font-semibold text-blue-800 dark:text-blue-400">
<div class="space-y-0.5 text-xs text-blue-700"> 💡 使用示例
</h5>
<div class="space-y-0.5 text-xs text-blue-700 dark:text-blue-300">
<div> <div>
<strong>示例1:</strong> 时间窗口=60请求次数=1000 每60分钟最多1000次请求 <strong>示例1:</strong> 时间窗口=60请求次数=1000 每60分钟最多1000次请求
</div> </div>
@@ -178,34 +202,34 @@
</div> </div>
<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 >每日费用限制 (美元)</label
> >
<div class="space-y-3"> <div class="space-y-3">
<div class="flex gap-2"> <div class="flex gap-2">
<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" type="button"
@click="form.dailyCostLimit = '50'" @click="form.dailyCostLimit = '50'"
> >
$50 $50
</button> </button>
<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" type="button"
@click="form.dailyCostLimit = '100'" @click="form.dailyCostLimit = '100'"
> >
$100 $100
</button> </button>
<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" type="button"
@click="form.dailyCostLimit = '200'" @click="form.dailyCostLimit = '200'"
> >
$200 $200
</button> </button>
<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" type="button"
@click="form.dailyCostLimit = ''" @click="form.dailyCostLimit = ''"
> >
@@ -214,28 +238,32 @@
</div> </div>
<input <input
v-model="form.dailyCostLimit" 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" min="0"
placeholder="0 表示无限制" placeholder="0 表示无限制"
step="0.01" step="0.01"
type="number" type="number"
/> />
<p class="text-xs text-gray-500"> <p class="text-xs text-gray-500 dark:text-gray-400">
设置此 API Key 每日的费用限制超过限制将拒绝请求0 或留空表示无限制 设置此 API Key 每日的费用限制超过限制将拒绝请求0 或留空表示无限制
</p> </p>
</div> </div>
</div> </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 <input
v-model="form.concurrencyLimit" 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" min="0"
placeholder="0 表示无限制" placeholder="0 表示无限制"
type="number" 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> </div>
<!-- 激活账号 --> <!-- 激活账号 -->
@@ -244,49 +272,75 @@
<input <input
id="editIsActive" id="editIsActive"
v-model="form.isActive" 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" type="checkbox"
/> />
<label <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" for="editIsActive"
> >
激活账号 激活账号
</label> </label>
</div> </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 错误 取消勾选将禁用此 API Key暂停所有请求客户端返回 401 错误
</p> </p>
</div> </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
>
<div class="flex gap-4"> <div class="flex gap-4">
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="all" /> <input
<span class="text-sm text-gray-700">全部服务</span> 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>
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="claude" /> <input
<span class="text-sm text-gray-700"> Claude</span> 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>
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="gemini" /> <input
<span class="text-sm text-gray-700"> Gemini</span> 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>
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="openai" /> <input
<span class="text-sm text-gray-700"> OpenAI</span> 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> </label>
</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>
<div> <div>
<div class="mb-3 flex items-center justify-between"> <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 <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" :disabled="accountsLoading"
title="刷新账号列表" title="刷新账号列表"
type="button" type="button"
@@ -304,7 +358,9 @@
</div> </div>
<div class="grid grid-cols-1 gap-3"> <div class="grid grid-cols-1 gap-3">
<div> <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 <AccountSelector
v-model="form.claudeAccountId" v-model="form.claudeAccountId"
:accounts="localAccounts.claude" :accounts="localAccounts.claude"
@@ -316,7 +372,9 @@
/> />
</div> </div>
<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 <AccountSelector
v-model="form.geminiAccountId" v-model="form.geminiAccountId"
:accounts="localAccounts.gemini" :accounts="localAccounts.gemini"
@@ -328,7 +386,9 @@
/> />
</div> </div>
<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 <AccountSelector
v-model="form.openaiAccountId" v-model="form.openaiAccountId"
:accounts="localAccounts.openai" :accounts="localAccounts.openai"
@@ -340,7 +400,9 @@
/> />
</div> </div>
<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 <AccountSelector
v-model="form.bedrockAccountId" v-model="form.bedrockAccountId"
:accounts="localAccounts.bedrock" :accounts="localAccounts.bedrock"
@@ -352,7 +414,9 @@
/> />
</div> </div>
</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>
<div> <div>
@@ -360,11 +424,11 @@
<input <input
id="editEnableModelRestriction" id="editEnableModelRestriction"
v-model="form.enableModelRestriction" 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" type="checkbox"
/> />
<label <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" for="editEnableModelRestriction"
> >
启用模型限制 启用模型限制
@@ -373,25 +437,30 @@
<div v-if="form.enableModelRestriction" class="space-y-3"> <div v-if="form.enableModelRestriction" class="space-y-3">
<div> <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 <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 <span
v-for="(model, index) in form.restrictedModels" v-for="(model, index) in form.restrictedModels"
:key="index" :key="index"
class="inline-flex items-center rounded-full bg-red-100 px-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 }} {{ model }}
<button <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" type="button"
@click="removeRestrictedModel(index)" @click="removeRestrictedModel(index)"
> >
<i class="fas fa-times text-xs" /> <i class="fas fa-times text-xs" />
</button> </button>
</span> </span>
<span v-if="form.restrictedModels.length === 0" class="text-sm text-gray-400"> <span
v-if="form.restrictedModels.length === 0"
class="text-sm text-gray-400 dark:text-gray-500"
>
暂无限制的模型 暂无限制的模型
</span> </span>
</div> </div>
@@ -401,7 +470,7 @@
<button <button
v-for="model in availableQuickModels" v-for="model in availableQuickModels"
:key="model" :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" type="button"
@click="quickAddRestrictedModel(model)" @click="quickAddRestrictedModel(model)"
> >
@@ -409,7 +478,7 @@
</button> </button>
<span <span
v-if="availableQuickModels.length === 0" v-if="availableQuickModels.length === 0"
class="text-sm italic text-gray-400" class="text-sm italic text-gray-400 dark:text-gray-500"
> >
所有常用模型已在限制列表中 所有常用模型已在限制列表中
</span> </span>
@@ -419,7 +488,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
v-model="form.modelInput" v-model="form.modelInput"
class="form-input flex-1" class="form-input flex-1 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
placeholder="输入模型名称,按回车添加" placeholder="输入模型名称,按回车添加"
type="text" type="text"
@keydown.enter.prevent="addRestrictedModel" @keydown.enter.prevent="addRestrictedModel"
@@ -433,7 +502,7 @@
</button> </button>
</div> </div>
</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 设置此API Key无法访问的模型例如claude-opus-4-20250514
</p> </p>
</div> </div>
@@ -446,11 +515,11 @@
<input <input
id="editEnableClientRestriction" id="editEnableClientRestriction"
v-model="form.enableClientRestriction" 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" type="checkbox"
/> />
<label <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" for="editEnableClientRestriction"
> >
启用客户端限制 启用客户端限制
@@ -459,8 +528,12 @@
<div v-if="form.enableClientRestriction" class="space-y-3"> <div v-if="form.enableClientRestriction" class="space-y-3">
<div> <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"
<p class="mb-3 text-xs text-gray-500">勾选允许使用此API Key的客户端</p> >允许的客户端</label
>
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">
勾选允许使用此API Key的客户端
</p>
<div class="space-y-2"> <div class="space-y-2">
<div v-for="client in supportedClients" :key="client.id" class="flex items-start"> <div v-for="client in supportedClients" :key="client.id" class="flex items-start">
<input <input
@@ -471,8 +544,12 @@
:value="client.id" :value="client.id"
/> />
<label class="ml-2 flex-1 cursor-pointer" :for="`edit_client_${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="text-sm font-medium text-gray-700 dark:text-gray-300">{{
<span class="block text-xs text-gray-500">{{ client.description }}</span> client.name
}}</span>
<span class="block text-xs text-gray-500 dark:text-gray-400">{{
client.description
}}</span>
</label> </label>
</div> </div>
</div> </div>
@@ -482,7 +559,7 @@
<div class="flex gap-3 pt-4"> <div class="flex gap-3 pt-4">
<button <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" type="button"
@click="$emit('close')" @click="$emit('close')"
> >

View File

@@ -1,8 +1,14 @@
<template> <template>
<Teleport to="body"> <Teleport to="body">
<div v-if="show" class="modal fixed inset-0 z-50 flex items-center justify-center p-4"> <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="mb-6 flex items-center justify-between">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
@@ -12,14 +18,14 @@
<i class="fas fa-clock text-white" /> <i class="fas fa-clock text-white" />
</div> </div>
<div> <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>
<p class="text-sm text-gray-600"> <p class="text-sm text-gray-600 dark:text-gray-400">
"{{ apiKey.name || 'API Key' }}" 设置新的过期时间 "{{ apiKey.name || 'API Key' }}" 设置新的过期时间
</p> </p>
</div> </div>
</div> </div>
<button <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')" @click="$emit('close')"
> >
<i class="fas fa-times text-xl" /> <i class="fas fa-times text-xl" />
@@ -29,12 +35,14 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- 当前状态显示 --> <!-- 当前状态显示 -->
<div <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 class="flex items-center justify-between">
<div> <div>
<p class="mb-1 text-xs font-medium text-gray-600">当前过期时间</p> <p class="mb-1 text-xs font-medium text-gray-600 dark:text-gray-400">
<p class="text-sm font-semibold text-gray-800"> 当前过期时间
</p>
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200">
<template v-if="apiKey.expiresAt"> <template v-if="apiKey.expiresAt">
{{ formatExpireDate(apiKey.expiresAt) }} {{ formatExpireDate(apiKey.expiresAt) }}
<span <span
@@ -51,7 +59,9 @@
</template> </template>
</p> </p>
</div> </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 <i
:class="[ :class="[
'fas fa-hourglass-half text-lg', 'fas fa-hourglass-half text-lg',
@@ -66,7 +76,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
>
<div class="mb-3 grid grid-cols-3 gap-2"> <div class="mb-3 grid grid-cols-3 gap-2">
<button <button
v-for="option in quickOptions" v-for="option in quickOptions"
@@ -75,7 +87,7 @@
'rounded-lg px-3 py-2 text-sm font-medium transition-all', 'rounded-lg px-3 py-2 text-sm font-medium transition-all',
localForm.expireDuration === option.value localForm.expireDuration === option.value
? 'bg-blue-500 text-white shadow-md' ? '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)" @click="selectQuickOption(option.value)"
> >
@@ -86,7 +98,7 @@
'rounded-lg px-3 py-2 text-sm font-medium transition-all', 'rounded-lg px-3 py-2 text-sm font-medium transition-all',
localForm.expireDuration === 'custom' localForm.expireDuration === 'custom'
? 'bg-blue-500 text-white shadow-md' ? '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')" @click="selectQuickOption('custom')"
> >
@@ -98,29 +110,33 @@
<!-- 自定义日期选择 --> <!-- 自定义日期选择 -->
<div v-if="localForm.expireDuration === 'custom'" class="animate-fadeIn"> <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 <input
v-model="localForm.customExpireDate" 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" :min="minDateTime"
type="datetime-local" type="datetime-local"
@change="updateCustomExpiryPreview" @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>
<!-- 预览新的过期时间 --> <!-- 预览新的过期时间 -->
<div <div
v-if="localForm.expiresAt !== apiKey.expiresAt" 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 class="flex items-center justify-between">
<div> <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" /> <i class="fas fa-arrow-right mr-1" />
新的过期时间 新的过期时间
</p> </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"> <template v-if="localForm.expiresAt">
{{ formatExpireDate(localForm.expiresAt) }} {{ formatExpireDate(localForm.expiresAt) }}
<span <span
@@ -137,7 +153,9 @@
</template> </template>
</p> </p>
</div> </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" /> <i class="fas fa-check text-lg text-green-500" />
</div> </div>
</div> </div>
@@ -146,7 +164,7 @@
<!-- 操作按钮 --> <!-- 操作按钮 -->
<div class="flex gap-3 pt-2"> <div class="flex gap-3 pt-2">
<button <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')" @click="$emit('close')"
> >
取消 取消

View File

@@ -12,12 +12,12 @@
<i class="fas fa-check text-lg text-white" /> <i class="fas fa-check text-lg text-white" />
</div> </div>
<div> <div>
<h3 class="text-xl font-bold text-gray-900">API Key 创建成功</h3> <h3 class="text-xl font-bold text-gray-900 dark:text-gray-100">API Key 创建成功</h3>
<p class="text-sm text-gray-600">请妥善保存您的 API Key</p> <p class="text-sm text-gray-600 dark:text-gray-400">请妥善保存您的 API Key</p>
</div> </div>
</div> </div>
<button <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="直接关闭(不推荐)" title="直接关闭(不推荐)"
@click="handleDirectClose" @click="handleDirectClose"
> >
@@ -26,16 +26,18 @@
</div> </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="flex items-start">
<div <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" /> <i class="fas fa-exclamation-triangle text-sm text-white" />
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h5 class="mb-1 font-semibold text-amber-900">重要提醒</h5> <h5 class="mb-1 font-semibold text-amber-900 dark:text-amber-400">重要提醒</h5>
<p class="text-sm text-amber-800"> <p class="text-sm text-amber-800 dark:text-amber-300">
这是您唯一能看到完整 API Key 的机会关闭此窗口后系统将不再显示完整的 API 这是您唯一能看到完整 API Key 的机会关闭此窗口后系统将不再显示完整的 API
Key请立即复制并妥善保存 Key请立即复制并妥善保存
</p> </p>
@@ -46,30 +48,42 @@
<!-- API Key 信息 --> <!-- API Key 信息 -->
<div class="mb-6 space-y-4"> <div class="mb-6 space-y-4">
<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"
<div class="rounded-lg border bg-gray-50 p-3"> >API Key 名称</label
<span class="font-medium text-gray-900">{{ apiKey.name }}</span> >
<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> </div>
<div v-if="apiKey.description"> <div v-if="apiKey.description">
<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"
<div class="rounded-lg border bg-gray-50 p-3"> >备注</label
<span class="text-gray-700">{{ apiKey.description || '无描述' }}</span> >
<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> </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="relative">
<div <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() }} {{ getDisplayedApiKey() }}
</div> </div>
<div class="absolute right-3 top-3"> <div class="absolute right-3 top-3">
<button <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'" :title="showFullKey ? '隐藏API Key' : '显示完整API Key'"
type="button" type="button"
@click="toggleKeyVisibility" @click="toggleKeyVisibility"
@@ -78,7 +92,7 @@
</button> </button>
</div> </div>
</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 点击眼睛图标切换显示模式使用下方按钮复制完整 API Key
</p> </p>
</div> </div>
@@ -94,7 +108,7 @@
复制 API Key 复制 API Key
</button> </button>
<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" @click="handleClose"
> >
我已保存 我已保存

View File

@@ -16,7 +16,7 @@
> >
<i class="fas fa-chart-line text-sm text-white sm:text-base" /> <i class="fas fa-chart-line text-sm text-white sm:text-base" />
</div> </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 }} 使用统计详情 - {{ apiKey.name }}
</h3> </h3>
</div> </div>
@@ -31,64 +31,68 @@
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2">
<!-- 请求统计卡片 --> <!-- 请求统计卡片 -->
<div <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"> <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" /> <i class="fas fa-paper-plane text-blue-500" />
</div> </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) }} {{ formatNumber(totalRequests) }}
</div> </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) }} 今日: {{ formatNumber(dailyRequests) }}
</div> </div>
</div> </div>
<!-- Token统计卡片 --> <!-- Token统计卡片 -->
<div <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"> <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" /> <i class="fas fa-coins text-green-500" />
</div> </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) }} {{ formatTokenCount(totalTokens) }}
</div> </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) }} 今日: {{ formatTokenCount(dailyTokens) }}
</div> </div>
</div> </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"> <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" /> <i class="fas fa-dollar-sign text-yellow-600" />
</div> </div>
<div class="text-2xl font-bold text-gray-900">${{ totalCost.toFixed(4) }}</div> <div class="text-2xl font-bold text-gray-900 dark:text-gray-100">
<div class="mt-1 text-xs text-gray-600">今日: ${{ dailyCost.toFixed(4) }}</div> ${{ totalCost.toFixed(4) }}
</div>
<div class="mt-1 text-xs text-gray-600 dark:text-gray-400">
今日: ${{ dailyCost.toFixed(4) }}
</div>
</div> </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"> <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" /> <i class="fas fa-tachometer-alt text-purple-500" />
</div> </div>
<div class="space-y-1 text-sm"> <div class="space-y-1 text-sm">
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-600">RPM:</span> <span class="text-gray-600 dark:text-gray-400">RPM:</span>
<span class="font-semibold">{{ rpm }}</span> <span class="font-semibold text-gray-900 dark:text-gray-100">{{ rpm }}</span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-600">TPM:</span> <span class="text-gray-600 dark:text-gray-400">TPM:</span>
<span class="font-semibold">{{ tpm }}</span> <span class="font-semibold text-gray-900 dark:text-gray-100">{{ tpm }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -96,33 +100,35 @@
<!-- Token详细分布 --> <!-- Token详细分布 -->
<div class="mb-6"> <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" /> <i class="fas fa-chart-pie mr-2 text-indigo-500" />
Token 使用分布 Token 使用分布
</h4> </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 justify-between">
<div class="flex items-center"> <div class="flex items-center">
<i class="fas fa-arrow-down mr-2 text-green-500" /> <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> </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) }} {{ formatTokenCount(inputTokens) }}
</span> </span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<i class="fas fa-arrow-up mr-2 text-blue-500" /> <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> </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) }} {{ formatTokenCount(outputTokens) }}
</span> </span>
</div> </div>
<div v-if="cacheCreateTokens > 0" class="flex items-center justify-between"> <div v-if="cacheCreateTokens > 0" class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<i class="fas fa-save mr-2 text-purple-500" /> <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> </div>
<span class="text-sm font-semibold text-purple-600"> <span class="text-sm font-semibold text-purple-600">
{{ formatTokenCount(cacheCreateTokens) }} {{ formatTokenCount(cacheCreateTokens) }}
@@ -131,7 +137,7 @@
<div v-if="cacheReadTokens > 0" class="flex items-center justify-between"> <div v-if="cacheReadTokens > 0" class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<i class="fas fa-download mr-2 text-purple-500" /> <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> </div>
<span class="text-sm font-semibold text-purple-600"> <span class="text-sm font-semibold text-purple-600">
{{ formatTokenCount(cacheReadTokens) }} {{ formatTokenCount(cacheReadTokens) }}
@@ -142,19 +148,21 @@
<!-- 限制信息 --> <!-- 限制信息 -->
<div v-if="hasLimits" class="mb-6"> <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" /> <i class="fas fa-shield-alt mr-2 text-red-500" />
限制设置 限制设置
</h4> </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 v-if="apiKey.dailyCostLimit > 0" class="space-y-2">
<div class="flex items-center justify-between text-sm"> <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-gray-900"> <span class="font-semibold text-gray-900 dark:text-gray-100">
${{ apiKey.dailyCostLimit.toFixed(2) }} ${{ apiKey.dailyCostLimit.toFixed(2) }}
</span> </span>
</div> </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 <div
class="h-2 rounded-full transition-all duration-300" class="h-2 rounded-full transition-all duration-300"
:class=" :class="
@@ -167,7 +175,7 @@
:style="{ width: Math.min(dailyCostPercentage, 100) + '%' }" :style="{ width: Math.min(dailyCostPercentage, 100) + '%' }"
/> />
</div> </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) }}% 已使用 {{ dailyCostPercentage.toFixed(1) }}%
</div> </div>
</div> </div>
@@ -176,14 +184,14 @@
v-if="apiKey.concurrencyLimit > 0" v-if="apiKey.concurrencyLimit > 0"
class="flex items-center justify-between text-sm" 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"> <span class="font-semibold text-purple-600">
{{ apiKey.currentConcurrency || 0 }} / {{ apiKey.concurrencyLimit }} {{ apiKey.currentConcurrency || 0 }} / {{ apiKey.concurrencyLimit }}
</span> </span>
</div> </div>
<div v-if="apiKey.rateLimitWindow > 0" class="space-y-2"> <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" /> <i class="fas fa-clock mr-1 text-blue-500" />
时间窗口限制 时间窗口限制
</h5> </h5>

View File

@@ -1,12 +1,12 @@
<template> <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"> <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" /> <i class="fas fa-chart-line mr-3" />
使用统计查询 使用统计查询
</h2> </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> </div>
<!-- 输入区域 --> <!-- 输入区域 -->
@@ -14,7 +14,7 @@
<div class="api-input-grid grid grid-cols-1 lg:grid-cols-4"> <div class="api-input-grid grid grid-cols-1 lg:grid-cols-4">
<!-- API Key 输入 --> <!-- API Key 输入 -->
<div class="lg:col-span-3"> <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" /> <i class="fas fa-key mr-2" />
输入您的 API Key 输入您的 API Key
</label> </label>
@@ -30,7 +30,9 @@
<!-- 查询按钮 --> <!-- 查询按钮 -->
<div class="lg:col-span-1"> <div class="lg:col-span-1">
<label class="mb-2 hidden text-sm font-medium text-gray-700 lg:block"> &nbsp; </label> <label class="mb-2 hidden text-sm font-medium text-gray-700 dark:text-gray-200 lg:block">
&nbsp;
</label>
<button <button
class="btn btn-primary btn-query flex h-full w-full items-center justify-center gap-2" class="btn btn-primary btn-query flex h-full w-full items-center justify-center gap-2"
:disabled="loading || !apiKey.trim()" :disabled="loading || !apiKey.trim()"
@@ -62,11 +64,11 @@ const { queryStats } = apiStatsStore
</script> </script>
<style scoped> <style scoped>
/* 宽卡片样式 */ /* 宽卡片样式 - 使用CSS变量 */
.api-input-wide-card { .api-input-wide-card {
background: rgba(255, 255, 255, 0.95); background: var(--surface-color);
backdrop-filter: blur(25px); backdrop-filter: blur(25px);
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid var(--border-color);
box-shadow: box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.05), 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); 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 { .api-input-wide-card:hover {
box-shadow: box-shadow:
0 32px 64px -12px rgba(0, 0, 0, 0.35), 0 32px 64px -12px rgba(0, 0, 0, 0.35),
@@ -82,6 +92,13 @@ const { queryStats } = apiStatsStore
transform: translateY(-1px); 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 { .wide-card-title h2 {
color: #1f2937; color: #1f2937;
@@ -89,11 +106,21 @@ const { queryStats } = apiStatsStore
font-weight: 700; 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 { .wide-card-title p {
color: #4b5563; color: #4b5563;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); 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 { .wide-card-title .fas.fa-chart-line {
color: #3b82f6; color: #3b82f6;
text-shadow: 0 1px 2px rgba(59, 130, 246, 0.2); text-shadow: 0 1px 2px rgba(59, 130, 246, 0.2);
@@ -105,23 +132,32 @@ const { queryStats } = apiStatsStore
gap: 1rem; gap: 1rem;
} }
/* 输入框样式 */ /* 输入框样式 - 使用CSS变量 */
.wide-card-input { .wide-card-input {
background: rgba(255, 255, 255, 0.95); background: var(--input-bg);
border: 2px solid rgba(255, 255, 255, 0.4); border: 2px solid var(--input-border);
border-radius: 12px; border-radius: 12px;
padding: 14px 16px; padding: 14px 16px;
font-size: 16px; font-size: 16px;
transition: all 0.3s ease; transition: all 0.3s ease;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
color: #1f2937; color: var(--text-primary);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 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 { .wide-card-input::placeholder {
color: #9ca3af; color: #9ca3af;
} }
:global(.dark) .wide-card-input::placeholder {
color: #64748b;
}
.wide-card-input:focus { .wide-card-input:focus {
outline: none; outline: none;
border-color: #60a5fa; border-color: #60a5fa;
@@ -129,6 +165,16 @@ const { queryStats } = apiStatsStore
0 0 0 3px rgba(96, 165, 250, 0.2), 0 0 0 3px rgba(96, 165, 250, 0.2),
0 10px 15px -3px rgba(0, 0, 0, 0.1); 0 10px 15px -3px rgba(0, 0, 0, 0.1);
background: white; 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 { .security-notice {
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.5);
border: 1px solid rgba(255, 255, 255, 0.25); border: 1px solid rgba(255, 255, 255, 0.4);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-radius: 8px; border-radius: 8px;
padding: 12px 16px; padding: 12px 16px;
@@ -182,9 +228,22 @@ const { queryStats } = apiStatsStore
transition: all 0.3s ease; 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 { .security-notice:hover {
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.6);
border-color: rgba(255, 255, 255, 0.35); 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 { .security-notice .fas.fa-shield-alt {

View File

@@ -2,7 +2,9 @@
<div> <div>
<!-- 限制配置 --> <!-- 限制配置 -->
<div class="card p-4 md:p-6"> <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" /> <i class="fas fa-shield-alt mr-2 text-sm text-red-500 md:mr-3 md:text-base" />
限制配置 限制配置
</h3> </h3>
@@ -10,8 +12,10 @@
<!-- 每日费用限制 --> <!-- 每日费用限制 -->
<div> <div>
<div class="mb-2 flex items-center justify-between"> <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-sm font-medium text-gray-600 dark:text-gray-400 md:text-base"
<span class="text-xs text-gray-500 md:text-sm"> >每日费用限制</span
>
<span class="text-xs text-gray-500 dark:text-gray-400 md:text-sm">
<span v-if="statsData.limits.dailyCostLimit > 0"> <span v-if="statsData.limits.dailyCostLimit > 0">
${{ statsData.limits.currentDailyCost.toFixed(4) }} / ${{ ${{ statsData.limits.currentDailyCost.toFixed(4) }} / ${{
statsData.limits.dailyCostLimit.toFixed(2) statsData.limits.dailyCostLimit.toFixed(2)
@@ -24,7 +28,7 @@
</div> </div>
<div <div
v-if="statsData.limits.dailyCostLimit > 0" 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 <div
class="h-2 rounded-full transition-all duration-300" class="h-2 rounded-full transition-all duration-300"
@@ -58,16 +62,16 @@
:window-start-time="statsData.limits.windowStartTime" :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" /> <i class="fas fa-info-circle mr-1" />
请求次数和Token使用量为"或"的关系,任一达到限制即触发限流 请求次数和Token使用量为"或"的关系,任一达到限制即触发限流
</div> </div>
</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"> <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 class="text-sm font-medium text-gray-900 md:text-base">
<span v-if="statsData.limits.concurrencyLimit > 0"> <span v-if="statsData.limits.concurrencyLimit > 0">
{{ statsData.limits.concurrencyLimit }} {{ statsData.limits.concurrencyLimit }}
@@ -78,7 +82,7 @@
</span> </span>
</div> </div>
<div class="flex items-center justify-between"> <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 class="text-sm font-medium text-gray-900 md:text-base">
<span <span
v-if=" v-if="
@@ -97,7 +101,7 @@
</span> </span>
</div> </div>
<div class="flex items-center justify-between"> <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 class="text-sm font-medium text-gray-900 md:text-base">
<span <span
v-if=" v-if="
@@ -129,7 +133,9 @@
" "
class="card mt-4 p-4 md:mt-6 md:p-6" 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" /> <i class="fas fa-list-alt mr-2 text-sm text-amber-500 md:mr-3 md:text-base" />
详细限制信息 详细限制信息
</h3> </h3>
@@ -141,9 +147,11 @@
statsData.restrictions.enableModelRestriction && statsData.restrictions.enableModelRestriction &&
statsData.restrictions.restrictedModels.length > 0 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" /> <i class="fas fa-robot mr-1 text-xs md:mr-2 md:text-sm" />
受限模型列表 受限模型列表
</h4> </h4>
@@ -151,13 +159,13 @@
<div <div
v-for="model in statsData.restrictions.restrictedModels" v-for="model in statsData.restrictions.restrictedModels"
:key="model" :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" /> <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>
</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" /> <i class="fas fa-info-circle mr-1" />
此 API Key 不能访问以上列出的模型 此 API Key 不能访问以上列出的模型
</p> </p>
@@ -169,9 +177,11 @@
statsData.restrictions.enableClientRestriction && statsData.restrictions.enableClientRestriction &&
statsData.restrictions.allowedClients.length > 0 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" /> <i class="fas fa-desktop mr-1 text-xs md:mr-2 md:text-sm" />
允许的客户端 允许的客户端
</h4> </h4>
@@ -179,13 +189,13 @@
<div <div
v-for="client in statsData.restrictions.allowedClients" v-for="client in statsData.restrictions.allowedClients"
:key="client" :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" /> <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>
</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" /> <i class="fas fa-info-circle mr-1" />
API Key 只能被以上列出的客户端使用 API Key 只能被以上列出的客户端使用
</p> </p>
@@ -222,11 +232,11 @@ const getDailyCostProgressColor = () => {
</script> </script>
<style scoped> <style scoped>
/* 卡片样式 */ /* 卡片样式 - 使用CSS变量 */
.card { .card {
background: rgba(255, 255, 255, 0.95); background: var(--surface-color);
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid var(--border-color);
box-shadow: box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05); 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 20px 25px -5px rgba(0, 0, 0, 0.15),
0 10px 10px -5px rgba(0, 0, 0, 0.08); 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> </style>

View File

@@ -2,13 +2,13 @@
<div class="card p-4 md:p-6"> <div class="card p-4 md:p-6">
<div class="mb-4 md:mb-6"> <div class="mb-4 md:mb-6">
<h3 <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"> <span class="flex items-center">
<i class="fas fa-robot mr-2 text-sm text-indigo-500 md:mr-3 md:text-base" /> <i class="fas fa-robot mr-2 text-sm text-indigo-500 md:mr-3 md:text-base" />
模型使用统计 模型使用统计
</span> </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 >({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span
> >
</h3> </h3>
@@ -16,8 +16,10 @@
<!-- 模型统计加载状态 --> <!-- 模型统计加载状态 -->
<div v-if="modelStatsLoading" class="py-6 text-center md:py-8"> <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" /> <i
<p class="text-sm text-gray-600 md:text-base">加载模型统计数据中...</p> 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> </div>
<!-- 模型统计数据 --> <!-- 模型统计数据 -->
@@ -25,41 +27,43 @@
<div v-for="(model, index) in modelStats" :key="index" class="model-usage-item"> <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="mb-2 flex items-start justify-between md:mb-3">
<div class="min-w-0 flex-1"> <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 }} {{ model.model }}
</h4> </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>
<div class="ml-3 flex-shrink-0 text-right"> <div class="ml-3 flex-shrink-0 text-right">
<div class="text-base font-bold text-green-600 md:text-lg"> <div class="text-base font-bold text-green-600 md:text-lg">
{{ model.formatted?.total || '$0.000000' }} {{ model.formatted?.total || '$0.000000' }}
</div> </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> </div>
<div class="grid grid-cols-2 gap-2 text-xs md:grid-cols-4 md:gap-3 md:text-sm"> <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="rounded bg-gray-50 p-2 dark:bg-gray-700">
<div class="text-gray-600">输入 Token</div> <div class="text-gray-600 dark:text-gray-400">输入 Token</div>
<div class="font-medium text-gray-900"> <div class="font-medium text-gray-900 dark:text-gray-100">
{{ formatNumber(model.inputTokens) }} {{ formatNumber(model.inputTokens) }}
</div> </div>
</div> </div>
<div class="rounded bg-gray-50 p-2"> <div class="rounded bg-gray-50 p-2 dark:bg-gray-700">
<div class="text-gray-600">输出 Token</div> <div class="text-gray-600 dark:text-gray-400">输出 Token</div>
<div class="font-medium text-gray-900"> <div class="font-medium text-gray-900 dark:text-gray-100">
{{ formatNumber(model.outputTokens) }} {{ formatNumber(model.outputTokens) }}
</div> </div>
</div> </div>
<div class="rounded bg-gray-50 p-2"> <div class="rounded bg-gray-50 p-2 dark:bg-gray-700">
<div class="text-gray-600">缓存创建</div> <div class="text-gray-600 dark:text-gray-400">缓存创建</div>
<div class="font-medium text-gray-900"> <div class="font-medium text-gray-900 dark:text-gray-100">
{{ formatNumber(model.cacheCreateTokens) }} {{ formatNumber(model.cacheCreateTokens) }}
</div> </div>
</div> </div>
<div class="rounded bg-gray-50 p-2"> <div class="rounded bg-gray-50 p-2 dark:bg-gray-700">
<div class="text-gray-600">缓存读取</div> <div class="text-gray-600 dark:text-gray-400">缓存读取</div>
<div class="font-medium text-gray-900"> <div class="font-medium text-gray-900 dark:text-gray-100">
{{ formatNumber(model.cacheReadTokens) }} {{ formatNumber(model.cacheReadTokens) }}
</div> </div>
</div> </div>
@@ -68,7 +72,7 @@
</div> </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" /> <i class="fas fa-chart-pie mb-3 text-2xl md:text-3xl" />
<p class="text-sm md:text-base"> <p class="text-sm md:text-base">
暂无{{ statsPeriod === 'daily' ? '今日' : '本月' }}模型使用数据 暂无{{ statsPeriod === 'daily' ? '今日' : '本月' }}模型使用数据
@@ -104,11 +108,11 @@ const formatNumber = (num) => {
</script> </script>
<style scoped> <style scoped>
/* 卡片样式 */ /* 卡片样式 - 使用CSS变量 */
.card { .card {
background: rgba(255, 255, 255, 0.95); background: var(--surface-color);
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid var(--border-color);
box-shadow: box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05); 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); 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 { .model-usage-item {
background: rgba(255, 255, 255, 0.95); background: var(--surface-color);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid var(--border-color);
border-radius: 12px; border-radius: 12px;
padding: 12px; padding: 12px;
transition: all 0.3s ease; transition: all 0.3s ease;
@@ -169,6 +179,13 @@ const formatNumber = (num) => {
border-color: rgba(255, 255, 255, 0.3); 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 { .loading-spinner {
animation: spin 1s linear infinite; animation: spin 1s linear infinite;

View File

@@ -2,19 +2,22 @@
<div class="mb-6 grid grid-cols-1 gap-4 md:mb-8 md:gap-6 lg:grid-cols-2"> <div class="mb-6 grid grid-cols-1 gap-4 md:mb-8 md:gap-6 lg:grid-cols-2">
<!-- API Key 基本信息 --> <!-- API Key 基本信息 -->
<div class="card p-4 md:p-6"> <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" /> <i class="fas fa-info-circle mr-2 text-sm text-blue-500 md:mr-3 md:text-base" />
API Key 信息 API Key 信息
</h3> </h3>
<div class="space-y-2 md:space-y-3"> <div class="space-y-2 md:space-y-3">
<div class="flex items-center justify-between"> <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="break-all text-sm font-medium text-gray-900 md:text-base">{{ <span
statsData.name class="break-all text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base"
}}</span> >{{ statsData.name }}</span
>
</div> </div>
<div class="flex items-center justify-between"> <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 <span
class="text-sm font-medium md:text-base" class="text-sm font-medium md:text-base"
:class="statsData.isActive ? 'text-green-600' : 'text-red-600'" :class="statsData.isActive ? 'text-green-600' : 'text-red-600'"
@@ -27,19 +30,22 @@
</span> </span>
</div> </div>
<div class="flex items-center justify-between"> <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 class="text-sm font-medium text-gray-900 dark:text-gray-100 md:text-base">{{
formatPermissions(statsData.permissions) formatPermissions(statsData.permissions)
}}</span> }}</span>
</div> </div>
<div class="flex items-center justify-between"> <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="break-all text-xs font-medium text-gray-900 md:text-base">{{ <span
formatDate(statsData.createdAt) class="break-all text-xs font-medium text-gray-900 dark:text-gray-100 md:text-base"
}}</span> >{{ formatDate(statsData.createdAt) }}</span
>
</div> </div>
<div class="flex items-start justify-between"> <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="statsData.expiresAt" class="text-right">
<div <div
v-if="isApiKeyExpired(statsData.expiresAt)" v-if="isApiKeyExpired(statsData.expiresAt)"
@@ -55,11 +61,14 @@
<i class="fas fa-clock mr-1 text-xs md:text-sm" /> <i class="fas fa-clock mr-1 text-xs md:text-sm" />
{{ formatExpireDate(statsData.expiresAt) }} {{ formatExpireDate(statsData.expiresAt) }}
</div> </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) }} {{ formatExpireDate(statsData.expiresAt) }}
</div> </div>
</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" /> <i class="fas fa-infinity mr-1 text-xs md:text-sm" />
永不过期 永不过期
</div> </div>
@@ -70,13 +79,13 @@
<!-- 使用统计概览 --> <!-- 使用统计概览 -->
<div class="card p-4 md:p-6"> <div class="card p-4 md:p-6">
<h3 <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"> <span class="flex items-center">
<i class="fas fa-chart-bar mr-2 text-sm text-green-500 md:mr-3 md:text-base" /> <i class="fas fa-chart-bar mr-2 text-sm text-green-500 md:mr-3 md:text-base" />
使用统计概览 使用统计概览
</span> </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 >({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span
> >
</h3> </h3>
@@ -85,7 +94,7 @@
<div class="text-lg font-bold text-green-600 md:text-3xl"> <div class="text-lg font-bold text-green-600 md:text-3xl">
{{ formatNumber(currentPeriodData.requests) }} {{ formatNumber(currentPeriodData.requests) }}
</div> </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' ? '今日' : '本月' }}请求数 {{ statsPeriod === 'daily' ? '今日' : '本月' }}请求数
</div> </div>
</div> </div>
@@ -93,7 +102,7 @@
<div class="text-lg font-bold text-blue-600 md:text-3xl"> <div class="text-lg font-bold text-blue-600 md:text-3xl">
{{ formatNumber(currentPeriodData.allTokens) }} {{ formatNumber(currentPeriodData.allTokens) }}
</div> </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数 {{ statsPeriod === 'daily' ? '今日' : '本月' }}Token数
</div> </div>
</div> </div>
@@ -101,7 +110,7 @@
<div class="text-lg font-bold text-purple-600 md:text-3xl"> <div class="text-lg font-bold text-purple-600 md:text-3xl">
{{ currentPeriodData.formattedCost || '$0.000000' }} {{ currentPeriodData.formattedCost || '$0.000000' }}
</div> </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' ? '今日' : '本月' }}费用 {{ statsPeriod === 'daily' ? '今日' : '本月' }}费用
</div> </div>
</div> </div>
@@ -109,7 +118,7 @@
<div class="text-lg font-bold text-yellow-600 md:text-3xl"> <div class="text-lg font-bold text-yellow-600 md:text-3xl">
{{ formatNumber(currentPeriodData.inputTokens) }} {{ formatNumber(currentPeriodData.inputTokens) }}
</div> </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 {{ statsPeriod === 'daily' ? '今日' : '本月' }}输入Token
</div> </div>
</div> </div>
@@ -197,11 +206,11 @@ const formatPermissions = (permissions) => {
</script> </script>
<style scoped> <style scoped>
/* 卡片样式 */ /* 卡片样式 - 使用CSS变量 */
.card { .card {
background: rgba(255, 255, 255, 0.95); background: var(--surface-color);
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid var(--border-color);
box-shadow: box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05); 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); 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 { .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-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid var(--border-color);
padding: 16px; padding: 16px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@@ -264,6 +279,12 @@ const formatPermissions = (permissions) => {
0 10px 10px -5px rgba(0, 0, 0, 0.04); 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 { .stat-card:hover::before {
opacity: 1; opacity: 1;
} }

View File

@@ -1,56 +1,56 @@
<template> <template>
<div class="card p-4 md:p-6"> <div class="card p-4 md:p-6">
<h3 <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"> <span class="flex items-center">
<i class="fas fa-coins mr-2 text-sm text-yellow-500 md:mr-3 md:text-base" /> <i class="fas fa-coins mr-2 text-sm text-yellow-500 md:mr-3 md:text-base" />
Token 使用分布 Token 使用分布
</span> </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 >({{ statsPeriod === 'daily' ? '今日' : '本月' }})</span
> >
</h3> </h3>
<div class="space-y-2 md:space-y-3"> <div class="space-y-2 md:space-y-3">
<div class="flex items-center justify-between"> <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" /> <i class="fas fa-arrow-right mr-1 text-xs text-green-500 md:mr-2 md:text-sm" />
输入 Token 输入 Token
</span> </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) formatNumber(currentPeriodData.inputTokens)
}}</span> }}</span>
</div> </div>
<div class="flex items-center justify-between"> <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" /> <i class="fas fa-arrow-left mr-1 text-xs text-blue-500 md:mr-2 md:text-sm" />
输出 Token 输出 Token
</span> </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) formatNumber(currentPeriodData.outputTokens)
}}</span> }}</span>
</div> </div>
<div class="flex items-center justify-between"> <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" /> <i class="fas fa-save mr-1 text-xs text-purple-500 md:mr-2 md:text-sm" />
缓存创建 Token 缓存创建 Token
</span> </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) formatNumber(currentPeriodData.cacheCreateTokens)
}}</span> }}</span>
</div> </div>
<div class="flex items-center justify-between"> <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" /> <i class="fas fa-download mr-1 text-xs text-orange-500 md:mr-2 md:text-sm" />
缓存读取 Token 缓存读取 Token
</span> </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) formatNumber(currentPeriodData.cacheReadTokens)
}}</span> }}</span>
</div> </div>
</div> </div>
<div class="mt-3 border-t border-gray-200 pt-3 md:mt-4 md:pt-4"> <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"> <div class="flex items-center justify-between font-bold text-gray-900 dark:text-gray-100">
<span class="text-sm md:text-base" <span class="text-sm md:text-base"
>{{ statsPeriod === 'daily' ? '今日' : '本月' }}总计</span >{{ statsPeriod === 'daily' ? '今日' : '本月' }}总计</span
> >
@@ -87,11 +87,11 @@ const formatNumber = (num) => {
</script> </script>
<style scoped> <style scoped>
/* 卡片样式 */ /* 卡片样式 - 使用CSS变量 */
.card { .card {
background: rgba(255, 255, 255, 0.95); background: var(--surface-color);
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid var(--border-color);
box-shadow: box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05); 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 20px 25px -5px rgba(0, 0, 0, 0.15),
0 10px 10px -5px rgba(0, 0, 0, 0.08); 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> </style>

View File

@@ -2,13 +2,18 @@
<div ref="triggerRef" class="relative"> <div ref="triggerRef" class="relative">
<!-- 选择器主体 --> <!-- 选择器主体 -->
<div <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 }" :class="{ 'opacity-50': disabled }"
@click="!disabled && toggleDropdown()" @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 <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 }" :class="{ 'rotate-180': showDropdown }"
/> />
</div> </div>
@@ -26,27 +31,27 @@
<div <div
v-if="showDropdown" v-if="showDropdown"
ref="dropdownRef" 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" :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"> <div class="relative">
<input <input
ref="searchInput" ref="searchInput"
v-model="searchQuery" 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="搜索账号名称..." placeholder="搜索账号名称..."
style="padding-left: 40px; padding-right: 36px" style="padding-left: 40px; padding-right: 36px"
type="text" type="text"
@input="handleSearch" @input="handleSearch"
/> />
<i <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 <button
v-if="searchQuery" 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" type="button"
@click="clearSearch" @click="clearSearch"
> >
@@ -59,59 +64,67 @@
<div class="custom-scrollbar flex-1 overflow-y-auto"> <div class="custom-scrollbar flex-1 overflow-y-auto">
<!-- 默认选项 --> <!-- 默认选项 -->
<div <div
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50" class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
:class="{ 'bg-blue-50': !modelValue }" :class="{ 'bg-blue-50 dark:bg-blue-900/20': !modelValue }"
@click="selectAccount(null)" @click="selectAccount(null)"
> >
<span class="text-gray-700">{{ defaultOptionText }}</span> <span class="text-gray-700 dark:text-gray-300">{{ defaultOptionText }}</span>
</div> </div>
<!-- 分组选项 --> <!-- 分组选项 -->
<div v-if="filteredGroups.length > 0"> <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 <div
v-for="group in filteredGroups" v-for="group in filteredGroups"
:key="`group:${group.id}`" :key="`group:${group.id}`"
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50" class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
:class="{ 'bg-blue-50': modelValue === `group:${group.id}` }" :class="{ 'bg-blue-50 dark:bg-blue-900/20': modelValue === `group:${group.id}` }"
@click="selectAccount(`group:${group.id}`)" @click="selectAccount(`group:${group.id}`)"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-gray-700">{{ group.name }}</span> <span class="text-gray-700 dark:text-gray-300">{{ group.name }}</span>
<span class="text-xs text-gray-500">{{ group.memberCount || 0 }} 个成员</span> <span class="text-xs text-gray-500 dark:text-gray-400"
>{{ group.memberCount || 0 }} 个成员</span
>
</div> </div>
</div> </div>
</div> </div>
<!-- OAuth 账号 --> <!-- OAuth 账号 -->
<div v-if="filteredOAuthAccounts.length > 0"> <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 专属账号' }} {{ platform === 'claude' ? 'Claude OAuth 专属账号' : 'OAuth 专属账号' }}
</div> </div>
<div <div
v-for="account in filteredOAuthAccounts" v-for="account in filteredOAuthAccounts"
:key="account.id" :key="account.id"
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50" class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
:class="{ 'bg-blue-50': modelValue === account.id }" :class="{ 'bg-blue-50 dark:bg-blue-900/20': modelValue === account.id }"
@click="selectAccount(account.id)" @click="selectAccount(account.id)"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<span class="text-gray-700">{{ account.name }}</span> <span class="text-gray-700 dark:text-gray-300">{{ account.name }}</span>
<span <span
class="ml-2 rounded-full px-2 py-0.5 text-xs" class="ml-2 rounded-full px-2 py-0.5 text-xs"
:class=" :class="
account.isActive 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' : account.status === 'unauthorized'
? 'bg-orange-100 text-orange-700' ? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
: 'bg-red-100 text-red-700' : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
" "
> >
{{ getAccountStatusText(account) }} {{ getAccountStatusText(account) }}
</span> </span>
</div> </div>
<span class="text-xs text-gray-400"> <span class="text-xs text-gray-400 dark:text-gray-500">
{{ formatDate(account.createdAt) }} {{ formatDate(account.createdAt) }}
</span> </span>
</div> </div>
@@ -120,33 +133,37 @@
<!-- Console 账号 Claude --> <!-- Console 账号 Claude -->
<div v-if="platform === 'claude' && filteredConsoleAccounts.length > 0"> <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 专属账号 Claude Console 专属账号
</div> </div>
<div <div
v-for="account in filteredConsoleAccounts" v-for="account in filteredConsoleAccounts"
:key="account.id" :key="account.id"
class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50" class="cursor-pointer px-4 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
:class="{ 'bg-blue-50': modelValue === `console:${account.id}` }" :class="{
'bg-blue-50 dark:bg-blue-900/20': modelValue === `console:${account.id}`
}"
@click="selectAccount(`console:${account.id}`)" @click="selectAccount(`console:${account.id}`)"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<span class="text-gray-700">{{ account.name }}</span> <span class="text-gray-700 dark:text-gray-300">{{ account.name }}</span>
<span <span
class="ml-2 rounded-full px-2 py-0.5 text-xs" class="ml-2 rounded-full px-2 py-0.5 text-xs"
:class=" :class="
account.isActive 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' : account.status === 'unauthorized'
? 'bg-orange-100 text-orange-700' ? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
: 'bg-red-100 text-red-700' : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
" "
> >
{{ getAccountStatusText(account) }} {{ getAccountStatusText(account) }}
</span> </span>
</div> </div>
<span class="text-xs text-gray-400"> <span class="text-xs text-gray-400 dark:text-gray-500">
{{ formatDate(account.createdAt) }} {{ formatDate(account.createdAt) }}
</span> </span>
</div> </div>
@@ -154,7 +171,10 @@
</div> </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" /> <i class="fas fa-search mb-2 text-2xl" />
<p class="text-sm">没有找到匹配的账号</p> <p class="text-sm">没有找到匹配的账号</p>
</div> </div>

View File

@@ -14,10 +14,10 @@
<i class="fas fa-exclamation-triangle text-lg text-white" /> <i class="fas fa-exclamation-triangle text-lg text-white" />
</div> </div>
<div class="flex-1"> <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 }} {{ title }}
</h3> </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 }} {{ message }}
</div> </div>
</div> </div>
@@ -25,7 +25,7 @@
<div class="flex items-center justify-end gap-3"> <div class="flex items-center justify-end gap-3">
<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="isProcessing" :disabled="isProcessing"
@click="handleCancel" @click="handleCancel"
> >
@@ -141,6 +141,10 @@ defineExpose({
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
:global(.dark) .modal {
background: rgba(0, 0, 0, 0.7);
}
.modal-content { .modal-content {
background: white; background: white;
border-radius: 16px; border-radius: 16px;
@@ -150,6 +154,12 @@ defineExpose({
overflow-y: auto; 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 { .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; @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; border-radius: 3px;
} }
:global(.dark) .modal-content::-webkit-scrollbar-track {
background: #374151;
}
.modal-content::-webkit-scrollbar-thumb { .modal-content::-webkit-scrollbar-thumb {
background: #cbd5e1; background: #cbd5e1;
border-radius: 3px; border-radius: 3px;
} }
:global(.dark) .modal-content::-webkit-scrollbar-thumb {
background: #4b5563;
}
.modal-content::-webkit-scrollbar-thumb:hover { .modal-content::-webkit-scrollbar-thumb:hover {
background: #94a3b8; background: #94a3b8;
} }
:global(.dark) .modal-content::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
</style> </style>

View File

@@ -1,7 +1,9 @@
<template> <template>
<Teleport to="body"> <Teleport to="body">
<div v-if="show" class="modal fixed inset-0 z-50 flex items-center justify-center p-4"> <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="mb-6 flex items-start gap-4">
<div <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" 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" /> <i class="fas fa-exclamation text-xl text-white" />
</div> </div>
<div class="flex-1"> <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 }} {{ title }}
</h3> </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 }} {{ message }}
</p> </p>
</div> </div>
@@ -20,7 +22,7 @@
<div class="flex gap-3"> <div class="flex gap-3">
<button <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')" @click="$emit('cancel')"
> >
{{ cancelText }} {{ cancelText }}
@@ -63,3 +65,15 @@ defineProps({
defineEmits(['confirm', 'cancel']) defineEmits(['confirm', 'cancel'])
</script> </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>

View File

@@ -3,17 +3,19 @@
<!-- 触发器 --> <!-- 触发器 -->
<div <div
ref="triggerRef" 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']" :class="[isOpen && 'border-blue-400 shadow-md']"
@click="toggleDropdown" @click="toggleDropdown"
> >
<i v-if="icon" :class="['fas', icon, 'text-sm', iconColor]"></i> <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 }} {{ selectedLabel || placeholder }}
</span> </span>
<i <i
:class="[ :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' isOpen && 'rotate-180'
]" ]"
></i> ></i>
@@ -32,7 +34,7 @@
<div <div
v-if="isOpen" v-if="isOpen"
ref="dropdownRef" 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" :style="dropdownStyle"
> >
<div class="max-h-60 overflow-y-auto py-1"> <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="flex cursor-pointer items-center gap-2 whitespace-nowrap px-3 py-2 text-sm transition-colors duration-150"
:class="[ :class="[
option.value === modelValue option.value === modelValue
? 'bg-blue-50 font-medium text-blue-700' ? 'bg-blue-50 font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
: 'text-gray-700 hover:bg-gray-50' : 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700'
]" ]"
@click="selectOption(option)" @click="selectOption(option)"
> >

View File

@@ -2,7 +2,7 @@
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<!-- Logo区域 --> <!-- Logo区域 -->
<div <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"> <template v-if="!loading">
<img <img
@@ -12,9 +12,9 @@
:src="logoSrc" :src="logoSrc"
@error="handleLogoError" @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> </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> </div>
<!-- 标题区域 --> <!-- 标题区域 -->
@@ -25,11 +25,14 @@
{{ title }} {{ title }}
</h1> </h1>
</template> </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" /> <slot name="after-title" />
</div> </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 }} {{ subtitle }}
</p> </p>
</div> </div>

View File

@@ -2,13 +2,16 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div class="flex-1"> <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 }} {{ title }}
</p> </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 }} {{ value }}
</p> </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 }} {{ subtitle }}
</p> </p>
</div> </div>

View 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>

View File

@@ -162,6 +162,12 @@ defineExpose({
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 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 { .toast-show {
transform: translateX(0); transform: translateX(0);
opacity: 1; opacity: 1;
@@ -227,6 +233,11 @@ defineExpose({
color: #6b7280; color: #6b7280;
} }
:global(.dark) .toast-close:hover {
background: #374151;
color: #9ca3af;
}
.toast-progress { .toast-progress {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
@@ -256,14 +267,26 @@ defineExpose({
background: #d1fae5; background: #d1fae5;
} }
:global(.dark) .toast-success .toast-icon {
background: #064e3b;
}
.toast-success .toast-title { .toast-success .toast-title {
color: #065f46; color: #065f46;
} }
:global(.dark) .toast-success .toast-title {
color: #10b981;
}
.toast-success .toast-message { .toast-success .toast-message {
color: #047857; color: #047857;
} }
:global(.dark) .toast-success .toast-message {
color: #34d399;
}
.toast-success .toast-progress { .toast-success .toast-progress {
background: #10b981; background: #10b981;
} }
@@ -278,14 +301,26 @@ defineExpose({
background: #fee2e2; background: #fee2e2;
} }
:global(.dark) .toast-error .toast-icon {
background: #7f1d1d;
}
.toast-error .toast-title { .toast-error .toast-title {
color: #991b1b; color: #991b1b;
} }
:global(.dark) .toast-error .toast-title {
color: #ef4444;
}
.toast-error .toast-message { .toast-error .toast-message {
color: #dc2626; color: #dc2626;
} }
:global(.dark) .toast-error .toast-message {
color: #f87171;
}
.toast-error .toast-progress { .toast-error .toast-progress {
background: #ef4444; background: #ef4444;
} }
@@ -300,14 +335,26 @@ defineExpose({
background: #fef3c7; background: #fef3c7;
} }
:global(.dark) .toast-warning .toast-icon {
background: #78350f;
}
.toast-warning .toast-title { .toast-warning .toast-title {
color: #92400e; color: #92400e;
} }
:global(.dark) .toast-warning .toast-title {
color: #f59e0b;
}
.toast-warning .toast-message { .toast-warning .toast-message {
color: #d97706; color: #d97706;
} }
:global(.dark) .toast-warning .toast-message {
color: #fbbf24;
}
.toast-warning .toast-progress { .toast-warning .toast-progress {
background: #f59e0b; background: #f59e0b;
} }
@@ -322,14 +369,26 @@ defineExpose({
background: #dbeafe; background: #dbeafe;
} }
:global(.dark) .toast-info .toast-icon {
background: #1e3a8a;
}
.toast-info .toast-title { .toast-info .toast-title {
color: #1e40af; color: #1e40af;
} }
:global(.dark) .toast-info .toast-title {
color: #3b82f6;
}
.toast-info .toast-message { .toast-info .toast-message {
color: #2563eb; color: #2563eb;
} }
:global(.dark) .toast-info .toast-message {
color: #60a5fa;
}
.toast-info .toast-progress { .toast-info .toast-progress {
background: #3b82f6; background: #3b82f6;
} }

View File

@@ -13,12 +13,12 @@
:logo-src="oemSettings.siteIconData || oemSettings.siteIcon" :logo-src="oemSettings.siteIconData || oemSettings.siteIcon"
subtitle="管理后台" subtitle="管理后台"
:title="oemSettings.siteName" :title="oemSettings.siteName"
title-class="text-white" title-class="text-white dark:text-gray-100"
> >
<template #after-title> <template #after-title>
<!-- 版本信息 --> <!-- 版本信息 -->
<div class="flex items-center gap-1 sm:gap-2"> <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 >v{{ versionInfo.current || '...' }}</span
> >
<!-- 更新提示 --> <!-- 更新提示 -->
@@ -36,95 +36,112 @@
</template> </template>
</LogoTitle> </LogoTitle>
</div> </div>
<!-- 用户菜单 --> <!-- 主题切换和用户菜单 -->
<div class="user-menu-container relative"> <div class="flex items-center gap-2 sm:gap-4">
<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" <div class="flex items-center">
@click="userMenuOpen = !userMenuOpen" <ThemeToggle mode="dropdown" />
> </div>
<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 <div
v-if="userMenuOpen" class="h-8 w-px bg-gradient-to-b from-transparent via-gray-300 to-transparent opacity-50 dark:via-gray-600"
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="user-menu-container relative">
<!-- 版本信息 --> <button
<div class="border-b border-gray-100 px-4 py-3"> 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"
<div class="flex items-center justify-between text-sm"> @click="userMenuOpen = !userMenuOpen"
<span class="text-gray-500">当前版本</span> >
<span class="font-mono text-gray-700">v{{ versionInfo.current || '...' }}</span> <i class="fas fa-user-circle text-sm sm:text-base" />
</div> <span class="hidden sm:inline">{{ currentUser.username || 'Admin' }}</span>
<div v-if="versionInfo.hasUpdate" class="mt-2"> <i
<div class="mb-2 flex items-center justify-between text-sm"> class="fas fa-chevron-down ml-1 text-xs transition-transform duration-200"
<span class="font-medium text-green-600"> :class="{ 'rotate-180': userMenuOpen }"
<i class="fas fa-arrow-up mr-1" />有新版本 />
</span> </button>
<span class="font-mono text-green-600">v{{ versionInfo.latest }}</span>
<!-- 悬浮菜单 -->
<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> </div>
<a <div v-if="versionInfo.hasUpdate" class="mt-2">
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" <div class="mb-2 flex items-center justify-between text-sm">
:href="versionInfo.releaseInfo?.htmlUrl || '#'" <span class="font-medium text-green-600 dark:text-green-400">
target="_blank" <i class="fas fa-arrow-up mr-1" />有新版本
> </span>
<i class="fas fa-external-link-alt mr-1" />查看更新 <span class="font-mono text-green-600 dark:text-green-400"
</a> >v{{ versionInfo.latest }}</span
</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> </div>
<button <a
v-else 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"
key="button" :href="versionInfo.releaseInfo?.htmlUrl || '#'"
class="text-xs text-blue-500 transition-colors hover:text-blue-700" target="_blank"
@click="checkForUpdates()"
> >
<i class="fas fa-sync-alt mr-1" />查更新 <i class="fas fa-external-link-alt mr-1" />更新
</button> </a>
</transition> </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> </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> </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> </div>
</div> </div>
@@ -143,10 +160,10 @@
> >
<i class="fas fa-key text-white" /> <i class="fas fa-key text-white" />
</div> </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> </div>
<button <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" @click="closeChangePasswordModal"
> >
<i class="fas fa-times text-xl" /> <i class="fas fa-times text-xl" />
@@ -158,29 +175,37 @@
@submit.prevent="changePassword" @submit.prevent="changePassword"
> >
<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 <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 disabled
type="text" type="text"
:value="currentUser.username || 'Admin'" :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>
<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 <input
v-model="changePasswordForm.newUsername" v-model="changePasswordForm.newUsername"
class="form-input w-full" class="form-input w-full"
placeholder="输入新用户名(留空保持不变)" placeholder="输入新用户名(留空保持不变)"
type="text" 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>
<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 <input
v-model="changePasswordForm.currentPassword" v-model="changePasswordForm.currentPassword"
class="form-input w-full" class="form-input w-full"
@@ -191,7 +216,9 @@
</div> </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 <input
v-model="changePasswordForm.newPassword" v-model="changePasswordForm.newPassword"
class="form-input w-full" class="form-input w-full"
@@ -199,11 +226,13 @@
required required
type="password" 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>
<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 <input
v-model="changePasswordForm.confirmPassword" v-model="changePasswordForm.confirmPassword"
class="form-input w-full" class="form-input w-full"
@@ -215,7 +244,7 @@
<div class="flex gap-3 pt-4"> <div class="flex gap-3 pt-4">
<button <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" type="button"
@click="closeChangePasswordModal" @click="closeChangePasswordModal"
> >
@@ -243,6 +272,7 @@ import { useAuthStore } from '@/stores/auth'
import { showToast } from '@/utils/toast' import { showToast } from '@/utils/toast'
import { apiClient } from '@/config/api' import { apiClient } from '@/config/api'
import LogoTitle from '@/components/common/LogoTitle.vue' import LogoTitle from '@/components/common/LogoTitle.vue'
import ThemeToggle from '@/components/common/ThemeToggle.vue'
const router = useRouter() const router = useRouter()
const authStore = useAuthStore() const authStore = useAuthStore()
@@ -430,9 +460,44 @@ onUnmounted(() => {
</script> </script>
<style scoped> <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 { .user-menu-dropdown {
margin-top: 8px; margin-top: 8px;
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
/* fade过渡动画 */ /* fade过渡动画 */

View File

@@ -1,9 +1,9 @@
<template> <template>
<div class="mb-4 sm:mb-6"> <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 <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" :value="activeTab"
@change="$emit('tab-change', $event.target.value)" @change="$emit('tab-change', $event.target.value)"
> >
@@ -14,13 +14,17 @@
</div> </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 <button
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.key" :key="tab.key"
:class="[ :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', '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)" @click="$emit('tab-change', tab.key)"
> >

View File

@@ -3,6 +3,7 @@ import { createPinia } from 'pinia'
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs' import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import './assets/styles/main.css' import './assets/styles/main.css'

View 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
}
})

View File

@@ -3,8 +3,12 @@
<div class="card p-4 sm:p-6"> <div class="card p-4 sm:p-6">
<div class="mb-4 flex flex-col gap-4 sm:mb-6"> <div class="mb-4 flex flex-col gap-4 sm:mb-6">
<div> <div>
<h3 class="mb-1 text-lg font-bold text-gray-900 sm:mb-2 sm:text-xl">账户管理</h3> <h3 class="mb-1 text-lg font-bold text-gray-900 dark:text-gray-100 sm:mb-2 sm:text-xl">
<p class="text-sm text-gray-600 sm:text-base">管理您的 Claude Gemini 账户及代理配置</p> 账户管理
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">
管理您的 Claude Gemini 账户及代理配置
</p>
</div> </div>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<!-- 筛选器组 --> <!-- 筛选器组 -->
@@ -62,7 +66,7 @@
placement="bottom" placement="bottom"
> >
<button <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" :disabled="accountsLoading"
@click.ctrl.exact="loadAccounts(true)" @click.ctrl.exact="loadAccounts(true)"
@click.exact="loadAccounts(false)" @click.exact="loadAccounts(false)"
@@ -96,26 +100,26 @@
<div v-if="accountsLoading" class="py-12 text-center"> <div v-if="accountsLoading" class="py-12 text-center">
<div class="loading-spinner mx-auto mb-4" /> <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>
<div v-else-if="sortedAccounts.length === 0" class="py-12 text-center"> <div v-else-if="sortedAccounts.length === 0" class="py-12 text-center">
<div <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" /> <i class="fas fa-user-circle text-xl text-gray-400" />
</div> </div>
<p class="text-lg text-gray-500">暂无账户</p> <p class="text-lg text-gray-500 dark:text-gray-400">暂无账户</p>
<p class="mt-2 text-sm text-gray-400">点击上方按钮添加您的第一个账户</p> <p class="mt-2 text-sm text-gray-400 dark:text-gray-500">点击上方按钮添加您的第一个账户</p>
</div> </div>
<!-- 桌面端表格视图 --> <!-- 桌面端表格视图 -->
<div v-else class="table-container hidden md:block"> <div v-else class="table-container hidden md:block">
<table class="w-full table-fixed"> <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> <tr>
<th <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')" @click="sortAccounts('name')"
> >
名称 名称
@@ -130,7 +134,7 @@
<i v-else class="fas fa-sort ml-1 text-gray-400" /> <i v-else class="fas fa-sort ml-1 text-gray-400" />
</th> </th>
<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')" @click="sortAccounts('platform')"
> >
平台/类型 平台/类型
@@ -145,7 +149,7 @@
<i v-else class="fas fa-sort ml-1 text-gray-400" /> <i v-else class="fas fa-sort ml-1 text-gray-400" />
</th> </th>
<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')" @click="sortAccounts('status')"
> >
状态 状态
@@ -160,7 +164,7 @@
<i v-else class="fas fa-sort ml-1 text-gray-400" /> <i v-else class="fas fa-sort ml-1 text-gray-400" />
</th> </th>
<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')" @click="sortAccounts('priority')"
> >
优先级 优先级
@@ -175,33 +179,33 @@
<i v-else class="fas fa-sort ml-1 text-gray-400" /> <i v-else class="fas fa-sort ml-1 text-gray-400" />
</th> </th>
<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>
<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>
<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>
<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>
<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> </th>
</tr> </tr>
</thead> </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"> <tr v-for="account in sortedAccounts" :key="account.id" class="table-row">
<td class="px-3 py-4"> <td class="px-3 py-4">
<div class="flex items-center"> <div class="flex items-center">
@@ -213,7 +217,7 @@
<div class="min-w-0"> <div class="min-w-0">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div <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" :title="account.name"
> >
{{ account.name }} {{ account.name }}
@@ -238,13 +242,16 @@
</span> </span>
<span <span
v-if="account.groupInfo" 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}`" :title="`所属分组: ${account.groupInfo.name}`"
> >
<i class="fas fa-folder mr-1" />{{ account.groupInfo.name }} <i class="fas fa-folder mr-1" />{{ account.groupInfo.name }}
</span> </span>
</div> </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 }} {{ account.id }}
</div> </div>
</div> </div>
@@ -376,12 +383,15 @@
</span> </span>
<span <span
v-if="account.status === 'blocked' && account.errorMessage" 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" :title="account.errorMessage"
> >
{{ account.errorMessage }} {{ account.errorMessage }}
</span> </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 绑定: {{ account.boundApiKeysCount || 0 }} 个API Key
</span> </span>
</div> </div>
@@ -403,7 +413,7 @@
:style="{ width: 101 - (account.priority || 50) + '%' }" :style="{ width: 101 - (account.priority || 50) + '%' }"
/> />
</div> </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 }} {{ account.priority || 50 }}
</span> </span>
</div> </div>
@@ -425,19 +435,19 @@
<div v-if="account.usage && account.usage.daily" class="space-y-1"> <div v-if="account.usage && account.usage.daily" class="space-y-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="h-2 w-2 rounded-full bg-green-500" /> <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 >{{ account.usage.daily.requests || 0 }} 次</span
> >
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="h-2 w-2 rounded-full bg-blue-500" /> <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 >{{ formatNumber(account.usage.daily.allTokens || 0) }} tokens</span
> >
</div> </div>
<div <div
v-if="account.usage.averages && account.usage.averages.rpm > 0" 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 平均 {{ account.usage.averages.rpm.toFixed(2) }} RPM
</div> </div>
@@ -460,11 +470,11 @@
:style="{ width: account.sessionWindow.progress + '%' }" :style="{ width: account.sessionWindow.progress + '%' }"
/> />
</div> </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 }}% {{ account.sessionWindow.progress }}%
</span> </span>
</div> </div>
<div class="text-xs text-gray-600"> <div class="text-xs text-gray-600 dark:text-gray-300">
<div> <div>
{{ {{
formatSessionWindow( formatSessionWindow(
@@ -488,7 +498,7 @@
<span class="text-xs">N/A</span> <span class="text-xs">N/A</span>
</div> </div>
</td> </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) }} {{ formatLastUsed(account.lastUsedAt) }}
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm font-medium"> <td class="whitespace-nowrap px-3 py-4 text-sm font-medium">
@@ -590,9 +600,11 @@
{{ account.name || account.email }} {{ account.name || account.email }}
</h4> </h4>
<div class="mt-0.5 flex items-center gap-2"> <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-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> </div>
</div> </div>
@@ -612,20 +624,20 @@
<!-- 使用统计 --> <!-- 使用统计 -->
<div class="mb-3 grid grid-cols-2 gap-3"> <div class="mb-3 grid grid-cols-2 gap-3">
<div> <div>
<p class="text-xs text-gray-500">今日使用</p> <p class="text-xs text-gray-500 dark:text-gray-400">今日使用</p>
<p class="text-sm font-semibold text-gray-900"> <p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ formatNumber(account.usage?.daily?.requests || 0) }} 次 {{ formatNumber(account.usage?.daily?.requests || 0) }} 次
</p> </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 {{ formatNumber(account.usage?.daily?.allTokens || 0) }} tokens
</p> </p>
</div> </div>
<div> <div>
<p class="text-xs text-gray-500">总使用量</p> <p class="text-xs text-gray-500 dark:text-gray-400">总使用量</p>
<p class="text-sm font-semibold text-gray-900"> <p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ formatNumber(account.usage?.total?.requests || 0) }} 次 {{ formatNumber(account.usage?.total?.requests || 0) }} 次
</p> </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 {{ formatNumber(account.usage?.total?.allTokens || 0) }} tokens
</p> </p>
</div> </div>
@@ -640,22 +652,22 @@
account.sessionWindow && account.sessionWindow &&
account.sessionWindow.hasActiveWindow 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"> <div class="flex items-center justify-between text-xs">
<span class="font-medium text-gray-600">会话窗口</span> <span class="font-medium text-gray-600 dark:text-gray-300">会话窗口</span>
<span class="font-medium text-gray-700"> <span class="font-medium text-gray-700 dark:text-gray-200">
{{ account.sessionWindow.progress }}% {{ account.sessionWindow.progress }}%
</span> </span>
</div> </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 <div
class="h-full bg-gradient-to-r from-blue-500 to-indigo-600 transition-all duration-300" class="h-full bg-gradient-to-r from-blue-500 to-indigo-600 transition-all duration-300"
:style="{ width: account.sessionWindow.progress + '%' }" :style="{ width: account.sessionWindow.progress + '%' }"
/> />
</div> </div>
<div class="flex items-center justify-between text-xs"> <div class="flex items-center justify-between text-xs">
<span class="text-gray-500"> <span class="text-gray-500 dark:text-gray-400">
{{ {{
formatSessionWindow( formatSessionWindow(
account.sessionWindow.windowStart, account.sessionWindow.windowStart,
@@ -675,8 +687,8 @@
<!-- 最后使用时间 --> <!-- 最后使用时间 -->
<div class="flex items-center justify-between text-xs"> <div class="flex items-center justify-between text-xs">
<span class="text-gray-500">最后使用</span> <span class="text-gray-500 dark:text-gray-400">最后使用</span>
<span class="text-gray-700"> <span class="text-gray-700 dark:text-gray-200">
{{ account.lastUsedAt ? formatRelativeTime(account.lastUsedAt) : '从未使用' }} {{ account.lastUsedAt ? formatRelativeTime(account.lastUsedAt) : '从未使用' }}
</span> </span>
</div> </div>
@@ -686,16 +698,16 @@
v-if="account.proxyConfig && account.proxyConfig.type !== 'none'" v-if="account.proxyConfig && account.proxyConfig.type !== 'none'"
class="flex items-center justify-between text-xs" class="flex items-center justify-between text-xs"
> >
<span class="text-gray-500">代理</span> <span class="text-gray-500 dark:text-gray-400">代理</span>
<span class="text-gray-700"> <span class="text-gray-700 dark:text-gray-200">
{{ account.proxyConfig.type.toUpperCase() }} {{ account.proxyConfig.type.toUpperCase() }}
</span> </span>
</div> </div>
<!-- 调度优先级 --> <!-- 调度优先级 -->
<div class="flex items-center justify-between text-xs"> <div class="flex items-center justify-between text-xs">
<span class="text-gray-500">优先级</span> <span class="text-gray-500 dark:text-gray-400">优先级</span>
<span class="font-medium text-gray-700"> <span class="font-medium text-gray-700 dark:text-gray-200">
{{ account.priority || 50 }} {{ account.priority || 50 }}
</span> </span>
</div> </div>

View File

@@ -3,8 +3,12 @@
<div class="card p-4 sm:p-6"> <div class="card p-4 sm:p-6">
<div class="mb-4 flex flex-col gap-4 sm:mb-6"> <div class="mb-4 flex flex-col gap-4 sm:mb-6">
<div> <div>
<h3 class="mb-1 text-lg font-bold text-gray-900 sm:mb-2 sm:text-xl">API Keys 管理</h3> <h3 class="mb-1 text-lg font-bold text-gray-900 dark:text-gray-100 sm:mb-2 sm:text-xl">
<p class="text-sm text-gray-600 sm:text-base">管理和监控您的 API 密钥</p> API Keys 管理
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">
管理和监控您的 API 密钥
</p>
</div> </div>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<!-- 筛选器组 --> <!-- 筛选器组 -->
@@ -55,7 +59,7 @@
<div class="relative flex items-center"> <div class="relative flex items-center">
<input <input
v-model="searchKeyword" 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="搜索名称..." placeholder="搜索名称..."
type="text" type="text"
@input="currentPage = 1" @input="currentPage = 1"
@@ -63,7 +67,7 @@
<i class="fas fa-search absolute left-3 text-sm text-cyan-500" /> <i class="fas fa-search absolute left-3 text-sm text-cyan-500" />
<button <button
v-if="searchKeyword" 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" @click="clearSearch"
> >
<i class="fas fa-times text-xs" /> <i class="fas fa-times text-xs" />
@@ -73,7 +77,7 @@
<!-- 刷新按钮 --> <!-- 刷新按钮 -->
<button <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" :disabled="apiKeysLoading"
@click="loadApiKeys()" @click="loadApiKeys()"
> >
@@ -131,7 +135,7 @@
<!-- 桌面端表格视图 --> <!-- 桌面端表格视图 -->
<div v-else class="table-container hidden md:block"> <div v-else class="table-container hidden md:block">
<table class="w-full table-fixed"> <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> <tr>
<th class="w-[50px] px-3 py-4 text-left"> <th class="w-[50px] px-3 py-4 text-left">
<div class="flex items-center"> <div class="flex items-center">
@@ -145,7 +149,7 @@
</div> </div>
</th> </th>
<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')" @click="sortApiKeys('name')"
> >
名称 名称
@@ -160,12 +164,12 @@
<i v-else class="fas fa-sort ml-1 text-gray-400" /> <i v-else class="fas fa-sort ml-1 text-gray-400" />
</th> </th>
<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>
<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')" @click="sortApiKeys('status')"
> >
状态 状态
@@ -180,11 +184,11 @@
<i v-else class="fas fa-sort ml-1 text-gray-400" /> <i v-else class="fas fa-sort ml-1 text-gray-400" />
</th> </th>
<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 <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')" @click="sortApiKeys('cost')"
> >
(费用 (费用
@@ -200,7 +204,7 @@
</span> </span>
</th> </th>
<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')" @click="sortApiKeys('createdAt')"
> >
创建时间 创建时间
@@ -215,7 +219,7 @@
<i v-else class="fas fa-sort ml-1 text-gray-400" /> <i v-else class="fas fa-sort ml-1 text-gray-400" />
</th> </th>
<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')" @click="sortApiKeys('expiresAt')"
> >
过期时间 过期时间
@@ -230,13 +234,13 @@
<i v-else class="fas fa-sort ml-1 text-gray-400" /> <i v-else class="fas fa-sort ml-1 text-gray-400" />
</th> </th>
<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> </th>
</tr> </tr>
</thead> </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"> <template v-for="key in paginatedApiKeys" :key="key.id">
<!-- API Key 主行 --> <!-- API Key 主行 -->
<tr class="table-row"> <tr class="table-row">
@@ -259,10 +263,16 @@
<i class="fas fa-key text-xs text-white" /> <i class="fas fa-key text-xs text-white" />
</div> </div>
<div class="min-w-0"> <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 }} {{ key.name }}
</div> </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 }} {{ key.id }}
</div> </div>
<!-- 账户绑定信息 --> <!-- 账户绑定信息 -->
@@ -278,7 +288,7 @@
<i class="fas fa-brain mr-1 text-[10px]" /> <i class="fas fa-brain mr-1 text-[10px]" />
Claude Claude
</span> </span>
<span class="truncate text-gray-600"> <span class="truncate text-gray-600 dark:text-gray-400">
{{ getClaudeBindingInfo(key) }} {{ getClaudeBindingInfo(key) }}
</span> </span>
</div> </div>
@@ -290,7 +300,7 @@
<i class="fas fa-robot mr-1 text-[10px]" /> <i class="fas fa-robot mr-1 text-[10px]" />
Gemini Gemini
</span> </span>
<span class="truncate text-gray-600"> <span class="truncate text-gray-600 dark:text-gray-400">
{{ getGeminiBindingInfo(key) }} {{ getGeminiBindingInfo(key) }}
</span> </span>
</div> </div>
@@ -302,7 +312,7 @@
<i class="fa-openai mr-1 text-[10px]" /> <i class="fa-openai mr-1 text-[10px]" />
OpenAI OpenAI
</span> </span>
<span class="truncate text-gray-600"> <span class="truncate text-gray-600 dark:text-gray-400">
{{ getOpenAIBindingInfo(key) }} {{ getOpenAIBindingInfo(key) }}
</span> </span>
</div> </div>
@@ -358,20 +368,20 @@
<!-- 今日使用统计 --> <!-- 今日使用统计 -->
<div class="mb-2"> <div class="mb-2">
<div class="mb-1 flex items-center justify-between text-sm"> <div class="mb-1 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-gray-900" <span class="font-semibold text-gray-900 dark:text-gray-100"
>{{ formatNumber(key.usage?.daily?.requests || 0) }}次</span >{{ formatNumber(key.usage?.daily?.requests || 0) }}次</span
> >
</div> </div>
<div class="flex items-center justify-between text-sm"> <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" <span class="font-semibold text-green-600"
>${{ (key.dailyCost || 0).toFixed(4) }}</span >${{ (key.dailyCost || 0).toFixed(4) }}</span
> >
</div> </div>
<div class="flex items-center justify-between text-sm"> <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-medium text-gray-700">{{ <span class="font-medium text-gray-700 dark:text-gray-300">{{
formatLastUsed(key.lastUsedAt) formatLastUsed(key.lastUsedAt)
}}</span> }}</span>
</div> </div>
@@ -380,8 +390,8 @@
<!-- 每日费用限制进度条 --> <!-- 每日费用限制进度条 -->
<div v-if="key.dailyCostLimit > 0" class="space-y-1"> <div v-if="key.dailyCostLimit > 0" class="space-y-1">
<div class="flex items-center justify-between text-xs"> <div class="flex items-center justify-between text-xs">
<span class="text-gray-500">费用限额</span> <span class="text-gray-500 dark:text-gray-400">费用限额</span>
<span class="text-gray-700"> <span class="text-gray-700 dark:text-gray-300">
${{ (key.dailyCost || 0).toFixed(2) }} / ${{ ${{ (key.dailyCost || 0).toFixed(2) }} / ${{
key.dailyCostLimit.toFixed(2) key.dailyCostLimit.toFixed(2)
}} }}
@@ -414,7 +424,7 @@
<!-- 查看详情按钮 --> <!-- 查看详情按钮 -->
<div class="pt-1"> <div class="pt-1">
<button <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)" @click="showUsageDetails(key)"
> >
<i class="fas fa-chart-line" /> <i class="fas fa-chart-line" />
@@ -540,7 +550,7 @@
<!-- 模型统计展开区域 --> <!-- 模型统计展开区域 -->
<tr v-if="key && key.id && expandedApiKeys[key.id]"> <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 v-if="!apiKeyModelStats[key.id]" class="py-4 text-center">
<div class="loading-spinner mx-auto" /> <div class="loading-spinner mx-auto" />
<p class="mt-2 text-sm text-gray-500">加载模型统计...</p> <p class="mt-2 text-sm text-gray-500">加载模型统计...</p>
@@ -548,14 +558,16 @@
<div class="space-y-4"> <div class="space-y-4">
<!-- 通用的标题和时间筛选器,无论是否有数据都显示 --> <!-- 通用的标题和时间筛选器,无论是否有数据都显示 -->
<div class="mb-4 flex items-center justify-between"> <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" /> <i class="fas fa-chart-pie mr-2 text-indigo-500" />
模型使用分布 模型使用分布
</h5> </h5>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span <span
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0" 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 }} 个模型 {{ apiKeyModelStats[key.id].length }} 个模型
</span> </span>
@@ -563,7 +575,7 @@
<!-- API Keys日期筛选器 --> <!-- API Keys日期筛选器 -->
<div class="flex items-center gap-1"> <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 <button
v-for="option in getApiKeyDateFilter(key.id).presetOptions" v-for="option in getApiKeyDateFilter(key.id).presetOptions"
:key="option.value" :key="option.value"
@@ -571,8 +583,8 @@
'rounded px-2 py-1 text-xs font-medium transition-colors', 'rounded px-2 py-1 text-xs font-medium transition-colors',
getApiKeyDateFilter(key.id).preset === option.value && getApiKeyDateFilter(key.id).preset === option.value &&
getApiKeyDateFilter(key.id).type === 'preset' getApiKeyDateFilter(key.id).type === 'preset'
? 'bg-white text-blue-600 shadow-sm' ? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
]" ]"
@click="setApiKeyDateFilterPreset(option.value, key.id)" @click="setApiKeyDateFilterPreset(option.value, key.id)"
> >
@@ -630,14 +642,16 @@
<div <div
v-for="stat in apiKeyModelStats[key.id]" v-for="stat in apiKeyModelStats[key.id]"
:key="stat.model" :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="mb-3 flex items-start justify-between">
<div class="flex-1"> <div class="flex-1">
<span class="mb-1 block text-sm font-semibold text-gray-800">{{ <span
stat.model class="mb-1 block text-sm font-semibold text-gray-800 dark:text-gray-200"
}}</span> >{{ stat.model }}</span
<span class="rounded-full bg-blue-50 px-2 py-1 text-xs text-gray-500" >
<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 >{{ stat.requests }} 次请求</span
> >
</div> </div>
@@ -645,16 +659,16 @@
<div class="mb-3 space-y-2"> <div class="mb-3 space-y-2">
<div class="flex items-center justify-between text-sm"> <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" /> <i class="fas fa-coins mr-1 text-xs text-yellow-500" />
总Token: 总Token:
</span> </span>
<span class="font-semibold text-gray-900">{{ <span class="font-semibold text-gray-900 dark:text-gray-100">{{
formatTokenCount(stat.allTokens) formatTokenCount(stat.allTokens)
}}</span> }}</span>
</div> </div>
<div class="flex items-center justify-between text-sm"> <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" /> <i class="fas fa-dollar-sign mr-1 text-xs text-green-500" />
费用: 费用:
</span> </span>
@@ -662,8 +676,10 @@
calculateModelCost(stat) calculateModelCost(stat)
}}</span> }}</span>
</div> </div>
<div class="mt-2 border-t border-gray-100 pt-2"> <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"> <div
class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"
>
<span class="flex items-center"> <span class="flex items-center">
<i class="fas fa-arrow-down mr-1 text-green-500" /> <i class="fas fa-arrow-down mr-1 text-green-500" />
输入: 输入:
@@ -672,7 +688,9 @@
formatTokenCount(stat.inputTokens) formatTokenCount(stat.inputTokens)
}}</span> }}</span>
</div> </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"> <span class="flex items-center">
<i class="fas fa-arrow-up mr-1 text-blue-500" /> <i class="fas fa-arrow-up mr-1 text-blue-500" />
输出: 输出:
@@ -737,23 +755,25 @@
<!-- 总计统计,仅在有数据时显示 --> <!-- 总计统计,仅在有数据时显示 -->
<div <div
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0" 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"> <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" /> <i class="fas fa-calculator mr-2 text-indigo-500" />
总计统计 总计统计
</span> </span>
<div class="flex gap-4 text-xs"> <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) apiKeyModelStats[key.id].reduce((sum, stat) => sum + stat.requests, 0)
}}</span> }}</span>
</span> </span>
<span class="text-gray-600"> <span class="text-gray-600 dark:text-gray-400">
总Token: 总Token:
<span class="font-semibold text-gray-800">{{ <span class="font-semibold text-gray-800 dark:text-gray-200">{{
formatTokenCount( formatTokenCount(
apiKeyModelStats[key.id].reduce( apiKeyModelStats[key.id].reduce(
(sum, stat) => sum + stat.allTokens, (sum, stat) => sum + stat.allTokens,
@@ -796,10 +816,10 @@
<i class="fas fa-key text-sm text-white" /> <i class="fas fa-key text-sm text-white" />
</div> </div>
<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 }} {{ key.name }}
</h4> </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 }} {{ key.id }}
</p> </p>
</div> </div>
@@ -889,21 +909,21 @@
</div> </div>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<div> <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) }} 次 {{ formatNumber(key.usage?.daily?.requests || 0) }} 次
</p> </p>
<p class="text-xs text-gray-500">请求</p> <p class="text-xs text-gray-500 dark:text-gray-400">请求</p>
</div> </div>
<div> <div>
<p class="text-sm font-semibold text-green-600"> <p class="text-sm font-semibold text-green-600">
${{ (key.dailyCost || 0).toFixed(4) }} ${{ (key.dailyCost || 0).toFixed(4) }}
</p> </p>
<p class="text-xs text-gray-500">费用</p> <p class="text-xs text-gray-500 dark:text-gray-400">费用</p>
</div> </div>
</div> </div>
<div class="mt-2 flex items-center justify-between"> <div class="mt-2 flex items-center justify-between">
<span class="text-xs text-gray-600">最后使用</span> <span class="text-xs text-gray-600 dark:text-gray-400">最后使用</span>
<span class="text-xs font-medium text-gray-700">{{ <span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
formatLastUsed(key.lastUsedAt) formatLastUsed(key.lastUsedAt)
}}</span> }}</span>
</div> </div>
@@ -1042,28 +1062,28 @@
class="mt-4 flex flex-col items-center justify-between gap-4 sm:mt-6 sm:flex-row" 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"> <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 }} 条记录 共 {{ sortedApiKeys.length }} 条记录
</span> </span>
<div class="flex items-center gap-2"> <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 <select
v-model="pageSize" 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" @change="currentPage = 1"
> >
<option v-for="size in pageSizeOptions" :key="size" :value="size"> <option v-for="size in pageSizeOptions" :key="size" :value="size">
{{ size }} {{ size }}
</option> </option>
</select> </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> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- 上一页 --> <!-- 上一页 -->
<button <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" :disabled="currentPage === 1"
@click="currentPage--" @click="currentPage--"
> >
@@ -1075,12 +1095,16 @@
<!-- 第一页 --> <!-- 第一页 -->
<button <button
v-if="currentPage > 3" 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" @click="currentPage = 1"
> >
1 1
</button> </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 <button
@@ -1090,7 +1114,7 @@
'rounded-md px-2 py-1 text-xs font-medium sm:px-3 sm:text-sm', 'rounded-md px-2 py-1 text-xs font-medium sm:px-3 sm:text-sm',
page === currentPage page === currentPage
? 'bg-blue-600 text-white' ? '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" @click="currentPage = page"
> >
@@ -1098,12 +1122,14 @@
</button> </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 >...</span
> >
<button <button
v-if="totalPages > 1 && currentPage < totalPages - 2" 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" @click="currentPage = totalPages"
> >
{{ totalPages }} {{ totalPages }}
@@ -1112,7 +1138,7 @@
<!-- 下一页 --> <!-- 下一页 -->
<button <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" :disabled="currentPage === totalPages || totalPages === 0"
@click="currentPage++" @click="currentPage++"
> >

View File

@@ -1,5 +1,5 @@
<template> <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="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"> <div class="flex flex-col items-center justify-between gap-4 md:flex-row">
@@ -9,13 +9,24 @@
:subtitle="currentTab === 'stats' ? 'API Key 使用统计' : '使用教程'" :subtitle="currentTab === 'stats' ? 'API Key 使用统计' : '使用教程'"
:title="oemSettings.siteName" :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 <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" to="/dashboard"
> >
<i class="fas fa-cog text-sm" /> <i class="fas fa-shield-alt text-sm md:text-base" />
<span class="text-xs font-medium md:text-sm">管理后台</span> <span class="text-xs font-semibold tracking-wide md:text-sm">管理后台</span>
</router-link> </router-link>
</div> </div>
</div> </div>
@@ -53,7 +64,7 @@
<!-- 错误提示 --> <!-- 错误提示 -->
<div v-if="error" class="mb-6 md:mb-8"> <div v-if="error" class="mb-6 md:mb-8">
<div <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" /> <i class="fas fa-exclamation-triangle mr-2" />
{{ error }} {{ error }}
@@ -64,13 +75,15 @@
<div v-if="statsData" class="fade-in"> <div v-if="statsData" class="fade-in">
<div class="glass-strong rounded-3xl p-4 shadow-xl md:p-6"> <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 <div
class="flex flex-col items-start justify-between gap-3 md:flex-row md:items-center md:gap-4" 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"> <div class="flex items-center gap-2 md:gap-3">
<i class="fas fa-clock text-base text-blue-500 md:text-lg" /> <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>
<div class="flex w-full gap-2 md:w-auto"> <div class="flex w-full gap-2 md:w-auto">
<button <button
@@ -120,11 +133,13 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue' import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useApiStatsStore } from '@/stores/apistats' import { useApiStatsStore } from '@/stores/apistats'
import { useThemeStore } from '@/stores/theme'
import LogoTitle from '@/components/common/LogoTitle.vue' import LogoTitle from '@/components/common/LogoTitle.vue'
import ThemeToggle from '@/components/common/ThemeToggle.vue'
import ApiKeyInput from '@/components/apistats/ApiKeyInput.vue' import ApiKeyInput from '@/components/apistats/ApiKeyInput.vue'
import StatsOverview from '@/components/apistats/StatsOverview.vue' import StatsOverview from '@/components/apistats/StatsOverview.vue'
import TokenDistribution from '@/components/apistats/TokenDistribution.vue' import TokenDistribution from '@/components/apistats/TokenDistribution.vue'
@@ -134,10 +149,14 @@ import TutorialView from './TutorialView.vue'
const route = useRoute() const route = useRoute()
const apiStatsStore = useApiStatsStore() const apiStatsStore = useApiStatsStore()
const themeStore = useThemeStore()
// 当前标签页 // 当前标签页
const currentTab = ref('stats') const currentTab = ref('stats')
// 主题相关
const isDarkMode = computed(() => themeStore.isDarkMode)
const { const {
apiKey, apiKey,
apiId, apiId,
@@ -172,6 +191,9 @@ const handleKeyDown = (event) => {
onMounted(() => { onMounted(() => {
console.log('API Stats Page loaded') console.log('API Stats Page loaded')
// 初始化主题(因为该页面不在 MainLayout 内)
themeStore.initTheme()
// 加载 OEM 设置 // 加载 OEM 设置
loadOemSettings() loadOemSettings()
@@ -217,6 +239,14 @@ watch(apiKey, (newValue) => {
position: relative; 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 { .gradient-bg::before {
content: ''; content: '';
position: fixed; position: fixed;
@@ -232,11 +262,27 @@ watch(apiKey, (newValue) => {
z-index: 0; 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 { .glass-strong {
background: rgba(255, 255, 255, 0.95); background: var(--glass-strong-color);
backdrop-filter: blur(25px); backdrop-filter: blur(25px);
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid var(--border-color);
box-shadow: box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.05), 0 0 0 1px rgba(255, 255, 255, 0.05),
@@ -245,6 +291,14 @@ watch(apiKey, (newValue) => {
z-index: 1; 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 { .header-title {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@@ -255,38 +309,76 @@ watch(apiKey, (newValue) => {
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
/* 管理后台按钮 */ /* 管理后台按钮 - 精致版本 */
.admin-button { .admin-button-refined {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 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; text-decoration: none;
box-shadow: box-shadow:
0 4px 6px -1px rgba(102, 126, 234, 0.3), 0 4px 12px rgba(102, 126, 234, 0.25),
0 2px 4px -1px rgba(102, 126, 234, 0.1); inset 0 1px 1px rgba(255, 255, 255, 0.2);
position: relative; position: relative;
overflow: hidden; 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: ''; content: '';
position: absolute; position: absolute;
top: 0; top: 0;
left: -100%; left: 0;
width: 100%; right: 0;
height: 100%; bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
transition: left 0.5s; opacity: 0;
transition: opacity 0.3s ease;
} }
.admin-button:hover { .admin-button-refined:hover {
transform: translateY(-2px); transform: translateY(-2px) scale(1.02);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
box-shadow: box-shadow:
0 10px 15px -3px rgba(102, 126, 234, 0.4), 0 8px 20px rgba(118, 75, 162, 0.35),
0 4px 6px -2px rgba(102, 126, 234, 0.15); inset 0 1px 1px rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.4);
color: white;
} }
.admin-button:hover::before { .admin-button-refined:hover::before {
left: 100%; 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) { .period-btn:not(.active) {
color: #374151; 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 { .period-btn:not(.active):hover {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.8);
color: #1f2937; 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 胶囊按钮样式 */ /* Tab 胶囊按钮样式 */
@@ -339,6 +445,11 @@ watch(apiKey, (newValue) => {
justify-content: center; justify-content: center;
} }
/* 暗夜模式下的Tab按钮基础样式 */
:global(html.dark) .tab-pill-button {
color: rgba(209, 213, 219, 0.8);
}
@media (min-width: 768px) { @media (min-width: 768px) {
.tab-pill-button { .tab-pill-button {
padding: 0.625rem 1.25rem; padding: 0.625rem 1.25rem;
@@ -351,6 +462,11 @@ watch(apiKey, (newValue) => {
background: rgba(255, 255, 255, 0.1); 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 { .tab-pill-button.active {
background: white; background: white;
color: #764ba2; color: #764ba2;
@@ -359,6 +475,14 @@ watch(apiKey, (newValue) => {
0 2px 4px -1px rgba(0, 0, 0, 0.06); 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 { .tab-pill-button i {
font-size: 0.875rem; font-size: 0.875rem;
} }

View File

@@ -7,11 +7,15 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="mb-1 text-xs font-semibold text-gray-600 sm:text-sm">总API Keys</p> <p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
<p class="text-2xl font-bold text-gray-900 sm:text-3xl"> 总API Keys
</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100 sm:text-3xl">
{{ dashboardData.totalApiKeys }} {{ dashboardData.totalApiKeys }}
</p> </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>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-blue-500 to-blue-600"> <div class="stat-icon flex-shrink-0 bg-gradient-to-br from-blue-500 to-blue-600">
<i class="fas fa-key" /> <i class="fas fa-key" />
@@ -22,9 +26,11 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex-1"> <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"> <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 }} {{ dashboardData.totalAccounts }}
</p> </p>
<!-- 各平台账户数量展示 --> <!-- 各平台账户数量展示 -->
@@ -39,7 +45,7 @@
:title="`Claude: ${dashboardData.accountsByPlatform.claude.total} 个 (正常: ${dashboardData.accountsByPlatform.claude.normal})`" :title="`Claude: ${dashboardData.accountsByPlatform.claude.total} 个 (正常: ${dashboardData.accountsByPlatform.claude.normal})`"
> >
<i class="fas fa-brain text-xs text-indigo-600" /> <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 dashboardData.accountsByPlatform.claude.total
}}</span> }}</span>
</div> </div>
@@ -53,7 +59,7 @@
:title="`Console: ${dashboardData.accountsByPlatform['claude-console'].total} 个 (正常: ${dashboardData.accountsByPlatform['claude-console'].normal})`" :title="`Console: ${dashboardData.accountsByPlatform['claude-console'].total} 个 (正常: ${dashboardData.accountsByPlatform['claude-console'].normal})`"
> >
<i class="fas fa-terminal text-xs text-purple-600" /> <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 dashboardData.accountsByPlatform['claude-console'].total
}}</span> }}</span>
</div> </div>
@@ -67,7 +73,7 @@
:title="`Gemini: ${dashboardData.accountsByPlatform.gemini.total} 个 (正常: ${dashboardData.accountsByPlatform.gemini.normal})`" :title="`Gemini: ${dashboardData.accountsByPlatform.gemini.total} 个 (正常: ${dashboardData.accountsByPlatform.gemini.normal})`"
> >
<i class="fas fa-robot text-xs text-yellow-600" /> <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 dashboardData.accountsByPlatform.gemini.total
}}</span> }}</span>
</div> </div>
@@ -81,7 +87,7 @@
:title="`Bedrock: ${dashboardData.accountsByPlatform.bedrock.total} 个 (正常: ${dashboardData.accountsByPlatform.bedrock.normal})`" :title="`Bedrock: ${dashboardData.accountsByPlatform.bedrock.total} 个 (正常: ${dashboardData.accountsByPlatform.bedrock.normal})`"
> >
<i class="fab fa-aws text-xs text-orange-600" /> <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 dashboardData.accountsByPlatform.bedrock.total
}}</span> }}</span>
</div> </div>
@@ -95,18 +101,21 @@
:title="`OpenAI: ${dashboardData.accountsByPlatform.openai.total} 个 (正常: ${dashboardData.accountsByPlatform.openai.normal})`" :title="`OpenAI: ${dashboardData.accountsByPlatform.openai.total} 个 (正常: ${dashboardData.accountsByPlatform.openai.normal})`"
> >
<i class="fas fa-openai text-xs text-gray-100" /> <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 dashboardData.accountsByPlatform.openai.total
}}</span> }}</span>
</div> </div>
</div> </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 }} 正常: {{ dashboardData.normalAccounts || 0 }}
<span v-if="dashboardData.abnormalAccounts > 0" class="text-red-600"> <span v-if="dashboardData.abnormalAccounts > 0" class="text-red-600">
| 异常: {{ dashboardData.abnormalAccounts }} | 异常: {{ dashboardData.abnormalAccounts }}
</span> </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 }} | 停止调度: {{ dashboardData.pausedAccounts }}
</span> </span>
<span v-if="dashboardData.rateLimitedAccounts > 0" class="text-yellow-600"> <span v-if="dashboardData.rateLimitedAccounts > 0" class="text-yellow-600">
@@ -123,11 +132,13 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <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 class="text-2xl font-bold text-gray-900 sm:text-3xl"> 今日请求
</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100 sm:text-3xl">
{{ dashboardData.todayRequests }} {{ dashboardData.todayRequests }}
</p> </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) }} 总请求: {{ formatNumber(dashboardData.totalRequests || 0) }}
</p> </p>
</div> </div>
@@ -140,11 +151,15 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <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"> <p class="text-2xl font-bold text-green-600 sm:text-3xl">
{{ dashboardData.systemStatus }} {{ dashboardData.systemStatus }}
</p> </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>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-yellow-500 to-orange-500"> <div class="stat-icon flex-shrink-0 bg-gradient-to-br from-yellow-500 to-orange-500">
<i class="fas fa-heartbeat" /> <i class="fas fa-heartbeat" />
@@ -160,7 +175,9 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="mr-8 flex-1"> <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"> <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"> <p class="text-xl font-bold text-blue-600 sm:text-2xl md:text-3xl">
{{ {{
@@ -176,7 +193,7 @@
>/ {{ costsData.todayCosts.formatted.totalCost }}</span >/ {{ costsData.todayCosts.formatted.totalCost }}</span
> >
</div> </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"> <div class="flex flex-wrap items-center justify-between gap-x-4">
<span <span
>输入: >输入:
@@ -214,7 +231,9 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="mr-8 flex-1"> <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"> <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"> <p class="text-xl font-bold text-emerald-600 sm:text-2xl md:text-3xl">
{{ {{
@@ -230,7 +249,7 @@
>/ {{ costsData.totalCosts.formatted.totalCost }}</span >/ {{ costsData.totalCosts.formatted.totalCost }}</span
> >
</div> </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"> <div class="flex flex-wrap items-center justify-between gap-x-4">
<span <span
>输入: >输入:
@@ -268,14 +287,14 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <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 实时RPM
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span> <span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span>
</p> </p>
<p class="text-2xl font-bold text-orange-600 sm:text-3xl"> <p class="text-2xl font-bold text-orange-600 sm:text-3xl">
{{ dashboardData.realtimeRPM || 0 }} {{ dashboardData.realtimeRPM || 0 }}
</p> </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"> <span v-if="dashboardData.isHistoricalMetrics" class="text-yellow-600">
<i class="fas fa-exclamation-circle" /> 历史数据 <i class="fas fa-exclamation-circle" /> 历史数据
@@ -291,14 +310,14 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <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 实时TPM
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span> <span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}分钟)</span>
</p> </p>
<p class="text-2xl font-bold text-rose-600 sm:text-3xl"> <p class="text-2xl font-bold text-rose-600 sm:text-3xl">
{{ formatNumber(dashboardData.realtimeTPM || 0) }} {{ formatNumber(dashboardData.realtimeTPM || 0) }}
</p> </p>
<p class="mt-1 text-xs text-gray-500"> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
每分钟Token数 每分钟Token数
<span v-if="dashboardData.isHistoricalMetrics" class="text-yellow-600"> <span v-if="dashboardData.isHistoricalMetrics" class="text-yellow-600">
<i class="fas fa-exclamation-circle" /> 历史数据 <i class="fas fa-exclamation-circle" /> 历史数据
@@ -315,18 +334,22 @@
<!-- 模型消费统计 --> <!-- 模型消费统计 -->
<div class="mb-8"> <div class="mb-8">
<div class="mb-4 flex flex-col gap-4 sm:mb-6"> <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-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 <button
v-for="option in dateFilter.presetOptions" v-for="option in dateFilter.presetOptions"
:key="option.value" :key="option.value"
:class="[ :class="[
'rounded-md px-3 py-1 text-sm font-medium transition-colors', 'rounded-md px-3 py-1 text-sm font-medium transition-colors',
dateFilter.preset === option.value && dateFilter.type === 'preset' dateFilter.preset === option.value && dateFilter.type === 'preset'
? 'bg-white text-blue-600 shadow-sm' ? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100'
]" ]"
@click="setDateFilterPreset(option.value)" @click="setDateFilterPreset(option.value)"
> >
@@ -335,13 +358,13 @@
</div> </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 <button
:class="[ :class="[
'rounded-md px-3 py-1 text-sm font-medium transition-colors', 'rounded-md px-3 py-1 text-sm font-medium transition-colors',
trendGranularity === 'day' trendGranularity === 'day'
? 'bg-white text-blue-600 shadow-sm' ? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100'
]" ]"
@click="setTrendGranularity('day')" @click="setTrendGranularity('day')"
> >
@@ -351,8 +374,8 @@
:class="[ :class="[
'rounded-md px-3 py-1 text-sm font-medium transition-colors', 'rounded-md px-3 py-1 text-sm font-medium transition-colors',
trendGranularity === 'hour' trendGranularity === 'hour'
? 'bg-white text-blue-600 shadow-sm' ? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100'
]" ]"
@click="setTrendGranularity('hour')" @click="setTrendGranularity('hour')"
> >
@@ -385,17 +408,17 @@
<!-- 刷新控制 --> <!-- 刷新控制 -->
<div class="flex items-center gap-2"> <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"> <label class="relative inline-flex cursor-pointer items-center">
<input v-model="autoRefreshEnabled" class="peer sr-only" type="checkbox" /> <input v-model="autoRefreshEnabled" class="peer sr-only" type="checkbox" />
<!-- 更小的开关 --> <!-- 更小的开关 -->
<div <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 <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>自动刷新</span>
<span <span
v-if="autoRefreshEnabled" v-if="autoRefreshEnabled"
@@ -410,7 +433,7 @@
<!-- 刷新按钮 --> <!-- 刷新按钮 -->
<button <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" :disabled="isRefreshing"
title="立即刷新数据" title="立即刷新数据"
@click="refreshAllData()" @click="refreshAllData()"
@@ -425,7 +448,9 @@
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2"> <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<!-- 饼图 --> <!-- 饼图 -->
<div class="card p-4 sm:p-6"> <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"> <div class="relative" style="height: 250px">
<canvas ref="modelUsageChart" /> <canvas ref="modelUsageChart" />
</div> </div>
@@ -433,48 +458,62 @@
<!-- 详细数据表格 --> <!-- 详细数据表格 -->
<div class="card p-4 sm:p-6"> <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"> <div v-if="dashboardModelStats.length === 0" class="py-8 text-center">
<p class="text-sm text-gray-500 sm:text-base">暂无模型使用数据</p> <p class="text-sm text-gray-500 sm:text-base">暂无模型使用数据</p>
</div> </div>
<div v-else class="max-h-[250px] overflow-auto sm:max-h-[300px]"> <div v-else class="max-h-[250px] overflow-auto sm:max-h-[300px]">
<table class="min-w-full"> <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> <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>
<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>
<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 总Token
</th> </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>
<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>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <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"> <tr
<td class="px-2 py-2 text-xs text-gray-900 sm:px-4 sm:text-sm"> 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"> <span class="block max-w-[100px] truncate sm:max-w-none" :title="stat.model">
{{ stat.model }} {{ stat.model }}
</span> </span>
</td> </td>
<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) }} {{ formatNumber(stat.requests) }}
</td> </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) }} {{ formatNumber(stat.allTokens) }}
</td> </td>
<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" class="hidden px-2 py-2 text-right text-xs font-medium sm:table-cell sm:px-4 sm:text-sm"
> >
<span <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) }}% {{ calculatePercentage(stat.allTokens, dashboardModelStats) }}%
</span> </span>
@@ -512,15 +551,17 @@
<div class="mb-4 sm:mb-6 md:mb-8"> <div class="mb-4 sm:mb-6 md:mb-8">
<div class="card p-4 sm:p-6"> <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"> <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 <button
:class="[ :class="[
'rounded-md px-2 py-1 text-xs font-medium transition-colors sm:px-3 sm:text-sm', 'rounded-md px-2 py-1 text-xs font-medium transition-colors sm:px-3 sm:text-sm',
apiKeysTrendMetric === 'requests' apiKeysTrendMetric === 'requests'
? 'bg-white text-blue-600 shadow-sm' ? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100'
]" ]"
@click="((apiKeysTrendMetric = 'requests'), updateApiKeysUsageTrendChart())" @click="((apiKeysTrendMetric = 'requests'), updateApiKeysUsageTrendChart())"
> >
@@ -531,8 +572,8 @@
:class="[ :class="[
'rounded-md px-2 py-1 text-xs font-medium transition-colors sm:px-3 sm:text-sm', 'rounded-md px-2 py-1 text-xs font-medium transition-colors sm:px-3 sm:text-sm',
apiKeysTrendMetric === 'tokens' apiKeysTrendMetric === 'tokens'
? 'bg-white text-blue-600 shadow-sm' ? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
: 'text-gray-600 hover:text-gray-900' : 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100'
]" ]"
@click="((apiKeysTrendMetric = 'tokens'), updateApiKeysUsageTrendChart())" @click="((apiKeysTrendMetric = 'tokens'), updateApiKeysUsageTrendChart())"
> >
@@ -541,7 +582,7 @@
</button> </button>
</div> </div>
</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"> <span v-if="apiKeysTrendData.totalApiKeys > 10">
共 {{ apiKeysTrendData.totalApiKeys }} 个 API Key显示使用量前 10 个 共 {{ apiKeysTrendData.totalApiKeys }} 个 API Key显示使用量前 10 个
</span> </span>
@@ -556,12 +597,16 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue' import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useDashboardStore } from '@/stores/dashboard' import { useDashboardStore } from '@/stores/dashboard'
import { useThemeStore } from '@/stores/theme'
import Chart from 'chart.js/auto' import Chart from 'chart.js/auto'
const dashboardStore = useDashboardStore() const dashboardStore = useDashboardStore()
const themeStore = useThemeStore()
const { isDarkMode } = storeToRefs(themeStore)
const { const {
dashboardData, dashboardData,
costsData, costsData,
@@ -607,6 +652,13 @@ const isRefreshing = ref(false)
// return `${refreshCountdown.value}秒后刷新` // 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) { function formatNumber(num) {
if (num >= 1000000) { if (num >= 1000000) {
@@ -670,7 +722,8 @@ function createModelUsageChart() {
usePointStyle: true, usePointStyle: true,
font: { font: {
size: 12 size: 12
} },
color: chartColors.value.legend
} }
}, },
tooltip: { tooltip: {
@@ -800,10 +853,14 @@ function createUsageTrendChart() {
font: { font: {
size: 16, size: 16,
weight: 'bold' weight: 'bold'
} },
color: chartColors.value.text
}, },
legend: { legend: {
position: 'top' position: 'top',
labels: {
color: chartColors.value.legend
}
}, },
tooltip: { tooltip: {
mode: 'index', mode: 'index',
@@ -858,7 +915,14 @@ function createUsageTrendChart() {
display: true, display: true,
title: { title: {
display: true, display: true,
text: trendGranularity.value === 'hour' ? '时间' : '日期' text: trendGranularity === 'hour' ? '时间' : '日期',
color: chartColors.value.text
},
ticks: {
color: chartColors.value.text
},
grid: {
color: chartColors.value.grid
} }
}, },
y: { y: {
@@ -867,12 +931,17 @@ function createUsageTrendChart() {
position: 'left', position: 'left',
title: { title: {
display: true, display: true,
text: 'Token数量' text: 'Token数量',
color: chartColors.value.text
}, },
ticks: { ticks: {
callback: function (value) { callback: function (value) {
return formatNumber(value) return formatNumber(value)
} },
color: chartColors.value.text
},
grid: {
color: chartColors.value.grid
} }
}, },
y1: { y1: {
@@ -881,7 +950,8 @@ function createUsageTrendChart() {
position: 'right', position: 'right',
title: { title: {
display: true, display: true,
text: '请求数' text: '请求数',
color: chartColors.value.text
}, },
grid: { grid: {
drawOnChartArea: false drawOnChartArea: false
@@ -889,7 +959,8 @@ function createUsageTrendChart() {
ticks: { ticks: {
callback: function (value) { callback: function (value) {
return value.toLocaleString() return value.toLocaleString()
} },
color: chartColors.value.text
} }
}, },
y2: { y2: {
@@ -911,7 +982,7 @@ function createApiKeysUsageTrendChart() {
} }
const data = apiKeysTrendData.value.data || [] const data = apiKeysTrendData.value.data || []
const metric = apiKeysTrendMetric.value const metric = apiKeysTrendMetric
// 颜色数组 // 颜色数组
const colors = [ const colors = [
@@ -998,7 +1069,8 @@ function createApiKeysUsageTrendChart() {
usePointStyle: true, usePointStyle: true,
font: { font: {
size: 12 size: 12
} },
color: chartColors.value.legend
} }
}, },
tooltip: { tooltip: {
@@ -1032,7 +1104,7 @@ function createApiKeysUsageTrendChart() {
else if (rank === 2) rankIcon = '🥈 ' else if (rank === 2) rankIcon = '🥈 '
else if (rank === 3) rankIcon = '🥉 ' else if (rank === 3) rankIcon = '🥉 '
if (apiKeysTrendMetric.value === 'tokens') { if (apiKeysTrendMetric === 'tokens') {
// 格式化token显示 // 格式化token显示
let formattedValue = '' let formattedValue = ''
if (value >= 1000000) { if (value >= 1000000) {
@@ -1062,19 +1134,31 @@ function createApiKeysUsageTrendChart() {
display: true, display: true,
title: { title: {
display: true, display: true,
text: trendGranularity.value === 'hour' ? '时间' : '日期' text: trendGranularity === 'hour' ? '时间' : '日期',
color: chartColors.value.text
},
ticks: {
color: chartColors.value.text
},
grid: {
color: chartColors.value.grid
} }
}, },
y: { y: {
beginAtZero: true, beginAtZero: true,
title: { title: {
display: true, display: true,
text: apiKeysTrendMetric.value === 'tokens' ? 'Token 数量' : '请求次数' text: apiKeysTrendMetric === 'tokens' ? 'Token 数量' : '请求次数',
color: chartColors.value.text
}, },
ticks: { ticks: {
callback: function (value) { callback: function (value) {
return formatNumber(value) return formatNumber(value)
} },
color: chartColors.value.text
},
grid: {
color: chartColors.value.grid
} }
} }
} }
@@ -1084,7 +1168,7 @@ function createApiKeysUsageTrendChart() {
// 更新API Keys使用趋势图 // 更新API Keys使用趋势图
async function updateApiKeysUsageTrendChart() { async function updateApiKeysUsageTrendChart() {
await loadApiKeysTrend(apiKeysTrendMetric.value) await loadApiKeysTrend(apiKeysTrendMetric)
await nextTick() await nextTick()
createApiKeysUsageTrendChart() createApiKeysUsageTrendChart()
} }
@@ -1179,6 +1263,15 @@ watch(autoRefreshEnabled, (newVal) => {
} }
}) })
// 监听主题变化,重新创建图表
watch(isDarkMode, () => {
nextTick(() => {
createModelUsageChart()
createUsageTrendChart()
createApiKeysUsageTrendChart()
})
})
// 初始化 // 初始化
onMounted(async () => { onMounted(async () => {
// 加载所有数据 // 加载所有数据
@@ -1208,19 +1301,8 @@ onUnmounted(() => {
</script> </script>
<style scoped> <style scoped>
/* 自定义日期选择器样式 */ /* 日期选择器基本样式调整 - 让Element Plus官方暗黑模式生效 */
.custom-date-picker :deep(.el-input__inner) { .custom-date-picker {
@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) {
font-size: 13px; font-size: 13px;
} }

View File

@@ -1,5 +1,10 @@
<template> <template>
<div class="flex min-h-screen items-center justify-center p-4 sm:p-6"> <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 <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" 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" 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" 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> </div>
<form class="space-y-4 sm:space-y-6" @submit.prevent="handleLogin"> <form class="space-y-4 sm:space-y-6" @submit.prevent="handleLogin">
<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 <input
v-model="loginForm.username" v-model="loginForm.username"
class="form-input w-full" class="form-input w-full"
@@ -45,7 +52,9 @@
</div> </div>
<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 <input
v-model="loginForm.password" v-model="loginForm.password"
class="form-input w-full" class="form-input w-full"
@@ -68,7 +77,7 @@
<div <div
v-if="authStore.loginError" 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 }} <i class="fas fa-exclamation-triangle mr-2" />{{ authStore.loginError }}
</div> </div>
@@ -79,8 +88,11 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useThemeStore } from '@/stores/theme'
import ThemeToggle from '@/components/common/ThemeToggle.vue'
const authStore = useAuthStore() const authStore = useAuthStore()
const themeStore = useThemeStore()
const oemLoading = computed(() => authStore.oemLoading) const oemLoading = computed(() => authStore.oemLoading)
const loginForm = ref({ const loginForm = ref({
@@ -89,6 +101,8 @@ const loginForm = ref({
}) })
onMounted(() => { onMounted(() => {
// 初始化主题
themeStore.initTheme()
// 加载OEM设置 // 加载OEM设置
authStore.loadOemSettings() authStore.loadOemSettings()
}) })

View File

@@ -2,19 +2,21 @@
<div class="settings-container"> <div class="settings-container">
<div class="card p-4 sm:p-6"> <div class="card p-4 sm:p-6">
<div class="mb-4 sm:mb-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> <h3 class="mb-1 text-lg font-bold text-gray-900 dark:text-gray-100 sm:mb-2 sm:text-xl">
<p class="text-sm text-gray-600 sm:text-base">自定义网站名称和图标</p> 其他设置
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">自定义网站名称和图标</p>
</div> </div>
<div v-if="loading" class="py-12 text-center"> <div v-if="loading" class="py-12 text-center">
<div class="loading-spinner mx-auto mb-4" /> <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>
<!-- 桌面端表格视图 --> <!-- 桌面端表格视图 -->
<div v-else class="table-container hidden sm:block"> <div v-else class="table-container hidden sm:block">
<table class="min-w-full"> <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"> <tr class="table-row">
<td class="w-48 whitespace-nowrap px-6 py-4"> <td class="w-48 whitespace-nowrap px-6 py-4">
@@ -25,20 +27,24 @@
<i class="fas fa-font text-xs text-white" /> <i class="fas fa-font text-xs text-white" />
</div> </div>
<div> <div>
<div class="text-sm font-semibold text-gray-900">网站名称</div> <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
<div class="text-xs text-gray-500">品牌标识</div> 网站名称
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">品牌标识</div>
</div> </div>
</div> </div>
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<input <input
v-model="oemSettings.siteName" 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" maxlength="100"
placeholder="Claude Relay Service" placeholder="Claude Relay Service"
type="text" 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> </td>
</tr> </tr>
@@ -52,8 +58,10 @@
<i class="fas fa-image text-xs text-white" /> <i class="fas fa-image text-xs text-white" />
</div> </div>
<div> <div>
<div class="text-sm font-semibold text-gray-900">网站图标</div> <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
<div class="text-xs text-gray-500">Favicon</div> 网站图标
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">Favicon</div>
</div> </div>
</div> </div>
</td> </td>
@@ -62,7 +70,7 @@
<!-- 图标预览 --> <!-- 图标预览 -->
<div <div
v-if="oemSettings.siteIconData || oemSettings.siteIcon" 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 <img
alt="图标预览" alt="图标预览"
@@ -70,7 +78,7 @@
:src="oemSettings.siteIconData || oemSettings.siteIcon" :src="oemSettings.siteIconData || oemSettings.siteIcon"
@error="handleIconError" @error="handleIconError"
/> />
<span class="text-sm text-gray-600">当前图标</span> <span class="text-sm text-gray-600 dark:text-gray-400">当前图标</span>
<button <button
class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900" class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
@click="removeIcon" @click="removeIcon"
@@ -92,7 +100,7 @@
<i class="fas fa-upload mr-2" /> <i class="fas fa-upload mr-2" />
上传图标 上传图标
</button> </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 >支持 .ico, .png, .jpg, .svg 格式最大 350KB</span
> >
</div> </div>
@@ -117,7 +125,7 @@
</button> </button>
<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" :disabled="saving"
@click="resetOemSettings" @click="resetOemSettings"
> >
@@ -126,7 +134,10 @@
</button> </button>
</div> </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" /> <i class="fas fa-clock mr-1" />
最后更新{{ formatDateTime(oemSettings.updatedAt) }} 最后更新{{ formatDateTime(oemSettings.updatedAt) }}
</div> </div>
@@ -140,7 +151,7 @@
<!-- 移动端卡片视图 --> <!-- 移动端卡片视图 -->
<div v-if="!loading" class="space-y-4 sm:hidden"> <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="mb-3 flex items-center gap-3">
<div <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" 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" /> <i class="fas fa-font text-sm text-white" />
</div> </div>
<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> <p class="text-xs text-gray-500">品牌标识</p>
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<input <input
v-model="oemSettings.siteName" 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" maxlength="100"
placeholder="Claude Relay Service" placeholder="Claude Relay Service"
type="text" 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> </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="mb-3 flex items-center gap-3">
<div <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" 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" /> <i class="fas fa-image text-sm text-white" />
</div> </div>
<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">Favicon</p> <p class="text-xs text-gray-500 dark:text-gray-400">Favicon</p>
</div> </div>
</div> </div>
<div class="space-y-3"> <div class="space-y-3">
<!-- 图标预览 --> <!-- 图标预览 -->
<div <div
v-if="oemSettings.siteIconData || oemSettings.siteIcon" 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 <img
alt="图标预览" alt="图标预览"
@@ -189,7 +200,7 @@
:src="oemSettings.siteIconData || oemSettings.siteIcon" :src="oemSettings.siteIconData || oemSettings.siteIcon"
@error="handleIconError" @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 <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" 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" @click="removeIcon"
@@ -208,21 +219,23 @@
/> />
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<button <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()" @click="$refs.iconFileInput.click()"
> >
<i class="fas fa-upload" /> <i class="fas fa-upload" />
上传图标 上传图标
</button> </button>
<div class="text-xs text-gray-500">或者输入图标URL</div> <div class="text-xs text-gray-500 dark:text-gray-400">或者输入图标URL</div>
<input <input
v-model="oemSettings.siteIcon" 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" placeholder="https://example.com/icon.png"
type="url" type="url"
/> />
</div> </div>
<p class="text-xs text-gray-500">支持 PNGJPEGGIF 格式建议使用正方形图片</p> <p class="text-xs text-gray-500 dark:text-gray-400">
支持 PNGJPEGGIF 格式建议使用正方形图片
</p>
</div> </div>
</div> </div>
@@ -239,7 +252,7 @@
</button> </button>
<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" :disabled="saving"
@click="resetOemSettings" @click="resetOemSettings"
> >
@@ -247,7 +260,10 @@
重置为默认 重置为默认
</button> </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" /> <i class="fas fa-clock mr-1" />
最后更新{{ formatDateTime(oemSettings.updatedAt) }} 最后更新{{ formatDateTime(oemSettings.updatedAt) }}
</div> </div>
@@ -365,12 +381,22 @@ const formatDateTime = settingsStore.formatDateTime
border: 1px solid #e5e7eb; 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 { .table-container {
overflow: hidden; overflow: hidden;
border-radius: 8px; border-radius: 8px;
border: 1px solid #f3f4f6; border: 1px solid #f3f4f6;
} }
:root.dark .table-container {
border: 1px solid #4b5563;
}
.table-row { .table-row {
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
} }
@@ -379,6 +405,10 @@ const formatDateTime = settingsStore.formatDateTime
background-color: #f9fafb; background-color: #f9fafb;
} }
:root.dark .table-row:hover {
background-color: #374151;
}
.form-input { .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; @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;
} }

View File

@@ -1,26 +1,28 @@
<template> <template>
<div class="card p-3 sm:p-6"> <div class="card p-3 sm:p-6">
<div class="mb-4 sm:mb-8"> <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" /> <i class="fas fa-graduation-cap mr-2 text-blue-600 sm:mr-3" />
Claude Code 使用教程 Claude Code 使用教程
</h3> </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 跟着这个教程你可以轻松在自己的电脑上安装并使用 Claude Code
</p> </p>
</div> </div>
<!-- 系统选择标签 --> <!-- 系统选择标签 -->
<div class="mb-4 sm:mb-8"> <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 <button
v-for="system in tutorialSystems" v-for="system in tutorialSystems"
:key="system.key" :key="system.key"
:class="[ :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', '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 activeTutorialSystem === system.key
? 'bg-white text-blue-600 shadow-sm' ? 'bg-white text-blue-600 shadow-sm dark:bg-gray-800'
: 'text-gray-600 hover:bg-white/50 hover:text-gray-900' : '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" @click="activeTutorialSystem = system.key"
> >
@@ -34,14 +36,16 @@
<div v-if="activeTutorialSystem === 'windows'" class="tutorial-content"> <div v-if="activeTutorialSystem === 'windows'" class="tutorial-content">
<!-- 第一步安装 Node.js --> <!-- 第一步安装 Node.js -->
<div class="mb-4 sm:mb-10 sm:mb-6"> <div class="mb-4 sm:mb-10 sm:mb-6">
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl"> <h4
class="mb-3 flex items-center text-lg font-semibold text-gray-800 dark:text-gray-300 sm:mb-4 sm:text-xl"
>
<span <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" 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 >1</span
> >
安装 Node.js 环境 安装 Node.js 环境
</h4> </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 环境才能运行 Claude Code 需要 Node.js 环境才能运行
</p> </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" 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 <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" /> <i class="fab fa-windows mr-2 text-blue-600" />
Windows 安装方法 Windows 安装方法
</h5> </h5>
<div class="mb-3 sm:mb-4"> <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 <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> <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 >https://nodejs.org/</code
> >
</li> </li>
<li>点击 "LTS" 版本进行下载推荐长期支持版本</li> <li>点击 "LTS" 版本进行下载推荐长期支持版本</li>
<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>按照安装向导完成安装保持默认设置即可</li> <li>按照安装向导完成安装保持默认设置即可</li>
</ol> </ol>
</div> </div>
<div class="mb-3 sm:mb-4"> <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 class="mb-2 text-xs text-gray-600 sm:text-sm"> 方法二使用包管理器
</p>
<p class="mb-2 text-xs text-gray-600 dark:text-gray-400 sm:text-sm">
如果你安装了 Chocolatey Scoop可以使用命令行安装 如果你安装了 Chocolatey Scoop可以使用命令行安装
</p> </p>
<div <div
@@ -116,7 +128,9 @@
<!-- 第二步安装 Claude Code --> <!-- 第二步安装 Claude Code -->
<div class="mb-4 sm:mb-10 sm:mb-6"> <div class="mb-4 sm:mb-10 sm:mb-6">
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl"> <h4
class="mb-3 flex items-center text-lg font-semibold text-gray-800 dark:text-gray-300 sm:mb-4 sm:text-xl"
>
<span <span
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-green-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm" class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-green-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
>2</span >2</span
@@ -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" 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 <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" /> <i class="fas fa-download mr-2 text-green-600" />
安装 Claude Code 安装 Claude Code
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base"> <p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
打开 PowerShell CMD运行以下命令 打开 PowerShell CMD运行以下命令
</p> </p>
<div <div
@@ -144,7 +158,7 @@
npm install -g @anthropic-ai/claude-code npm install -g @anthropic-ai/claude-code
</div> </div>
</div> </div>
<p class="text-sm text-gray-600"> <p class="text-sm text-gray-600 dark:text-gray-400">
这个命令会从 npm 官方仓库下载并安装最新版本的 Claude Code 这个命令会从 npm 官方仓库下载并安装最新版本的 Claude Code
</p> </p>
@@ -159,7 +173,7 @@
<!-- 验证安装 --> <!-- 验证安装 -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<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> <p class="mb-3 text-sm text-green-700">安装完成后输入以下命令检查是否安装成功</p>
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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"> <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 <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" 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 >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" class="mb-4 rounded-xl border border-purple-100 bg-gradient-to-r from-purple-50 to-pink-50 p-4 sm:mb-6 sm:p-6"
> >
<h5 <h5
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg" class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
> >
<i class="fas fa-cog mr-2 text-purple-600" /> <i class="fas fa-cog mr-2 text-purple-600" />
配置 Claude Code 环境变量 配置 Claude Code 环境变量
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base"> <p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
为了让 Claude Code 连接到你的中转服务需要设置两个环境变量 为了让 Claude Code 连接到你的中转服务需要设置两个环境变量
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div class="rounded-lg border border-purple-200 bg-white p-3 sm:p-4"> <div
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base"> 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 临时设置当前会话 方法一PowerShell 临时设置当前会话
</h6> </h6>
<p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p> <p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p>
@@ -216,8 +234,10 @@
</p> </p>
</div> </div>
<div class="rounded-lg border border-purple-200 bg-white p-3 sm:p-4"> <div
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base"> 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 永久设置用户级 方法二PowerShell 永久设置用户级
</h6> </h6>
<p class="mb-3 text-sm text-gray-600"> <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"> <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 class="mb-3 text-sm text-blue-700">
设置完环境变量后可以通过以下命令验证是否设置成功 设置完环境变量后可以通过以下命令验证是否设置成功
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div> <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 中验证 PowerShell 中验证
</h6> </h6>
<div <div
@@ -279,7 +299,9 @@
</div> </div>
<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 <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" 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"> <p class="text-sm text-blue-700">
<strong>预期输出示例</strong> <strong>预期输出示例</strong>
</p> </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>{{ currentBaseUrl }}</div>
<div>cr_xxxxxxxxxxxxxxxxxx</div> <div>cr_xxxxxxxxxxxxxxxxxx</div>
</div> </div>
@@ -306,18 +328,18 @@
<!-- Gemini CLI 环境变量设置 --> <!-- Gemini CLI 环境变量设置 -->
<div class="mt-8"> <div class="mt-8">
<h5 <h5
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg" class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
> >
<i class="fas fa-robot mr-2 text-green-600" /> <i class="fas fa-robot mr-2 text-green-600" />
配置 Gemini CLI 环境变量 配置 Gemini CLI 环境变量
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base"> <p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
如果你使用 Gemini CLI需要设置以下环境变量 如果你使用 Gemini CLI需要设置以下环境变量
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base"> <h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
PowerShell 设置方法 PowerShell 设置方法
</h6> </h6>
<p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p> <p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p>
@@ -340,7 +362,7 @@
</div> </div>
<div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base"> <h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
PowerShell 永久设置用户级 PowerShell 永久设置用户级
</h6> </h6>
<p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p> <p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p>
@@ -368,7 +390,9 @@
</div> </div>
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<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> <p class="mb-3 text-sm text-green-700"> PowerShell 中验证</p>
<div <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" 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 环境变量设置 --> <!-- Codex 环境变量设置 -->
<div class="mt-8"> <div class="mt-8">
<h5 <h5
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg" class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
> >
<i class="fas fa-code mr-2 text-indigo-600" /> <i class="fas fa-code mr-2 text-indigo-600" />
配置 Codex 环境变量 配置 Codex 环境变量
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base"> <p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
如果你使用支持 OpenAI API 的工具 Codex需要设置以下环境变量 如果你使用支持 OpenAI API 的工具 Codex需要设置以下环境变量
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-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 设置方法 PowerShell 设置方法
</h6> </h6>
<p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p> <p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p>
@@ -417,7 +441,7 @@
</div> </div>
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-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 永久设置用户级 PowerShell 永久设置用户级
</h6> </h6>
<p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p> <p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p>
@@ -456,7 +480,9 @@
<!-- 第四步开始使用 --> <!-- 第四步开始使用 -->
<div class="mb-6 sm:mb-8"> <div class="mb-6 sm:mb-8">
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl"> <h4
class="mb-3 flex items-center text-lg font-semibold text-gray-800 dark:text-gray-300 sm:mb-4 sm:text-xl"
>
<span <span
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-orange-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm" class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-orange-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
>4</span >4</span
@@ -466,13 +492,15 @@
<div <div
class="rounded-xl border border-orange-100 bg-gradient-to-r from-orange-50 to-yellow-50 p-4 sm:p-6" class="rounded-xl border border-orange-100 bg-gradient-to-r from-orange-50 to-yellow-50 p-4 sm:p-6"
> >
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base"> <p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
现在你可以开始使用 Claude Code 现在你可以开始使用 Claude Code
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div> <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 <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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>
<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 <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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 故障排除 --> <!-- Windows 故障排除 -->
<div class="mb-8"> <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" /> <i class="fas fa-wrench mr-2 text-red-600 sm:mr-3" />
Windows 常见问题解决 Windows 常见问题解决
</h4> </h4>
@@ -567,7 +599,9 @@
<div v-else-if="activeTutorialSystem === 'macos'" class="tutorial-content"> <div v-else-if="activeTutorialSystem === 'macos'" class="tutorial-content">
<!-- 第一步安装 Node.js --> <!-- 第一步安装 Node.js -->
<div class="mb-6 sm:mb-10"> <div class="mb-6 sm:mb-10">
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl"> <h4
class="mb-3 flex items-center text-lg font-semibold text-gray-800 dark:text-gray-300 sm:mb-4 sm:text-xl"
>
<span <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" 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 >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" 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 <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" /> <i class="fab fa-apple mr-2 text-gray-700" />
macOS 安装方法 macOS 安装方法
</h5> </h5>
<div class="mb-4"> <div class="mb-4">
<p class="mb-3 text-gray-700">方法一使用 Homebrew推荐</p> <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 会更方便 如果你已经安装了 Homebrew使用它安装 Node.js 会更方便
</p> </p>
<div <div
@@ -602,18 +636,22 @@
<div class="mb-4"> <div class="mb-4">
<p class="mb-3 text-gray-700">方法二官网下载</p> <p class="mb-3 text-gray-700">方法二官网下载</p>
<ol <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> <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 >https://nodejs.org/</code
> >
</li> </li>
<li>下载适合 macOS LTS 版本</li> <li>下载适合 macOS LTS 版本</li>
<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>按照安装程序指引完成安装</li> <li>按照安装程序指引完成安装</li>
@@ -634,7 +672,7 @@
<!-- 验证安装 --> <!-- 验证安装 -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<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> <p class="mb-3 text-sm text-green-700">安装完成后打开 Terminal输入以下命令</p>
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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 --> <!-- 第二步安装 Claude Code -->
<div class="mb-6 sm:mb-10"> <div class="mb-6 sm:mb-10">
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl"> <h4
class="mb-3 flex items-center text-lg font-semibold text-gray-800 dark:text-gray-300 sm:mb-4 sm:text-xl"
>
<span <span
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-green-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm" class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-green-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
>2</span >2</span
@@ -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" class="mb-4 rounded-xl border border-purple-100 bg-gradient-to-r from-purple-50 to-pink-50 p-4 sm:mb-6 sm:p-6"
> >
<h5 <h5
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg" class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
> >
<i class="fas fa-download mr-2 text-purple-600" /> <i class="fas fa-download mr-2 text-purple-600" />
安装 Claude Code 安装 Claude Code
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base"> <p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
打开 Terminal运行以下命令 打开 Terminal运行以下命令
</p> </p>
<div <div
@@ -688,7 +728,7 @@
<!-- 验证安装 --> <!-- 验证安装 -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<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> <p class="mb-3 text-sm text-green-700">安装完成后输入以下命令检查是否安装成功</p>
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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"> <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 <span
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-orange-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm" class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-orange-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
>3</span >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" 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 <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" /> <i class="fas fa-cog mr-2 text-orange-600" />
配置 Claude Code 环境变量 配置 Claude Code 环境变量
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base"> <p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
为了让 Claude Code 连接到你的中转服务需要设置两个环境变量 为了让 Claude Code 连接到你的中转服务需要设置两个环境变量
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div class="rounded-lg border border-orange-200 bg-white p-3 sm:p-4"> <div class="rounded-lg border border-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> </h6>
<p class="mb-3 text-sm text-gray-600"> Terminal 中运行以下命令</p> <p class="mb-3 text-sm text-gray-600"> Terminal 中运行以下命令</p>
@@ -746,7 +788,9 @@
</div> </div>
<div class="rounded-lg border border-orange-200 bg-white p-3 sm:p-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> <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 class="mb-3 text-sm text-gray-600">
编辑你的 shell 配置文件根据你使用的 shell 编辑你的 shell 配置文件根据你使用的 shell
</p> </p>
@@ -781,18 +825,20 @@
<!-- Gemini CLI 环境变量设置 --> <!-- Gemini CLI 环境变量设置 -->
<div class="mt-8"> <div class="mt-8">
<h5 <h5
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg" class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
> >
<i class="fas fa-robot mr-2 text-green-600" /> <i class="fas fa-robot mr-2 text-green-600" />
配置 Gemini CLI 环境变量 配置 Gemini CLI 环境变量
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base"> <p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
如果你使用 Gemini CLI需要设置以下环境变量 如果你使用 Gemini CLI需要设置以下环境变量
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">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> <p class="mb-3 text-sm text-gray-600"> Terminal 中运行以下命令</p>
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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>
<div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">永久设置方法</h6> <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> <p class="mb-3 text-sm text-gray-600">添加到你的 shell 配置文件</p>
<div <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" 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>
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<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> <p class="mb-3 text-sm text-green-700"> Terminal 中验证</p>
<div <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" 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 环境变量设置 --> <!-- Codex 环境变量设置 -->
<div class="mt-8"> <div class="mt-8">
<h5 <h5
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg" class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
> >
<i class="fas fa-code mr-2 text-indigo-600" /> <i class="fas fa-code mr-2 text-indigo-600" />
配置 Codex 环境变量 配置 Codex 环境变量
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base"> <p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
如果你使用支持 OpenAI API 的工具 Codex需要设置以下环境变量 如果你使用支持 OpenAI API 的工具 Codex需要设置以下环境变量
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-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> <p class="mb-3 text-sm text-gray-600"> Terminal 中运行以下命令</p>
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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>
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-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">添加到你的 shell 配置文件</p> <p class="mb-3 text-sm text-gray-600">添加到你的 shell 配置文件</p>
<div <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" 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"> <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 <span
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-yellow-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm" class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-yellow-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
>4</span >4</span
@@ -947,13 +1003,15 @@
<div <div
class="rounded-xl border border-yellow-100 bg-gradient-to-r from-yellow-50 to-amber-50 p-4 sm:p-6" class="rounded-xl border border-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 现在你可以开始使用 Claude Code
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div> <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 <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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>
<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 <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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 故障排除 --> <!-- macOS 故障排除 -->
<div class="mb-8"> <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" /> <i class="fas fa-wrench mr-2 text-red-600 sm:mr-3" />
macOS 常见问题解决 macOS 常见问题解决
</h4> </h4>
@@ -1054,7 +1116,9 @@
<div v-else-if="activeTutorialSystem === 'linux'" class="tutorial-content"> <div v-else-if="activeTutorialSystem === 'linux'" class="tutorial-content">
<!-- 第一步安装 Node.js --> <!-- 第一步安装 Node.js -->
<div class="mb-6 sm:mb-10"> <div class="mb-6 sm:mb-10">
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl"> <h4
class="mb-3 flex items-center text-lg font-semibold text-gray-800 dark:text-gray-300 sm:mb-4 sm:text-xl"
>
<span <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" 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 >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" 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 <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" /> <i class="fab fa-ubuntu mr-2 text-orange-600" />
Linux 安装方法 Linux 安装方法
@@ -1087,7 +1151,7 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<p class="mb-3 text-gray-700">方法二使用系统包管理器</p> <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> </p>
<div <div
@@ -1112,7 +1176,7 @@
<!-- 验证安装 --> <!-- 验证安装 -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<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> <p class="mb-3 text-sm text-green-700">安装完成后打开终端输入以下命令</p>
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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 --> <!-- 第二步安装 Claude Code -->
<div class="mb-6 sm:mb-10"> <div class="mb-6 sm:mb-10">
<h4 class="mb-3 flex items-center text-lg font-semibold text-gray-800 sm:mb-4 sm:text-xl"> <h4
class="mb-3 flex items-center text-lg font-semibold text-gray-800 dark:text-gray-300 sm:mb-4 sm:text-xl"
>
<span <span
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-green-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm" class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-green-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
>2</span >2</span
@@ -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" class="mb-4 rounded-xl border border-purple-100 bg-gradient-to-r from-purple-50 to-pink-50 p-4 sm:mb-6 sm:p-6"
> >
<h5 <h5
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg" class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
> >
<i class="fas fa-download mr-2 text-purple-600" /> <i class="fas fa-download mr-2 text-purple-600" />
安装 Claude Code 安装 Claude Code
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base">打开终端运行以下命令</p> <p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
打开终端运行以下命令
</p>
<div <div
class="mb-4 overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-green-400 sm:p-4 sm:text-sm" class="mb-4 overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-green-400 sm:p-4 sm:text-sm"
> >
@@ -1164,7 +1232,7 @@
<!-- 验证安装 --> <!-- 验证安装 -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<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> <p class="mb-3 text-sm text-green-700">安装完成后输入以下命令检查是否安装成功</p>
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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"> <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 <span
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-orange-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm" class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-orange-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
>3</span >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" 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 <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" /> <i class="fas fa-cog mr-2 text-orange-600" />
配置 Claude Code 环境变量 配置 Claude Code 环境变量
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base"> <p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
为了让 Claude Code 连接到你的中转服务需要设置两个环境变量 为了让 Claude Code 连接到你的中转服务需要设置两个环境变量
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div class="rounded-lg border border-orange-200 bg-white p-3 sm:p-4"> <div class="rounded-lg border border-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> </h6>
<p class="mb-3 text-sm text-gray-600">在终端中运行以下命令</p> <p class="mb-3 text-sm text-gray-600">在终端中运行以下命令</p>
@@ -1222,7 +1292,9 @@
</div> </div>
<div class="rounded-lg border border-orange-200 bg-white p-3 sm:p-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> <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> <p class="mb-3 text-sm text-gray-600">编辑你的 shell 配置文件</p>
<div <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" 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 环境变量设置 --> <!-- Gemini CLI 环境变量设置 -->
<div class="mt-8"> <div class="mt-8">
<h5 <h5
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg" class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
> >
<i class="fas fa-robot mr-2 text-green-600" /> <i class="fas fa-robot mr-2 text-green-600" />
配置 Gemini CLI 环境变量 配置 Gemini CLI 环境变量
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base"> <p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
如果你使用 Gemini CLI需要设置以下环境变量 如果你使用 Gemini CLI需要设置以下环境变量
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">终端设置方法</h6> <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> <p class="mb-3 text-sm text-gray-600">在终端中运行以下命令</p>
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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>
<div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">永久设置方法</h6> <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> <p class="mb-3 text-sm text-gray-600">添加到你的 shell 配置文件</p>
<div <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" 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>
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4"> <div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<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> <p class="mb-3 text-sm text-green-700">在终端中验证</p>
<div <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" 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 环境变量设置 --> <!-- Codex 环境变量设置 -->
<div class="mt-8"> <div class="mt-8">
<h5 <h5
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg" class="mb-2 flex items-center text-base font-semibold text-gray-800 dark:text-gray-600 sm:mb-3 sm:text-lg"
> >
<i class="fas fa-code mr-2 text-indigo-600" /> <i class="fas fa-code mr-2 text-indigo-600" />
配置 Codex 环境变量 配置 Codex 环境变量
</h5> </h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base"> <p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
如果你使用支持 OpenAI API 的工具 Codex需要设置以下环境变量 如果你使用支持 OpenAI API 的工具 Codex需要设置以下环境变量
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-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> <p class="mb-3 text-sm text-gray-600">在终端中运行以下命令</p>
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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>
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-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">添加到你的 shell 配置文件</p> <p class="mb-3 text-sm text-gray-600">添加到你的 shell 配置文件</p>
<div <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" 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"> <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 <span
class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-yellow-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm" class="mr-2 flex h-6 w-6 items-center justify-center rounded-full bg-yellow-500 text-xs font-bold text-white sm:mr-3 sm:h-8 sm:w-8 sm:text-sm"
>4</span >4</span
@@ -1421,13 +1505,15 @@
<div <div
class="rounded-xl border border-yellow-100 bg-gradient-to-r from-yellow-50 to-amber-50 p-4 sm:p-6" class="rounded-xl border border-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 现在你可以开始使用 Claude Code
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<div> <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 <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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>
<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 <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" 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 故障排除 --> <!-- Linux 故障排除 -->
<div class="mb-8"> <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" /> <i class="fas fa-wrench mr-2 text-red-600 sm:mr-3" />
Linux 常见问题解决 Linux 常见问题解决
</h4> </h4>

View File

@@ -1,6 +1,7 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: 'class',
theme: { theme: {
extend: { extend: {
animation: { animation: {