mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6a4bae8c6 | ||
|
|
c9f09ccf37 | ||
|
|
5b8fd04ba3 | ||
|
|
3c791a2313 | ||
|
|
2ef64d8064 | ||
|
|
f2dc4bcf98 | ||
|
|
5f597afb42 | ||
|
|
e5bef7e2b0 | ||
|
|
d8df9ce680 | ||
|
|
44594220b2 | ||
|
|
86fc9fe86e | ||
|
|
67f009b81c | ||
|
|
6618312360 | ||
|
|
9e49824c52 | ||
|
|
015754237b |
61
.github/workflows/release.yml
vendored
Normal file
61
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build all-in-one HTML
|
||||
run: npm run build
|
||||
|
||||
- name: Prepare release assets
|
||||
run: |
|
||||
cd dist
|
||||
mv index.html cli-proxy-api-management-center.html
|
||||
ls -lh cli-proxy-api-management-center.html
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: dist/cli-proxy-api-management-center.html
|
||||
body: |
|
||||
## CLI Proxy API Management Center - ${{ github.ref_name }}
|
||||
|
||||
### Download and Usage
|
||||
1. Download the `cli-proxy-api-management-center.html` file
|
||||
2. Open it directly in your browser
|
||||
3. All assets (CSS, JavaScript, images) are bundled into this single file
|
||||
|
||||
### Features
|
||||
- Single file, no external dependencies required
|
||||
- Complete management interface for CLI Proxy API
|
||||
- Support for local and remote connections
|
||||
- Multi-language support (Chinese/English)
|
||||
- Dark/Light theme support
|
||||
|
||||
---
|
||||
🤖 Generated with GitHub Actions
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Temporary build files
|
||||
index.build.html
|
||||
|
||||
# npm lock files
|
||||
package-lock.json
|
||||
|
||||
# IDE and editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
69
BUILD_RELEASE.md
Normal file
69
BUILD_RELEASE.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Build and Release Instructions
|
||||
|
||||
## Overview
|
||||
|
||||
This project uses webpack to bundle all HTML, CSS, JavaScript, and images into a single all-in-one HTML file. The GitHub workflow automatically builds and releases this file when you create a new tag.
|
||||
|
||||
## How to Create a Release
|
||||
|
||||
1. Make sure all your changes are committed
|
||||
2. Create and push a new tag:
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
```
|
||||
3. The GitHub workflow will automatically:
|
||||
- Install dependencies
|
||||
- Build the all-in-one HTML file using webpack
|
||||
- Create a new release with the tag
|
||||
- Upload the bundled HTML file to the release
|
||||
|
||||
## Manual Build
|
||||
|
||||
To build locally:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build the all-in-one HTML file
|
||||
npm run build
|
||||
```
|
||||
|
||||
The output will be in the `dist/` directory as `index.html`.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **build-scripts/prepare-html.js**: Pre-build script
|
||||
- Reads the original `index.html`
|
||||
- Removes local CSS and JavaScript references
|
||||
- Generates temporary `index.build.html` for webpack
|
||||
|
||||
2. **webpack.config.js**: Configures webpack to bundle all assets
|
||||
- Uses `style-loader` to inline CSS
|
||||
- Uses `asset/inline` to embed images as base64
|
||||
- Uses `html-inline-script-webpack-plugin` to inline JavaScript
|
||||
- Uses `index.build.html` as template (generated dynamically)
|
||||
|
||||
3. **bundle-entry.js**: Entry point that imports all resources
|
||||
- Imports CSS files
|
||||
- Imports JavaScript modules
|
||||
- Imports and sets logo image
|
||||
|
||||
4. **package.json scripts**:
|
||||
- `prebuild`: Automatically runs before build to generate `index.build.html`
|
||||
- `build`: Runs webpack to bundle everything
|
||||
- `postbuild`: Cleans up temporary `index.build.html` file
|
||||
|
||||
5. **.github/workflows/release.yml**: GitHub workflow
|
||||
- Triggers on tag push
|
||||
- Builds the project (prebuild → build → postbuild)
|
||||
- Creates a release with the bundled HTML file
|
||||
|
||||
## External Dependencies
|
||||
|
||||
The bundled HTML file still relies on these CDN resources:
|
||||
- Font Awesome (icons)
|
||||
- Chart.js (charts and graphs)
|
||||
|
||||
These are loaded from CDN to keep the file size reasonable and leverage browser caching.
|
||||
214
README.md
214
README.md
@@ -1,145 +1,143 @@
|
||||
# Cli-Proxy-API-Management-Center
|
||||
这是一个用于管理 CLI Proxy API 的现代化 Web 界面。
|
||||
This is a modern web interface for managing the CLI Proxy API.
|
||||
|
||||
## 功能特点
|
||||
[中文文档](README_CN.md)
|
||||
|
||||
### 认证管理
|
||||
- 支持管理密钥认证
|
||||
- 可配置 API 基础地址
|
||||
- 实时连接状态检测
|
||||
Main Project:
|
||||
https://github.com/router-for-me/CLIProxyAPI
|
||||
|
||||
### 基础设置
|
||||
- **调试模式**: 开启/关闭调试功能
|
||||
- **代理设置**: 配置代理服务器 URL
|
||||
- **请求重试**: 设置请求重试次数
|
||||
- **配额管理**: 配置超出配额时的行为
|
||||
- **本地访问**: 允许本地未认证访问
|
||||
Example URL:
|
||||
https://remote.router-for.me/
|
||||
|
||||
### API 密钥管理
|
||||
- **代理服务认证密钥**: 管理用于代理服务的 API 密钥
|
||||
- **Gemini API**: 管理 Google Gemini 生成式语言 API 密钥
|
||||
- **Codex API**: 管理 OpenAI Codex API 配置
|
||||
- **Claude API**: 管理 Anthropic Claude API 配置
|
||||
- **OpenAI 兼容提供商**: 管理 OpenAI 兼容的第三方提供商
|
||||
Minimum required version: ≥ 5.0.0
|
||||
Recommended version: ≥ 5.2.6
|
||||
|
||||
### 认证文件管理
|
||||
- 上传认证 JSON 文件
|
||||
- 下载现有认证文件
|
||||
- 删除单个或所有认证文件
|
||||
- 显示文件详细信息
|
||||
## Features
|
||||
|
||||
### 系统监控
|
||||
- 实时 API 连接状态
|
||||
- 最后更新时间跟踪
|
||||
- 详细的错误信息提示
|
||||
### Authentication Management
|
||||
- Supports management key authentication
|
||||
- Configurable API base address
|
||||
- Real-time connection status detection
|
||||
|
||||
## 使用方法
|
||||
### Basic Settings
|
||||
- **Debug Mode**: Enable/disable debugging
|
||||
- **Proxy Settings**: Configure proxy server URL
|
||||
- **Request Retries**: Set the number of request retries
|
||||
- **Quota Management**: Configure behavior when the quota is exceeded
|
||||
- **Local Access**: Manage local unauthenticated access
|
||||
|
||||
### 1. 直接使用(推荐)
|
||||
直接用浏览器打开 `index.html` 文件即可使用。
|
||||
### API Key Management
|
||||
- **Proxy Service Authentication Key**: Manage API keys for the proxy service
|
||||
- **Gemini API**: Manage Google Gemini generative language API keys
|
||||
- **Codex API**: Manage OpenAI Codex API configuration
|
||||
- **Claude API**: Manage Anthropic Claude API configuration
|
||||
- **OpenAI-Compatible Providers**: Manage OpenAI-compatible third-party providers
|
||||
|
||||
### 2. 使用本地服务器
|
||||
### Authentication File Management
|
||||
- Upload authentication JSON files
|
||||
- Download existing authentication files
|
||||
- Delete single or all authentication files
|
||||
- Display file details
|
||||
|
||||
|
||||
## How to Use
|
||||
|
||||
### 1. Direct Use (Recommended)
|
||||
Simply open the `index.html` file directly in your browser to use it.
|
||||
|
||||
### 2. Use a Local Server
|
||||
```bash
|
||||
# 安装依赖
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# 使用默认端口(3000)
|
||||
# Start the server on the default port (3000)
|
||||
npm start
|
||||
```
|
||||
|
||||
### 3. 配置 API 连接
|
||||
1. 打开管理界面
|
||||
2. 在认证配置区域输入:
|
||||
- **API 地址**: `http://localhost:8317`/v0/management将会自动为您补全
|
||||
- **管理密钥**: 您的管理密钥
|
||||
3. 点击"测试连接"按钮
|
||||
4. 连接成功后即可使用所有功能
|
||||
### 3. Configure API Connection
|
||||
1. Open the management interface.
|
||||
2. On the login screen, enter:
|
||||
- **Remote Address**: `http://localhost:8317` (`/v0/management` will be auto-completed for you)
|
||||
- **Management Key**: Your management key
|
||||
3. Click the "Connect" button.
|
||||
4. Once connected successfully, all features will be available.
|
||||
|
||||
## 界面说明
|
||||
## Interface Description
|
||||
|
||||
### 导航菜单
|
||||
- **基础设置**: 调试、代理、重试等基本配置
|
||||
- **API 密钥**: 各种 API 服务的密钥管理
|
||||
- **AI 提供商**: AI 服务提供商配置
|
||||
- **认证文件**: 认证文件的上传下载管理
|
||||
- **系统信息**: 连接状态和系统信息
|
||||
### Navigation Menu
|
||||
- **Basic Settings**: Basic configurations like debugging, proxy, retries, etc.
|
||||
- **API Keys**: Management of keys for various API services.
|
||||
- **AI Providers**: Configuration for AI service providers.
|
||||
- **Auth Files**: Upload and download management for authentication files.
|
||||
- **System Info**: Connection status and system information.
|
||||
|
||||
### 操作按钮
|
||||
- **刷新全部**: 重新加载所有配置数据
|
||||
- **连接状态**: 检查 API 连接状态
|
||||
- **添加**: 添加新的配置项
|
||||
- **编辑**: 编辑现有配置
|
||||
- **删除**: 删除配置项
|
||||
## Feature Highlights
|
||||
|
||||
## 特性亮点
|
||||
### Modern UI
|
||||
- Responsive design, supports all screen sizes
|
||||
- Beautiful gradient colors and shadow effects
|
||||
- Smooth animations and transition effects
|
||||
- Intuitive icons and status indicators
|
||||
|
||||
### 现代化 UI
|
||||
- 响应式设计,支持各种屏幕尺寸
|
||||
- 美观的渐变色彩和阴影效果
|
||||
- 流畅的动画和过渡效果
|
||||
- 直观的图标和状态指示
|
||||
### Real-time Updates
|
||||
- Configuration changes take effect immediately
|
||||
- Real-time status feedback
|
||||
- Automatic data refresh
|
||||
|
||||
### 实时更新
|
||||
- 配置更改立即生效
|
||||
- 实时状态反馈
|
||||
- 自动数据刷新
|
||||
### Security Features
|
||||
- Masked display for keys
|
||||
|
||||
### 安全特性
|
||||
- 密钥遮蔽显示
|
||||
### Responsive Design
|
||||
- Perfectly adapts to desktop and mobile devices
|
||||
- Adaptive layout
|
||||
- Touch-friendly interactions
|
||||
|
||||
### 响应式设计
|
||||
- 完美适配桌面和移动设备
|
||||
- 自适应布局
|
||||
- 触摸友好的交互
|
||||
## Tech Stack
|
||||
|
||||
## 技术栈
|
||||
- **Frontend**: Plain HTML, CSS, JavaScript
|
||||
- **Styling**: CSS3 + Flexbox/Grid
|
||||
- **Icons**: Font Awesome 6.4.0
|
||||
- **Fonts**: Segoe UI system font
|
||||
- **API**: RESTful API calls
|
||||
|
||||
- **前端**: 纯 HTML、CSS、JavaScript
|
||||
- **样式**: CSS3 + Flexbox/Grid
|
||||
- **图标**: Font Awesome 6.4.0
|
||||
- **字体**: Segoe UI 系统字体
|
||||
- **API**: RESTful API 调用
|
||||
## Troubleshooting
|
||||
|
||||
## 故障排除
|
||||
### Connection Issues
|
||||
1. Confirm that the CLI Proxy API service is running.
|
||||
2. Check if the API address is correct.
|
||||
3. Verify that the management key is valid.
|
||||
4. Ensure your firewall settings allow the connection.
|
||||
|
||||
### 连接问题
|
||||
1. 确认 CLI Proxy API 服务正在运行
|
||||
2. 检查 API 地址是否正确
|
||||
3. 验证管理密钥是否有效
|
||||
4. 确认防火墙设置允许连接
|
||||
### Data Not Updating
|
||||
1. Click the "Refresh All" button.
|
||||
2. Check your network connection.
|
||||
3. Check the browser's console for any error messages.
|
||||
|
||||
### 数据不更新
|
||||
1. 点击"刷新全部"按钮
|
||||
2. 检查网络连接
|
||||
3. 查看浏览器控制台错误信息
|
||||
## Development Information
|
||||
|
||||
## 开发说明
|
||||
|
||||
### 文件结构
|
||||
### File Structure
|
||||
```
|
||||
webui/
|
||||
├── index.html # 主页面
|
||||
├── styles.css # 样式文件
|
||||
├── app.js # 应用逻辑
|
||||
├── package.json # 项目配置
|
||||
└── README.md # 说明文档
|
||||
├── index.html # Main page
|
||||
├── styles.css # Stylesheet
|
||||
├── app.js # Application logic
|
||||
├── package.json # Project configuration
|
||||
├── i18n.js # Internationalization support
|
||||
└── README.md # README document
|
||||
```
|
||||
|
||||
### API 调用
|
||||
所有 API 调用都通过 `CLIProxyManager` 类的 `makeRequest` 方法处理,包含:
|
||||
- 自动添加认证头
|
||||
- 错误处理
|
||||
- JSON 响应解析
|
||||
### API Calls
|
||||
All API calls are handled through the `makeRequest` method of the `ManagerAPI` class, which includes:
|
||||
- Automatic addition of authentication headers
|
||||
- Error handling
|
||||
- JSON response parsing
|
||||
|
||||
### 状态管理
|
||||
- 本地存储保存 API 地址和密钥
|
||||
- 内存中维护连接状态
|
||||
- 实时数据刷新机制
|
||||
### State Management
|
||||
- API address and key are saved in local storage
|
||||
- Connection status is maintained in memory
|
||||
- Real-time data refresh mechanism
|
||||
|
||||
## 许可证
|
||||
## Contributing
|
||||
We welcome Issues and Pull Requests to improve this project! We encourage more developers to contribute to the enhancement of this WebUI!
|
||||
|
||||
MIT License
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request 来改进这个项目!
|
||||
This project is licensed under the MIT License.
|
||||
|
||||
140
README_CN.md
Normal file
140
README_CN.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Cli-Proxy-API-Management-Center
|
||||
这是一个用于管理 CLI Proxy API 的现代化 Web 界面。
|
||||
主项目
|
||||
https://github.com/router-for-me/CLIProxyAPI
|
||||
|
||||
示例网站:
|
||||
https://remote.router-for.me/
|
||||
|
||||
最低可用版本 ≥ 5.0.0
|
||||
推荐版本 ≥ 5.2.6
|
||||
|
||||
## 功能特点
|
||||
|
||||
### 认证管理
|
||||
- 支持管理密钥认证
|
||||
- 可配置 API 基础地址
|
||||
- 实时连接状态检测
|
||||
|
||||
### 基础设置
|
||||
- **调试模式**: 开启/关闭调试功能
|
||||
- **代理设置**: 配置代理服务器 URL
|
||||
- **请求重试**: 设置请求重试次数
|
||||
- **配额管理**: 配置超出配额时的行为
|
||||
- **本地访问**: 管理本地未认证访问
|
||||
|
||||
### API 密钥管理
|
||||
- **代理服务认证密钥**: 管理用于代理服务的 API 密钥
|
||||
- **Gemini API**: 管理 Google Gemini 生成式语言 API 密钥
|
||||
- **Codex API**: 管理 OpenAI Codex API 配置
|
||||
- **Claude API**: 管理 Anthropic Claude API 配置
|
||||
- **OpenAI 兼容提供商**: 管理 OpenAI 兼容的第三方提供商
|
||||
|
||||
### 认证文件管理
|
||||
- 上传认证 JSON 文件
|
||||
- 下载现有认证文件
|
||||
- 删除单个或所有认证文件
|
||||
- 显示文件详细信息
|
||||
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 直接使用(推荐)
|
||||
直接用浏览器打开 `index.html` 文件即可使用。
|
||||
|
||||
### 2. 使用本地服务器
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 使用默认端口(3000)
|
||||
npm start
|
||||
```
|
||||
|
||||
### 3. 配置 API 连接
|
||||
1. 打开管理界面
|
||||
2. 在登录界面上输入:
|
||||
- **远程地址**: `http://localhost:8317`/v0/management将会自动为您补全
|
||||
- **管理密钥**: 您的管理密钥
|
||||
3. 点击"连接"按钮
|
||||
4. 连接成功后即可使用所有功能
|
||||
|
||||
## 界面说明
|
||||
|
||||
### 导航菜单
|
||||
- **基础设置**: 调试、代理、重试等基本配置
|
||||
- **API 密钥**: 各种 API 服务的密钥管理
|
||||
- **AI 提供商**: AI 服务提供商配置
|
||||
- **认证文件**: 认证文件的上传下载管理
|
||||
- **系统信息**: 连接状态和系统信息
|
||||
|
||||
## 特性亮点
|
||||
|
||||
### 现代化 UI
|
||||
- 响应式设计,支持各种屏幕尺寸
|
||||
- 美观的渐变色彩和阴影效果
|
||||
- 流畅的动画和过渡效果
|
||||
- 直观的图标和状态指示
|
||||
|
||||
### 实时更新
|
||||
- 配置更改立即生效
|
||||
- 实时状态反馈
|
||||
- 自动数据刷新
|
||||
|
||||
### 安全特性
|
||||
- 密钥遮蔽显示
|
||||
|
||||
### 响应式设计
|
||||
- 完美适配桌面和移动设备
|
||||
- 自适应布局
|
||||
- 触摸友好的交互
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **前端**: 纯 HTML、CSS、JavaScript
|
||||
- **样式**: CSS3 + Flexbox/Grid
|
||||
- **图标**: Font Awesome 6.4.0
|
||||
- **字体**: Segoe UI 系统字体
|
||||
- **API**: RESTful API 调用
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 连接问题
|
||||
1. 确认 CLI Proxy API 服务正在运行
|
||||
2. 检查 API 地址是否正确
|
||||
3. 验证管理密钥是否有效
|
||||
4. 确认防火墙设置允许连接
|
||||
|
||||
### 数据不更新
|
||||
1. 点击"刷新全部"按钮
|
||||
2. 检查网络连接
|
||||
3. 查看浏览器控制台错误信息
|
||||
|
||||
## 开发说明
|
||||
|
||||
### 文件结构
|
||||
```
|
||||
webui/
|
||||
├── index.html # 主页面
|
||||
├── styles.css # 样式文件
|
||||
├── app.js # 应用逻辑
|
||||
├── package.json # 项目配置
|
||||
├── i18n.js # 国际化支持
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
### API 调用
|
||||
所有 API 调用都通过 `ManagerAPI` 类的 `makeRequest` 方法处理,包含:
|
||||
- 自动添加认证头
|
||||
- 错误处理
|
||||
- JSON 响应解析
|
||||
|
||||
### 状态管理
|
||||
- 本地存储保存 API 地址和密钥
|
||||
- 内存中维护连接状态
|
||||
- 实时数据刷新机制
|
||||
|
||||
## 贡献
|
||||
欢迎提交 Issue 和 Pull Request 来改进这个项目!我们欢迎更多的大佬来对这个WebUI进行更新!
|
||||
|
||||
本项目采用MIT许可
|
||||
499
app.js
499
app.js
@@ -38,7 +38,6 @@ class CLIProxyManager {
|
||||
if (savedTheme && ['light', 'dark'].includes(savedTheme)) {
|
||||
this.currentTheme = savedTheme;
|
||||
} else {
|
||||
// 根据系统偏好自动选择
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
this.currentTheme = 'dark';
|
||||
} else {
|
||||
@@ -539,7 +538,6 @@ class CLIProxyManager {
|
||||
const updateRetry = document.getElementById('update-retry');
|
||||
const switchProjectToggle = document.getElementById('switch-project-toggle');
|
||||
const switchPreviewToggle = document.getElementById('switch-preview-model-toggle');
|
||||
const allowLocalhostToggle = document.getElementById('allow-localhost-toggle');
|
||||
|
||||
if (debugToggle) {
|
||||
debugToggle.addEventListener('change', (e) => this.updateDebug(e.target.checked));
|
||||
@@ -559,9 +557,6 @@ class CLIProxyManager {
|
||||
if (switchPreviewToggle) {
|
||||
switchPreviewToggle.addEventListener('change', (e) => this.updateSwitchPreviewModel(e.target.checked));
|
||||
}
|
||||
if (allowLocalhostToggle) {
|
||||
allowLocalhostToggle.addEventListener('change', (e) => this.updateAllowLocalhost(e.target.checked));
|
||||
}
|
||||
|
||||
// API 密钥管理
|
||||
const addApiKey = document.getElementById('add-api-key');
|
||||
@@ -586,6 +581,13 @@ class CLIProxyManager {
|
||||
addOpenaiProvider.addEventListener('click', () => this.showAddOpenAIProviderModal());
|
||||
}
|
||||
|
||||
|
||||
// Gemini Web Token
|
||||
const geminiWebTokenBtn = document.getElementById('gemini-web-token-btn');
|
||||
if (geminiWebTokenBtn) {
|
||||
geminiWebTokenBtn.addEventListener('click', () => this.showGeminiWebTokenModal());
|
||||
}
|
||||
|
||||
// 认证文件管理
|
||||
const uploadAuthFile = document.getElementById('upload-auth-file');
|
||||
const deleteAllAuthFiles = document.getElementById('delete-all-auth-files');
|
||||
@@ -601,6 +603,29 @@ class CLIProxyManager {
|
||||
authFileInput.addEventListener('change', (e) => this.handleFileUpload(e));
|
||||
}
|
||||
|
||||
// 使用统计
|
||||
const refreshUsageStats = document.getElementById('refresh-usage-stats');
|
||||
const requestsHourBtn = document.getElementById('requests-hour-btn');
|
||||
const requestsDayBtn = document.getElementById('requests-day-btn');
|
||||
const tokensHourBtn = document.getElementById('tokens-hour-btn');
|
||||
const tokensDayBtn = document.getElementById('tokens-day-btn');
|
||||
|
||||
if (refreshUsageStats) {
|
||||
refreshUsageStats.addEventListener('click', () => this.loadUsageStats());
|
||||
}
|
||||
if (requestsHourBtn) {
|
||||
requestsHourBtn.addEventListener('click', () => this.switchRequestsPeriod('hour'));
|
||||
}
|
||||
if (requestsDayBtn) {
|
||||
requestsDayBtn.addEventListener('click', () => this.switchRequestsPeriod('day'));
|
||||
}
|
||||
if (tokensHourBtn) {
|
||||
tokensHourBtn.addEventListener('click', () => this.switchTokensPeriod('hour'));
|
||||
}
|
||||
if (tokensDayBtn) {
|
||||
tokensDayBtn.addEventListener('click', () => this.switchTokensPeriod('day'));
|
||||
}
|
||||
|
||||
// 模态框
|
||||
const closeBtn = document.querySelector('.close');
|
||||
if (closeBtn) {
|
||||
@@ -726,8 +751,7 @@ class CLIProxyManager {
|
||||
this.managementKey = savedKey;
|
||||
}
|
||||
|
||||
// 注意:不再处理DOM元素,因为认证配置已改为只读显示
|
||||
// DOM更新由updateConnectionInfo()方法处理
|
||||
|
||||
}
|
||||
|
||||
// API 请求方法
|
||||
@@ -934,6 +958,9 @@ class CLIProxyManager {
|
||||
// 认证文件需要单独加载,因为不在配置中
|
||||
await this.loadAuthFiles();
|
||||
|
||||
// 使用统计需要单独加载
|
||||
await this.loadUsageStats();
|
||||
|
||||
console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid());
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error);
|
||||
@@ -970,10 +997,6 @@ class CLIProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 本地访问设置
|
||||
if (config['allow-localhost-unauthenticated'] !== undefined) {
|
||||
document.getElementById('allow-localhost-toggle').checked = config['allow-localhost-unauthenticated'];
|
||||
}
|
||||
|
||||
// API 密钥
|
||||
if (config['api-keys']) {
|
||||
@@ -1008,7 +1031,6 @@ class CLIProxyManager {
|
||||
this.loadProxySettings(),
|
||||
this.loadRetrySettings(),
|
||||
this.loadQuotaSettings(),
|
||||
this.loadLocalhostSettings(),
|
||||
this.loadApiKeys(),
|
||||
this.loadGeminiKeys(),
|
||||
this.loadCodexKeys(),
|
||||
@@ -1161,32 +1183,6 @@ class CLIProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载本地访问设置
|
||||
async loadLocalhostSettings() {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
if (config['allow-localhost-unauthenticated'] !== undefined) {
|
||||
document.getElementById('allow-localhost-toggle').checked = config['allow-localhost-unauthenticated'];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载本地访问设置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新本地访问设置
|
||||
async updateAllowLocalhost(enabled) {
|
||||
try {
|
||||
await this.makeRequest('/allow-localhost-unauthenticated', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ value: enabled })
|
||||
});
|
||||
this.clearCache(); // 清除缓存
|
||||
this.showNotification(i18n.t('notification.localhost_updated'), 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
|
||||
document.getElementById('allow-localhost-toggle').checked = !enabled;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载API密钥
|
||||
async loadApiKeys() {
|
||||
@@ -2187,6 +2183,433 @@ class CLIProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 显示 Gemini Web Token 模态框
|
||||
showGeminiWebTokenModal() {
|
||||
const inlineSecure1psid = document.getElementById('secure-1psid-input');
|
||||
const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
|
||||
const modalBody = document.getElementById('modal-body');
|
||||
modalBody.innerHTML = `
|
||||
<h3>${i18n.t('auth_login.gemini_web_button')}</h3>
|
||||
<div class="gemini-web-form">
|
||||
<div class="form-group">
|
||||
<label for="modal-secure-1psid">${i18n.t('auth_login.secure_1psid_label')}</label>
|
||||
<input type="text" id="modal-secure-1psid" placeholder="${i18n.t('auth_login.secure_1psid_placeholder')}" required>
|
||||
<div class="form-hint">从浏览器开发者工具 → Application → Cookies 中获取</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="modal-secure-1psidts">${i18n.t('auth_login.secure_1psidts_label')}</label>
|
||||
<input type="text" id="modal-secure-1psidts" placeholder="${i18n.t('auth_login.secure_1psidts_placeholder')}" required>
|
||||
<div class="form-hint">从浏览器开发者工具 → Application → Cookies 中获取</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||||
<button class="btn btn-primary" onclick="manager.saveGeminiWebToken()">${i18n.t('common.save')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.showModal();
|
||||
|
||||
const modalSecure1psid = document.getElementById('modal-secure-1psid');
|
||||
const modalSecure1psidts = document.getElementById('modal-secure-1psidts');
|
||||
|
||||
if (modalSecure1psid && inlineSecure1psid) {
|
||||
modalSecure1psid.value = inlineSecure1psid.value.trim();
|
||||
}
|
||||
if (modalSecure1psidts && inlineSecure1psidts) {
|
||||
modalSecure1psidts.value = inlineSecure1psidts.value.trim();
|
||||
}
|
||||
|
||||
if (modalSecure1psid) {
|
||||
modalSecure1psid.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// 保存 Gemini Web Token
|
||||
async saveGeminiWebToken() {
|
||||
const secure1psid = document.getElementById('modal-secure-1psid').value.trim();
|
||||
const secure1psidts = document.getElementById('modal-secure-1psidts').value.trim();
|
||||
|
||||
if (!secure1psid || !secure1psidts) {
|
||||
this.showNotification('请填写完整的 Cookie 信息', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest('/gemini-web-token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
secure_1psid: secure1psid,
|
||||
secure_1psidts: secure1psidts
|
||||
})
|
||||
});
|
||||
|
||||
this.closeModal();
|
||||
this.loadAuthFiles(); // 刷新认证文件列表
|
||||
const inlineSecure1psid = document.getElementById('secure-1psid-input');
|
||||
const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
|
||||
if (inlineSecure1psid) {
|
||||
inlineSecure1psid.value = secure1psid;
|
||||
}
|
||||
if (inlineSecure1psidts) {
|
||||
inlineSecure1psidts.value = secure1psidts;
|
||||
}
|
||||
this.showNotification(`${i18n.t('auth_login.gemini_web_saved')}: ${response.file}`, 'success');
|
||||
} catch (error) {
|
||||
this.showNotification(`保存失败: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 使用统计相关方法 =====
|
||||
|
||||
// 初始化图表变量
|
||||
requestsChart = null;
|
||||
tokensChart = null;
|
||||
currentUsageData = null;
|
||||
|
||||
// 加载使用统计
|
||||
async loadUsageStats() {
|
||||
try {
|
||||
const response = await this.makeRequest('/usage');
|
||||
const usage = response?.usage || null;
|
||||
this.currentUsageData = usage;
|
||||
|
||||
if (!usage) {
|
||||
throw new Error('usage payload missing');
|
||||
}
|
||||
|
||||
// 更新概览卡片
|
||||
this.updateUsageOverview(usage);
|
||||
|
||||
// 读取当前图表周期
|
||||
const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
|
||||
const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
|
||||
const requestsPeriod = requestsHourActive ? 'hour' : 'day';
|
||||
const tokensPeriod = tokensHourActive ? 'hour' : 'day';
|
||||
|
||||
// 初始化图表(使用当前周期)
|
||||
this.initializeRequestsChart(requestsPeriod);
|
||||
this.initializeTokensChart(tokensPeriod);
|
||||
|
||||
// 更新API详细统计表格
|
||||
this.updateApiStatsTable(usage);
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载使用统计失败:', error);
|
||||
this.currentUsageData = null;
|
||||
|
||||
// 清空概览数据
|
||||
['total-requests', 'success-requests', 'failed-requests', 'total-tokens'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = '-';
|
||||
});
|
||||
|
||||
// 清空图表
|
||||
if (this.requestsChart) {
|
||||
this.requestsChart.destroy();
|
||||
this.requestsChart = null;
|
||||
}
|
||||
if (this.tokensChart) {
|
||||
this.tokensChart.destroy();
|
||||
this.tokensChart = null;
|
||||
}
|
||||
|
||||
const tableElement = document.getElementById('api-stats-table');
|
||||
if (tableElement) {
|
||||
tableElement.innerHTML = `<div class="no-data-message">${i18n.t('usage_stats.loading_error')}: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新使用统计概览
|
||||
updateUsageOverview(data) {
|
||||
const safeData = data || {};
|
||||
document.getElementById('total-requests').textContent = safeData.total_requests ?? 0;
|
||||
document.getElementById('success-requests').textContent = safeData.success_count ?? 0;
|
||||
document.getElementById('failed-requests').textContent = safeData.failure_count ?? 0;
|
||||
document.getElementById('total-tokens').textContent = safeData.total_tokens ?? 0;
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
initializeCharts() {
|
||||
const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
|
||||
const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
|
||||
this.initializeRequestsChart(requestsHourActive ? 'hour' : 'day');
|
||||
this.initializeTokensChart(tokensHourActive ? 'hour' : 'day');
|
||||
}
|
||||
|
||||
// 初始化请求趋势图表
|
||||
initializeRequestsChart(period = 'day') {
|
||||
const ctx = document.getElementById('requests-chart');
|
||||
if (!ctx) return;
|
||||
|
||||
// 销毁现有图表
|
||||
if (this.requestsChart) {
|
||||
this.requestsChart.destroy();
|
||||
}
|
||||
|
||||
const data = this.getRequestsChartData(period);
|
||||
|
||||
this.requestsChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day')
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: i18n.t('usage_stats.requests_count')
|
||||
}
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
},
|
||||
point: {
|
||||
backgroundColor: '#3b82f6',
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 2,
|
||||
radius: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化Token使用趋势图表
|
||||
initializeTokensChart(period = 'day') {
|
||||
const ctx = document.getElementById('tokens-chart');
|
||||
if (!ctx) return;
|
||||
|
||||
// 销毁现有图表
|
||||
if (this.tokensChart) {
|
||||
this.tokensChart.destroy();
|
||||
}
|
||||
|
||||
const data = this.getTokensChartData(period);
|
||||
|
||||
this.tokensChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day')
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: i18n.t('usage_stats.tokens_count')
|
||||
}
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
borderColor: '#10b981',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
},
|
||||
point: {
|
||||
backgroundColor: '#10b981',
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 2,
|
||||
radius: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取请求图表数据
|
||||
getRequestsChartData(period) {
|
||||
if (!this.currentUsageData) {
|
||||
return { labels: [], datasets: [{ data: [] }] };
|
||||
}
|
||||
|
||||
let dataSource, labels, values;
|
||||
|
||||
if (period === 'hour') {
|
||||
dataSource = this.currentUsageData.requests_by_hour || {};
|
||||
labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
|
||||
values = labels.map(hour => dataSource[hour] || 0);
|
||||
} else {
|
||||
dataSource = this.currentUsageData.requests_by_day || {};
|
||||
labels = Object.keys(dataSource).sort();
|
||||
values = labels.map(day => dataSource[day] || 0);
|
||||
}
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: values
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// 获取Token图表数据
|
||||
getTokensChartData(period) {
|
||||
if (!this.currentUsageData) {
|
||||
return { labels: [], datasets: [{ data: [] }] };
|
||||
}
|
||||
|
||||
let dataSource, labels, values;
|
||||
|
||||
if (period === 'hour') {
|
||||
dataSource = this.currentUsageData.tokens_by_hour || {};
|
||||
labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
|
||||
values = labels.map(hour => dataSource[hour] || 0);
|
||||
} else {
|
||||
dataSource = this.currentUsageData.tokens_by_day || {};
|
||||
labels = Object.keys(dataSource).sort();
|
||||
values = labels.map(day => dataSource[day] || 0);
|
||||
}
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: values
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// 切换请求图表时间周期
|
||||
switchRequestsPeriod(period) {
|
||||
// 更新按钮状态
|
||||
document.getElementById('requests-hour-btn').classList.toggle('active', period === 'hour');
|
||||
document.getElementById('requests-day-btn').classList.toggle('active', period === 'day');
|
||||
|
||||
// 更新图表数据
|
||||
if (this.requestsChart) {
|
||||
const newData = this.getRequestsChartData(period);
|
||||
this.requestsChart.data = newData;
|
||||
this.requestsChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day');
|
||||
this.requestsChart.update();
|
||||
}
|
||||
}
|
||||
|
||||
// 切换Token图表时间周期
|
||||
switchTokensPeriod(period) {
|
||||
// 更新按钮状态
|
||||
document.getElementById('tokens-hour-btn').classList.toggle('active', period === 'hour');
|
||||
document.getElementById('tokens-day-btn').classList.toggle('active', period === 'day');
|
||||
|
||||
// 更新图表数据
|
||||
if (this.tokensChart) {
|
||||
const newData = this.getTokensChartData(period);
|
||||
this.tokensChart.data = newData;
|
||||
this.tokensChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day');
|
||||
this.tokensChart.update();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新API详细统计表格
|
||||
updateApiStatsTable(data) {
|
||||
const container = document.getElementById('api-stats-table');
|
||||
if (!container) return;
|
||||
|
||||
const apis = data.apis || {};
|
||||
|
||||
if (Object.keys(apis).length === 0) {
|
||||
container.innerHTML = `<div class="no-data-message">${i18n.t('usage_stats.no_data')}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let tableHtml = `
|
||||
<table class="stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${i18n.t('usage_stats.api_endpoint')}</th>
|
||||
<th>${i18n.t('usage_stats.requests_count')}</th>
|
||||
<th>${i18n.t('usage_stats.tokens_count')}</th>
|
||||
<th>${i18n.t('usage_stats.success_rate')}</th>
|
||||
<th>${i18n.t('usage_stats.models')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
Object.entries(apis).forEach(([endpoint, apiData]) => {
|
||||
const totalRequests = apiData.total_requests || 0;
|
||||
const successCount = apiData.success_count ?? null;
|
||||
const successRate = successCount !== null && totalRequests > 0
|
||||
? Math.round((successCount / totalRequests) * 100)
|
||||
: null;
|
||||
|
||||
// 构建模型详情
|
||||
let modelsHtml = '';
|
||||
if (apiData.models && Object.keys(apiData.models).length > 0) {
|
||||
modelsHtml = '<div class="model-details">';
|
||||
Object.entries(apiData.models).forEach(([modelName, modelData]) => {
|
||||
const modelRequests = modelData.total_requests ?? 0;
|
||||
const modelTokens = modelData.total_tokens ?? 0;
|
||||
modelsHtml += `
|
||||
<div class="model-item">
|
||||
<span class="model-name">${modelName}</span>
|
||||
<span>${modelRequests} 请求 / ${modelTokens} tokens</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
modelsHtml += '</div>';
|
||||
}
|
||||
|
||||
tableHtml += `
|
||||
<tr>
|
||||
<td>${endpoint}</td>
|
||||
<td>${totalRequests}</td>
|
||||
<td>${apiData.total_tokens || 0}</td>
|
||||
<td>${successRate !== null ? successRate + '%' : '-'}</td>
|
||||
<td>${modelsHtml || '-'}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
tableHtml += '</tbody></table>';
|
||||
container.innerHTML = tableHtml;
|
||||
}
|
||||
|
||||
showModal() {
|
||||
const modal = document.getElementById('modal');
|
||||
if (modal) {
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
closeModal() {
|
||||
document.getElementById('modal').style.display = 'none';
|
||||
|
||||
30
build-scripts/prepare-html.js
Normal file
30
build-scripts/prepare-html.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Read the original index.html
|
||||
const indexPath = path.resolve(__dirname, '../index.html');
|
||||
const outputPath = path.resolve(__dirname, '../index.build.html');
|
||||
|
||||
let htmlContent = fs.readFileSync(indexPath, 'utf8');
|
||||
|
||||
// Remove local CSS reference
|
||||
htmlContent = htmlContent.replace(
|
||||
/<link rel="stylesheet" href="styles\.css">\n?/g,
|
||||
''
|
||||
);
|
||||
|
||||
// Remove local JavaScript references
|
||||
htmlContent = htmlContent.replace(
|
||||
/<script src="i18n\.js"><\/script>\n?/g,
|
||||
''
|
||||
);
|
||||
|
||||
htmlContent = htmlContent.replace(
|
||||
/<script src="app\.js"><\/script>\n?/g,
|
||||
''
|
||||
);
|
||||
|
||||
// Write the modified HTML to a temporary build file
|
||||
fs.writeFileSync(outputPath, htmlContent, 'utf8');
|
||||
|
||||
console.log('✓ Generated index.build.html for webpack processing');
|
||||
25
bundle-entry.js
Normal file
25
bundle-entry.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// Import CSS
|
||||
import './styles.css';
|
||||
|
||||
// Import JavaScript modules
|
||||
import './i18n.js';
|
||||
import './app.js';
|
||||
|
||||
// Import logo image
|
||||
import logoImg from './logo.jpg';
|
||||
|
||||
// Set logo after DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const loginLogo = document.getElementById('login-logo');
|
||||
const siteLogo = document.getElementById('site-logo');
|
||||
|
||||
if (loginLogo) {
|
||||
loginLogo.src = logoImg;
|
||||
loginLogo.style.display = 'block';
|
||||
}
|
||||
|
||||
if (siteLogo) {
|
||||
siteLogo.src = logoImg;
|
||||
siteLogo.style.display = 'block';
|
||||
}
|
||||
});
|
||||
84
i18n.js
84
i18n.js
@@ -85,6 +85,7 @@ const i18n = {
|
||||
'nav.api_keys': 'API 密钥',
|
||||
'nav.ai_providers': 'AI 提供商',
|
||||
'nav.auth_files': '认证文件',
|
||||
'nav.usage_stats': '使用统计',
|
||||
'nav.system_info': '系统信息',
|
||||
|
||||
// 基础设置
|
||||
@@ -102,8 +103,6 @@ const i18n = {
|
||||
'basic_settings.quota_title': '配额超出行为',
|
||||
'basic_settings.quota_switch_project': '自动切换项目',
|
||||
'basic_settings.quota_switch_preview': '切换到预览模型',
|
||||
'basic_settings.localhost_title': '本地访问',
|
||||
'basic_settings.localhost_allow': '允许本地未认证访问',
|
||||
|
||||
// API 密钥管理
|
||||
'api_keys.title': 'API 密钥管理',
|
||||
@@ -182,9 +181,11 @@ const i18n = {
|
||||
'ai_providers.openai_keys_count': '密钥数量',
|
||||
'ai_providers.openai_models_count': '模型数量',
|
||||
|
||||
|
||||
// 认证文件管理
|
||||
'auth_files.title': '认证文件管理',
|
||||
'auth_files.title_section': '认证文件',
|
||||
'auth_files.description': '这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。',
|
||||
'auth_files.upload_button': '上传文件',
|
||||
'auth_files.delete_all_button': '删除全部',
|
||||
'auth_files.empty_title': '暂无认证文件',
|
||||
@@ -202,6 +203,36 @@ const i18n = {
|
||||
'auth_files.delete_all_success': '成功删除',
|
||||
'auth_files.files_count': '个文件',
|
||||
|
||||
// Gemini Web Token
|
||||
'auth_login.gemini_web_title': 'Gemini Web Token',
|
||||
'auth_login.gemini_web_button': '保存 Gemini Web Token',
|
||||
'auth_login.gemini_web_hint': '从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。',
|
||||
'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:',
|
||||
'auth_login.secure_1psid_placeholder': '输入 __Secure-1PSID cookie 值',
|
||||
'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:',
|
||||
'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值',
|
||||
'auth_login.gemini_web_saved': 'Gemini Web Token 保存成功',
|
||||
|
||||
// 使用统计
|
||||
'usage_stats.title': '使用统计',
|
||||
'usage_stats.total_requests': '总请求数',
|
||||
'usage_stats.success_requests': '成功请求',
|
||||
'usage_stats.failed_requests': '失败请求',
|
||||
'usage_stats.total_tokens': '总Token数',
|
||||
'usage_stats.requests_trend': '请求趋势',
|
||||
'usage_stats.tokens_trend': 'Token 使用趋势',
|
||||
'usage_stats.api_details': 'API 详细统计',
|
||||
'usage_stats.by_hour': '按小时',
|
||||
'usage_stats.by_day': '按天',
|
||||
'usage_stats.refresh': '刷新',
|
||||
'usage_stats.no_data': '暂无数据',
|
||||
'usage_stats.loading_error': '加载失败',
|
||||
'usage_stats.api_endpoint': 'API端点',
|
||||
'usage_stats.requests_count': '请求次数',
|
||||
'usage_stats.tokens_count': 'Token数量',
|
||||
'usage_stats.models': '模型统计',
|
||||
'usage_stats.success_rate': '成功率',
|
||||
|
||||
// 系统信息
|
||||
'system_info.title': '系统信息',
|
||||
'system_info.connection_status_title': '连接状态',
|
||||
@@ -220,7 +251,6 @@ const i18n = {
|
||||
'notification.retry_updated': '重试设置已更新',
|
||||
'notification.quota_switch_project_updated': '项目切换设置已更新',
|
||||
'notification.quota_switch_preview_updated': '预览模型切换设置已更新',
|
||||
'notification.localhost_updated': '本地访问设置已更新',
|
||||
'notification.api_key_added': 'API密钥添加成功',
|
||||
'notification.api_key_updated': 'API密钥更新成功',
|
||||
'notification.api_key_deleted': 'API密钥删除成功',
|
||||
@@ -264,7 +294,11 @@ const i18n = {
|
||||
'theme.dark': '暗色',
|
||||
'theme.switch_to_light': '切换到亮色模式',
|
||||
'theme.switch_to_dark': '切换到暗色模式',
|
||||
'theme.auto': '跟随系统'
|
||||
'theme.auto': '跟随系统',
|
||||
|
||||
// 页脚
|
||||
'footer.version': '版本',
|
||||
'footer.author': '作者'
|
||||
},
|
||||
|
||||
'en-US': {
|
||||
@@ -346,6 +380,7 @@ const i18n = {
|
||||
'nav.api_keys': 'API Keys',
|
||||
'nav.ai_providers': 'AI Providers',
|
||||
'nav.auth_files': 'Auth Files',
|
||||
'nav.usage_stats': 'Usage Statistics',
|
||||
'nav.system_info': 'System Info',
|
||||
|
||||
// Basic settings
|
||||
@@ -363,8 +398,6 @@ const i18n = {
|
||||
'basic_settings.quota_title': 'Quota Exceeded Behavior',
|
||||
'basic_settings.quota_switch_project': 'Auto Switch Project',
|
||||
'basic_settings.quota_switch_preview': 'Switch to Preview Model',
|
||||
'basic_settings.localhost_title': 'Local Access',
|
||||
'basic_settings.localhost_allow': 'Allow Localhost Unauthenticated Access',
|
||||
|
||||
// API Keys management
|
||||
'api_keys.title': 'API Keys Management',
|
||||
@@ -443,9 +476,11 @@ const i18n = {
|
||||
'ai_providers.openai_keys_count': 'Keys Count',
|
||||
'ai_providers.openai_models_count': 'Models Count',
|
||||
|
||||
|
||||
// Auth files management
|
||||
'auth_files.title': 'Auth Files Management',
|
||||
'auth_files.title_section': 'Auth Files',
|
||||
'auth_files.description': 'Here you can manage authentication configuration files for Qwen and Gemini. Upload JSON format authentication files to enable the corresponding AI services.',
|
||||
'auth_files.upload_button': 'Upload File',
|
||||
'auth_files.delete_all_button': 'Delete All',
|
||||
'auth_files.empty_title': 'No Auth Files',
|
||||
@@ -463,6 +498,36 @@ const i18n = {
|
||||
'auth_files.delete_all_success': 'Successfully deleted',
|
||||
'auth_files.files_count': 'files',
|
||||
|
||||
// Gemini Web Token
|
||||
'auth_login.gemini_web_title': 'Gemini Web Token',
|
||||
'auth_login.gemini_web_button': 'Save Gemini Web Token',
|
||||
'auth_login.gemini_web_hint': 'Obtain the Cookie value of the Gemini web version from the browser\'s developer tools, used for direct authentication to access Gemini.',
|
||||
'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:',
|
||||
'auth_login.secure_1psid_placeholder': 'Enter __Secure-1PSID cookie value',
|
||||
'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:',
|
||||
'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value',
|
||||
'auth_login.gemini_web_saved': 'Gemini Web Token saved successfully',
|
||||
|
||||
// Usage Statistics
|
||||
'usage_stats.title': 'Usage Statistics',
|
||||
'usage_stats.total_requests': 'Total Requests',
|
||||
'usage_stats.success_requests': 'Success Requests',
|
||||
'usage_stats.failed_requests': 'Failed Requests',
|
||||
'usage_stats.total_tokens': 'Total Tokens',
|
||||
'usage_stats.requests_trend': 'Request Trends',
|
||||
'usage_stats.tokens_trend': 'Token Usage Trends',
|
||||
'usage_stats.api_details': 'API Details',
|
||||
'usage_stats.by_hour': 'By Hour',
|
||||
'usage_stats.by_day': 'By Day',
|
||||
'usage_stats.refresh': 'Refresh',
|
||||
'usage_stats.no_data': 'No Data Available',
|
||||
'usage_stats.loading_error': 'Loading Failed',
|
||||
'usage_stats.api_endpoint': 'API Endpoint',
|
||||
'usage_stats.requests_count': 'Request Count',
|
||||
'usage_stats.tokens_count': 'Token Count',
|
||||
'usage_stats.models': 'Model Statistics',
|
||||
'usage_stats.success_rate': 'Success Rate',
|
||||
|
||||
// System info
|
||||
'system_info.title': 'System Information',
|
||||
'system_info.connection_status_title': 'Connection Status',
|
||||
@@ -481,7 +546,6 @@ const i18n = {
|
||||
'notification.retry_updated': 'Retry settings updated',
|
||||
'notification.quota_switch_project_updated': 'Project switch settings updated',
|
||||
'notification.quota_switch_preview_updated': 'Preview model switch settings updated',
|
||||
'notification.localhost_updated': 'Localhost access settings updated',
|
||||
'notification.api_key_added': 'API key added successfully',
|
||||
'notification.api_key_updated': 'API key updated successfully',
|
||||
'notification.api_key_deleted': 'API key deleted successfully',
|
||||
@@ -525,7 +589,11 @@ const i18n = {
|
||||
'theme.dark': 'Dark',
|
||||
'theme.switch_to_light': 'Switch to light mode',
|
||||
'theme.switch_to_dark': 'Switch to dark mode',
|
||||
'theme.auto': 'Follow system'
|
||||
'theme.auto': 'Follow system',
|
||||
|
||||
// Footer
|
||||
'footer.version': 'Version',
|
||||
'footer.author': 'Author'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
167
index.html
167
index.html
@@ -6,6 +6,7 @@
|
||||
<title data-i18n="title.login">CLI Proxy API Management Center</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="i18n.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -221,6 +222,9 @@
|
||||
<li><a href="#auth-files" class="nav-item" data-section="auth-files">
|
||||
<i class="fas fa-file-alt"></i> <span data-i18n="nav.auth_files">认证文件</span>
|
||||
</a></li>
|
||||
<li><a href="#usage-stats" class="nav-item" data-section="usage-stats">
|
||||
<i class="fas fa-chart-line"></i> <span data-i18n="nav.usage_stats">使用统计</span>
|
||||
</a></li>
|
||||
<li><a href="#system-info" class="nav-item" data-section="system-info">
|
||||
<i class="fas fa-info-circle"></i> <span data-i18n="nav.system_info">系统信息</span>
|
||||
</a></li>
|
||||
@@ -305,21 +309,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 本地访问设置 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-home"></i> <span data-i18n="basic_settings.localhost_title">本地访问</span></h3>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="toggle-group">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="allow-localhost-toggle">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span class="toggle-label" data-i18n="basic_settings.localhost_allow">允许本地未认证访问</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- API 密钥管理 -->
|
||||
@@ -394,12 +383,45 @@
|
||||
<div id="openai-providers-list" class="provider-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Gemini Web Token -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fab fa-google"></i> <span data-i18n="auth_login.gemini_web_title">Gemini Web Token</span></h3>
|
||||
<button id="gemini-web-token-btn" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> <span data-i18n="auth_login.gemini_web_button">保存 Gemini Web Token</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="form-hint" style="margin-bottom: 20px;" data-i18n="auth_login.gemini_web_hint">
|
||||
从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="secure-1psid-input" data-i18n="auth_login.secure_1psid_label">__Secure-1PSID Cookie:</label>
|
||||
<input type="text" id="secure-1psid-input" data-i18n="auth_login.secure_1psid_placeholder" placeholder="输入 __Secure-1PSID cookie 值">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="secure-1psidts-input" data-i18n="auth_login.secure_1psidts_label">__Secure-1PSIDTS Cookie:</label>
|
||||
<input type="text" id="secure-1psidts-input" data-i18n="auth_login.secure_1psidts_placeholder" placeholder="输入 __Secure-1PSIDTS cookie 值">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 认证文件管理 -->
|
||||
<section id="auth-files" class="content-section">
|
||||
<h2 data-i18n="auth_files.title">认证文件管理</h2>
|
||||
|
||||
<div class="card" style="margin-bottom: 20px;">
|
||||
<div class="card-content">
|
||||
<p class="form-hint" data-i18n="auth_files.description">
|
||||
这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 认证文件 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-file-alt"></i> <span data-i18n="auth_files.title_section">认证文件</span></h3>
|
||||
@@ -419,6 +441,112 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 使用统计 -->
|
||||
<section id="usage-stats" class="content-section">
|
||||
<h2 data-i18n="usage_stats.title">使用统计</h2>
|
||||
|
||||
<!-- 概览统计卡片 -->
|
||||
<div class="stats-overview">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="total-requests">0</div>
|
||||
<div class="stat-label" data-i18n="usage_stats.total_requests">总请求数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="success-requests">0</div>
|
||||
<div class="stat-label" data-i18n="usage_stats.success_requests">成功请求</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon error">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="failed-requests">0</div>
|
||||
<div class="stat-label" data-i18n="usage_stats.failed_requests">失败请求</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<i class="fas fa-coins"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-number" id="total-tokens">0</div>
|
||||
<div class="stat-label" data-i18n="usage_stats.total_tokens">总Token数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="charts-container">
|
||||
<!-- 请求趋势图 -->
|
||||
<div class="card chart-card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-chart-line"></i> <span data-i18n="usage_stats.requests_trend">请求趋势</span></h3>
|
||||
<div class="chart-controls">
|
||||
<button class="btn btn-small" data-period="hour" id="requests-hour-btn">
|
||||
<span data-i18n="usage_stats.by_hour">按小时</span>
|
||||
</button>
|
||||
<button class="btn btn-small active" data-period="day" id="requests-day-btn">
|
||||
<span data-i18n="usage_stats.by_day">按天</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="chart-container">
|
||||
<canvas id="requests-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token使用趋势图 -->
|
||||
<div class="card chart-card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-chart-area"></i> <span data-i18n="usage_stats.tokens_trend">Token 使用趋势</span></h3>
|
||||
<div class="chart-controls">
|
||||
<button class="btn btn-small" data-period="hour" id="tokens-hour-btn">
|
||||
<span data-i18n="usage_stats.by_hour">按小时</span>
|
||||
</button>
|
||||
<button class="btn btn-small active" data-period="day" id="tokens-day-btn">
|
||||
<span data-i18n="usage_stats.by_day">按天</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="chart-container">
|
||||
<canvas id="tokens-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API详细统计 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-list"></i> <span data-i18n="usage_stats.api_details">API 详细统计</span></h3>
|
||||
<button id="refresh-usage-stats" class="btn btn-primary">
|
||||
<i class="fas fa-sync-alt"></i> <span data-i18n="usage_stats.refresh">刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div id="api-stats-table" class="api-stats-table">
|
||||
<div class="loading-placeholder" data-i18n="common.loading">正在加载...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 系统信息 -->
|
||||
<section id="system-info" class="content-section">
|
||||
<h2 data-i18n="system_info.title">系统信息</h2>
|
||||
@@ -447,6 +575,15 @@
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 版本信息 -->
|
||||
<footer class="version-footer">
|
||||
<div class="version-info">
|
||||
<span data-i18n="footer.version">版本</span>: v0.0.3
|
||||
<span class="separator">•</span>
|
||||
<span data-i18n="footer.author">作者</span>: Supra4E8C
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- 模态框 -->
|
||||
|
||||
3136
package-lock.json
generated
Normal file
3136
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -6,7 +6,9 @@
|
||||
"scripts": {
|
||||
"start": "npx serve .",
|
||||
"dev": "npx serve . --port 3000",
|
||||
"build": "echo '无需构建,直接使用静态文件'",
|
||||
"prebuild": "node build-scripts/prepare-html.js",
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"postbuild": "rm -f index.build.html",
|
||||
"lint": "echo '使用浏览器开发者工具检查代码'"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -18,7 +20,14 @@
|
||||
"author": "CLI Proxy API WebUI",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"serve": "^14.2.1"
|
||||
"css-loader": "^6.8.1",
|
||||
"html-inline-script-webpack-plugin": "^3.2.1",
|
||||
"html-loader": "^4.2.0",
|
||||
"html-webpack-plugin": "^5.5.4",
|
||||
"serve": "^14.2.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"webpack": "^5.102.0",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -26,6 +35,5 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "local"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
}
|
||||
|
||||
275
styles.css
275
styles.css
@@ -27,6 +27,9 @@
|
||||
--accent-primary: linear-gradient(135deg, #475569, #334155);
|
||||
--accent-secondary: #e2e8f0;
|
||||
--accent-tertiary: #f8fafc;
|
||||
--primary-color: #3b82f6;
|
||||
--card-bg: #ffffff;
|
||||
--border-color: #e2e8f0;
|
||||
|
||||
--success-bg: linear-gradient(135deg, #dcfce7, #bbf7d0);
|
||||
--success-text: #166534;
|
||||
@@ -69,6 +72,9 @@
|
||||
--accent-primary: linear-gradient(135deg, #64748b, #475569);
|
||||
--accent-secondary: #334155;
|
||||
--accent-tertiary: #1e293b;
|
||||
--primary-color: #38bdf8;
|
||||
--card-bg: #1e293b;
|
||||
--border-color: #334155;
|
||||
|
||||
--success-bg: linear-gradient(135deg, #064e3b, #047857);
|
||||
--success-text: #bbf7d0;
|
||||
@@ -436,6 +442,7 @@
|
||||
|
||||
.local-url-group input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
@@ -1572,3 +1579,271 @@ input:checked + .slider:before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Gemini Web Token 模态框样式 */
|
||||
.gemini-web-form .form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.gemini-web-form .form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.gemini-web-form .form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
|
||||
.gemini-web-form .form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: 0 0 0 3px var(--border-primary);
|
||||
}
|
||||
|
||||
.gemini-web-form .form-hint {
|
||||
margin-top: 6px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 使用统计样式 */
|
||||
.stats-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
border-color: var(--border-primary);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-icon.success {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.stat-icon.error {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.charts-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn.btn-small {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--card-bg);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn.btn-small:hover {
|
||||
border-color: var(--border-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn.btn-small.active {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.api-stats-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.stats-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.stats-table th,
|
||||
.stats-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stats-table th {
|
||||
background: var(--bg-secondary);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-table td {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-table tr:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.model-details {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.model-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.model-name {
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
color: var(--text-tertiary);
|
||||
font-style: italic;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
/* 暗色主题适配 */
|
||||
[data-theme="dark"] .stat-card {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .stat-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn.btn-small {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
/* 版本信息样式 */
|
||||
.version-footer {
|
||||
margin-top: 2rem;
|
||||
padding: 1rem 0;
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.version-info {
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.version-info .separator {
|
||||
margin: 0 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 暗黑主题下的版本信息 */
|
||||
[data-theme="dark"] .version-footer {
|
||||
border-top-color: var(--border);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .version-info {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
|
||||
64
webpack.config.js
Normal file
64
webpack.config.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HtmlInlineScriptPlugin = require('html-inline-script-webpack-plugin');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: {
|
||||
main: './bundle-entry.js'
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: '[name].bundle.js',
|
||||
clean: true
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: ['style-loader', 'css-loader']
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|jpeg|gif|svg)$/i,
|
||||
type: 'asset/inline'
|
||||
},
|
||||
{
|
||||
test: /\.html$/i,
|
||||
loader: 'html-loader',
|
||||
options: {
|
||||
sources: {
|
||||
list: [
|
||||
{
|
||||
tag: 'img',
|
||||
attribute: 'src',
|
||||
type: 'src'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: './index.build.html',
|
||||
filename: 'index.html',
|
||||
inject: 'body',
|
||||
minify: {
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
removeRedundantAttributes: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
useShortDoctype: true
|
||||
}
|
||||
}),
|
||||
new HtmlInlineScriptPlugin({
|
||||
htmlMatchPattern: [/index.html$/],
|
||||
scriptMatchPattern: [/.js$/]
|
||||
})
|
||||
],
|
||||
optimization: {
|
||||
minimize: true
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user