Compare commits

...

65 Commits

Author SHA1 Message Date
Calcium-Ion
c1a5b0e1bb Merge pull request #623 from Calcium-Ion/channel-setting
feat: implement channel settings configuration
2024-12-15 15:55:13 +08:00
CalciumIon
f2809917f8 feat: implement channel settings configuration
fix #620
2024-12-15 15:52:41 +08:00
CalciumIon
a4c43bb83b feat: Enhance Operation Settings with Group and Model Ratio Management
- Added new components for GroupRatioSettings and ModelRatioSettings to manage group and model ratios.
- Integrated tabs in OperationSetting to switch between model and visual ratio settings.
- Updated translations for new settings and improved existing ones in the English locale file.
- Refactored ModelSettingsVisualEditor to support dynamic pricing and ratio configurations.

This update improves the user interface for managing operational settings, enhancing usability and localization support.
2024-12-14 22:13:31 +08:00
Calcium-Ion
6ca68651ff Merge pull request #618 from HynoR/feat/modeledit
feat: 可视化模型定价编辑器
2024-12-14 21:32:05 +08:00
Calcium-Ion
e2f6444b83 Merge pull request #617 from kingxjs/main
Available models could not be populated when adding a new channel
2024-12-14 21:31:05 +08:00
Calcium-Ion
8e2b6d0aad Merge pull request #622 from Calcium-Ion/i18n-fix
feat: Enhance i18n support
2024-12-14 14:12:27 +08:00
CalciumIon
41a7cee98e feat: Refactor App and ChannelsTable components for improved i18n support
- Removed redundant user and status loading logic from the App component, centralizing it in the PageLayout component for better maintainability.
- Enhanced the ChannelsTable component by integrating translation functions for various UI elements, ensuring consistent localization of titles and modal messages.
- Updated the English locale file with new translation keys for sub-channel modifications, improving the overall localization coverage.
- Streamlined the code structure in multiple components to enhance readability and performance.
2024-12-14 14:09:30 +08:00
CalciumIon
68b87736b6 feat: Enhance i18n support in Home component and update translations
- Integrated translation functions in the Home component to support dynamic localization for various UI elements, improving accessibility for users in different languages.
- Added new translation keys for "Telegram authentication", "Linux DO authentication", and "License" in the English locale file, expanding the localization coverage.
- Updated existing text elements to utilize translation functions, ensuring consistency in language display across the application.
2024-12-14 12:58:10 +08:00
CalciumIon
b86aeb9150 feat: Implement status loading in App component and refactor SiderBar
- Added a new function to load status data from the API in the App component, enhancing the application's ability to display real-time status updates.
- Integrated error handling for API calls to improve user feedback in case of connection issues.
- Removed the redundant status loading logic from the SiderBar component, streamlining the code and ensuring a single source of truth for status management.
- Updated the useEffect hook in SiderBar to maintain sidebar collapse state based on local storage, improving user experience.
2024-12-14 12:57:56 +08:00
CalciumIon
cfdf6e48f1 Update README 2024-12-13 23:48:18 +08:00
CalciumIon
68b7e0e96a refactor: Remove unused translation function calls in LogsTable component
- Eliminated unnecessary calls to the translation function in the LogsTable component, streamlining the code and improving performance.
- This change enhances readability and reduces potential overhead from unused localization logic.
2024-12-13 22:34:10 +08:00
CalciumIon
84130cdf50 Update README.en.md 2024-12-13 21:21:28 +08:00
CalciumIon
d54cab2ab9 Add README.en.md 2024-12-13 20:21:34 +08:00
CalciumIon
7aa8b88c89 Update README.md 2024-12-13 20:15:50 +08:00
CalciumIon
a485db5065 fix: Refine sider visibility logic in HeaderBar component
- Updated the click handler in HeaderBar to correctly toggle the visibility of the sider based on the current item selection.
- Ensured that the sider is hidden when navigating to the home item and displayed conditionally for other items, improving the user interface responsiveness.
2024-12-13 19:28:09 +08:00
Calcium-Ion
5f06feb9a1 feat: support i18n
feat: support i18n
2024-12-13 19:24:15 +08:00
CalciumIon
221d7b5c99 feat: Integrate i18n support and enhance UI text localization
- Added internationalization (i18n) support across various components, enabling dynamic language switching and improved user experience.
- Updated multiple components to utilize translation functions for labels, buttons, and messages, ensuring consistent language display.
- Enhanced the user interface by refining text elements in the ChannelsTable, LogsTable, and various settings pages, improving clarity and accessibility.
- Adjusted CSS styles for better responsiveness and layout consistency across different screen sizes.
2024-12-13 19:03:14 +08:00
HynoR
3587f2c6e9 feat: 增加价格和倍率的互斥验证,优化模型名称输入提示 2024-12-13 14:45:49 +08:00
HynoR
498590d9fd feat: 优化模型设置可视化编辑器,增强输入验证和提示信息 2024-12-13 14:42:02 +08:00
HynoR
8504e07245 feat: 添加保存功能并优化模型数据提交逻辑 2024-12-13 14:29:43 +08:00
TAKO
2eddb93432 Merge branch 'Calcium-Ion:main' into feat/modeledit 2024-12-13 14:11:31 +08:00
HynoR
369ecf365a feat: 添加模型设置可视化编辑器组件 2024-12-13 14:10:38 +08:00
迷糊虫
36bf4b3926 Update EditChannel.js
Fixes an issue with the OpenAI models interface where it fails to get a list of models.
2024-12-13 13:36:03 +08:00
CalciumIon
2a6f3ad27f fix: Adjust inner padding style in PageLayout component
- Updated the overflowY style to always be 'auto', ensuring consistent scrolling behavior.
- Maintained conditional inner padding based on the styleState, enhancing layout responsiveness.
2024-12-12 23:34:14 +08:00
CalciumIon
cd21aa1c56 feat: init i18n 2024-12-12 23:32:55 +08:00
CalciumIon
16599a900b fix: Refine sider visibility and inner padding logic in StyleProvider component
- Consolidated the logic for managing sider visibility and inner padding based on the current pathname, improving responsiveness to navigation changes.
- Ensured that the sider is hidden on specific paths and adjusted inner padding accordingly, enhancing the user interface experience on both mobile and desktop views.
2024-12-12 20:52:22 +08:00
CalciumIon
d241e4fe29 fix: Update label truncation logic in Playground and adjust sider visibility in HeaderBar
- Modified the group label truncation in the Playground component to shorten labels exceeding 16 characters for better mobile display.
- Corrected the conditional rendering logic in the HeaderBar to toggle the sider visibility based on its current state, enhancing user interface responsiveness.
2024-12-12 20:39:49 +08:00
CalciumIon
e17f36e7b7 fix: Correct inner padding and sider visibility logic in HeaderBar, PageLayout, and SiderBar components
- Updated the click handler in HeaderBar to toggle inner padding and sider visibility correctly based on the selected item.
- Adjusted the conditional rendering of SiderBar in PageLayout to ensure it displays when the sider is shown.
- Refined the inner padding logic in SiderBar to maintain consistent behavior when selecting items.
- Introduced a new function in Style context to manage sider visibility based on the current pathname, enhancing responsiveness to navigation changes.
2024-12-12 20:31:40 +08:00
CalciumIon
263547ebb7 refactor: Simplify average calculations in Detail component
- Streamlined the calculation of average RPM and average TPM by removing unnecessary function calls and directly applying the `toFixed(3)` method within the JSX.
- Improved code readability and maintainability by reducing the number of lines and enhancing clarity in the calculation logic.
2024-12-12 19:21:08 +08:00
CalciumIon
5d338337a0 feat: 兼容OpenAI格式下设置gemini模型联网搜索 #615 2024-12-12 17:58:25 +08:00
CalciumIon
b1fb595610 feat: add model gemini-2.0-flash-exp 2024-12-12 17:21:37 +08:00
CalciumIon
44512d3c28 feat: Enhance group label display in Playground component
- Updated the group selection input to truncate long labels on mobile devices, ensuring better readability and a cleaner interface.
- Implemented a conditional label adjustment that shortens group names exceeding 18 characters, appending '...' for clarity.
2024-12-12 16:35:13 +08:00
Calcium-Ion
430d5fcd6a Merge pull request #616 from Calcium-Ion/panel
feat: 完善数据看板功能
2024-12-12 16:19:27 +08:00
CalciumIon
6625563f80 feat: Enhance quota data handling and CSS styling
- Updated the `increaseQuotaData` function to include `tokenUsed` parameter for better quota tracking.
- Modified the `GetAllQuotaDates` function to sum `token_used` alongside `count` and `quota` for comprehensive data retrieval.
- Improved CSS styles for better layout responsiveness, including padding adjustments for navigation elements and description cards.
2024-12-12 16:18:14 +08:00
CalciumIon
b2d36b946d feat: Update SiderBar and Detail components for improved navigation and data visualization
- Removed the '模型价格' (Pricing) link from the SiderBar for a cleaner interface.
- Added a new '数据看板' (Data Dashboard) link to the SiderBar, enhancing navigation options.
- Refactored the Detail component to include user context and style context for better state management.
- Introduced new state variables to track token consumption and updated data handling for charts.
- Enhanced the layout with additional cards and tabs for displaying user quota and usage statistics.
- Improved data processing logic for pie and line charts, ensuring accurate representation of user data.
2024-12-12 16:11:17 +08:00
CalciumIon
ab4c9fdb8f feat: Enhance color mapping and chart rendering in Detail component
- Added base and extended color palettes for improved model color mapping.
- Introduced a new `modelToColor` function to dynamically assign colors based on model names.
- Updated the Detail component to utilize the new color mapping for pie and line charts.
- Refactored chart data handling to support dynamic color assignment and improved data visualization.
- Cleaned up unused state variables and optimized data loading logic for better performance.
2024-12-12 14:56:16 +08:00
CalciumIon
79de02b05f chore: Update dependencies and refactor JSON handling #614
- Removed the `bytedance/sonic` dependency and replaced its usage with the standard `encoding/json` package for JSON marshalling in `relay-text.go`.
- Updated `go.mod` to reflect the removal of `sonic` and adjusted the version of `sonic/loader`.
- Cleaned up `go.sum` to ensure consistency with the updated dependencies.
2024-12-12 14:14:24 +08:00
Calcium-Ion
0455f30d16 Merge pull request #613 from Calcium-Ion/mobile
feat: Add pricing link to HeaderBar component
2024-12-11 23:14:45 +08:00
Calcium-Ion
21d4dcadab Merge pull request #612 from Calcium-Ion/mobile
feat: Refactor style management for inner padding in layout components
2024-12-11 23:14:10 +08:00
CalciumIon
f0d9c89659 feat: Add pricing link to HeaderBar component
- Introduced a new '定价' (Pricing) item in the HeaderBar navigation for better accessibility to pricing information.
- Updated routing to include the new '/pricing' path.
- Adjusted user display in the HeaderBar for mobile responsiveness, hiding the username on smaller screens for a cleaner interface.
2024-12-11 23:13:46 +08:00
CalciumIon
28fa77cc92 feat: Refactor style management for inner padding in layout components
- Updated HeaderBar, PageLayout, and SiderBar components to manage inner padding state based on selected items.
- Replaced `isChatPage` state with `shouldInnerPadding` in Style context for better clarity and functionality.
- Enhanced user experience by dynamically adjusting content padding based on navigation selections.
2024-12-11 23:08:52 +08:00
Calcium-Ion
db84b26e2d Merge pull request #611 from Calcium-Ion/mobile
feat: 前端美化
2024-12-11 21:41:09 +08:00
CalciumIon
024cdb08df feat: Update model lists and enhance model retrieval in Adaptor
- Refactored ModelList in the gemini constant to include new models and remove outdated ones.
- Modified the GetModelList function in the Adaptor to consolidate model lists from multiple sources, ensuring a comprehensive and updated list is returned.
- Commented out deprecated models in the vertex constants for clarity and future reference.
2024-12-11 21:39:41 +08:00
CalciumIon
89136dfa9e feat: Add filtering and search functionality to model selection in EditChannel and EditTagModal
- Implemented filter and search position options in the model selection dropdowns for both EditChannel and EditTagModal components.
- Enhanced user experience by allowing users to easily find and select models from a potentially large list.
2024-12-11 21:33:30 +08:00
CalciumIon
5f0322b672 feat: Add custom model input functionality in EditTagModal
- Introduced a new input field for adding custom model names in the EditTagModal component.
- Implemented logic to handle the addition of custom models, including validation to prevent duplicates.
- Enhanced user experience by providing feedback when attempting to add existing models.
- Updated state management to reflect changes in the model options dynamically.
2024-12-11 21:31:29 +08:00
CalciumIon
379b08f691 feat: Update user group handling in Playground component
- Enhanced the Playground component to prioritize the user's group by moving it to the front of the local group options if it exists.
- Improved user experience by ensuring the default group selection reflects the user's current group, if available.
2024-12-11 21:25:50 +08:00
CalciumIon
afb7b661ee feat: Implement chat page state management in layout and sidebar
- Added `isChatPage` state to the Style context to manage chat page layout.
- Updated `PageLayout` component to adjust padding based on the chat page state.
- Enhanced `SiderBar` component to dispatch chat page state changes when chat-related items are selected.
2024-12-11 21:17:46 +08:00
CalciumIon
60710d6c68 feat: Add renderModelPriceSimple function and update LogsTable component
- Introduced a new helper function `renderModelPriceSimple` to simplify the rendering of model price information.
- Updated the `LogsTable` component to utilize `renderModelPriceSimple`, enhancing the display of model pricing and grouping information.
- Removed the previous implementation of `renderModelPrice` from the `LogsTable` for cleaner code.
2024-12-11 21:06:26 +08:00
CalciumIon
77b8d918de refactor: Simplify PersonalSetting component layout
- Moved footer content from the Card component to a separate Descriptions component for better structure.
- Maintained the display of user quota, historical consumption, and request count while improving readability.
2024-12-11 20:36:44 +08:00
Calcium-Ion
69f57728b2 Merge pull request #610 from Calcium-Ion/mobile
feat: Update dependencies and restructure Playground component
2024-12-11 18:28:11 +08:00
CalciumIon
7cab9d7c8a feat: Update dependencies and restructure Playground component
- Upgraded @douyinfe/semi-ui from version 2.63.1 to 2.69.1 in package.json.
- Updated pnpm-lock.yaml to reflect new dependency versions and lockfile format.
- Moved Playground component to a new directory structure under pages.
- Enhanced Playground component with new features and improved user experience.
2024-12-11 18:27:30 +08:00
Calcium-Ion
e5dc21d56b Merge pull request #609 from Calcium-Ion/mobile
feat: 界面美化
2024-12-11 17:33:32 +08:00
CalciumIon
713de36ecd feat: Enhance EditRedemption component with default name handling 2024-12-11 17:28:59 +08:00
CalciumIon
64e085dc4c feat: 首页优化 2024-12-11 17:19:03 +08:00
CalciumIon
3622c664b6 feat: 侧边栏移动端优化 2024-12-11 16:11:27 +08:00
CalciumIon
18a8216a43 feat: 优化playground搜索模型功能 2024-12-10 23:48:55 +08:00
CalciumIon
5d1087a6a9 fix: 编辑标签文字错误 2024-12-09 23:45:12 +08:00
CalciumIon
cf8b30edfa fix: edit channel weight and priority 2024-12-09 21:26:17 +08:00
CalciumIon
56ccb30a94 fix: 渠道标签开启下使用ID排序出错 2024-12-09 20:38:03 +08:00
CalciumIon
2c79811cb1 feat: update playground roleConfig 2024-12-09 15:03:04 +08:00
Calcium-Ion
1e1a22e7b3 Merge pull request #605 from jochne/patch-1
Update relay-xunfei.go
2024-12-08 18:50:56 +08:00
jochne
70b5a7fd88 Update relay-xunfei.go
按照讯飞的最新文档,Spark Lite请求地址,对应的domain参数为lite
参考来源:https://www.xfyun.cn/doc/spark/Web.html#_1-接口说明
2024-12-08 01:04:43 +08:00
CalciumIon
dd293f80ae fix: telegram register 2024-12-07 18:08:51 +08:00
Calcium-Ion
904a1858e4 Merge pull request #600 from wzxjohn/upstream
feat: support Azure Comm Service SMTP
2024-12-07 15:24:17 +08:00
wzxjohn
568d4e3f71 feat: support Azure Comm Service SMTP 2024-12-07 00:37:11 +08:00
73 changed files with 8987 additions and 4501 deletions

150
README.en.md Normal file
View File

@@ -0,0 +1,150 @@
<div align="center">
![new-api](/web/public/logo.png)
# New API
🍥 Next Generation LLM Gateway and AI Asset Management System
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<p align="center">
<a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
<img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="license">
</a>
<a href="https://github.com/Calcium-Ion/new-api/releases/latest">
<img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="release">
</a>
<a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
<img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
</a>
<a href="https://hub.docker.com/r/CalciumIon/new-api">
<img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
</a>
<a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
<img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
</a>
</p>
</div>
## 📝 Project Description
> [!NOTE]
> This is an open-source project developed based on [One API](https://github.com/songquanpeng/one-api)
> [!IMPORTANT]
> - Users must comply with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and relevant laws and regulations. Not to be used for illegal purposes.
> - This project is for personal learning only. Stability is not guaranteed, and no technical support is provided.
## ✨ Key Features
1. 🎨 New UI interface (some interfaces pending update)
2. 🌍 Multi-language support (work in progress)
3. 🎨 Added [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) interface support, [Integration Guide](Midjourney.md)
4. 💰 Online recharge support, configurable in system settings:
- [x] EasyPay
5. 🔍 Query usage quota by key:
- Works with [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)
6. 📑 Configurable items per page in pagination
7. 🔄 Compatible with original One API database (one-api.db)
8. 💵 Support per-request model pricing, configurable in System Settings - Operation Settings
9. ⚖️ Support channel **weighted random** selection
10. 📈 Data dashboard (console)
11. 🔒 Configurable model access per token
12. 🤖 Telegram authorization login support:
1. System Settings - Configure Login Registration - Allow Telegram Login
2. Send /setdomain command to [@Botfather](https://t.me/botfather)
3. Select your bot, then enter http(s)://your-website/login
4. Telegram Bot name is the bot username without @
13. 🎵 Added [Suno API](https://github.com/Suno-API/Suno-API) interface support, [Integration Guide](Suno.md)
14. 🔄 Support for Rerank models, compatible with Cohere and Jina, can integrate with Dify, [Integration Guide](Rerank.md)
15.**[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - Support for OpenAI's Realtime API, including Azure channels
## Model Support
This version additionally supports:
1. Third-party model **gps** (gpt-4-gizmo-*)
2. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) interface, [Integration Guide](Midjourney.md)
3. Custom channels with full API URL support
4. [Suno API](https://github.com/Suno-API/Suno-API) interface, [Integration Guide](Suno.md)
5. Rerank models, supporting [Cohere](https://cohere.ai/) and [Jina](https://jina.ai/), [Integration Guide](Rerank.md)
6. Dify
You can add custom models gpt-4-gizmo-* in channels. These are third-party models and cannot be called with official OpenAI keys.
## Additional Configurations Beyond One API
- `GENERATE_DEFAULT_TOKEN`: Generate initial token for new users, default `false`
- `STREAMING_TIMEOUT`: Set streaming response timeout, default 60 seconds
- `DIFY_DEBUG`: Output workflow and node info to client for Dify channel, default `true`
- `FORCE_STREAM_OPTION`: Override client stream_options parameter, default `true`
- `GET_MEDIA_TOKEN`: Calculate image tokens, default `true`
- `GET_MEDIA_TOKEN_NOT_STREAM`: Calculate image tokens in non-stream mode, default `true`
- `UPDATE_TASK`: Update async tasks (Midjourney, Suno), default `true`
- `GEMINI_MODEL_MAP`: Specify Gemini model versions (v1/v1beta), format: "model:version", comma-separated
- `COHERE_SAFETY_SETTING`: Cohere model [safety settings](https://docs.cohere.com/docs/safety-modes#overview), options: `NONE`, `CONTEXTUAL`, `STRICT`, default `NONE`
## Deployment
> [!TIP]
> Latest Docker image: `calciumion/new-api:latest`
> Default account: root, password: 123456
> Update command:
> ```
> docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
> ```
### Requirements
- Local database (default): SQLite (Docker deployment must mount `/data` directory)
- Remote database: MySQL >= 5.7.8, PgSQL >= 9.6
### Docker Deployment
### Using Docker Compose (Recommended)
```shell
# Clone project
git clone https://github.com/Calcium-Ion/new-api.git
cd new-api
# Edit docker-compose.yml as needed
# Start
docker-compose up -d
```
### Direct Docker Image Usage
```shell
# SQLite deployment:
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
# MySQL deployment (add -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"), modify database connection parameters as needed
# Example:
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
```
## Channel Retry
Channel retry is implemented, configurable in `Settings->Operation Settings->General Settings`. **Cache recommended**.
First retry uses same priority, second retry uses next priority, and so on.
### Cache Configuration
1. `REDIS_CONN_STRING`: Use Redis as cache
+ Example: `REDIS_CONN_STRING=redis://default:redispw@localhost:49153`
2. `MEMORY_CACHE_ENABLED`: Enable memory cache, default `false`
+ Example: `MEMORY_CACHE_ENABLED=true`
### Why Some Errors Don't Retry
Error codes 400, 504, 524 won't retry
### To Enable Retry for 400
In `Channel->Edit`, set `Status Code Override` to:
```json
{
"400": "500"
}
```
## Integration Guides
- [Midjourney Integration](Midjourney.md)
- [Suno Integration](Suno.md)
## Related Projects
- [One API](https://github.com/songquanpeng/one-api): Original project
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy): Midjourney interface support
- [chatnio](https://github.com/Deeptrain-Community/chatnio): Next-gen AI B/C solution
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool): Query usage quota by key
## 🌟 Star History
[![Star History Chart](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date)

View File

@@ -1,59 +1,78 @@
<p align="right">
<strong>中文</strong> | <a href="./README.en.md">English</a>
</p>
<div align="center"> <div align="center">
![new-api](/web/public/logo.png) ![new-api](/web/public/logo.png)
# New API # New API
🍥新一代大模型网关与AI资产管理系统
<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<p align="center">
<a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
<img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="license">
</a>
<a href="https://github.com/Calcium-Ion/new-api/releases/latest">
<img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="release">
</a>
<a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
<img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
</a>
<a href="https://hub.docker.com/r/CalciumIon/new-api">
<img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
</a>
<a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
<img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
</a>
</p>
</div> </div>
> [!NOTE] ## 📝 项目说明
> [!NOTE]
> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发 > 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发
> [!IMPORTANT] > [!IMPORTANT]
> 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。 > - 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
> 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持。 > - 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持。
> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。 > - 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
## 主要变更 ## ✨ 主要特性
此分叉版本的主要变更如下:
1. 全新的UI界面部分界面还待更新 1. 🎨 全新的UI界面部分界面还待更新
2. 添加[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口的支持,[对接文档](Midjourney.md) 2. 🌍 多语言支持(待完善)
3. 支持在线充值功能,可在系统设置中设置,当前支持的支付接口: 3. 🎨 添加[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口支持,[对接文档](Midjourney.md)
+ [x] 易支付 4. 💰 支持在线充值功能,可在系统设置中设置:
4. 支持用key查询使用额度: - [x] 易支付
+ 配合项目[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)可实现用key查询使用 5. 🔍 支持用key查询使用额度:
5. 渠道显示已使用额度,支持指定组织访问 - 配合项目[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)可实现用key查询使用
6. 分页支持选择每页显示数量 6. 📑 分页支持选择每页显示数量
7. 兼容原版One API的数据库可直接使用原版数据库one-api.db 7. 🔄 兼容原版One API的数据库可直接使用原版数据库one-api.db
8. 支持模型按次数收费,可在 系统设置-运营设置 中设置 8. 💵 支持模型按次数收费,可在 系统设置-运营设置 中设置
9. 支持渠道**加权随机** 9. ⚖️ 支持渠道**加权随机**
10. 数据看板 10. 📈 数据看板(控制台)
11. 可设置令牌能调用的模型 11. 🔒 可设置令牌能调用的模型
12. 支持Telegram授权登录 12. 🤖 支持Telegram授权登录
1. 系统设置-配置登录注册-允许通过Telegram登录 1. 系统设置-配置登录注册-允许通过Telegram登录
2. 对[@Botfather](https://t.me/botfather)输入指令/setdomain 2. 对[@Botfather](https://t.me/botfather)输入指令/setdomain
3. 选择你的bot然后输入http(s)://你的网站地址/login 3. 选择你的bot然后输入http(s)://你的网站地址/login
4. Telegram Bot 名称是bot username 去掉@后的字符串 4. Telegram Bot 名称是bot username 去掉@后的字符串
13. 添加 [Suno API](https://github.com/Suno-API/Suno-API)接口支持,[对接文档](Suno.md) 13. 🎵 添加 [Suno API](https://github.com/Suno-API/Suno-API)接口支持,[对接文档](Suno.md)
14. 支持Rerank模型目前兼容Cohere和Jina可接入Dify[对接文档](Rerank.md) 14. 🔄 支持Rerank模型目前兼容Cohere和Jina可接入Dify[对接文档](Rerank.md)
15. **[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - 支持OpenAI的Realtime API支持Azure渠道 15. **[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - 支持OpenAI的Realtime API支持Azure渠道
## 模型支持 ## 模型支持
此版本额外支持以下模型: 此版本额外支持以下模型:
1. 第三方模型 **gps** gpt-4-gizmo-* 1. 第三方模型 **gps** gpt-4-gizmo-*
2. 智谱glm-4vglm-4v识图 2. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[对接文档](Midjourney.md)
3. Anthropic Claude 3 3. 自定义渠道,支持填入完整调用地址
4. [Ollama](https://github.com/ollama/ollama?tab=readme-ov-file),添加渠道时,密钥可以随便填写,默认的请求地址是[http://localhost:11434](http://localhost:11434),如果需要修改请在渠道中修改 4. [Suno API](https://github.com/Suno-API/Suno-API) 接口,[对接文档](Suno.md)
5. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口[对接文档](Midjourney.md) 5. Rerank模型目前支持[Cohere](https://cohere.ai/)和[Jina](https://jina.ai/)[对接文档](Rerank.md)
6. [零一万物](https://platform.lingyiwanwu.com/) 6. Dify
7. 自定义渠道,支持填入完整调用地址
8. [Suno API](https://github.com/Suno-API/Suno-API) 接口,[对接文档](Suno.md)
9. Rerank模型目前支持[Cohere](https://cohere.ai/)和[Jina](https://jina.ai/)[对接文档](Rerank.md)
10. Dify
11. Vertex AI目前兼容ClaudeGeminiLlama3.1
您可以在渠道中添加自定义模型gpt-4-gizmo-*此模型并非OpenAI官方模型而是第三方模型使用官方key无法调用。 您可以在渠道中添加自定义模型gpt-4-gizmo-*此模型并非OpenAI官方模型而是第三方模型使用官方key无法调用。
@@ -151,6 +170,6 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
- [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代 AI 一站式 B/C 端解决方案 - [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代 AI 一站式 B/C 端解决方案
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)用key查询使用额度 - [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)用key查询使用额度
## Star History ## 🌟 Star History
[![Star History Chart](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date) [![Star History Chart](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date)

View File

@@ -10,22 +10,22 @@ import (
) )
func generateMessageID() (string, error) { func generateMessageID() (string, error) {
split := strings.Split(SMTPAccount, "@") split := strings.Split(SMTPFrom, "@")
if len(split) < 2 { if len(split) < 2 {
return "", fmt.Errorf("invalid SMTP account") return "", fmt.Errorf("invalid SMTP account")
} }
domain := strings.Split(SMTPAccount, "@")[1] domain := strings.Split(SMTPFrom, "@")[1]
return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain), nil return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain), nil
} }
func SendEmail(subject string, receiver string, content string) error { func SendEmail(subject string, receiver string, content string) error {
if SMTPFrom == "" { // for compatibility
SMTPFrom = SMTPAccount
}
id, err2 := generateMessageID() id, err2 := generateMessageID()
if err2 != nil { if err2 != nil {
return err2 return err2
} }
if SMTPFrom == "" { // for compatibility
SMTPFrom = SMTPAccount
}
if SMTPServer == "" && SMTPAccount == "" { if SMTPServer == "" && SMTPAccount == "" {
return fmt.Errorf("SMTP 服务器未配置") return fmt.Errorf("SMTP 服务器未配置")
} }
@@ -79,11 +79,11 @@ func SendEmail(subject string, receiver string, content string) error {
if err != nil { if err != nil {
return err return err
} }
} else if isOutlookServer(SMTPAccount) { } else if isOutlookServer(SMTPAccount) || SMTPServer == "smtp.azurecomm.net" {
auth = LoginAuth(SMTPAccount, SMTPToken) auth = LoginAuth(SMTPAccount, SMTPToken)
err = smtp.SendMail(addr, auth, SMTPAccount, to, mail) err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
} else { } else {
err = smtp.SendMail(addr, auth, SMTPAccount, to, mail) err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
} }
return err return err
} }

View File

@@ -0,0 +1,5 @@
package constant
var (
ForceFormat = "force_format" // ForceFormat 强制格式化为OpenAI格式
)

View File

@@ -63,7 +63,7 @@ func GetAllChannels(c *gin.Context) {
} }
for _, tag := range tags { for _, tag := range tags {
if tag != nil && *tag != "" { if tag != nil && *tag != "" {
tagChannel, err := model.GetChannelsByTag(*tag) tagChannel, err := model.GetChannelsByTag(*tag, idSort)
if err == nil { if err == nil {
channelData = append(channelData, tagChannel...) channelData = append(channelData, tagChannel...)
} }
@@ -181,7 +181,7 @@ func SearchChannels(c *gin.Context) {
} }
for _, tag := range tags { for _, tag := range tags {
if tag != nil && *tag != "" { if tag != nil && *tag != "" {
tagChannel, err := model.GetChannelsByTag(*tag) tagChannel, err := model.GetChannelsByTag(*tag, idSort)
if err == nil { if err == nil {
channelData = append(channelData, tagChannel...) channelData = append(channelData, tagChannel...)
} }

4
go.mod
View File

@@ -11,7 +11,6 @@ require (
github.com/aws/aws-sdk-go-v2/credentials v1.17.11 github.com/aws/aws-sdk-go-v2/credentials v1.17.11
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b
github.com/bytedance/sonic v1.12.4
github.com/gin-contrib/cors v1.7.2 github.com/gin-contrib/cors v1.7.2
github.com/gin-contrib/gzip v0.0.6 github.com/gin-contrib/gzip v0.0.6
github.com/gin-contrib/sessions v0.0.5 github.com/gin-contrib/sessions v0.0.5
@@ -42,7 +41,8 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
github.com/aws/smithy-go v1.20.2 // indirect github.com/aws/smithy-go v1.20.2 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/iasm v0.2.0 // indirect

9
go.sum
View File

@@ -22,11 +22,10 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b h1:LTGVFpNmNHhj0vhOlfgWueFJ32eK9blaIlHR2ciXOT0= github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b h1:LTGVFpNmNHhj0vhOlfgWueFJ32eK9blaIlHR2ciXOT0=
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q=
github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
@@ -205,6 +204,7 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -270,3 +270,4 @@ gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU= gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -174,11 +174,11 @@
"\"验证码\"": "\"Verification code\"", "\"验证码\"": "\"Verification code\"",
"全部用户": "All users", "全部用户": "All users",
"当前用户": "Current user", "当前用户": "Current user",
"'全部'": "'All'", "全部'": "All'",
"'充值'": "'Recharge'", "充值'": "Recharge'",
"'消费'": "'Consumption'", "消费'": "Consumption'",
"'管理'": "'Management'", "管理'": "Management'",
"'系统'": "'System'", "系统'": "System'",
" 充值 ": " Recharge ", " 充值 ": " Recharge ",
" 消费 ": " Consumption ", " 消费 ": " Consumption ",
" 管理 ": " Management ", " 管理 ": " Management ",
@@ -377,6 +377,7 @@
"添加新的用户": "Add New User", "添加新的用户": "Add New User",
"自定义": "Custom", "自定义": "Custom",
"等价金额": "Equivalent Amount", "等价金额": "Equivalent Amount",
"等价金额:{{quota}}": "Equivalent amount: {{quota}}",
"未登录或登录已过期,请重新登录": "Not logged in or login has expired, please log in again", "未登录或登录已过期,请重新登录": "Not logged in or login has expired, please log in again",
"请求次数过多,请稍后再试": "Too many requests, please try again later", "请求次数过多,请稍后再试": "Too many requests, please try again later",
"服务器内部错误,请联系管理员": "Server internal error, please contact the administrator", "服务器内部错误,请联系管理员": "Server internal error, please contact the administrator",
@@ -525,5 +526,13 @@
"模型版本": "Model version", "模型版本": "Model version",
"请输入星火大模型版本注意是接口地址中的版本号例如v2.1": "Please enter the version of the Starfire model, note that it is the version number in the interface address, for example: v2.1", "请输入星火大模型版本注意是接口地址中的版本号例如v2.1": "Please enter the version of the Starfire model, note that it is the version number in the interface address, for example: v2.1",
"点击查看": "click to view", "点击查看": "click to view",
"请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!": "Please make sure that the gpt-35-turbo model has been created on Azure, and the apiVersion has been filled in correctly!" "请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!": "Please make sure that the gpt-35-turbo model has been created on Azure, and the apiVersion has been filled in correctly!",
"第 {{start}} - {{end}} 条,共 {{total}} 条": "Items {{start}} - {{end}} of {{total}}",
"模型测试": "Model test",
"请选择最长响应时间": "Please select the longest response time",
"成功时自动启用通道": "Enable channel when successful",
"分钟": "minutes",
"设置过短会影响数据库性能": "Setting too short will affect database performance",
"仅修改展示粒度,统计精确到小时": "Only modify display granularity, statistics accurate to the hour",
"当运行通道全部测试时,超过此时间将自动禁用通道": "When running all channel tests, the channel will be automatically disabled when this time is exceeded"
} }

View File

@@ -33,7 +33,7 @@ var indexPage []byte
func main() { func main() {
err := godotenv.Load(".env") err := godotenv.Load(".env")
if err != nil { if err != nil {
common.SysLog("Can't load .env file") common.SysError("failed to load .env file: " + err.Error())
} }
common.SetupLogger() common.SetupLogger()

View File

@@ -213,6 +213,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
c.Set("channel_id", channel.Id) c.Set("channel_id", channel.Id)
c.Set("channel_name", channel.Name) c.Set("channel_name", channel.Name)
c.Set("channel_type", channel.Type) c.Set("channel_type", channel.Type)
c.Set("channel_setting", channel.GetSetting())
if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization { if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization {
c.Set("channel_organization", *channel.OpenAIOrganization) c.Set("channel_organization", *channel.OpenAIOrganization)
} }

View File

@@ -34,6 +34,7 @@ type Channel struct {
AutoBan *int `json:"auto_ban" gorm:"default:1"` AutoBan *int `json:"auto_ban" gorm:"default:1"`
OtherInfo string `json:"other_info"` OtherInfo string `json:"other_info"`
Tag *string `json:"tag" gorm:"index"` Tag *string `json:"tag" gorm:"index"`
Setting string `json:"setting" gorm:"type:text"`
} }
func (channel *Channel) GetModels() []string { func (channel *Channel) GetModels() []string {
@@ -100,9 +101,13 @@ func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Chan
return channels, err return channels, err
} }
func GetChannelsByTag(tag string) ([]*Channel, error) { func GetChannelsByTag(tag string, idSort bool) ([]*Channel, error) {
var channels []*Channel var channels []*Channel
err := DB.Where("tag = ?", tag).Find(&channels).Error order := "priority desc"
if idSort {
order = "id desc"
}
err := DB.Where("tag = ?", tag).Order(order).Find(&channels).Error
return channels, err return channels, err
} }
@@ -362,7 +367,7 @@ func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *
return err return err
} }
if shouldReCreateAbilities { if shouldReCreateAbilities {
channels, err := GetChannelsByTag(updatedTag) channels, err := GetChannelsByTag(updatedTag, false)
if err == nil { if err == nil {
for _, channel := range channels { for _, channel := range channels {
err = channel.UpdateAbilities() err = channel.UpdateAbilities()
@@ -450,10 +455,13 @@ func SearchTags(keyword string, group string, model string, idSort bool) ([]*str
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%") args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%")
} }
err := baseQuery.Where(whereClause, args...). subQuery := baseQuery.Where(whereClause, args...).
Select("DISTINCT tag"). Select("tag").
Where("tag != ''"). Where("tag != ''").
Order(order). Order(order)
err := DB.Table("(?) as sub", subQuery).
Select("DISTINCT tag").
Find(&tags).Error Find(&tags).Error
if err != nil { if err != nil {
@@ -462,3 +470,23 @@ func SearchTags(keyword string, group string, model string, idSort bool) ([]*str
return tags, nil return tags, nil
} }
func (channel *Channel) GetSetting() map[string]interface{} {
setting := make(map[string]interface{})
if channel.Setting != "" {
err := json.Unmarshal([]byte(channel.Setting), &setting)
if err != nil {
common.SysError("failed to unmarshal setting: " + err.Error())
}
}
return setting
}
func (channel *Channel) SetSetting(setting map[string]interface{}) {
settingBytes, err := json.Marshal(setting)
if err != nil {
common.SysError("failed to marshal setting: " + err.Error())
return
}
channel.Setting = string(settingBytes)
}

View File

@@ -85,7 +85,7 @@ func SaveQuotaDataCache() {
//quotaDataDB.Count += quotaData.Count //quotaDataDB.Count += quotaData.Count
//quotaDataDB.Quota += quotaData.Quota //quotaDataDB.Quota += quotaData.Quota
//DB.Table("quota_data").Save(quotaDataDB) //DB.Table("quota_data").Save(quotaDataDB)
increaseQuotaData(quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.Count, quotaData.Quota, quotaData.CreatedAt) increaseQuotaData(quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.Count, quotaData.Quota, quotaData.CreatedAt, quotaData.TokenUsed)
} else { } else {
DB.Table("quota_data").Create(quotaData) DB.Table("quota_data").Create(quotaData)
} }
@@ -94,11 +94,12 @@ func SaveQuotaDataCache() {
common.SysLog(fmt.Sprintf("保存数据看板数据成功,共保存%d条数据", size)) common.SysLog(fmt.Sprintf("保存数据看板数据成功,共保存%d条数据", size))
} }
func increaseQuotaData(userId int, username string, modelName string, count int, quota int, createdAt int64) { func increaseQuotaData(userId int, username string, modelName string, count int, quota int, createdAt int64, tokenUsed int) {
err := DB.Table("quota_data").Where("user_id = ? and username = ? and model_name = ? and created_at = ?", err := DB.Table("quota_data").Where("user_id = ? and username = ? and model_name = ? and created_at = ?",
userId, username, modelName, createdAt).Updates(map[string]interface{}{ userId, username, modelName, createdAt).Updates(map[string]interface{}{
"count": gorm.Expr("count + ?", count), "count": gorm.Expr("count + ?", count),
"quota": gorm.Expr("quota + ?", quota), "quota": gorm.Expr("quota + ?", quota),
"token_used": gorm.Expr("token_used + ?", tokenUsed),
}).Error }).Error
if err != nil { if err != nil {
common.SysLog(fmt.Sprintf("increaseQuotaData error: %s", err)) common.SysLog(fmt.Sprintf("increaseQuotaData error: %s", err))
@@ -127,6 +128,6 @@ func GetAllQuotaDates(startTime int64, endTime int64, username string) (quotaDat
// 从quota_data表中查询数据 // 从quota_data表中查询数据
// only select model_name, sum(count) as count, sum(quota) as quota, model_name, created_at from quota_data group by model_name, created_at; // only select model_name, sum(count) as count, sum(quota) as quota, model_name, created_at from quota_data group by model_name, created_at;
//err = DB.Table("quota_data").Where("created_at >= ? and created_at <= ?", startTime, endTime).Find(&quotaDatas).Error //err = DB.Table("quota_data").Where("created_at >= ? and created_at <= ?", startTime, endTime).Find(&quotaDatas).Error
err = DB.Table("quota_data").Select("model_name, sum(count) as count, sum(quota) as quota, created_at").Where("created_at >= ? and created_at <= ?", startTime, endTime).Group("model_name, created_at").Find(&quotaDatas).Error err = DB.Table("quota_data").Select("model_name, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used, created_at").Where("created_at >= ? and created_at <= ?", startTime, endTime).Group("model_name, created_at").Find(&quotaDatas).Error
return quotaDatas, err return quotaDatas, err
} }

View File

@@ -5,9 +5,10 @@ const (
) )
var ModelList = []string{ var ModelList = []string{
"gemini-1.0-pro-latest", "gemini-1.0-pro-001", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-ultra", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-ultra",
"gemini-1.0-pro-vision-latest", "gemini-1.0-pro-vision-001", "gemini-1.5-pro-exp-0827", "gemini-1.5-flash-exp-0827", "gemini-1.5-pro-exp-0827", "gemini-1.5-flash-exp-0827",
"gemini-exp-1114", "gemini-exp-1114", "gemini-exp-1206",
"gemini-2.0-flash-exp",
} }
var ChannelName = "google gemini" var ChannelName = "google gemini"

View File

@@ -34,6 +34,7 @@ type GeminiChatSafetySettings struct {
} }
type GeminiChatTools struct { type GeminiChatTools struct {
GoogleSearch any `json:"googleSearch,omitempty"`
FunctionDeclarations any `json:"functionDeclarations,omitempty"` FunctionDeclarations any `json:"functionDeclarations,omitempty"`
} }

View File

@@ -45,13 +45,25 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
} }
if textRequest.Tools != nil { if textRequest.Tools != nil {
functions := make([]dto.FunctionCall, 0, len(textRequest.Tools)) functions := make([]dto.FunctionCall, 0, len(textRequest.Tools))
googleSearch := false
for _, tool := range textRequest.Tools { for _, tool := range textRequest.Tools {
if tool.Function.Name == "googleSearch" {
googleSearch = true
continue
}
functions = append(functions, tool.Function) functions = append(functions, tool.Function)
} }
geminiRequest.Tools = []GeminiChatTools{ if len(functions) > 0 {
{ geminiRequest.Tools = []GeminiChatTools{
FunctionDeclarations: functions, {
}, FunctionDeclarations: functions,
},
}
}
if googleSearch {
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTools{
GoogleSearch: make(map[string]string),
})
} }
} else if textRequest.Functions != nil { } else if textRequest.Functions != nil {
geminiRequest.Tools = []GeminiChatTools{ geminiRequest.Tools = []GeminiChatTools{
@@ -134,7 +146,6 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
shouldAddDummyModelMessage = false shouldAddDummyModelMessage = false
} }
} }
return &geminiRequest return &geminiRequest
} }

View File

@@ -5,9 +5,6 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/bytedance/gopkg/util/gopool"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"io" "io"
"net/http" "net/http"
"one-api/common" "one-api/common"
@@ -19,9 +16,33 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/bytedance/gopkg/util/gopool"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
) )
func sendStreamData(c *gin.Context, data string, forceFormat bool) error {
if data == "" {
return nil
}
if forceFormat {
var lastStreamResponse dto.ChatCompletionsStreamResponse
if err := json.Unmarshal(common.StringToByteSlice(data), &lastStreamResponse); err != nil {
return err
}
return service.ObjectData(c, lastStreamResponse)
}
return service.StringData(c, data)
}
func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
if resp == nil || resp.Body == nil {
common.LogError(c, "invalid response or response body")
return service.OpenAIErrorWrapper(fmt.Errorf("invalid response"), "invalid_response", http.StatusInternalServerError), nil
}
containStreamUsage := false containStreamUsage := false
var responseId string var responseId string
var createAt int64 = 0 var createAt int64 = 0
@@ -31,6 +52,13 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
var responseTextBuilder strings.Builder var responseTextBuilder strings.Builder
var usage = &dto.Usage{} var usage = &dto.Usage{}
var streamItems []string // store stream items var streamItems []string // store stream items
var forceFormat bool
if info.ChannelType == common.ChannelTypeCustom {
if forceFmt, ok := info.ChannelSetting["force_format"].(bool); ok {
forceFormat = forceFmt
}
}
toolCount := 0 toolCount := 0
scanner := bufio.NewScanner(resp.Body) scanner := bufio.NewScanner(resp.Body)
@@ -62,7 +90,7 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
data = data[6:] data = data[6:]
if !strings.HasPrefix(data, "[DONE]") { if !strings.HasPrefix(data, "[DONE]") {
if lastStreamData != "" { if lastStreamData != "" {
err := service.StringData(c, lastStreamData) err := sendStreamData(c, lastStreamData, forceFormat)
if err != nil { if err != nil {
common.LogError(c, "streaming error: "+err.Error()) common.LogError(c, "streaming error: "+err.Error())
} }
@@ -105,7 +133,7 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
} }
} }
if shouldSendLastResp { if shouldSendLastResp {
service.StringData(c, lastStreamData) sendStreamData(c, lastStreamData, forceFormat)
} }
// 计算token // 计算token
@@ -375,6 +403,10 @@ func getTextFromJSON(body []byte) (string, error) {
} }
func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.RealtimeUsage) { func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.RealtimeUsage) {
if info == nil || info.ClientWs == nil || info.TargetWs == nil {
return service.OpenAIErrorWrapper(fmt.Errorf("invalid websocket connection"), "invalid_connection", http.StatusBadRequest), nil
}
info.IsStream = true info.IsStream = true
clientConn := info.ClientWs clientConn := info.ClientWs
targetConn := info.TargetWs targetConn := info.TargetWs
@@ -390,6 +422,11 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
sumUsage := &dto.RealtimeUsage{} sumUsage := &dto.RealtimeUsage{}
gopool.Go(func() { gopool.Go(func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("panic in client reader: %v", r)
}
}()
for { for {
select { select {
case <-c.Done(): case <-c.Done():
@@ -445,6 +482,11 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
}) })
gopool.Go(func() { gopool.Go(func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("panic in target reader: %v", r)
}
}()
for { for {
select { select {
case <-c.Done(): case <-c.Done():
@@ -568,6 +610,10 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
} }
func preConsumeUsage(ctx *gin.Context, info *relaycommon.RelayInfo, usage *dto.RealtimeUsage, totalUsage *dto.RealtimeUsage) error { func preConsumeUsage(ctx *gin.Context, info *relaycommon.RelayInfo, usage *dto.RealtimeUsage, totalUsage *dto.RealtimeUsage) error {
if usage == nil || totalUsage == nil {
return fmt.Errorf("invalid usage pointer")
}
totalUsage.TotalTokens += usage.TotalTokens totalUsage.TotalTokens += usage.TotalTokens
totalUsage.InputTokens += usage.InputTokens totalUsage.InputTokens += usage.InputTokens
totalUsage.OutputTokens += usage.OutputTokens totalUsage.OutputTokens += usage.OutputTokens

View File

@@ -176,7 +176,20 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
} }
func (a *Adaptor) GetModelList() []string { func (a *Adaptor) GetModelList() []string {
return ModelList var modelList []string
for i, s := range ModelList {
modelList = append(modelList, s)
ModelList[i] = s
}
for i, s := range claude.ModelList {
modelList = append(modelList, s)
claude.ModelList[i] = s
}
for i, s := range gemini.ModelList {
modelList = append(modelList, s)
gemini.ModelList[i] = s
}
return modelList
} }
func (a *Adaptor) GetChannelName() string { func (a *Adaptor) GetChannelName() string {

View File

@@ -1,13 +1,13 @@
package vertex package vertex
var ModelList = []string{ var ModelList = []string{
"claude-3-sonnet-20240229", //"claude-3-sonnet-20240229",
"claude-3-opus-20240229", //"claude-3-opus-20240229",
"claude-3-haiku-20240307", //"claude-3-haiku-20240307",
"claude-3-5-sonnet-20240620", //"claude-3-5-sonnet-20240620",
//"gemini-1.5-pro-latest", "gemini-1.5-flash-latest", //"gemini-1.5-pro-latest", "gemini-1.5-flash-latest",
"gemini-1.5-pro-001", "gemini-1.5-flash-001", "gemini-pro", "gemini-pro-vision", //"gemini-1.5-pro-001", "gemini-1.5-flash-001", "gemini-pro", "gemini-pro-vision",
"meta/llama3-405b-instruct-maas", "meta/llama3-405b-instruct-maas",
} }

View File

@@ -245,7 +245,7 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap
func apiVersion2domain(apiVersion string) string { func apiVersion2domain(apiVersion string) string {
switch apiVersion { switch apiVersion {
case "v1.1": case "v1.1":
return "general" return "lite"
case "v2.1": case "v2.1":
return "generalv2" return "generalv2"
case "v3.1": case "v3.1":

View File

@@ -1,13 +1,14 @@
package common package common
import ( import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"one-api/common" "one-api/common"
"one-api/dto" "one-api/dto"
"one-api/relay/constant" "one-api/relay/constant"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
) )
type RelayInfo struct { type RelayInfo struct {
@@ -43,6 +44,7 @@ type RelayInfo struct {
RealtimeTools []dto.RealTimeTool RealtimeTools []dto.RealTimeTool
IsFirstRequest bool IsFirstRequest bool
AudioUsage bool AudioUsage bool
ChannelSetting map[string]interface{}
} }
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo { func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
@@ -57,6 +59,7 @@ func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
func GenRelayInfo(c *gin.Context) *RelayInfo { func GenRelayInfo(c *gin.Context) *RelayInfo {
channelType := c.GetInt("channel_type") channelType := c.GetInt("channel_type")
channelId := c.GetInt("channel_id") channelId := c.GetInt("channel_id")
channelSetting := c.GetStringMap("channel_setting")
tokenId := c.GetInt("token_id") tokenId := c.GetInt("token_id")
tokenKey := c.GetString("token_key") tokenKey := c.GetString("token_key")
@@ -87,6 +90,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
ApiVersion: c.GetString("api_version"), ApiVersion: c.GetString("api_version"),
ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "), ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
Organization: c.GetString("channel_organization"), Organization: c.GetString("channel_organization"),
ChannelSetting: channelSetting,
} }
if strings.HasPrefix(c.Request.URL.Path, "/pg") { if strings.HasPrefix(c.Request.URL.Path, "/pg") {
info.IsPlayground = true info.IsPlayground = true

View File

@@ -18,8 +18,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/bytedance/sonic"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -180,7 +178,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
if err != nil { if err != nil {
return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError) return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
} }
jsonData, err := sonic.Marshal(convertedRequest) jsonData, err := json.Marshal(convertedRequest)
if err != nil { if err != nil {
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError) return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
} }

View File

@@ -5,7 +5,7 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@douyinfe/semi-icons": "^2.63.1", "@douyinfe/semi-icons": "^2.63.1",
"@douyinfe/semi-ui": "^2.63.1", "@douyinfe/semi-ui": "^2.69.1",
"@visactor/react-vchart": "~1.8.8", "@visactor/react-vchart": "~1.8.8",
"@visactor/vchart": "~1.8.8", "@visactor/vchart": "~1.8.8",
"@visactor/vchart-semi-theme": "~1.8.8", "@visactor/vchart-semi-theme": "~1.8.8",
@@ -23,7 +23,10 @@
"react-turnstile": "^1.0.5", "react-turnstile": "^1.0.5",
"semantic-ui-offline": "^2.5.0", "semantic-ui-offline": "^2.5.0",
"semantic-ui-react": "^2.1.3", "semantic-ui-react": "^2.1.3",
"sse": "github:mpetazzoni/sse.js" "sse": "github:mpetazzoni/sse.js",
"i18next": "^23.16.8",
"react-i18next": "^13.0.0",
"i18next-browser-languagedetector": "^7.2.0"
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

5224
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,40 +24,18 @@ import { Layout } from '@douyinfe/semi-ui';
import Midjourney from './pages/Midjourney'; import Midjourney from './pages/Midjourney';
import Pricing from './pages/Pricing/index.js'; import Pricing from './pages/Pricing/index.js';
import Task from "./pages/Task/index.js"; import Task from "./pages/Task/index.js";
import Playground from './components/Playground.js'; import Playground from './pages/Playground/Playground.js';
import OAuth2Callback from "./components/OAuth2Callback.js"; import OAuth2Callback from "./components/OAuth2Callback.js";
import { useTranslation } from 'react-i18next';
import { StatusContext } from './context/Status';
import { setStatusData } from './helpers/data.js';
import { API, showError } from './helpers';
const Home = lazy(() => import('./pages/Home')); const Home = lazy(() => import('./pages/Home'));
const Detail = lazy(() => import('./pages/Detail')); const Detail = lazy(() => import('./pages/Detail'));
const About = lazy(() => import('./pages/About')); const About = lazy(() => import('./pages/About'));
function App() { function App() {
const [userState, userDispatch] = useContext(UserContext);
// const [statusState, statusDispatch] = useContext(StatusContext);
const loadUser = () => {
let user = localStorage.getItem('user');
if (user) {
let data = JSON.parse(user);
userDispatch({ type: 'login', payload: data });
}
};
useEffect(() => {
loadUser();
let systemName = getSystemName();
if (systemName) {
document.title = systemName;
}
let logo = getLogo();
if (logo) {
let linkElement = document.querySelector("link[rel~='icon']");
if (linkElement) {
linkElement.href = logo;
}
}
}, []);
return ( return (
<> <>
<Routes> <Routes>

View File

@@ -36,43 +36,111 @@ import { IconList, IconTreeTriangleDown } from '@douyinfe/semi-icons';
import { loadChannelModels } from './utils.js'; import { loadChannelModels } from './utils.js';
import EditTagModal from '../pages/Channel/EditTagModal.js'; import EditTagModal from '../pages/Channel/EditTagModal.js';
import TextNumberInput from './custom/TextNumberInput.js'; import TextNumberInput from './custom/TextNumberInput.js';
import { useTranslation } from 'react-i18next';
function renderTimestamp(timestamp) { function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}</>; return <>{timestamp2string(timestamp)}</>;
} }
let type2label = undefined;
function renderType(type) {
if (!type2label) {
type2label = new Map();
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
}
type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
}
return (
<Tag size="large" color={type2label[type]?.color}>
{type2label[type]?.text}
</Tag>
);
}
function renderTagType(type) {
return (
<Tag
color='light-blue'
prefixIcon={<IconList />}
size='large'
shape='circle'
type='light'
>
标签聚合
</Tag>
);
}
const ChannelsTable = () => { const ChannelsTable = () => {
const { t } = useTranslation();
let type2label = undefined;
const renderType = (type) => {
if (!type2label) {
type2label = new Map();
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
}
type2label[0] = { value: 0, text: t('未知类型'), color: 'grey' };
}
return (
<Tag size="large" color={type2label[type]?.color}>
{type2label[type]?.text}
</Tag>
);
};
const renderTagType = () => {
return (
<Tag
color='light-blue'
prefixIcon={<IconList />}
size='large'
shape='circle'
type='light'
>
{t('标签聚合')}
</Tag>
);
};
const renderStatus = (status) => {
switch (status) {
case 1:
return (
<Tag size="large" color="green">
{t('已启用')}
</Tag>
);
case 2:
return (
<Tag size="large" color="yellow">
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag size="large" color="yellow">
{t('自动禁用')}
</Tag>
);
default:
return (
<Tag size="large" color="grey">
{t('未知状态')}
</Tag>
);
}
};
const renderResponseTime = (responseTime) => {
let time = responseTime / 1000;
time = time.toFixed(2) + t(' 秒');
if (responseTime === 0) {
return (
<Tag size="large" color="grey">
{t('未测试')}
</Tag>
);
} else if (responseTime <= 1000) {
return (
<Tag size="large" color="green">
{time}
</Tag>
);
} else if (responseTime <= 3000) {
return (
<Tag size="large" color="lime">
{time}
</Tag>
);
} else if (responseTime <= 5000) {
return (
<Tag size="large" color="yellow">
{time}
</Tag>
);
} else {
return (
<Tag size="large" color="red">
{time}
</Tag>
);
}
};
const columns = [ const columns = [
// { // {
// title: '', // title: '',
@@ -80,15 +148,15 @@ const ChannelsTable = () => {
// className: 'checkbox', // className: 'checkbox',
// }, // },
{ {
title: 'ID', title: t('ID'),
dataIndex: 'id' dataIndex: 'id'
}, },
{ {
title: '名称', title: t('名称'),
dataIndex: 'name' dataIndex: 'name'
}, },
{ {
title: '分组', title: t('分组'),
dataIndex: 'group', dataIndex: 'group',
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
@@ -103,18 +171,18 @@ const ChannelsTable = () => {
} }
}, },
{ {
title: '类型', title: t('类型'),
dataIndex: 'type', dataIndex: 'type',
render: (text, record, index) => { render: (text, record, index) => {
if (record.children === undefined) { if (record.children === undefined) {
return <>{renderType(text)}</>; return <>{renderType(text)}</>;
} else { } else {
return <>{renderTagType(0)}</>; return <>{renderTagType()}</>;
} }
} }
}, },
{ {
title: '状态', title: t('状态'),
dataIndex: 'status', dataIndex: 'status',
render: (text, record, index) => { render: (text, record, index) => {
if (text === 3) { if (text === 3) {
@@ -126,7 +194,7 @@ const ChannelsTable = () => {
let time = otherInfo['status_time']; let time = otherInfo['status_time'];
return ( return (
<div> <div>
<Tooltip content={'原因:' + reason + ',时间:' + timestamp2string(time)}> <Tooltip content={t('原因:') + reason + t(',时间:') + timestamp2string(time)}>
{renderStatus(text)} {renderStatus(text)}
</Tooltip> </Tooltip>
</div> </div>
@@ -137,26 +205,26 @@ const ChannelsTable = () => {
} }
}, },
{ {
title: '响应时间', title: t('响应时间'),
dataIndex: 'response_time', dataIndex: 'response_time',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{renderResponseTime(text)}</div>; return <div>{renderResponseTime(text)}</div>;
} }
}, },
{ {
title: '已用/剩余', title: t('已用/剩余'),
dataIndex: 'expired_time', dataIndex: 'expired_time',
render: (text, record, index) => { render: (text, record, index) => {
if (record.children === undefined) { if (record.children === undefined) {
return ( return (
<div> <div>
<Space spacing={1}> <Space spacing={1}>
<Tooltip content={'已用额度'}> <Tooltip content={t('已用额度')}>
<Tag color="white" type="ghost" size="large"> <Tag color="white" type="ghost" size="large">
{renderQuota(record.used_quota)} {renderQuota(record.used_quota)}
</Tag> </Tag>
</Tooltip> </Tooltip>
<Tooltip content={'剩余额度' + record.balance + ',点击更新'}> <Tooltip content={t('剩余额度') + record.balance + t(',点击更新')}>
<Tag <Tag
color="white" color="white"
type="ghost" type="ghost"
@@ -172,7 +240,7 @@ const ChannelsTable = () => {
</div> </div>
); );
} else { } else {
return <Tooltip content={'已用额度'}> return <Tooltip content={t('已用额度')}>
<Tag color="white" type="ghost" size="large"> <Tag color="white" type="ghost" size="large">
{renderQuota(record.used_quota)} {renderQuota(record.used_quota)}
</Tag> </Tag>
@@ -181,7 +249,7 @@ const ChannelsTable = () => {
} }
}, },
{ {
title: '优先级', title: t('优先级'),
dataIndex: 'priority', dataIndex: 'priority',
render: (text, record, index) => { render: (text, record, index) => {
if (record.children === undefined) { if (record.children === undefined) {
@@ -208,8 +276,8 @@ const ChannelsTable = () => {
keepFocus={true} keepFocus={true}
onBlur={(e) => { onBlur={(e) => {
Modal.warning({ Modal.warning({
title: '修改子渠道优先级', title: t('修改子渠道优先级'),
content: '确定要修改所有子渠道优先级为 ' + e.target.value + ' 吗?', content: t('确定要修改所有子渠道优先级为 ') + e.target.value + t(' 吗?'),
onOk: () => { onOk: () => {
if (e.target.value === '') { if (e.target.value === '') {
return; return;
@@ -230,7 +298,7 @@ const ChannelsTable = () => {
} }
}, },
{ {
title: '权重', title: t('权重'),
dataIndex: 'weight', dataIndex: 'weight',
render: (text, record, index) => { render: (text, record, index) => {
if (record.children === undefined) { if (record.children === undefined) {
@@ -257,8 +325,8 @@ const ChannelsTable = () => {
keepFocus={true} keepFocus={true}
onBlur={(e) => { onBlur={(e) => {
Modal.warning({ Modal.warning({
title: '修改子渠道权重', title: t('修改子渠道权重'),
content: '确定要修改所有子渠道权重为 ' + e.target.value + ' 吗?', content: t('确定要修改所有子渠道权重为 ') + e.target.value + t(' 吗?'),
onOk: () => { onOk: () => {
if (e.target.value === '') { if (e.target.value === '') {
return; return;
@@ -287,7 +355,7 @@ const ChannelsTable = () => {
<div> <div>
<SplitButtonGroup <SplitButtonGroup
style={{ marginRight: 1 }} style={{ marginRight: 1 }}
aria-label="测试单个渠道操作项目组" aria-label={t('测试单个渠道操作项目组')}
> >
<Button <Button
theme="light" theme="light"
@@ -295,7 +363,7 @@ const ChannelsTable = () => {
testChannel(record, ''); testChannel(record, '');
}} }}
> >
测试 {t('测试')}
</Button> </Button>
<Dropdown <Dropdown
trigger="click" trigger="click"
@@ -309,10 +377,9 @@ const ChannelsTable = () => {
></Button> ></Button>
</Dropdown> </Dropdown>
</SplitButtonGroup> </SplitButtonGroup>
{/*<Button theme='light' type='primary' style={{marginRight: 1}} onClick={()=>testChannel(record)}>测试</Button>*/}
<Popconfirm <Popconfirm
title="确定是否要删除此渠道?" title={t('确定是否要删除此渠道?')}
content="此修改将不可逆" content={t('此修改将不可逆')}
okType={'danger'} okType={'danger'}
position={'left'} position={'left'}
onConfirm={() => { onConfirm={() => {
@@ -322,7 +389,7 @@ const ChannelsTable = () => {
}} }}
> >
<Button theme="light" type="danger" style={{ marginRight: 1 }}> <Button theme="light" type="danger" style={{ marginRight: 1 }}>
删除 {t('删除')}
</Button> </Button>
</Popconfirm> </Popconfirm>
{record.status === 1 ? ( {record.status === 1 ? (
@@ -334,7 +401,7 @@ const ChannelsTable = () => {
manageChannel(record.id, 'disable', record); manageChannel(record.id, 'disable', record);
}} }}
> >
禁用 {t('禁用')}
</Button> </Button>
) : ( ) : (
<Button <Button
@@ -345,7 +412,7 @@ const ChannelsTable = () => {
manageChannel(record.id, 'enable', record); manageChannel(record.id, 'enable', record);
}} }}
> >
启用 {t('启用')}
</Button> </Button>
)} )}
<Button <Button
@@ -357,11 +424,11 @@ const ChannelsTable = () => {
setShowEdit(true); setShowEdit(true);
}} }}
> >
编辑 {t('编辑')}
</Button> </Button>
<Popconfirm <Popconfirm
title="确定是否要复制此渠道?" title={t('确定是否要复制此渠道?')}
content="复制渠道的所有信息" content={t('复制渠道的所有信息')}
okType={'danger'} okType={'danger'}
position={'left'} position={'left'}
onConfirm={async () => { onConfirm={async () => {
@@ -369,7 +436,7 @@ const ChannelsTable = () => {
}} }}
> >
<Button theme="light" type="primary" style={{ marginRight: 1 }}> <Button theme="light" type="primary" style={{ marginRight: 1 }}>
复制 {t('复制')}
</Button> </Button>
</Popconfirm> </Popconfirm>
</div> </div>
@@ -385,7 +452,7 @@ const ChannelsTable = () => {
manageTag(record.key, 'enable'); manageTag(record.key, 'enable');
}} }}
> >
启用全部 {t('启用全部')}
</Button> </Button>
<Button <Button
theme="light" theme="light"
@@ -395,7 +462,7 @@ const ChannelsTable = () => {
manageTag(record.key, 'disable'); manageTag(record.key, 'disable');
}} }}
> >
禁用全部 {t('禁用全部')}
</Button> </Button>
<Button <Button
theme="light" theme="light"
@@ -406,7 +473,7 @@ const ChannelsTable = () => {
setEditingTag(record.key); setEditingTag(record.key);
}} }}
> >
编辑 {t('编辑')}
</Button> </Button>
</> </>
); );
@@ -579,25 +646,25 @@ const ChannelsTable = () => {
const copySelectedChannel = async (record) => { const copySelectedChannel = async (record) => {
const channelToCopy = record const channelToCopy = record
channelToCopy.name += '_复制'; channelToCopy.name += t('_复制');
channelToCopy.created_time = null; channelToCopy.created_time = null;
channelToCopy.balance = 0; channelToCopy.balance = 0;
channelToCopy.used_quota = 0; channelToCopy.used_quota = 0;
if (!channelToCopy) { if (!channelToCopy) {
showError('渠道未找到,请刷新页面后重试。'); showError(t('渠道未找到,请刷新页面后重试。'));
return; return;
} }
try { try {
const newChannel = { ...channelToCopy, id: undefined }; const newChannel = { ...channelToCopy, id: undefined };
const response = await API.post('/api/channel/', newChannel); const response = await API.post('/api/channel/', newChannel);
if (response.data.success) { if (response.data.success) {
showSuccess('渠道复制成功'); showSuccess(t('渠道复制成功'));
await refresh(); await refresh();
} else { } else {
showError(response.data.message); showError(response.data.message);
} }
} catch (error) { } catch (error) {
showError('渠道复制失败: ' + error.message); showError(t('渠道复制失败: ') + error.message);
} }
}; };
@@ -656,7 +723,7 @@ const ChannelsTable = () => {
} }
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess('操作成功完成!'); showSuccess(t('操作成功完成!'));
let channel = res.data.data; let channel = res.data.data;
let newChannels = [...channels]; let newChannels = [...channels];
if (action === 'delete') { if (action === 'delete') {
@@ -703,71 +770,6 @@ const ChannelsTable = () => {
} }
}; };
const renderStatus = (status) => {
switch (status) {
case 1:
return (
<Tag size="large" color="green">
已启用
</Tag>
);
case 2:
return (
<Tag size="large" color="yellow">
已禁用
</Tag>
);
case 3:
return (
<Tag size="large" color="yellow">
自动禁用
</Tag>
);
default:
return (
<Tag size="large" color="grey">
未知状态
</Tag>
);
}
};
const renderResponseTime = (responseTime) => {
let time = responseTime / 1000;
time = time.toFixed(2) + ' 秒';
if (responseTime === 0) {
return (
<Tag size="large" color="grey">
未测试
</Tag>
);
} else if (responseTime <= 1000) {
return (
<Tag size="large" color="green">
{time}
</Tag>
);
} else if (responseTime <= 3000) {
return (
<Tag size="large" color="lime">
{time}
</Tag>
);
} else if (responseTime <= 5000) {
return (
<Tag size="large" color="yellow">
{time}
</Tag>
);
} else {
return (
<Tag size="large" color="red">
{time}
</Tag>
);
}
};
const searchChannels = async (searchKeyword, searchGroup, searchModel, enableTagMode) => { const searchChannels = async (searchKeyword, searchGroup, searchModel, enableTagMode) => {
if (searchKeyword === '' && searchGroup === '' && searchModel === '') { if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
await loadChannels(0, pageSize, idSort, enableTagMode); await loadChannels(0, pageSize, idSort, enableTagMode);
@@ -794,7 +796,7 @@ const ChannelsTable = () => {
if (success) { if (success) {
record.response_time = time * 1000; record.response_time = time * 1000;
record.test_time = Date.now() / 1000; record.test_time = Date.now() / 1000;
showInfo(`通道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`); showInfo(t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。').replace('${name}', record.name).replace('${time.toFixed(2)}', time.toFixed(2)));
} else { } else {
showError(message); showError(message);
} }
@@ -804,7 +806,7 @@ const ChannelsTable = () => {
const res = await API.get(`/api/channel/test`); const res = await API.get(`/api/channel/test`);
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
showInfo('已成功开始测试所有通道,请刷新页面查看结果。'); showInfo(t('已成功开始测试所有已启用通道,请刷新页面查看结果。'));
} else { } else {
showError(message); showError(message);
} }
@@ -814,7 +816,7 @@ const ChannelsTable = () => {
const res = await API.delete(`/api/channel/disabled`); const res = await API.delete(`/api/channel/disabled`);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
showSuccess(`已删除所有禁用渠道,共计 ${data}`); showSuccess(t('已删除所有禁用渠道,共计 ${data} 个').replace('${data}', data));
await refresh(); await refresh();
} else { } else {
showError(message); showError(message);
@@ -827,7 +829,7 @@ const ChannelsTable = () => {
if (success) { if (success) {
record.balance = balance; record.balance = balance;
record.balance_updated_time = Date.now() / 1000; record.balance_updated_time = Date.now() / 1000;
showInfo(`通道 ${record.name} 余额更新成功!`); showInfo(t('通道 ${name} 余额更新成功!').replace('${name}', record.name));
} else { } else {
showError(message); showError(message);
} }
@@ -838,7 +840,7 @@ const ChannelsTable = () => {
const res = await API.get(`/api/channel/update_balance`); const res = await API.get(`/api/channel/update_balance`);
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
showInfo('已更新完毕所有已启用通道余额!'); showInfo(t('已更新完毕所有已启用通道余额!'));
} else { } else {
showError(message); showError(message);
} }
@@ -847,7 +849,7 @@ const ChannelsTable = () => {
const batchDeleteChannels = async () => { const batchDeleteChannels = async () => {
if (selectedChannels.length === 0) { if (selectedChannels.length === 0) {
showError('请先选择要删除的通道!'); showError(t('请先选择要删除的通道!'));
return; return;
} }
setLoading(true); setLoading(true);
@@ -858,7 +860,7 @@ const ChannelsTable = () => {
const res = await API.post(`/api/channel/batch`, { ids: ids }); const res = await API.post(`/api/channel/batch`, { ids: ids });
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
showSuccess(`已删除 ${data} 个通道!`); showSuccess(t('已删除 ${data} 个通道!').replace('${data}', data));
await refresh(); await refresh();
} else { } else {
showError(message); showError(message);
@@ -870,7 +872,7 @@ const ChannelsTable = () => {
const res = await API.post(`/api/channel/fix`); const res = await API.post(`/api/channel/fix`);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
showSuccess(`已修复 ${data} 个通道!`); showSuccess(t('已修复 ${data} 个通道!').replace('${data}', data));
await refresh(); await refresh();
} else { } else {
showError(message); showError(message);
@@ -990,8 +992,8 @@ const ChannelsTable = () => {
<Space> <Space>
<Form.Input <Form.Input
field="search_keyword" field="search_keyword"
label="搜索渠道关键词" label={t('搜索渠道关键词')}
placeholder="ID名称和密钥 ..." placeholder={t('搜索渠道的 ID名称和密钥 ...')}
value={searchKeyword} value={searchKeyword}
loading={searching} loading={searching}
onChange={(v) => { onChange={(v) => {
@@ -1000,8 +1002,8 @@ const ChannelsTable = () => {
/> />
<Form.Input <Form.Input
field="search_model" field="search_model"
label="模型" label={t('模型')}
placeholder="模型关键字" placeholder={t('模型关键字')}
value={searchModel} value={searchModel}
loading={searching} loading={searching}
onChange={(v) => { onChange={(v) => {
@@ -1010,8 +1012,8 @@ const ChannelsTable = () => {
/> />
<Form.Select <Form.Select
field="group" field="group"
label="分组" label={t('分组')}
optionList={[{ label: '选择分组', value: null }, ...groupOptions]} optionList={[{ label: t('选择分组'), value: null }, ...groupOptions]}
initValue={null} initValue={null}
onChange={(v) => { onChange={(v) => {
setSearchGroup(v); setSearchGroup(v);
@@ -1019,13 +1021,13 @@ const ChannelsTable = () => {
}} }}
/> />
<Button <Button
label="查询" label={t('查询')}
type="primary" type="primary"
htmlType="submit" htmlType="submit"
className="btn-margin-right" className="btn-margin-right"
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
> >
查询 {t('查询')}
</Button> </Button>
</Space> </Space>
</div> </div>
@@ -1042,12 +1044,12 @@ const ChannelsTable = () => {
<Space <Space
style={{ pointerEvents: 'auto', marginTop: isMobile() ? 0 : 45 }} style={{ pointerEvents: 'auto', marginTop: isMobile() ? 0 : 45 }}
> >
<Typography.Text strong>使用ID排序</Typography.Text> <Typography.Text strong>{t('使用ID排序')}</Typography.Text>
<Switch <Switch
checked={idSort} checked={idSort}
label="使用ID排序" label={t('使用ID排序')}
uncheckedText="关" uncheckedText={t('关')}
aria-label="是否用ID排序" aria-label={t('是否用ID排序')}
onChange={(v) => { onChange={(v) => {
localStorage.setItem('id-sort', v + ''); localStorage.setItem('id-sort', v + '');
setIdSort(v); setIdSort(v);
@@ -1069,35 +1071,35 @@ const ChannelsTable = () => {
setShowEdit(true); setShowEdit(true);
}} }}
> >
添加渠道 {t('添加渠道')}
</Button> </Button>
<Popconfirm <Popconfirm
title="确定?" title={t('确定?')}
okType={'warning'} okType={'warning'}
onConfirm={testAllChannels} onConfirm={testAllChannels}
position={isMobile() ? 'top' : 'top'} position={isMobile() ? 'top' : 'top'}
> >
<Button theme="light" type="warning" style={{ marginRight: 8 }}> <Button theme="light" type="warning" style={{ marginRight: 8 }}>
测试所有通道 {t('测试所有通道')}
</Button> </Button>
</Popconfirm> </Popconfirm>
<Popconfirm <Popconfirm
title="确定?" title={t('确定?')}
okType={'secondary'} okType={'secondary'}
onConfirm={updateAllChannelsBalance} onConfirm={updateAllChannelsBalance}
> >
<Button theme="light" type="secondary" style={{ marginRight: 8 }}> <Button theme="light" type="secondary" style={{ marginRight: 8 }}>
更新所有已启用通道余额 {t('更新所有已启用通道余额')}
</Button> </Button>
</Popconfirm> </Popconfirm>
<Popconfirm <Popconfirm
title="确定是否要删除禁用通道?" title={t('确定是否要删除禁用通道?')}
content="此修改将不可逆" content={t('此修改将不可逆')}
okType={'danger'} okType={'danger'}
onConfirm={deleteAllDisabledChannels} onConfirm={deleteAllDisabledChannels}
> >
<Button theme="light" type="danger" style={{ marginRight: 8 }}> <Button theme="light" type="danger" style={{ marginRight: 8 }}>
删除禁用通道 {t('删除禁用通道')}
</Button> </Button>
</Popconfirm> </Popconfirm>
@@ -1107,24 +1109,24 @@ const ChannelsTable = () => {
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
onClick={refresh} onClick={refresh}
> >
刷新 {t('刷新')}
</Button> </Button>
</Space> </Space>
</div> </div>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Space> <Space>
<Typography.Text strong>开启批量删除</Typography.Text> <Typography.Text strong>{t('开启批量删除')}</Typography.Text>
<Switch <Switch
label="开启批量删除" label={t('开启批量删除')}
uncheckedText="关" uncheckedText={t('关')}
aria-label="是否开启批量删除" aria-label={t('是否开启批量删除')}
onChange={(v) => { onChange={(v) => {
setEnableBatchDelete(v); setEnableBatchDelete(v);
}} }}
></Switch> ></Switch>
<Popconfirm <Popconfirm
title="确定是否要删除所选通道?" title={t('确定是否要删除所选通道?')}
content="此修改将不可逆" content={t('此修改将不可逆')}
okType={'danger'} okType={'danger'}
onConfirm={batchDeleteChannels} onConfirm={batchDeleteChannels}
disabled={!enableBatchDelete} disabled={!enableBatchDelete}
@@ -1136,33 +1138,32 @@ const ChannelsTable = () => {
type="danger" type="danger"
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
> >
删除所选通道 {t('删除所选通道')}
</Button> </Button>
</Popconfirm> </Popconfirm>
<Popconfirm <Popconfirm
title="确定是否要修复数据库一致性?" title={t('确定是否要修复数据库一致性?')}
content="进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用" content={t('进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用')}
okType={'warning'} okType={'warning'}
onConfirm={fixChannelsAbilities} onConfirm={fixChannelsAbilities}
position={'top'} position={'top'}
> >
<Button theme="light" type="secondary" style={{ marginRight: 8 }}> <Button theme="light" type="secondary" style={{ marginRight: 8 }}>
修复数据库一致性 {t('修复数据库一致性')}
</Button> </Button>
</Popconfirm> </Popconfirm>
</Space> </Space>
</div> </div>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Space> <Space>
<Typography.Text strong>标签聚合模式</Typography.Text> <Typography.Text strong>{t('标签聚合模式')}</Typography.Text>
<Switch <Switch
checked={enableTagMode} checked={enableTagMode}
label="标签聚合模式" label={t('标签聚合模式')}
uncheckedText="关" uncheckedText={t('关')}
aria-label="是否启用标签聚合" aria-label={t('是否启用标签聚合')}
onChange={(v) => { onChange={(v) => {
setEnableTagMode(v); setEnableTagMode(v);
// 切换模式时重新加载数据
loadChannels(0, pageSize, idSort, v); loadChannels(0, pageSize, idSort, v);
}} }}
/> />

View File

@@ -1,9 +1,10 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getFooterHTML, getSystemName } from '../helpers'; import { getFooterHTML, getSystemName } from '../helpers';
import { Layout, Tooltip } from '@douyinfe/semi-ui'; import { Layout, Tooltip } from '@douyinfe/semi-ui';
const FooterBar = () => { const FooterBar = () => {
const { t } = useTranslation();
const systemName = getSystemName(); const systemName = getSystemName();
const [footer, setFooter] = useState(getFooterHTML()); const [footer, setFooter] = useState(getFooterHTML());
let remainCheckTimes = 5; let remainCheckTimes = 5;
@@ -24,7 +25,7 @@ const FooterBar = () => {
> >
New API {import.meta.env.VITE_REACT_APP_VERSION}{' '} New API {import.meta.env.VITE_REACT_APP_VERSION}{' '}
</a> </a>
{' '} {t('由')}{' '}
<a <a
href='https://github.com/Calcium-Ion' href='https://github.com/Calcium-Ion'
target='_blank' target='_blank'
@@ -32,7 +33,7 @@ const FooterBar = () => {
> >
Calcium-Ion Calcium-Ion
</a>{' '} </a>{' '}
开发基于{' '} {t('开发,基于')}{' '}
<a <a
href='https://github.com/songquanpeng/one-api' href='https://github.com/songquanpeng/one-api'
target='_blank' target='_blank'

View File

@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User'; import { UserContext } from '../context/User';
import { useSetTheme, useTheme } from '../context/Theme'; import { useSetTheme, useTheme } from '../context/Theme';
import { useTranslation } from 'react-i18next';
import { API, getLogo, getSystemName, isMobile, showSuccess } from '../helpers'; import { API, getLogo, getSystemName, isMobile, showSuccess } from '../helpers';
import '../index.css'; import '../index.css';
@@ -9,70 +10,61 @@ import '../index.css';
import fireworks from 'react-fireworks'; import fireworks from 'react-fireworks';
import { import {
IconClose,
IconHelpCircle, IconHelpCircle,
IconHome, IconHome,
IconHomeStroked, IconHomeStroked, IconIndentLeft,
IconKey, IconKey, IconMenu,
IconNoteMoneyStroked, IconNoteMoneyStroked,
IconPriceTag, IconPriceTag,
IconUser IconUser,
IconLanguage
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui'; import { Avatar, Button, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
import { stringToColor } from '../helpers/render'; import { stringToColor } from '../helpers/render';
import Text from '@douyinfe/semi-ui/lib/es/typography/text'; import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import { StyleContext } from '../context/Style/index.js';
// HeaderBar Buttons
let headerButtons = [
{
text: '关于',
itemKey: 'about',
to: '/about',
icon: <IconHelpCircle />,
},
];
let buttons = [
{
text: '首页',
itemKey: 'home',
to: '/',
// icon: <IconHomeStroked />,
},
// {
// text: 'Playground',
// itemKey: 'playground',
// to: '/playground',
// // icon: <IconNoteMoneyStroked />,
// },
];
if (localStorage.getItem('chat_link')) {
headerButtons.splice(1, 0, {
name: '聊天',
to: '/chat',
icon: 'comments',
});
}
const HeaderBar = () => { const HeaderBar = () => {
const { t, i18n } = useTranslation();
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
const [styleState, styleDispatch] = useContext(StyleContext);
let navigate = useNavigate(); let navigate = useNavigate();
const [currentLang, setCurrentLang] = useState(i18n.language);
const [showSidebar, setShowSidebar] = useState(false);
const systemName = getSystemName(); const systemName = getSystemName();
const logo = getLogo(); const logo = getLogo();
const currentDate = new Date(); const currentDate = new Date();
// enable fireworks on new year(1.1 and 2.9-2.24) // enable fireworks on new year(1.1 and 2.9-2.24)
const isNewYear = const isNewYear =
(currentDate.getMonth() === 0 && currentDate.getDate() === 1) || (currentDate.getMonth() === 0 && currentDate.getDate() === 1);
(currentDate.getMonth() === 1 &&
currentDate.getDate() >= 9 && let buttons = [
currentDate.getDate() <= 24); {
text: t('首页'),
itemKey: 'home',
to: '/',
},
{
text: t('控制台'),
itemKey: 'detail',
to: '/',
},
{
text: t('定价'),
itemKey: 'pricing',
to: '/pricing',
},
{
text: t('关于'),
itemKey: 'about',
to: '/about',
},
];
async function logout() { async function logout() {
setShowSidebar(false);
await API.get('/api/user/logout'); await API.get('/api/user/logout');
showSuccess('注销成功!'); showSuccess(t('注销成功!'));
userDispatch({ type: 'logout' }); userDispatch({ type: 'logout' });
localStorage.removeItem('user'); localStorage.removeItem('user');
navigate('/login'); navigate('/login');
@@ -102,42 +94,82 @@ const HeaderBar = () => {
} }
}, []); }, []);
useEffect(() => {
const handleLanguageChanged = (lng) => {
setCurrentLang(lng);
};
i18n.on('languageChanged', handleLanguageChanged);
return () => {
i18n.off('languageChanged', handleLanguageChanged);
};
}, [i18n]);
const handleLanguageChange = (lang) => {
i18n.changeLanguage(lang);
};
return ( return (
<> <>
<Layout> <Layout>
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<Nav <Nav
className={'topnav'}
mode={'horizontal'} mode={'horizontal'}
// bodyStyle={{ height: 100 }}
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => { renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
const routerMap = { const routerMap = {
about: '/about', about: '/about',
login: '/login', login: '/login',
register: '/register', register: '/register',
pricing: '/pricing',
detail: '/detail',
home: '/', home: '/',
}; };
return ( return (
<Link <div onClick={(e) => {
style={{ textDecoration: 'none' }} if (props.itemKey === 'home') {
to={routerMap[props.itemKey]} styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
> styleDispatch({ type: 'SET_SIDER', payload: false });
{itemElement} } else {
</Link> styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
if (!styleState.isMobile) {
styleDispatch({ type: 'SET_SIDER', payload: true });
}
}
}}>
<Link
className="header-bar-text"
style={{ textDecoration: 'none' }}
to={routerMap[props.itemKey]}
>
{itemElement}
</Link>
</div>
); );
}} }}
selectedKeys={[]} selectedKeys={[]}
// items={headerButtons} // items={headerButtons}
onSelect={(key) => {}} onSelect={(key) => {}}
header={isMobile()?{ header={styleState.isMobile?{
logo: ( logo: (
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} /> <>
{
!styleState.showSider ?
<Button icon={<IconMenu />} theme="light" aria-label={t('展开侧边栏')} onClick={
() => styleDispatch({ type: 'SET_SIDER', payload: true })
} />:
<Button icon={<IconIndentLeft />} theme="light" aria-label={t('闭侧边栏')} onClick={
() => styleDispatch({ type: 'SET_SIDER', payload: false })
} />
}
</>
), ),
}:{ }:{
logo: ( logo: (
<img src={logo} alt='logo' /> <img src={logo} alt='logo' />
), ),
text: systemName, text: systemName,
}} }}
items={buttons} items={buttons}
footer={ footer={
@@ -157,27 +189,49 @@ const HeaderBar = () => {
<Nav.Item itemKey={'new-year'} text={'🏮'} /> <Nav.Item itemKey={'new-year'} text={'🏮'} />
</Dropdown> </Dropdown>
)} )}
<Nav.Item itemKey={'about'} icon={<IconHelpCircle />} /> {/* <Nav.Item itemKey={'about'} icon={<IconHelpCircle />} /> */}
<> <>
{!isMobile() && ( <Switch
<Switch checkedText='🌞'
checkedText='🌞' size={styleState.isMobile?'default':'large'}
size={'large'} checked={theme === 'dark'}
checked={theme === 'dark'} uncheckedText='🌙'
uncheckedText='🌙' onChange={(checked) => {
onChange={(checked) => { setTheme(checked);
setTheme(checked); }}
}} />
/>
)}
</> </>
<Dropdown
position='bottomRight'
render={
<Dropdown.Menu>
<Dropdown.Item
onClick={() => handleLanguageChange('zh')}
type={currentLang === 'zh' ? 'primary' : 'tertiary'}
>
中文
</Dropdown.Item>
<Dropdown.Item
onClick={() => handleLanguageChange('en')}
type={currentLang === 'en' ? 'primary' : 'tertiary'}
>
English
</Dropdown.Item>
</Dropdown.Menu>
}
>
<Nav.Item
itemKey={'language'}
icon={<IconLanguage />}
/>
</Dropdown>
{userState.user ? ( {userState.user ? (
<> <>
<Dropdown <Dropdown
position='bottomRight' position='bottomRight'
render={ render={
<Dropdown.Menu> <Dropdown.Menu>
<Dropdown.Item onClick={logout}>退出</Dropdown.Item> <Dropdown.Item onClick={logout}>{t('退出')}</Dropdown.Item>
</Dropdown.Menu> </Dropdown.Menu>
} }
> >
@@ -188,21 +242,25 @@ const HeaderBar = () => {
> >
{userState.user.username[0]} {userState.user.username[0]}
</Avatar> </Avatar>
<span>{userState.user.username}</span> {styleState.isMobile?null:<Text>{userState.user.username}</Text>}
</Dropdown> </Dropdown>
</> </>
) : ( ) : (
<> <>
<Nav.Item <Nav.Item
itemKey={'login'} itemKey={'login'}
text={'登录'} text={!styleState.isMobile?t('登录'):null}
// icon={<IconKey />}
/>
<Nav.Item
itemKey={'register'}
text={'注册'}
icon={<IconUser />} icon={<IconUser />}
/> />
{
!styleState.isMobile && (
<Nav.Item
itemKey={'register'}
text={t('注册')}
icon={<IconKey />}
/>
)
}
</> </>
)} )}
</> </>

View File

@@ -28,6 +28,7 @@ import { IconGithubLogo, IconAlarm } from '@douyinfe/semi-icons';
import WeChatIcon from './WeChatIcon'; import WeChatIcon from './WeChatIcon';
import { setUserData } from '../helpers/data.js'; import { setUserData } from '../helpers/data.js';
import LinuxDoIcon from './LinuxDoIcon.js'; import LinuxDoIcon from './LinuxDoIcon.js';
import { useTranslation } from 'react-i18next';
const LoginForm = () => { const LoginForm = () => {
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
@@ -45,6 +46,7 @@ const LoginForm = () => {
let navigate = useNavigate(); let navigate = useNavigate();
const [status, setStatus] = useState({}); const [status, setStatus] = useState({});
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false); const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
const { t } = useTranslation();
const logo = getLogo(); const logo = getLogo();
@@ -55,7 +57,7 @@ const LoginForm = () => {
useEffect(() => { useEffect(() => {
if (searchParams.get('expired')) { if (searchParams.get('expired')) {
showError('未登录或登录已过期,请重新登录'); showError(t('未登录或登录已过期,请重新登录'));
} }
let status = localStorage.getItem('status'); let status = localStorage.getItem('status');
if (status) { if (status) {
@@ -182,20 +184,20 @@ const LoginForm = () => {
<div style={{ width: 500 }}> <div style={{ width: 500 }}>
<Card> <Card>
<Title heading={2} style={{ textAlign: 'center' }}> <Title heading={2} style={{ textAlign: 'center' }}>
用户登录 {t('用户登录')}
</Title> </Title>
<Form> <Form>
<Form.Input <Form.Input
field={'username'} field={'username'}
label={'用户名'} label={t('用户名/邮箱')}
placeholder='用户名' placeholder={t('用户名/邮箱')}
name='username' name='username'
onChange={(value) => handleChange('username', value)} onChange={(value) => handleChange('username', value)}
/> />
<Form.Input <Form.Input
field={'password'} field={'password'}
label={'密码'} label={t('密码')}
placeholder='密码' placeholder={t('密码')}
name='password' name='password'
type='password' type='password'
onChange={(value) => handleChange('password', value)} onChange={(value) => handleChange('password', value)}
@@ -209,7 +211,7 @@ const LoginForm = () => {
htmlType={'submit'} htmlType={'submit'}
onClick={handleSubmit} onClick={handleSubmit}
> >
登录 {t('登录')}
</Button> </Button>
</Form> </Form>
<div <div
@@ -220,10 +222,10 @@ const LoginForm = () => {
}} }}
> >
<Text> <Text>
没有账号请先 <Link to='/register'>注册账号</Link> {t('没有账户?')} <Link to='/register'>{t('点击注册')}</Link>
</Text> </Text>
<Text> <Text>
忘记密码 <Link to='/reset'>点击重置</Link> {t('忘记密码?')} <Link to='/reset'>{t('点击重置')}</Link>
</Text> </Text>
</div> </div>
{status.github_oauth || {status.github_oauth ||
@@ -232,7 +234,7 @@ const LoginForm = () => {
status.linuxdo_oauth ? ( status.linuxdo_oauth ? (
<> <>
<Divider margin='12px' align='center'> <Divider margin='12px' align='center'>
第三方登录 {t('第三方登录')}
</Divider> </Divider>
<div <div
style={{ style={{
@@ -296,12 +298,12 @@ const LoginForm = () => {
<></> <></>
)} )}
<Modal <Modal
title='微信扫码登录' title={t('微信扫码登录')}
visible={showWeChatLoginModal} visible={showWeChatLoginModal}
maskClosable={true} maskClosable={true}
onOk={onSubmitWeChatVerificationCode} onOk={onSubmitWeChatVerificationCode}
onCancel={() => setShowWeChatLoginModal(false)} onCancel={() => setShowWeChatLoginModal(false)}
okText={'登录'} okText={t('登录')}
size={'small'} size={'small'}
centered={true} centered={true}
> >
@@ -316,14 +318,14 @@ const LoginForm = () => {
</div> </div>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<p> <p>
微信扫码关注公众号输入验证码获取验证码三分钟内有效 {t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
</p> </p>
</div> </div>
<Form size='large'> <Form size='large'>
<Form.Input <Form.Input
field={'wechat_verification_code'} field={'wechat_verification_code'}
placeholder='验证码' placeholder={t('验证码')}
label={'验证码'} label={t('验证码')}
value={inputs.wechat_verification_code} value={inputs.wechat_verification_code}
onChange={(value) => onChange={(value) =>
handleChange('wechat_verification_code', value) handleChange('wechat_verification_code', value)

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
API, API,
copy, copy,
@@ -25,7 +26,7 @@ import {
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import { import {
renderAudioModelPrice, renderAudioModelPrice,
renderModelPrice, renderModelPrice, renderModelPriceSimple,
renderNumber, renderNumber,
renderQuota, renderQuota,
stringToColor stringToColor
@@ -40,8 +41,8 @@ function renderTimestamp(timestamp) {
} }
const MODE_OPTIONS = [ const MODE_OPTIONS = [
{ key: 'all', text: '全部用户', value: 'all' }, { key: 'all', text: 'all', value: 'all' },
{ key: 'self', text: '当前用户', value: 'self' }, { key: 'self', text: 'current user', value: 'self' },
]; ];
const colors = [ const colors = [
@@ -62,123 +63,92 @@ const colors = [
'yellow', 'yellow',
]; ];
function renderType(type) { const LogsTable = () => {
switch (type) { const { t } = useTranslation();
case 1:
function renderType(type) {
switch (type) {
case 1:
return <Tag color='cyan' size='large'>{t('充值')}</Tag>;
case 2:
return <Tag color='lime' size='large'>{t('消费')}</Tag>;
case 3:
return <Tag color='orange' size='large'>{t('管理')}</Tag>;
case 4:
return <Tag color='purple' size='large'>{t('系统')}</Tag>;
default:
return <Tag color='black' size='large'>{t('未知')}</Tag>;
}
}
function renderIsStream(bool) {
if (bool) {
return <Tag color='blue' size='large'>{t('流')}</Tag>;
} else {
return <Tag color='purple' size='large'>{t('非流')}</Tag>;
}
}
function renderUseTime(type) {
const time = parseInt(type);
if (time < 101) {
return ( return (
<Tag color='cyan' size='large'> <Tag color='green' size='large'>
{' '} {' '}
充值{' '} {time} s{' '}
</Tag> </Tag>
); );
case 2: } else if (time < 300) {
return (
<Tag color='lime' size='large'>
{' '}
消费{' '}
</Tag>
);
case 3:
return ( return (
<Tag color='orange' size='large'> <Tag color='orange' size='large'>
{' '} {' '}
管理{' '} {time} s{' '}
</Tag> </Tag>
); );
case 4: } else {
return ( return (
<Tag color='purple' size='large'> <Tag color='red' size='large'>
{' '} {' '}
系统{' '} {time} s{' '}
</Tag> </Tag>
); );
default: }
}
function renderFirstUseTime(type) {
let time = parseFloat(type) / 1000.0;
time = time.toFixed(1);
if (time < 3) {
return ( return (
<Tag color='black' size='large'> <Tag color='green' size='large'>
{' '} {' '}
未知{' '} {time} s{' '}
</Tag> </Tag>
); );
} } else if (time < 10) {
} return (
<Tag color='orange' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' size='large'>
{' '}
{time} s{' '}
</Tag>
);
}
}
function renderIsStream(bool) {
if (bool) {
return (
<Tag color='blue' size='large'>
</Tag>
);
} else {
return (
<Tag color='purple' size='large'>
非流
</Tag>
);
}
}
function renderUseTime(type) {
const time = parseInt(type);
if (time < 101) {
return (
<Tag color='green' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else if (time < 300) {
return (
<Tag color='orange' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' size='large'>
{' '}
{time} s{' '}
</Tag>
);
}
}
function renderFirstUseTime(type) {
let time = parseFloat(type) / 1000.0;
time = time.toFixed(1);
if (time < 3) {
return (
<Tag color='green' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else if (time < 10) {
return (
<Tag color='orange' size='large'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' size='large'>
{' '}
{time} s{' '}
</Tag>
);
}
}
const LogsTable = () => {
const columns = [ const columns = [
{ {
title: '时间', title: t('时间'),
dataIndex: 'timestamp2string', dataIndex: 'timestamp2string',
}, },
{ {
title: '渠道', title: t('渠道'),
dataIndex: 'channel', dataIndex: 'channel',
className: isAdmin() ? 'tableShow' : 'tableHiddle', className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => { render: (text, record, index) => {
@@ -204,7 +174,7 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '用户', title: t('用户'),
dataIndex: 'username', dataIndex: 'username',
className: isAdmin() ? 'tableShow' : 'tableHiddle', className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => { render: (text, record, index) => {
@@ -226,7 +196,7 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '令牌', title: t('令牌'),
dataIndex: 'token_name', dataIndex: 'token_name',
render: (text, record, index) => { render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? ( return record.type === 0 || record.type === 2 ? (
@@ -239,7 +209,7 @@ const LogsTable = () => {
}} }}
> >
{' '} {' '}
{text}{' '} {t(text)}{' '}
</Tag> </Tag>
</div> </div>
) : ( ) : (
@@ -248,14 +218,14 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '类型', title: t('类型'),
dataIndex: 'type', dataIndex: 'type',
render: (text, record, index) => { render: (text, record, index) => {
return <>{renderType(text)}</>; return <>{renderType(text)}</>;
}, },
}, },
{ {
title: '模型', title: t('模型'),
dataIndex: 'model_name', dataIndex: 'model_name',
render: (text, record, index) => { render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? ( return record.type === 0 || record.type === 2 ? (
@@ -277,7 +247,7 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '用时/首字', title: t('用时/首字'),
dataIndex: 'use_time', dataIndex: 'use_time',
render: (text, record, index) => { render: (text, record, index) => {
if (record.is_stream) { if (record.is_stream) {
@@ -304,7 +274,7 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '提示', title: t('提示'),
dataIndex: 'prompt_tokens', dataIndex: 'prompt_tokens',
render: (text, record, index) => { render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? ( return record.type === 0 || record.type === 2 ? (
@@ -315,7 +285,7 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '补全', title: t('补全'),
dataIndex: 'completion_tokens', dataIndex: 'completion_tokens',
render: (text, record, index) => { render: (text, record, index) => {
return parseInt(text) > 0 && return parseInt(text) > 0 &&
@@ -327,7 +297,7 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '花费', title: t('花费'),
dataIndex: 'quota', dataIndex: 'quota',
render: (text, record, index) => { render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? ( return record.type === 0 || record.type === 2 ? (
@@ -338,11 +308,11 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '重试', title: t('重试'),
dataIndex: 'retry', dataIndex: 'retry',
className: isAdmin() ? 'tableShow' : 'tableHiddle', className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => { render: (text, record, index) => {
let content = '渠道' + record.channel; let content = t('渠道') + `${record.channel}`;
if (record.other !== '') { if (record.other !== '') {
let other = JSON.parse(record.other); let other = JSON.parse(record.other);
if (other === null) { if (other === null) {
@@ -357,7 +327,7 @@ const LogsTable = () => {
// channel id array // channel id array
let useChannel = other.admin_info.use_channel; let useChannel = other.admin_info.use_channel;
let useChannelStr = useChannel.join('->'); let useChannelStr = useChannel.join('->');
content = `渠道${useChannelStr}`; content = t('渠道') + `${useChannelStr}`;
} }
} }
} }
@@ -365,7 +335,7 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '详情', title: t('详情'),
dataIndex: 'content', dataIndex: 'content',
render: (text, record, index) => { render: (text, record, index) => {
let other = getLogOther(record.other); let other = getLogOther(record.other);
@@ -386,14 +356,11 @@ const LogsTable = () => {
); );
} }
// let content = renderModelPrice( let content = renderModelPriceSimple(
// record.prompt_tokens, other.model_ratio,
// record.completion_tokens, other.model_price,
// other.model_ratio, other.group_ratio,
// other.model_price, );
// other.completion_ratio,
// other.group_ratio,
// );
return ( return (
<Paragraph <Paragraph
ellipsis={{ ellipsis={{
@@ -401,7 +368,7 @@ const LogsTable = () => {
}} }}
style={{ maxWidth: 240 }} style={{ maxWidth: 240 }}
> >
调用消费 {content}
</Paragraph> </Paragraph>
); );
}, },
@@ -496,13 +463,13 @@ const LogsTable = () => {
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
Modal.info({ Modal.info({
title: '用户信息', title: t('用户信息'),
content: ( content: (
<div style={{ padding: 12 }}> <div style={{ padding: 12 }}>
<p>用户名: {data.username}</p> <p>{t('用户名')}: {data.username}</p>
<p>余额: {renderQuota(data.quota)}</p> <p>{t('余额')}: {renderQuota(data.quota)}</p>
<p>已用额度{renderQuota(data.used_quota)}</p> <p>{t('已用额度')}{renderQuota(data.used_quota)}</p>
<p>请求次数{renderNumber(data.request_count)}</p> <p>{t('请求次数')}{renderNumber(data.request_count)}</p>
</div> </div>
), ),
centered: true, centered: true,
@@ -540,26 +507,26 @@ const LogsTable = () => {
} }
if (other?.ws || other?.audio) { if (other?.ws || other?.audio) {
expandDataLocal.push({ expandDataLocal.push({
key: '语音输入', key: t('语音输入'),
value: other.audio_input, value: other.audio_input,
}); });
expandDataLocal.push({ expandDataLocal.push({
key: '语音输出', key: t('语音输出'),
value: other.audio_output, value: other.audio_output,
}); });
expandDataLocal.push({ expandDataLocal.push({
key: '文字输入', key: t('文字输入'),
value: other.text_input, value: other.text_input,
}); });
expandDataLocal.push({ expandDataLocal.push({
key: '文字输出', key: t('文字输出'),
value: other.text_output, value: other.text_output,
}); });
} }
expandDataLocal.push({ expandDataLocal.push({
key: '日志详情', key: t('日志详情'),
value: logs[i].content, value: logs[i].content,
}) });
if (logs[i].type === 2) { if (logs[i].type === 2) {
let content = ''; let content = '';
if (other?.ws || other?.audio) { if (other?.ws || other?.audio) {
@@ -586,7 +553,7 @@ const LogsTable = () => {
); );
} }
expandDataLocal.push({ expandDataLocal.push({
key: '计费过程', key: t('计费过程'),
value: content, value: content,
}); });
} }
@@ -679,7 +646,7 @@ const LogsTable = () => {
<Spin spinning={loadingStat}> <Spin spinning={loadingStat}>
<Space> <Space>
<Tag color='green' size='large' style={{ padding: 15 }}> <Tag color='green' size='large' style={{ padding: 15 }}>
总消耗额度: {renderQuota(stat.quota)} {t('总消耗额度')}: {renderQuota(stat.quota)}
</Tag> </Tag>
<Tag color='blue' size='large' style={{ padding: 15 }}> <Tag color='blue' size='large' style={{ padding: 15 }}>
RPM: {stat.rpm} RPM: {stat.rpm}
@@ -694,25 +661,25 @@ const LogsTable = () => {
<> <>
<Form.Input <Form.Input
field='token_name' field='token_name'
label='令牌名称' label={t('令牌名称')}
style={{ width: 176 }} style={{ width: 176 }}
value={token_name} value={token_name}
placeholder={'可选值'} placeholder={t('可选值')}
name='token_name' name='token_name'
onChange={(value) => handleInputChange(value, 'token_name')} onChange={(value) => handleInputChange(value, 'token_name')}
/> />
<Form.Input <Form.Input
field='model_name' field='model_name'
label='模型名称' label={t('模型名称')}
style={{ width: 176 }} style={{ width: 176 }}
value={model_name} value={model_name}
placeholder='可选值' placeholder={t('可选值')}
name='model_name' name='model_name'
onChange={(value) => handleInputChange(value, 'model_name')} onChange={(value) => handleInputChange(value, 'model_name')}
/> />
<Form.DatePicker <Form.DatePicker
field='start_timestamp' field='start_timestamp'
label='起始时间' label={t('起始时间')}
style={{ width: 272 }} style={{ width: 272 }}
initValue={start_timestamp} initValue={start_timestamp}
value={start_timestamp} value={start_timestamp}
@@ -723,7 +690,7 @@ const LogsTable = () => {
<Form.DatePicker <Form.DatePicker
field='end_timestamp' field='end_timestamp'
fluid fluid
label='结束时间' label={t('结束时间')}
style={{ width: 272 }} style={{ width: 272 }}
initValue={end_timestamp} initValue={end_timestamp}
value={end_timestamp} value={end_timestamp}
@@ -735,26 +702,26 @@ const LogsTable = () => {
<> <>
<Form.Input <Form.Input
field='channel' field='channel'
label='渠道 ID' label={t('渠道 ID')}
style={{ width: 176 }} style={{ width: 176 }}
value={channel} value={channel}
placeholder='可选值' placeholder={t('可选值')}
name='channel' name='channel'
onChange={(value) => handleInputChange(value, 'channel')} onChange={(value) => handleInputChange(value, 'channel')}
/> />
<Form.Input <Form.Input
field='username' field='username'
label='用户名称' label={t('用户名称')}
style={{ width: 176 }} style={{ width: 176 }}
value={username} value={username}
placeholder={'可选值'} placeholder={t('可选值')}
name='username' name='username'
onChange={(value) => handleInputChange(value, 'username')} onChange={(value) => handleInputChange(value, 'username')}
/> />
</> </>
)} )}
<Button <Button
label='查询' label={t('查询')}
type='primary' type='primary'
htmlType='submit' htmlType='submit'
className='btn-margin-right' className='btn-margin-right'
@@ -762,7 +729,7 @@ const LogsTable = () => {
loading={loading} loading={loading}
style={{ marginTop: 24 }} style={{ marginTop: 24 }}
> >
查询 {t('查询')}
</Button> </Button>
<Form.Section></Form.Section> <Form.Section></Form.Section>
</> </>
@@ -776,11 +743,11 @@ const LogsTable = () => {
loadLogs(0, pageSize, parseInt(value)); loadLogs(0, pageSize, parseInt(value));
}} }}
> >
<Select.Option value='0'>全部</Select.Option> <Select.Option value='0'>{t('全部')}</Select.Option>
<Select.Option value='1'>充值</Select.Option> <Select.Option value='1'>{t('充值')}</Select.Option>
<Select.Option value='2'>消费</Select.Option> <Select.Option value='2'>{t('消费')}</Select.Option>
<Select.Option value='3'>管理</Select.Option> <Select.Option value='3'>{t('管理')}</Select.Option>
<Select.Option value='4'>系统</Select.Option> <Select.Option value='4'>{t('系统')}</Select.Option>
</Select> </Select>
</div> </div>
<Table <Table

View File

@@ -21,6 +21,7 @@ import {
Typography, Typography,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import { useTranslation } from 'react-i18next';
const colors = [ const colors = [
'amber', 'amber',
@@ -40,247 +41,241 @@ const colors = [
'yellow', 'yellow',
]; ];
function renderType(type) {
switch (type) {
case 'IMAGINE':
return (
<Tag color='blue' size='large'>
绘图
</Tag>
);
case 'UPSCALE':
return (
<Tag color='orange' size='large'>
放大
</Tag>
);
case 'VARIATION':
return (
<Tag color='purple' size='large'>
变换
</Tag>
);
case 'HIGH_VARIATION':
return (
<Tag color='purple' size='large'>
强变换
</Tag>
);
case 'LOW_VARIATION':
return (
<Tag color='purple' size='large'>
弱变换
</Tag>
);
case 'PAN':
return (
<Tag color='cyan' size='large'>
平移
</Tag>
);
case 'DESCRIBE':
return (
<Tag color='yellow' size='large'>
图生文
</Tag>
);
case 'BLEND':
return (
<Tag color='lime' size='large'>
图混合
</Tag>
);
case 'UPLOAD':
return (
<Tag color='blue' size='large'>
上传文件
</Tag>
);
case 'SHORTEN':
return (
<Tag color='pink' size='large'>
缩词
</Tag>
);
case 'REROLL':
return (
<Tag color='indigo' size='large'>
重绘
</Tag>
);
case 'INPAINT':
return (
<Tag color='violet' size='large'>
局部重绘-提交
</Tag>
);
case 'ZOOM':
return (
<Tag color='teal' size='large'>
变焦
</Tag>
);
case 'CUSTOM_ZOOM':
return (
<Tag color='teal' size='large'>
自定义变焦-提交
</Tag>
);
case 'MODAL':
return (
<Tag color='green' size='large'>
窗口处理
</Tag>
);
case 'SWAP_FACE':
return (
<Tag color='light-green' size='large'>
换脸
</Tag>
);
default:
return (
<Tag color='white' size='large'>
未知
</Tag>
);
}
}
function renderCode(code) {
switch (code) {
case 1:
return (
<Tag color='green' size='large'>
已提交
</Tag>
);
case 21:
return (
<Tag color='lime' size='large'>
等待中
</Tag>
);
case 22:
return (
<Tag color='orange' size='large'>
重复提交
</Tag>
);
case 0:
return (
<Tag color='yellow' size='large'>
未提交
</Tag>
);
default:
return (
<Tag color='white' size='large'>
未知
</Tag>
);
}
}
function renderStatus(type) {
// Ensure all cases are string literals by adding quotes.
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' size='large'>
成功
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' size='large'>
未启动
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' size='large'>
队列中
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' size='large'>
执行中
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' size='large'>
失败
</Tag>
);
case 'MODAL':
return (
<Tag color='yellow' size='large'>
窗口等待
</Tag>
);
default:
return (
<Tag color='white' size='large'>
未知
</Tag>
);
}
}
const renderTimestamp = (timestampInSeconds) => {
const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
const year = date.getFullYear(); // 获取年份
const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份从0开始需要+1并保证两位数
const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
};
// 修改renderDuration函数以包含颜色逻辑
function renderDuration(submit_time, finishTime) {
// 确保startTime和finishTime都是有效的时间戳
if (!submit_time || !finishTime) return 'N/A';
// 将时间戳转换为Date对象
const start = new Date(submit_time);
const finish = new Date(finishTime);
// 计算时间差(毫秒)
const durationMs = finish - start;
// 将时间差转换为秒,并保留一位小数
const durationSec = (durationMs / 1000).toFixed(1);
// 设置颜色大于60秒则为红色小于等于60秒则为绿色
const color = durationSec > 60 ? 'red' : 'green';
// 返回带有样式的颜色标签
return (
<Tag color={color} size="large">
{durationSec}
</Tag>
);
}
const LogsTable = () => { const LogsTable = () => {
const { t } = useTranslation();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState(''); const [modalContent, setModalContent] = useState('');
function renderType(type) {
switch (type) {
case 'IMAGINE':
return (
<Tag color='blue' size='large'>
{t('绘图')}
</Tag>
);
case 'UPSCALE':
return (
<Tag color='orange' size='large'>
{t('放大')}
</Tag>
);
case 'VARIATION':
return (
<Tag color='purple' size='large'>
{t('变换')}
</Tag>
);
case 'HIGH_VARIATION':
return (
<Tag color='purple' size='large'>
{t('强变换')}
</Tag>
);
case 'LOW_VARIATION':
return (
<Tag color='purple' size='large'>
{t('弱变换')}
</Tag>
);
case 'PAN':
return (
<Tag color='cyan' size='large'>
{t('平移')}
</Tag>
);
case 'DESCRIBE':
return (
<Tag color='yellow' size='large'>
{t('图生文')}
</Tag>
);
case 'BLEND':
return (
<Tag color='lime' size='large'>
{t('图混合')}
</Tag>
);
case 'UPLOAD':
return (
<Tag color='blue' size='large'>
上传文件
</Tag>
);
case 'SHORTEN':
return (
<Tag color='pink' size='large'>
{t('缩词')}
</Tag>
);
case 'REROLL':
return (
<Tag color='indigo' size='large'>
{t('重绘')}
</Tag>
);
case 'INPAINT':
return (
<Tag color='violet' size='large'>
{t('局部重绘-提交')}
</Tag>
);
case 'ZOOM':
return (
<Tag color='teal' size='large'>
{t('变焦')}
</Tag>
);
case 'CUSTOM_ZOOM':
return (
<Tag color='teal' size='large'>
{t('自定义变焦-提交')}
</Tag>
);
case 'MODAL':
return (
<Tag color='green' size='large'>
{t('窗口处理')}
</Tag>
);
case 'SWAP_FACE':
return (
<Tag color='light-green' size='large'>
{t('换脸')}
</Tag>
);
default:
return (
<Tag color='white' size='large'>
{t('未知')}
</Tag>
);
}
}
function renderCode(code) {
switch (code) {
case 1:
return (
<Tag color='green' size='large'>
{t('已提交')}
</Tag>
);
case 21:
return (
<Tag color='lime' size='large'>
{t('等待中')}
</Tag>
);
case 22:
return (
<Tag color='orange' size='large'>
{t('重复提交')}
</Tag>
);
case 0:
return (
<Tag color='yellow' size='large'>
{t('未提交')}
</Tag>
);
default:
return (
<Tag color='white' size='large'>
{t('未知')}
</Tag>
);
}
}
function renderStatus(type) {
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' size='large'>
{t('成功')}
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' size='large'>
{t('未启动')}
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' size='large'>
{t('队列中')}
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' size='large'>
{t('执行中')}
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' size='large'>
{t('失败')}
</Tag>
);
case 'MODAL':
return (
<Tag color='yellow' size='large'>
{t('窗口等待')}
</Tag>
);
default:
return (
<Tag color='white' size='large'>
{t('未知')}
</Tag>
);
}
}
const renderTimestamp = (timestampInSeconds) => {
const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
const year = date.getFullYear(); // 获取年份
const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份从0开始需要+1并保证两位数
const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
};
// 修改renderDuration函数以包含颜色逻辑
function renderDuration(submit_time, finishTime) {
if (!submit_time || !finishTime) return 'N/A';
const start = new Date(submit_time);
const finish = new Date(finishTime);
const durationMs = finish - start;
const durationSec = (durationMs / 1000).toFixed(1);
const color = durationSec > 60 ? 'red' : 'green';
return (
<Tag color={color} size="large">
{durationSec} {t('秒')}
</Tag>
);
}
const columns = [ const columns = [
{ {
title: '提交时间', title: t('提交时间'),
dataIndex: 'submit_time', dataIndex: 'submit_time',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{renderTimestamp(text / 1000)}</div>; return <div>{renderTimestamp(text / 1000)}</div>;
}, },
}, },
{ {
title: '花费时间', title: t('花费时间'),
dataIndex: 'finish_time', // 以finish_time作为dataIndex dataIndex: 'finish_time', // 以finish_time作为dataIndex
key: 'finish_time', key: 'finish_time',
render: (finish, record) => { render: (finish, record) => {
@@ -289,7 +284,7 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '渠道', title: t('渠道'),
dataIndex: 'channel_id', dataIndex: 'channel_id',
className: isAdmin() ? 'tableShow' : 'tableHiddle', className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => { render: (text, record, index) => {
@@ -310,21 +305,21 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '类型', title: t('类型'),
dataIndex: 'action', dataIndex: 'action',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{renderType(text)}</div>; return <div>{renderType(text)}</div>;
}, },
}, },
{ {
title: '任务ID', title: t('任务ID'),
dataIndex: 'mj_id', dataIndex: 'mj_id',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{text}</div>; return <div>{text}</div>;
}, },
}, },
{ {
title: '提交结果', title: t('提交结果'),
dataIndex: 'code', dataIndex: 'code',
className: isAdmin() ? 'tableShow' : 'tableHiddle', className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => { render: (text, record, index) => {
@@ -332,7 +327,7 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '任务状态', title: t('任务状态'),
dataIndex: 'status', dataIndex: 'status',
className: isAdmin() ? 'tableShow' : 'tableHiddle', className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => { render: (text, record, index) => {
@@ -340,7 +335,7 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '进度', title: t('进度'),
dataIndex: 'progress', dataIndex: 'progress',
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
@@ -363,11 +358,11 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '结果图片', title: t('结果图片'),
dataIndex: 'image_url', dataIndex: 'image_url',
render: (text, record, index) => { render: (text, record, index) => {
if (!text) { if (!text) {
return '无'; return t('无');
} }
return ( return (
<Button <Button
@@ -376,7 +371,7 @@ const LogsTable = () => {
setIsModalOpenurl(true); // 打开模态框 setIsModalOpenurl(true); // 打开模态框
}} }}
> >
查看图片 {t('查看图片')}
</Button> </Button>
); );
}, },
@@ -387,7 +382,7 @@ const LogsTable = () => {
render: (text, record, index) => { render: (text, record, index) => {
// 如果text未定义返回替代文本例如空字符串''或其他 // 如果text未定义返回替代文本例如空字符串''或其他
if (!text) { if (!text) {
return '无'; return t('无');
} }
return ( return (
@@ -410,7 +405,7 @@ const LogsTable = () => {
render: (text, record, index) => { render: (text, record, index) => {
// 如果text未定义返回替代文本例如空字符串''或其他 // 如果text未定义返回替代文本例如空字符串''或其他
if (!text) { if (!text) {
return '无'; return t('无');
} }
return ( return (
@@ -428,12 +423,12 @@ const LogsTable = () => {
}, },
}, },
{ {
title: '失败原因', title: t('失败原因'),
dataIndex: 'fail_reason', dataIndex: 'fail_reason',
render: (text, record, index) => { render: (text, record, index) => {
// 如果text未定义返回替代文本例如空字符串''或其他 // 如果text未定义返回替代文本例如空字符串''或其他
if (!text) { if (!text) {
return '无'; return t('无');
} }
return ( return (
@@ -565,7 +560,7 @@ const LogsTable = () => {
{isAdminUser && showBanner ? ( {isAdminUser && showBanner ? (
<Banner <Banner
type='info' type='info'
description='当前未开启Midjourney回调部分项目可能无法获得绘图结果可在运营设置中开启。' description={t('当前未开启Midjourney回调部分项目可能无法获得绘图结果可在运营设置中开启。')}
/> />
) : ( ) : (
<></> <></>
@@ -574,25 +569,25 @@ const LogsTable = () => {
<> <>
<Form.Input <Form.Input
field='channel_id' field='channel_id'
label='渠道 ID' label={t('渠道 ID')}
style={{ width: 176 }} style={{ width: 176 }}
value={channel_id} value={channel_id}
placeholder={'可选值'} placeholder={t('可选值')}
name='channel_id' name='channel_id'
onChange={(value) => handleInputChange(value, 'channel_id')} onChange={(value) => handleInputChange(value, 'channel_id')}
/> />
<Form.Input <Form.Input
field='mj_id' field='mj_id'
label='任务 ID' label={t('任务 ID')}
style={{ width: 176 }} style={{ width: 176 }}
value={mj_id} value={mj_id}
placeholder='可选值' placeholder={t('可选值')}
name='mj_id' name='mj_id'
onChange={(value) => handleInputChange(value, 'mj_id')} onChange={(value) => handleInputChange(value, 'mj_id')}
/> />
<Form.DatePicker <Form.DatePicker
field='start_timestamp' field='start_timestamp'
label='起始时间' label={t('起始时间')}
style={{ width: 272 }} style={{ width: 272 }}
initValue={start_timestamp} initValue={start_timestamp}
value={start_timestamp} value={start_timestamp}
@@ -603,7 +598,7 @@ const LogsTable = () => {
<Form.DatePicker <Form.DatePicker
field='end_timestamp' field='end_timestamp'
fluid fluid
label='结束时间' label={t('结束时间')}
style={{ width: 272 }} style={{ width: 272 }}
initValue={end_timestamp} initValue={end_timestamp}
value={end_timestamp} value={end_timestamp}
@@ -614,13 +609,13 @@ const LogsTable = () => {
<Form.Section> <Form.Section>
<Button <Button
label='查询' label={t('查询')}
type='primary' type='primary'
htmlType='submit' htmlType='submit'
className='btn-margin-right' className='btn-margin-right'
onClick={refresh} onClick={refresh}
> >
查询 {t('查询')}
</Button> </Button>
</Form.Section> </Form.Section>
</> </>
@@ -635,6 +630,12 @@ const LogsTable = () => {
total: logCount, total: logCount,
pageSizeOpts: [10, 20, 50, 100], pageSizeOpts: [10, 20, 50, 100],
onPageChange: handlePageChange, onPageChange: handlePageChange,
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: logCount
}),
}} }}
loading={loading} loading={loading}
/> />

View File

@@ -1,5 +1,6 @@
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react'; import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
import { API, copy, showError, showInfo, showSuccess } from '../helpers'; import { API, copy, showError, showInfo, showSuccess } from '../helpers';
import { useTranslation } from 'react-i18next';
import { import {
Banner, Banner,
@@ -23,65 +24,8 @@ import {
import { UserContext } from '../context/User/index.js'; import { UserContext } from '../context/User/index.js';
import Text from '@douyinfe/semi-ui/lib/es/typography/text'; import Text from '@douyinfe/semi-ui/lib/es/typography/text';
function renderQuotaType(type) {
// Ensure all cases are string literals by adding quotes.
switch (type) {
case 1:
return (
<Tag color='teal' size='large'>
按次计费
</Tag>
);
case 0:
return (
<Tag color='violet' size='large'>
按量计费
</Tag>
);
default:
return '未知';
}
}
function renderAvailable(available) {
return available ? (
<Popover
content={
<div style={{ padding: 8 }}>您的分组可以使用该模型</div>
}
position='top'
key={available}
style={{
backgroundColor: 'rgba(var(--semi-blue-4),1)',
borderColor: 'rgba(var(--semi-blue-4),1)',
color: 'var(--semi-color-white)',
borderWidth: 1,
borderStyle: 'solid',
}}
>
<IconVerify style={{ color: 'green' }} size="large" />
</Popover>
) : (
<Popover
content={
<div style={{ padding: 8 }}>您的分组无权使用该模型</div>
}
position='top'
key={available}
style={{
backgroundColor: 'rgba(var(--semi-blue-4),1)',
borderColor: 'rgba(var(--semi-blue-4),1)',
color: 'var(--semi-color-white)',
borderWidth: 1,
borderStyle: 'solid',
}}
>
<IconUploadError style={{ color: '#FFA54F' }} size="large" />
</Popover>
);
}
const ModelPricing = () => { const ModelPricing = () => {
const { t } = useTranslation();
const [filteredValue, setFilteredValue] = useState([]); const [filteredValue, setFilteredValue] = useState([]);
const compositionRef = useRef({ isComposition: false }); const compositionRef = useRef({ isComposition: false });
const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [selectedRowKeys, setSelectedRowKeys] = useState([]);
@@ -115,10 +59,68 @@ const ModelPricing = () => {
const newFilteredValue = value ? [value] : []; const newFilteredValue = value ? [value] : [];
setFilteredValue(newFilteredValue); setFilteredValue(newFilteredValue);
}; };
function renderQuotaType(type) {
// Ensure all cases are string literals by adding quotes.
switch (type) {
case 1:
return (
<Tag color='teal' size='large'>
{t('按次计费')}
</Tag>
);
case 0:
return (
<Tag color='violet' size='large'>
{t('按量计费')}
</Tag>
);
default:
return t('未知');
}
}
function renderAvailable(available) {
return available ? (
<Popover
content={
<div style={{ padding: 8 }}>{t('您的分组可以使用该模型')}</div>
}
position='top'
key={available}
style={{
backgroundColor: 'rgba(var(--semi-blue-4),1)',
borderColor: 'rgba(var(--semi-blue-4),1)',
color: 'var(--semi-color-white)',
borderWidth: 1,
borderStyle: 'solid',
}}
>
<IconVerify style={{ color: 'green' }} size="large" />
</Popover>
) : (
<Popover
content={
<div style={{ padding: 8 }}>{t('您的分组无权使用该模型')}</div>
}
position='top'
key={available}
style={{
backgroundColor: 'rgba(var(--semi-blue-4),1)',
borderColor: 'rgba(var(--semi-blue-4),1)',
color: 'var(--semi-color-white)',
borderWidth: 1,
borderStyle: 'solid',
}}
>
<IconUploadError style={{ color: '#FFA54F' }} size="large" />
</Popover>
);
}
const columns = [ const columns = [
{ {
title: '可用性', title: t('可用性'),
dataIndex: 'available', dataIndex: 'available',
render: (text, record, index) => { render: (text, record, index) => {
// if record.enable_groups contains selectedGroup, then available is true // if record.enable_groups contains selectedGroup, then available is true
@@ -127,20 +129,8 @@ const ModelPricing = () => {
sorter: (a, b) => a.available - b.available, sorter: (a, b) => a.available - b.available,
}, },
{ {
title: ( title: t('模型名称'),
<Space> dataIndex: 'model_name',
<span>模型名称</span>
<Input
placeholder='模糊搜索'
style={{ width: 200 }}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onChange={handleChange}
showClear
/>
</Space>
),
dataIndex: 'model_name', // 以finish_time作为dataIndex
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<> <>
@@ -161,7 +151,7 @@ const ModelPricing = () => {
filteredValue, filteredValue,
}, },
{ {
title: '计费类型', title: t('计费类型'),
dataIndex: 'quota_type', dataIndex: 'quota_type',
render: (text, record, index) => { render: (text, record, index) => {
return renderQuotaType(parseInt(text)); return renderQuotaType(parseInt(text));
@@ -169,7 +159,7 @@ const ModelPricing = () => {
sorter: (a, b) => a.quota_type - b.quota_type, sorter: (a, b) => a.quota_type - b.quota_type,
}, },
{ {
title: '可用分组', title: t('可用分组'),
dataIndex: 'enable_groups', dataIndex: 'enable_groups',
render: (text, record, index) => { render: (text, record, index) => {
// enable_groups is a string array // enable_groups is a string array
@@ -193,7 +183,10 @@ const ModelPricing = () => {
size='large' size='large'
onClick={() => { onClick={() => {
setSelectedGroup(group); setSelectedGroup(group);
showInfo('当前查看的分组为:' + group + ',倍率为:' + groupRatio[group]); showInfo(t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
group: group,
ratio: groupRatio[group]
}));
}} }}
> >
{group} {group}
@@ -208,10 +201,13 @@ const ModelPricing = () => {
{ {
title: () => ( title: () => (
<span style={{'display':'flex','alignItems':'center'}}> <span style={{'display':'flex','alignItems':'center'}}>
倍率 {t('倍率')}
<Popover <Popover
content={ content={
<div style={{ padding: 8 }}>倍率是为了方便换算不同价格的模型<br/>点击查看倍率说明</div> <div style={{ padding: 8 }}>
{t('倍率是为了方便换算不同价格的模型')}<br/>
{t('点击查看倍率说明')}
</div>
} }
position='top' position='top'
style={{ style={{
@@ -237,18 +233,18 @@ const ModelPricing = () => {
let completionRatio = parseFloat(record.completion_ratio.toFixed(3)); let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
content = ( content = (
<> <>
<Text>模型{record.quota_type === 0 ? text : '无'}</Text> <Text>{t('模型倍率')}{record.quota_type === 0 ? text : t('无')}</Text>
<br /> <br />
<Text>补全{record.quota_type === 0 ? completionRatio : '无'}</Text> <Text>{t('补全倍率')}{record.quota_type === 0 ? completionRatio : t('无')}</Text>
<br /> <br />
<Text>分组{groupRatio[selectedGroup]}</Text> <Text>{t('分组倍率')}{groupRatio[selectedGroup]}</Text>
</> </>
); );
return <div>{content}</div>; return <div>{content}</div>;
}, },
}, },
{ {
title: '模型价格', title: t('模型价格'),
dataIndex: 'model_price', dataIndex: 'model_price',
render: (text, record, index) => { render: (text, record, index) => {
let content = text; let content = text;
@@ -261,14 +257,14 @@ const ModelPricing = () => {
groupRatio[selectedGroup]; groupRatio[selectedGroup];
content = ( content = (
<> <>
<Text>提示 ${inputRatioPrice} / 1M tokens</Text> <Text>{t('提示')} ${inputRatioPrice} / 1M tokens</Text>
<br /> <br />
<Text>补全 ${completionRatioPrice} / 1M tokens</Text> <Text>{t('补全')} ${completionRatioPrice} / 1M tokens</Text>
</> </>
); );
} else { } else {
let price = parseFloat(text) * groupRatio[selectedGroup]; let price = parseFloat(text) * groupRatio[selectedGroup];
content = <>模型价格${price}</>; content = <>${t('模型价格')}${price}</>;
} }
return <div>{content}</div>; return <div>{content}</div>;
}, },
@@ -349,41 +345,62 @@ const ModelPricing = () => {
type="success" type="success"
fullMode={false} fullMode={false}
closeIcon="null" closeIcon="null"
description={`您的默认分组为:${userState.user.group},分组倍率为:${groupRatio[userState.user.group]}`} description={t('您的默认分组为:{{group}},分组倍率为:{{ratio}}', {
group: userState.user.group,
ratio: groupRatio[userState.user.group]
})}
/> />
) : ( ) : (
<Banner <Banner
type='warning' type='warning'
fullMode={false} fullMode={false}
closeIcon="null" closeIcon="null"
description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio['default']}`} description={t('您还未登陆,显示的价格为默认分组倍率: {{ratio}}', {
ratio: groupRatio['default']
})}
/> />
)} )}
<br/> <br/>
<Banner <Banner
type="info" type="info"
fullMode={false} fullMode={false}
description={<div>按量计费费用 = 分组倍率 × 模型倍率 × 提示token数 + 补全token数 × 补全倍率/ 500000 单位美元</div>} description={<div>{t('按量计费费用 = 分组倍率 × 模型倍率 × 提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')}</div>}
closeIcon="null" closeIcon="null"
/> />
<br/> <br/>
<Button <Space style={{ marginBottom: 16 }}>
theme='light' <Input
type='tertiary' placeholder={t('模糊搜索模型名称')}
style={{width: 150}} style={{ width: 200 }}
onClick={() => { onCompositionStart={handleCompositionStart}
copyText(selectedRowKeys); onCompositionEnd={handleCompositionEnd}
}} onChange={handleChange}
disabled={selectedRowKeys == ""} showClear
> />
复制选中模型 <Button
</Button> theme='light'
type='tertiary'
style={{width: 150}}
onClick={() => {
copyText(selectedRowKeys);
}}
disabled={selectedRowKeys == ""}
>
{t('复制选中模型')}
</Button>
</Space>
<Table <Table
style={{ marginTop: 5 }} style={{ marginTop: 5 }}
columns={columns} columns={columns}
dataSource={models} dataSource={models}
loading={loading} loading={loading}
pagination={{ pagination={{
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: models.length
}),
pageSize: models.length, pageSize: models.length,
showSizeChanger: false, showSizeChanger: false,
}} }}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Card, Spin } from '@douyinfe/semi-ui'; import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js'; import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js';
import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js'; import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js';
import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js'; import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js';
@@ -8,11 +8,17 @@ import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashbo
import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js'; import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js';
import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js'; import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js';
import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js'; import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsVisualEditor.js';
import GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js';
import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js';
import { API, showError, showSuccess } from '../helpers'; import { API, showError, showSuccess } from '../helpers';
import SettingsChats from '../pages/Setting/Operation/SettingsChats.js'; import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
import { useTranslation } from 'react-i18next';
const OperationSetting = () => { const OperationSetting = () => {
const { t } = useTranslation();
let [inputs, setInputs] = useState({ let [inputs, setInputs] = useState({
QuotaForNewUser: 0, QuotaForNewUser: 0,
QuotaForInviter: 0, QuotaForInviter: 0,
@@ -137,9 +143,20 @@ const OperationSetting = () => {
<Card style={{ marginTop: '10px' }}> <Card style={{ marginTop: '10px' }}>
<SettingsChats options={inputs} refresh={onRefresh} /> <SettingsChats options={inputs} refresh={onRefresh} />
</Card> </Card>
{/* 倍率设置 */} {/* 分组倍率设置 */}
<Card style={{ marginTop: '10px' }}> <Card style={{ marginTop: '10px' }}>
<SettingsMagnification options={inputs} refresh={onRefresh} /> <GroupRatioSettings options={inputs} refresh={onRefresh} />
</Card>
{/* 合并模型倍率设置和可视化倍率设置 */}
<Card style={{ marginTop: '10px' }}>
<Tabs type="line">
<Tabs.TabPane tab={t('模型倍率设置')} itemKey="model">
<ModelRatioSettings options={inputs} refresh={onRefresh} />
</Tabs.TabPane>
<Tabs.TabPane tab={t('可视化倍率设置')} itemKey="visual">
<ModelSettingsVisualEditor options={inputs} refresh={onRefresh} />
</Tabs.TabPane>
</Tabs>
</Card> </Card>
</Spin> </Spin>
</> </>

View File

@@ -2,8 +2,10 @@ import React, { useEffect, useRef, useState } from 'react';
import { Banner, Button, Col, Form, Row } from '@douyinfe/semi-ui'; import { Banner, Button, Col, Form, Row } from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../helpers'; import { API, showError, showSuccess } from '../helpers';
import { marked } from 'marked'; import { marked } from 'marked';
import { useTranslation } from 'react-i18next';
const OtherSetting = () => { const OtherSetting = () => {
const { t } = useTranslation();
let [inputs, setInputs] = useState({ let [inputs, setInputs] = useState({
Notice: '', Notice: '',
SystemName: '', SystemName: '',
@@ -54,10 +56,10 @@ const OtherSetting = () => {
try { try {
setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: true })); setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: true }));
await updateOption('Notice', inputs.Notice); await updateOption('Notice', inputs.Notice);
showSuccess('公告已更新'); showSuccess(t('公告已更新'));
} catch (error) { } catch (error) {
console.error('公告更新失败', error); console.error(t('公告更新失败'), error);
showError('公告更新失败'); showError(t('公告更新失败'));
} finally { } finally {
setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false })); setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false }));
} }
@@ -72,10 +74,10 @@ const OtherSetting = () => {
SystemName: true, SystemName: true,
})); }));
await updateOption('SystemName', inputs.SystemName); await updateOption('SystemName', inputs.SystemName);
showSuccess('系统名称已更新'); showSuccess(t('系统名称已更新'));
} catch (error) { } catch (error) {
console.error('系统名称更新失败', error); console.error(t('系统名称更新失败'), error);
showError('系统名称更新失败'); showError(t('系统名称更新失败'));
} finally { } finally {
setLoadingInput((loadingInput) => ({ setLoadingInput((loadingInput) => ({
...loadingInput, ...loadingInput,
@@ -193,17 +195,17 @@ const OtherSetting = () => {
getFormApi={(formAPI) => (formAPISettingGeneral.current = formAPI)} getFormApi={(formAPI) => (formAPISettingGeneral.current = formAPI)}
style={{ marginBottom: 15 }} style={{ marginBottom: 15 }}
> >
<Form.Section text={'通用设置'}> <Form.Section text={t('通用设置')}>
<Form.TextArea <Form.TextArea
label={'公告'} label={t('公告')}
placeholder={'在此输入新的公告内容,支持 Markdown & HTML 代码'} placeholder={t('在此输入新的公告内容,支持 Markdown & HTML 代码')}
field={'Notice'} field={'Notice'}
onChange={handleInputChange} onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }} style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
/> />
<Button onClick={submitNotice} loading={loadingInput['Notice']}> <Button onClick={submitNotice} loading={loadingInput['Notice']}>
设置公告 {t('设置公告')}
</Button> </Button>
</Form.Section> </Form.Section>
</Form> </Form>
@@ -213,10 +215,10 @@ const OtherSetting = () => {
getFormApi={(formAPI) => (formAPIPersonalization.current = formAPI)} getFormApi={(formAPI) => (formAPIPersonalization.current = formAPI)}
style={{ marginBottom: 15 }} style={{ marginBottom: 15 }}
> >
<Form.Section text={'个性化设置'}> <Form.Section text={t('个性化设置')}>
<Form.Input <Form.Input
label={'系统名称'} label={t('系统名称')}
placeholder={'在此输入系统名称'} placeholder={t('在此输入系统名称')}
field={'SystemName'} field={'SystemName'}
onChange={handleInputChange} onChange={handleInputChange}
/> />
@@ -224,22 +226,20 @@ const OtherSetting = () => {
onClick={submitSystemName} onClick={submitSystemName}
loading={loadingInput['SystemName']} loading={loadingInput['SystemName']}
> >
设置系统名称 {t('设置系统名称')}
</Button> </Button>
<Form.Input <Form.Input
label={'Logo 图片地址'} label={t('Logo 图片地址')}
placeholder={'在此输入 Logo 图片地址'} placeholder={t('在此输入 Logo 图片地址')}
field={'Logo'} field={'Logo'}
onChange={handleInputChange} onChange={handleInputChange}
/> />
<Button onClick={submitLogo} loading={loadingInput['Logo']}> <Button onClick={submitLogo} loading={loadingInput['Logo']}>
设置 Logo {t('设置 Logo')}
</Button> </Button>
<Form.TextArea <Form.TextArea
label={'首页内容'} label={t('首页内容')}
placeholder={ placeholder={t('在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页')}
'在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'
}
field={'HomePageContent'} field={'HomePageContent'}
onChange={handleInputChange} onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }} style={{ fontFamily: 'JetBrains Mono, Consolas' }}
@@ -249,39 +249,35 @@ const OtherSetting = () => {
onClick={() => submitOption('HomePageContent')} onClick={() => submitOption('HomePageContent')}
loading={loadingInput['HomePageContent']} loading={loadingInput['HomePageContent']}
> >
设置首页内容 {t('设置首页内容')}
</Button> </Button>
<Form.TextArea <Form.TextArea
label={'关于'} label={t('关于')}
placeholder={ placeholder={t('在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面')}
'在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'
}
field={'About'} field={'About'}
onChange={handleInputChange} onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }} style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
/> />
<Button onClick={submitAbout} loading={loadingInput['About']}> <Button onClick={submitAbout} loading={loadingInput['About']}>
设置关于 {t('设置关于')}
</Button> </Button>
{/* */} {/* */}
<Banner <Banner
fullMode={false} fullMode={false}
type='info' type='info'
description='移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目' description={t('移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目')}
closeIcon={null} closeIcon={null}
style={{ marginTop: 15 }} style={{ marginTop: 15 }}
/> />
<Form.Input <Form.Input
label={'页脚'} label={t('页脚')}
placeholder={ placeholder={t('在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码')}
'在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'
}
field={'Footer'} field={'Footer'}
onChange={handleInputChange} onChange={handleInputChange}
/> />
<Button onClick={submitFooter} loading={loadingInput['Footer']}> <Button onClick={submitFooter} loading={loadingInput['Footer']}>
设置页脚 {t('设置页脚')}
</Button> </Button>
</Form.Section> </Form.Section>
</Form> </Form>

View File

@@ -0,0 +1,92 @@
import HeaderBar from './HeaderBar.js';
import { Layout } from '@douyinfe/semi-ui';
import SiderBar from './SiderBar.js';
import App from '../App.js';
import FooterBar from './Footer.js';
import { ToastContainer } from 'react-toastify';
import React, { useContext, useEffect } from 'react';
import { StyleContext } from '../context/Style/index.js';
import { useTranslation } from 'react-i18next';
import { API, getLogo, getSystemName, showError } from '../helpers/index.js';
import { setStatusData } from '../helpers/data.js';
import { UserContext } from '../context/User/index.js';
import { StatusContext } from '../context/Status/index.js';
const { Sider, Content, Header, Footer } = Layout;
const PageLayout = () => {
const [userState, userDispatch] = useContext(UserContext);
const [statusState, statusDispatch] = useContext(StatusContext);
const [styleState, styleDispatch] = useContext(StyleContext);
const { i18n } = useTranslation();
const loadUser = () => {
let user = localStorage.getItem('user');
if (user) {
let data = JSON.parse(user);
userDispatch({ type: 'login', payload: data });
}
};
const loadStatus = async () => {
try {
const res = await API.get('/api/status');
const { success, data } = res.data;
if (success) {
statusDispatch({ type: 'set', payload: data });
setStatusData(data);
} else {
showError('Unable to connect to server');
}
} catch (error) {
showError('Failed to load status');
}
};
useEffect(() => {
loadUser();
loadStatus().catch(console.error);
let systemName = getSystemName();
if (systemName) {
document.title = systemName;
}
let logo = getLogo();
if (logo) {
let linkElement = document.querySelector("link[rel~='icon']");
if (linkElement) {
linkElement.href = logo;
}
}
// 从localStorage获取上次使用的语言
const savedLang = localStorage.getItem('i18nextLng');
if (savedLang) {
i18n.changeLanguage(savedLang);
}
}, [i18n]);
return (
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Header>
<HeaderBar />
</Header>
<Layout style={{ flex: 1, overflow: 'hidden' }}>
<Sider>
{styleState.showSider ? <SiderBar /> : null}
</Sider>
<Layout>
<Content
style={{ overflowY: 'auto', padding: styleState.shouldInnerPadding? '24px': '0' }}
>
<App />
</Content>
<Layout.Footer>
<FooterBar />
</Layout.Footer>
</Layout>
</Layout>
<ToastContainer />
</Layout>
)
}
export default PageLayout;

View File

@@ -33,10 +33,12 @@ import {
stringToColor, stringToColor,
} from '../helpers/render'; } from '../helpers/render';
import TelegramLoginButton from 'react-telegram-login'; import TelegramLoginButton from 'react-telegram-login';
import { useTranslation } from 'react-i18next';
const PersonalSetting = () => { const PersonalSetting = () => {
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
let navigate = useNavigate(); let navigate = useNavigate();
const { t } = useTranslation();
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
wechat_verification_code: '', wechat_verification_code: '',
@@ -110,7 +112,7 @@ const PersonalSetting = () => {
if (success) { if (success) {
setSystemToken(data); setSystemToken(data);
await copy(data); await copy(data);
showSuccess(`令牌已重置并已复制到剪贴板`); showSuccess(t('令牌已重置并已复制到剪贴板'));
} else { } else {
showError(message); showError(message);
} }
@@ -151,18 +153,18 @@ const PersonalSetting = () => {
const handleAffLinkClick = async (e) => { const handleAffLinkClick = async (e) => {
e.target.select(); e.target.select();
await copy(e.target.value); await copy(e.target.value);
showSuccess(`邀请链接已复制到剪切板`); showSuccess(t('邀请链接已复制到剪切板'));
}; };
const handleSystemTokenClick = async (e) => { const handleSystemTokenClick = async (e) => {
e.target.select(); e.target.select();
await copy(e.target.value); await copy(e.target.value);
showSuccess(`系统令牌已复制到剪切板`); showSuccess(t('系统令牌已复制到剪切板'));
}; };
const deleteAccount = async () => { const deleteAccount = async () => {
if (inputs.self_account_deletion_confirmation !== userState.user.username) { if (inputs.self_account_deletion_confirmation !== userState.user.username) {
showError('请输入你的账户名以确认删除!'); showError(t('请输入你的账户名以确认删除!'));
return; return;
} }
@@ -170,7 +172,7 @@ const PersonalSetting = () => {
const {success, message} = res.data; const {success, message} = res.data;
if (success) { if (success) {
showSuccess('账户已删除!'); showSuccess(t('账户已删除!'));
await API.get('/api/user/logout'); await API.get('/api/user/logout');
userDispatch({type: 'logout'}); userDispatch({type: 'logout'});
localStorage.removeItem('user'); localStorage.removeItem('user');
@@ -187,7 +189,7 @@ const PersonalSetting = () => {
); );
const {success, message} = res.data; const {success, message} = res.data;
if (success) { if (success) {
showSuccess('微信账户绑定成功!'); showSuccess(t('微信账户绑定成功!'));
setShowWeChatBindModal(false); setShowWeChatBindModal(false);
} else { } else {
showError(message); showError(message);
@@ -196,7 +198,7 @@ const PersonalSetting = () => {
const changePassword = async () => { const changePassword = async () => {
if (inputs.set_new_password !== inputs.set_new_password_confirmation) { if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
showError('两次输入的密码不一致!'); showError(t('两次输入的密码不一致!'));
return; return;
} }
const res = await API.put(`/api/user/self`, { const res = await API.put(`/api/user/self`, {
@@ -204,7 +206,7 @@ const PersonalSetting = () => {
}); });
const {success, message} = res.data; const {success, message} = res.data;
if (success) { if (success) {
showSuccess('密码修改成功!'); showSuccess(t('密码修改成功!'));
setShowWeChatBindModal(false); setShowWeChatBindModal(false);
} else { } else {
showError(message); showError(message);
@@ -214,7 +216,7 @@ const PersonalSetting = () => {
const transfer = async () => { const transfer = async () => {
if (transferAmount < getQuotaPerUnit()) { if (transferAmount < getQuotaPerUnit()) {
showError('划转金额最低为' + renderQuota(getQuotaPerUnit())); showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
return; return;
} }
const res = await API.post(`/api/user/aff_transfer`, { const res = await API.post(`/api/user/aff_transfer`, {
@@ -232,7 +234,7 @@ const PersonalSetting = () => {
const sendVerificationCode = async () => { const sendVerificationCode = async () => {
if (inputs.email === '') { if (inputs.email === '') {
showError('请输入邮箱!'); showError(t('请输入邮箱!'));
return; return;
} }
setDisableButton(true); setDisableButton(true);
@@ -246,7 +248,7 @@ const PersonalSetting = () => {
); );
const {success, message} = res.data; const {success, message} = res.data;
if (success) { if (success) {
showSuccess('验证码发送成功,请检查邮箱!'); showSuccess(t('验证码发送成功,请检查邮箱!'));
} else { } else {
showError(message); showError(message);
} }
@@ -255,7 +257,7 @@ const PersonalSetting = () => {
const bindEmail = async () => { const bindEmail = async () => {
if (inputs.email_verification_code === '') { if (inputs.email_verification_code === '') {
showError('请输入邮箱验证码!'); showError(t('请输入邮箱验证码!'));
return; return;
} }
setLoading(true); setLoading(true);
@@ -264,7 +266,7 @@ const PersonalSetting = () => {
); );
const {success, message} = res.data; const {success, message} = res.data;
if (success) { if (success) {
showSuccess('邮箱账户绑定成功!'); showSuccess(t('邮箱账户绑定成功!'));
setShowEmailBindModal(false); setShowEmailBindModal(false);
userState.user.email = inputs.email; userState.user.email = inputs.email;
} else { } else {
@@ -299,7 +301,7 @@ const PersonalSetting = () => {
<Layout> <Layout>
<Layout.Content> <Layout.Content>
<Modal <Modal
title='请输入要划转的数量' title={t('请输入要划转的数量')}
visible={openTransfer} visible={openTransfer}
onOk={transfer} onOk={transfer}
onCancel={handleCancel} onCancel={handleCancel}
@@ -308,7 +310,7 @@ const PersonalSetting = () => {
centered={true} centered={true}
> >
<div style={{marginTop: 20}}> <div style={{marginTop: 20}}>
<Typography.Text>{`可用额度${renderQuotaWithPrompt(userState?.user?.aff_quota)}`}</Typography.Text> <Typography.Text>{t('可用额度')}{renderQuotaWithPrompt(userState?.user?.aff_quota)}</Typography.Text>
<Input <Input
style={{marginTop: 5}} style={{marginTop: 5}}
value={userState?.user?.aff_quota} value={userState?.user?.aff_quota}
@@ -317,8 +319,7 @@ const PersonalSetting = () => {
</div> </div>
<div style={{marginTop: 20}}> <div style={{marginTop: 20}}>
<Typography.Text> <Typography.Text>
{`划转额度${renderQuotaWithPrompt(transferAmount)} 最低` + {t('划转额度')}{renderQuotaWithPrompt(transferAmount)} {t('最低') + renderQuota(getQuotaPerUnit())}
renderQuota(getQuotaPerUnit())}
</Typography.Text> </Typography.Text>
<div> <div>
<InputNumber <InputNumber
@@ -348,9 +349,9 @@ const PersonalSetting = () => {
title={<Typography.Text>{getUsername()}</Typography.Text>} title={<Typography.Text>{getUsername()}</Typography.Text>}
description={ description={
isRoot() ? ( isRoot() ? (
<Tag color='red'>管理员</Tag> <Tag color='red'>{t('管理员')}</Tag>
) : ( ) : (
<Tag color='blue'>普通用户</Tag> <Tag color='blue'>{t('普通用户')}</Tag>
) )
} }
></Card.Meta> ></Card.Meta>
@@ -363,42 +364,24 @@ const PersonalSetting = () => {
</Space> </Space>
</> </>
} }
footer={
<Descriptions row>
<Descriptions.Item itemKey='当前余额'>
{renderQuota(userState?.user?.quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='历史消耗'>
{renderQuota(userState?.user?.used_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='请求次数'>
{userState.user?.request_count}
</Descriptions.Item>
</Descriptions>
}
> >
<Typography.Title heading={6}>可用模型</Typography.Title> <Descriptions row>
<div style={{marginTop: 10}}> <Descriptions.Item itemKey={t('当前余额')}>
<Space wrap> {renderQuota(userState?.user?.quota)}
{models.map((model) => ( </Descriptions.Item>
<Tag <Descriptions.Item itemKey={t('历史消耗')}>
key={model} {renderQuota(userState?.user?.used_quota)}
color='cyan' </Descriptions.Item>
onClick={() => { <Descriptions.Item itemKey={t('请求次数')}>
copyText(model); {userState.user?.request_count}
}} </Descriptions.Item>
> </Descriptions>
{model}
</Tag>
))}
</Space>
</div>
</Card> </Card>
<Card <Card
style={{marginTop: 10}} style={{marginTop: 10}}
footer={ footer={
<div> <div>
<Typography.Text>邀请链接</Typography.Text> <Typography.Text>{t('邀请链接')}</Typography.Text>
<Input <Input
style={{marginTop: 10}} style={{marginTop: 10}}
value={affLink} value={affLink}
@@ -408,35 +391,35 @@ const PersonalSetting = () => {
</div> </div>
} }
> >
<Typography.Title heading={6}>邀请信息</Typography.Title> <Typography.Title heading={6}>{t('邀请信息')}</Typography.Title>
<div style={{marginTop: 10}}> <div style={{marginTop: 10}}>
<Descriptions row> <Descriptions row>
<Descriptions.Item itemKey='待使用收益'> <Descriptions.Item itemKey={t('待使用收益')}>
<span style={{color: 'rgba(var(--semi-red-5), 1)'}}> <span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
{renderQuota(userState?.user?.aff_quota)} {renderQuota(userState?.user?.aff_quota)}
</span> </span>
<Button <Button
type={'secondary'} type={'secondary'}
onClick={() => setOpenTransfer(true)} onClick={() => setOpenTransfer(true)}
size={'small'} size={'small'}
style={{marginLeft: 10}} style={{marginLeft: 10}}
> >
划转 {t('划转')}
</Button> </Button>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item itemKey='总收益'> <Descriptions.Item itemKey={t('总收益')}>
{renderQuota(userState?.user?.aff_history_quota)} {renderQuota(userState?.user?.aff_history_quota)}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item itemKey='邀请人数'> <Descriptions.Item itemKey={t('邀请人数')}>
{userState?.user?.aff_count} {userState?.user?.aff_count}
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
</div> </div>
</Card> </Card>
<Card style={{marginTop: 10}}> <Card style={{marginTop: 10}}>
<Typography.Title heading={6}>个人信息</Typography.Title> <Typography.Title heading={6}>{t('个人信息')}</Typography.Title>
<div style={{marginTop: 20}}> <div style={{marginTop: 20}}>
<Typography.Text strong>邮箱</Typography.Text> <Typography.Text strong>{t('邮箱')}</Typography.Text>
<div <div
style={{display: 'flex', justifyContent: 'space-between'}} style={{display: 'flex', justifyContent: 'space-between'}}
> >
@@ -445,7 +428,7 @@ const PersonalSetting = () => {
value={ value={
userState.user && userState.user.email !== '' userState.user && userState.user.email !== ''
? userState.user.email ? userState.user.email
: '未绑定' : t('未绑定')
} }
readonly={true} readonly={true}
></Input> ></Input>
@@ -457,14 +440,14 @@ const PersonalSetting = () => {
}} }}
> >
{userState.user && userState.user.email !== '' {userState.user && userState.user.email !== ''
? '修改绑定' ? t('修改绑定')
: '绑定邮箱'} : t('绑定邮箱')}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
<div style={{marginTop: 10}}> <div style={{marginTop: 10}}>
<Typography.Text strong>微信</Typography.Text> <Typography.Text strong>{t('微信')}</Typography.Text>
<div <div
style={{display: 'flex', justifyContent: 'space-between'}} style={{display: 'flex', justifyContent: 'space-between'}}
> >
@@ -472,8 +455,8 @@ const PersonalSetting = () => {
<Input <Input
value={ value={
userState.user && userState.user.wechat_id !== '' userState.user && userState.user.wechat_id !== ''
? '已绑定' ? t('已绑定')
: '未绑定' : t('未绑定')
} }
readonly={true} readonly={true}
></Input> ></Input>
@@ -485,13 +468,13 @@ const PersonalSetting = () => {
!status.wechat_login !status.wechat_login
} }
> >
{status.wechat_login ? '绑定' : '未启用'} {status.wechat_login ? t('绑定') : t('未启用')}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
<div style={{marginTop: 10}}> <div style={{marginTop: 10}}>
<Typography.Text strong>GitHub</Typography.Text> <Typography.Text strong>{t('GitHub')}</Typography.Text>
<div <div
style={{display: 'flex', justifyContent: 'space-between'}} style={{display: 'flex', justifyContent: 'space-between'}}
> >
@@ -500,7 +483,7 @@ const PersonalSetting = () => {
value={ value={
userState.user && userState.user.github_id !== '' userState.user && userState.user.github_id !== ''
? userState.user.github_id ? userState.user.github_id
: '未绑定' : t('未绑定')
} }
readonly={true} readonly={true}
></Input> ></Input>
@@ -515,13 +498,13 @@ const PersonalSetting = () => {
!status.github_oauth !status.github_oauth
} }
> >
{status.github_oauth ? '绑定' : '未启用'} {status.github_oauth ? t('绑定') : t('未启用')}
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
<div style={{marginTop: 10}}> <div style={{marginTop: 10}}>
<Typography.Text strong>Telegram</Typography.Text> <Typography.Text strong>{t('Telegram')}</Typography.Text>
<div <div
style={{display: 'flex', justifyContent: 'space-between'}} style={{display: 'flex', justifyContent: 'space-between'}}
> >
@@ -530,7 +513,7 @@ const PersonalSetting = () => {
value={ value={
userState.user && userState.user.telegram_id !== '' userState.user && userState.user.telegram_id !== ''
? userState.user.telegram_id ? userState.user.telegram_id
: '未绑定' : t('未绑定')
} }
readonly={true} readonly={true}
></Input> ></Input>
@@ -538,7 +521,7 @@ const PersonalSetting = () => {
<div> <div>
{status.telegram_oauth ? ( {status.telegram_oauth ? (
userState.user.telegram_id !== '' ? ( userState.user.telegram_id !== '' ? (
<Button disabled={true}>已绑定</Button> <Button disabled={true}>{t('已绑定')}</Button>
) : ( ) : (
<TelegramLoginButton <TelegramLoginButton
dataAuthUrl='/api/oauth/telegram/bind' dataAuthUrl='/api/oauth/telegram/bind'
@@ -546,13 +529,13 @@ const PersonalSetting = () => {
/> />
) )
) : ( ) : (
<Button disabled={true}>未启用</Button> <Button disabled={true}>{t('未启用')}</Button>
)} )}
</div> </div>
</div> </div>
</div> </div>
<div style={{marginTop: 10}}> <div style={{marginTop: 10}}>
<Typography.Text strong>LinuxDO</Typography.Text> <Typography.Text strong>{t('LinuxDO')}</Typography.Text>
<div <div
style={{display: 'flex', justifyContent: 'space-between'}} style={{display: 'flex', justifyContent: 'space-between'}}
> >
@@ -561,7 +544,7 @@ const PersonalSetting = () => {
value={ value={
userState.user && userState.user.linux_do_id !== '' userState.user && userState.user.linux_do_id !== ''
? userState.user.linux_do_id ? userState.user.linux_do_id
: '未绑定' : t('未绑定')
} }
readonly={true} readonly={true}
></Input> ></Input>
@@ -576,7 +559,7 @@ const PersonalSetting = () => {
!status.linuxdo_oauth !status.linuxdo_oauth
} }
> >
{status.linuxdo_oauth ? '绑定' : '未启用'} {status.linuxdo_oauth ? t('绑定') : t('未启用')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -584,14 +567,14 @@ const PersonalSetting = () => {
<div style={{marginTop: 10}}> <div style={{marginTop: 10}}>
<Space> <Space>
<Button onClick={generateAccessToken}> <Button onClick={generateAccessToken}>
生成系统访问令牌 {t('生成系统访问令牌')}
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
setShowChangePasswordModal(true); setShowChangePasswordModal(true);
}} }}
> >
修改密码 {t('修改密码')}
</Button> </Button>
<Button <Button
type={'danger'} type={'danger'}
@@ -599,7 +582,7 @@ const PersonalSetting = () => {
setShowAccountDeleteModal(true); setShowAccountDeleteModal(true);
}} }}
> >
删除个人账户 {t('删除个人账户')}
</Button> </Button>
</Space> </Space>
@@ -617,7 +600,7 @@ const PersonalSetting = () => {
setShowWeChatBindModal(true); setShowWeChatBindModal(true);
}} }}
> >
绑定微信账号 {t('绑定微信账号')}
</Button> </Button>
)} )}
<Modal <Modal
@@ -641,7 +624,7 @@ const PersonalSetting = () => {
} }
/> />
<Button color='' fluid size='large' onClick={bindWeChat}> <Button color='' fluid size='large' onClick={bindWeChat}>
绑定 {t('绑定')}
</Button> </Button>
</Modal> </Modal>
</div> </div>
@@ -655,7 +638,7 @@ const PersonalSetting = () => {
centered={true} centered={true}
maskClosable={false} maskClosable={false}
> >
<Typography.Title heading={6}>绑定邮箱地址</Typography.Title> <Typography.Title heading={6}>{t('绑定邮箱地址')}</Typography.Title>
<div <div
style={{ style={{
marginTop: 20, marginTop: 20,
@@ -747,7 +730,7 @@ const PersonalSetting = () => {
<div style={{marginTop: 20}}> <div style={{marginTop: 20}}>
<Input <Input
name='set_new_password' name='set_new_password'
placeholder='新密码' placeholder={t('新密码')}
value={inputs.set_new_password} value={inputs.set_new_password}
onChange={(value) => onChange={(value) =>
handleInputChange('set_new_password', value) handleInputChange('set_new_password', value)
@@ -756,7 +739,7 @@ const PersonalSetting = () => {
<Input <Input
style={{marginTop: 20}} style={{marginTop: 20}}
name='set_new_password_confirmation' name='set_new_password_confirmation'
placeholder='确认新密码' placeholder={t('确认新密码')}
value={inputs.set_new_password_confirmation} value={inputs.set_new_password_confirmation}
onChange={(value) => onChange={(value) =>
handleInputChange('set_new_password_confirmation', value) handleInputChange('set_new_password_confirmation', value)

View File

@@ -19,55 +19,55 @@ import {
Tag, Tag,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import EditRedemption from '../pages/Redemption/EditRedemption'; import EditRedemption from '../pages/Redemption/EditRedemption';
import { useTranslation } from 'react-i18next';
function renderTimestamp(timestamp) { function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}</>; return <>{timestamp2string(timestamp)}</>;
} }
function renderStatus(status) {
switch (status) {
case 1:
return (
<Tag color='green' size='large'>
未使用
</Tag>
);
case 2:
return (
<Tag color='red' size='large'>
{' '}
已禁用{' '}
</Tag>
);
case 3:
return (
<Tag color='grey' size='large'>
{' '}
已使用{' '}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
{' '}
未知状态{' '}
</Tag>
);
}
}
const RedemptionsTable = () => { const RedemptionsTable = () => {
const { t } = useTranslation();
const renderStatus = (status) => {
switch (status) {
case 1:
return (
<Tag color='green' size='large'>
{t('未使用')}
</Tag>
);
case 2:
return (
<Tag color='red' size='large'>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag color='grey' size='large'>
{t('已使用')}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
{t('未知状态')}
</Tag>
);
}
};
const columns = [ const columns = [
{ {
title: 'ID', title: t('ID'),
dataIndex: 'id', dataIndex: 'id',
}, },
{ {
title: '名称', title: t('名称'),
dataIndex: 'name', dataIndex: 'name',
}, },
{ {
title: '状态', title: t('状态'),
dataIndex: 'status', dataIndex: 'status',
key: 'status', key: 'status',
render: (text, record, index) => { render: (text, record, index) => {
@@ -75,24 +75,24 @@ const RedemptionsTable = () => {
}, },
}, },
{ {
title: '额度', title: t('额度'),
dataIndex: 'quota', dataIndex: 'quota',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{renderQuota(parseInt(text))}</div>; return <div>{renderQuota(parseInt(text))}</div>;
}, },
}, },
{ {
title: '创建时间', title: t('创建时间'),
dataIndex: 'created_time', dataIndex: 'created_time',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{renderTimestamp(text)}</div>; return <div>{renderTimestamp(text)}</div>;
}, },
}, },
{ {
title: '兑换人ID', title: t('兑换人ID'),
dataIndex: 'used_user_id', dataIndex: 'used_user_id',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{text === 0 ? '无' : text}</div>; return <div>{text === 0 ? t('无') : text}</div>;
}, },
}, },
{ {
@@ -102,7 +102,7 @@ const RedemptionsTable = () => {
<div> <div>
<Popover content={record.key} style={{ padding: 20 }} position='top'> <Popover content={record.key} style={{ padding: 20 }} position='top'>
<Button theme='light' type='tertiary' style={{ marginRight: 1 }}> <Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
查看 {t('查看')}
</Button> </Button>
</Popover> </Popover>
<Button <Button
@@ -113,11 +113,11 @@ const RedemptionsTable = () => {
await copyText(record.key); await copyText(record.key);
}} }}
> >
复制 {t('复制')}
</Button> </Button>
<Popconfirm <Popconfirm
title='确定是否要删除此兑换码?' title={t('确定是否要删除此兑换码?')}
content='此修改将不可逆' content={t('此修改将不可逆')}
okType={'danger'} okType={'danger'}
position={'left'} position={'left'}
onConfirm={() => { onConfirm={() => {
@@ -127,7 +127,7 @@ const RedemptionsTable = () => {
}} }}
> >
<Button theme='light' type='danger' style={{ marginRight: 1 }}> <Button theme='light' type='danger' style={{ marginRight: 1 }}>
删除 {t('删除')}
</Button> </Button>
</Popconfirm> </Popconfirm>
{record.status === 1 ? ( {record.status === 1 ? (
@@ -139,7 +139,7 @@ const RedemptionsTable = () => {
manageRedemption(record.id, 'disable', record); manageRedemption(record.id, 'disable', record);
}} }}
> >
禁用 {t('禁用')}
</Button> </Button>
) : ( ) : (
<Button <Button
@@ -151,7 +151,7 @@ const RedemptionsTable = () => {
}} }}
disabled={record.status === 3} disabled={record.status === 3}
> >
启用 {t('启用')}
</Button> </Button>
)} )}
<Button <Button
@@ -164,7 +164,7 @@ const RedemptionsTable = () => {
}} }}
disabled={record.status !== 1} disabled={record.status !== 1}
> >
编辑 {t('编辑')}
</Button> </Button>
</div> </div>
), ),
@@ -239,10 +239,10 @@ const RedemptionsTable = () => {
const copyText = async (text) => { const copyText = async (text) => {
if (await copy(text)) { if (await copy(text)) {
showSuccess('已复制到剪贴板!'); showSuccess(t('已复制到剪贴板!'));
} else { } else {
// setSearchKeyword(text); // setSearchKeyword(text);
Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text }); Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
} }
}; };
@@ -286,7 +286,7 @@ const RedemptionsTable = () => {
} }
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess('操作成功完成!'); showSuccess(t('操作成功完成!'));
let redemption = res.data.data; let redemption = res.data.data;
let newRedemptions = [...redemptions]; let newRedemptions = [...redemptions];
// let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
@@ -381,11 +381,11 @@ const RedemptionsTable = () => {
></EditRedemption> ></EditRedemption>
<Form onSubmit={searchRedemptions}> <Form onSubmit={searchRedemptions}>
<Form.Input <Form.Input
label='搜索关键字' label={t('搜索关键字')}
field='keyword' field='keyword'
icon='search' icon='search'
iconPosition='left' iconPosition='left'
placeholder='关键字(id或者名称)' placeholder={t('关键字(id或者名称)')}
value={searchKeyword} value={searchKeyword}
loading={searching} loading={searching}
onChange={handleKeywordChange} onChange={handleKeywordChange}
@@ -404,14 +404,14 @@ const RedemptionsTable = () => {
setShowEdit(true); setShowEdit(true);
}} }}
> >
添加兑换码 {t('添加兑换码')}
</Button> </Button>
<Button <Button
label='复制所选兑换码' label={t('复制所选兑换码')}
type='warning' type='warning'
onClick={async () => { onClick={async () => {
if (selectedKeys.length === 0) { if (selectedKeys.length === 0) {
showError('请至少选择一个兑换码!'); showError(t('请至少选择一个兑换码!'));
return; return;
} }
let keys = ''; let keys = '';
@@ -421,7 +421,7 @@ const RedemptionsTable = () => {
await copyText(keys); await copyText(keys);
}} }}
> >
复制所选兑换码到剪贴板 {t('复制所选兑换码到剪贴板')}
</Button> </Button>
</div> </div>
@@ -436,7 +436,11 @@ const RedemptionsTable = () => {
// showSizeChanger: true, // showSizeChanger: true,
// pageSizeOptions: [10, 20, 50, 100], // pageSizeOptions: [10, 20, 50, 100],
formatPageText: (page) => formatPageText: (page) =>
`${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length}`, t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: redemptions.length
}),
// onPageSizeChange: (size) => { // onPageSizeChange: (size) => {
// setPageSize(size); // setPageSize(size);
// setActivePage(1); // setActivePage(1);

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { API, getLogo, showError, showInfo, showSuccess, updateAPI } from '../helpers'; import { API, getLogo, showError, showInfo, showSuccess, updateAPI } from '../helpers';
import Turnstile from 'react-turnstile'; import Turnstile from 'react-turnstile';
@@ -11,8 +11,11 @@ import LinuxDoIcon from './LinuxDoIcon.js';
import WeChatIcon from './WeChatIcon.js'; import WeChatIcon from './WeChatIcon.js';
import TelegramLoginButton from 'react-telegram-login/src'; import TelegramLoginButton from 'react-telegram-login/src';
import { setUserData } from '../helpers/data.js'; import { setUserData } from '../helpers/data.js';
import { UserContext } from '../context/User/index.js';
import { useTranslation } from 'react-i18next';
const RegisterForm = () => { const RegisterForm = () => {
const { t } = useTranslation();
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
username: '', username: '',
password: '', password: '',
@@ -22,6 +25,7 @@ const RegisterForm = () => {
}); });
const { username, password, password2 } = inputs; const { username, password, password2 } = inputs;
const [showEmailVerification, setShowEmailVerification] = useState(false); const [showEmailVerification, setShowEmailVerification] = useState(false);
const [userState, userDispatch] = useContext(UserContext);
const [turnstileEnabled, setTurnstileEnabled] = useState(false); const [turnstileEnabled, setTurnstileEnabled] = useState(false);
const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
const [turnstileToken, setTurnstileToken] = useState(''); const [turnstileToken, setTurnstileToken] = useState('');
@@ -133,6 +137,38 @@ const RegisterForm = () => {
setLoading(false); setLoading(false);
}; };
const onTelegramLoginClicked = async (response) => {
const fields = [
'id',
'first_name',
'last_name',
'username',
'photo_url',
'auth_date',
'hash',
'lang',
];
const params = {};
fields.forEach((field) => {
if (response[field]) {
params[field] = response[field];
}
});
const res = await API.get(`/api/oauth/telegram/login`, { params });
const { success, message, data } = res.data;
if (success) {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
showSuccess('登录成功!');
setUserData(data);
updateAPI();
navigate('/');
} else {
showError(message);
}
};
return ( return (
<div> <div>
<Layout> <Layout>
@@ -148,28 +184,28 @@ const RegisterForm = () => {
<div style={{ width: 500 }}> <div style={{ width: 500 }}>
<Card> <Card>
<Title heading={2} style={{ textAlign: 'center' }}> <Title heading={2} style={{ textAlign: 'center' }}>
新用户注册 {t('新用户注册')}
</Title> </Title>
<Form size="large"> <Form size="large">
<Form.Input <Form.Input
field={'username'} field={'username'}
label={'用户名'} label={t('用户名')}
placeholder="用户名" placeholder={t('用户名')}
name="username" name="username"
onChange={(value) => handleChange('username', value)} onChange={(value) => handleChange('username', value)}
/> />
<Form.Input <Form.Input
field={'password'} field={'password'}
label={'密码'} label={t('密码')}
placeholder="密码,最短 8 位,最长 20 位" placeholder={t('输入密码,最短 8 位,最长 20 位')}
name="password" name="password"
type="password" type="password"
onChange={(value) => handleChange('password', value)} onChange={(value) => handleChange('password', value)}
/> />
<Form.Input <Form.Input
field={'password2'} field={'password2'}
label={'确认密码'} label={t('确认密码')}
placeholder="确认密码" placeholder={t('确认密码')}
name="password2" name="password2"
type="password" type="password"
onChange={(value) => handleChange('password2', value)} onChange={(value) => handleChange('password2', value)}
@@ -178,21 +214,21 @@ const RegisterForm = () => {
<> <>
<Form.Input <Form.Input
field={'email'} field={'email'}
label={'邮箱'} label={t('邮箱')}
placeholder="输入邮箱地址" placeholder={t('输入邮箱地址')}
onChange={(value) => handleChange('email', value)} onChange={(value) => handleChange('email', value)}
name="email" name="email"
type="email" type="email"
suffix={ suffix={
<Button onClick={sendVerificationCode} disabled={loading}> <Button onClick={sendVerificationCode} disabled={loading}>
获取验证码 {t('获取验证码')}
</Button> </Button>
} }
/> />
<Form.Input <Form.Input
field={'verification_code'} field={'verification_code'}
label={'验证码'} label={t('验证码')}
placeholder="输入验证码" placeholder={t('输入验证码')}
onChange={(value) => handleChange('verification_code', value)} onChange={(value) => handleChange('verification_code', value)}
name="verification_code" name="verification_code"
/> />
@@ -208,7 +244,7 @@ const RegisterForm = () => {
htmlType={'submit'} htmlType={'submit'}
onClick={handleSubmit} onClick={handleSubmit}
> >
注册 {t('注册')}
</Button> </Button>
</Form> </Form>
<div <div
@@ -219,9 +255,9 @@ const RegisterForm = () => {
}} }}
> >
<Text> <Text>
已有账户 {t('已有账户?')}
<Link to="/login"> <Link to="/login">
点击登录 {t('点击登录')}
</Link> </Link>
</Text> </Text>
</div> </div>
@@ -231,7 +267,7 @@ const RegisterForm = () => {
status.linuxdo_oauth ? ( status.linuxdo_oauth ? (
<> <>
<Divider margin='12px' align='center'> <Divider margin='12px' align='center'>
第三方登录 {t('第三方登录')}
</Divider> </Divider>
<div <div
style={{ style={{
@@ -296,12 +332,12 @@ const RegisterForm = () => {
)} )}
</Card> </Card>
<Modal <Modal
title='微信扫码登录' title={t('微信扫码登录')}
visible={showWeChatLoginModal} visible={showWeChatLoginModal}
maskClosable={true} maskClosable={true}
onOk={onSubmitWeChatVerificationCode} onOk={onSubmitWeChatVerificationCode}
onCancel={() => setShowWeChatLoginModal(false)} onCancel={() => setShowWeChatLoginModal(false)}
okText={'登录'} okText={t('登录')}
size={'small'} size={'small'}
centered={true} centered={true}
> >
@@ -316,14 +352,14 @@ const RegisterForm = () => {
</div> </div>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<p> <p>
微信扫码关注公众号输入验证码获取验证码三分钟内有效 {t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
</p> </p>
</div> </div>
<Form size='large'> <Form size='large'>
<Form.Input <Form.Input
field={'wechat_verification_code'} field={'wechat_verification_code'}
placeholder='验证码' placeholder={t('验证码')}
label={'验证码'} label={t('验证码')}
value={inputs.wechat_verification_code} value={inputs.wechat_verification_code}
onChange={(value) => onChange={(value) =>
handleChange('wechat_verification_code', value) handleChange('wechat_verification_code', value)

View File

@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User'; import { UserContext } from '../context/User';
import { StatusContext } from '../context/Status'; import { StatusContext } from '../context/Status';
import { useTranslation } from 'react-i18next';
import { import {
API, API,
@@ -31,14 +32,16 @@ import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
import { setStatusData } from '../helpers/data.js'; import { setStatusData } from '../helpers/data.js';
import { stringToColor } from '../helpers/render.js'; import { stringToColor } from '../helpers/render.js';
import { useSetTheme, useTheme } from '../context/Theme/index.js'; import { useSetTheme, useTheme } from '../context/Theme/index.js';
import { StyleContext } from '../context/Style/index.js';
// HeaderBar Buttons // HeaderBar Buttons
const SiderBar = () => { const SiderBar = () => {
const [userState, userDispatch] = useContext(UserContext); const { t } = useTranslation();
const [styleState, styleDispatch] = useContext(StyleContext);
const [statusState, statusDispatch] = useContext(StatusContext); const [statusState, statusDispatch] = useContext(StatusContext);
const defaultIsCollapsed = const defaultIsCollapsed =
isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true'; localStorage.getItem('default_collapse_sidebar') === 'true';
const [selectedKeys, setSelectedKeys] = useState(['home']); const [selectedKeys, setSelectedKeys] = useState(['home']);
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed); const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
@@ -73,62 +76,26 @@ const SiderBar = () => {
icon: <IconCommentStroked />, icon: <IconCommentStroked />,
}, },
{ {
text: '模型价格', text: t('渠道'),
itemKey: 'pricing',
to: '/pricing',
icon: <IconPriceTag />,
},
{
text: '渠道',
itemKey: 'channel', itemKey: 'channel',
to: '/channel', to: '/channel',
icon: <IconLayers />, icon: <IconLayers />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle', className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
}, },
{ {
text: '聊天', text: t('聊天'),
itemKey: 'chat', itemKey: 'chat',
// to: '/chat',
items: chatItems, items: chatItems,
icon: <IconComment />, icon: <IconComment />,
// className: localStorage.getItem('chat_link')
// ? 'semi-navigation-item-normal'
// : 'tableHiddle',
}, },
{ {
text: '令牌', text: t('令牌'),
itemKey: 'token', itemKey: 'token',
to: '/token', to: '/token',
icon: <IconKey />, icon: <IconKey />,
}, },
{ {
text: '兑换码', text: t('数据看板'),
itemKey: 'redemption',
to: '/redemption',
icon: <IconGift />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
},
{
text: '钱包',
itemKey: 'topup',
to: '/topup',
icon: <IconCreditCard />,
},
{
text: '用户管理',
itemKey: 'user',
to: '/user',
icon: <IconUser />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
},
{
text: '日志',
itemKey: 'log',
to: '/log',
icon: <IconHistogram />,
},
{
text: '数据看板',
itemKey: 'detail', itemKey: 'detail',
to: '/detail', to: '/detail',
icon: <IconCalendarClock />, icon: <IconCalendarClock />,
@@ -138,7 +105,33 @@ const SiderBar = () => {
: 'tableHiddle', : 'tableHiddle',
}, },
{ {
text: '绘图', text: t('兑换码'),
itemKey: 'redemption',
to: '/redemption',
icon: <IconGift />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
},
{
text: t('钱包'),
itemKey: 'topup',
to: '/topup',
icon: <IconCreditCard />,
},
{
text: t('用户管理'),
itemKey: 'user',
to: '/user',
icon: <IconUser />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
},
{
text: t('日志'),
itemKey: 'log',
to: '/log',
icon: <IconHistogram />,
},
{
text: t('绘图'),
itemKey: 'midjourney', itemKey: 'midjourney',
to: '/midjourney', to: '/midjourney',
icon: <IconImage />, icon: <IconImage />,
@@ -148,7 +141,7 @@ const SiderBar = () => {
: 'tableHiddle', : 'tableHiddle',
}, },
{ {
text: '异步任务', text: t('异步任务'),
itemKey: 'task', itemKey: 'task',
to: '/task', to: '/task',
icon: <IconChecklistStroked />, icon: <IconChecklistStroked />,
@@ -158,53 +151,30 @@ const SiderBar = () => {
: 'tableHiddle', : 'tableHiddle',
}, },
{ {
text: '设置', text: t('设置'),
itemKey: 'setting', itemKey: 'setting',
to: '/setting', to: '/setting',
icon: <IconSetting />, icon: <IconSetting />,
}, },
// {
// text: '关于',
// itemKey: 'about',
// to: '/about',
// icon: <IconAt/>
// }
], ],
[ [
localStorage.getItem('enable_data_export'), localStorage.getItem('enable_data_export'),
localStorage.getItem('enable_drawing'), localStorage.getItem('enable_drawing'),
localStorage.getItem('enable_task'), localStorage.getItem('enable_task'),
localStorage.getItem('chat_link'), chatItems, localStorage.getItem('chat_link'),
chatItems,
isAdmin(), isAdmin(),
t,
], ],
); );
const loadStatus = async () => {
const res = await API.get('/api/status');
if (res === undefined) {
return;
}
const { success, data } = res.data;
if (success) {
statusDispatch({ type: 'set', payload: data });
setStatusData(data);
} else {
showError('无法正常连接至服务器!');
}
};
useEffect(() => { useEffect(() => {
loadStatus().then(() => {
setIsCollapsed(
isMobile() ||
localStorage.getItem('default_collapse_sidebar') === 'true',
);
});
let localKey = window.location.pathname.split('/')[1]; let localKey = window.location.pathname.split('/')[1];
if (localKey === '') { if (localKey === '') {
localKey = 'home'; localKey = 'home';
} }
setSelectedKeys([localKey]); setSelectedKeys([localKey]);
let chatLink = localStorage.getItem('chat_link'); let chatLink = localStorage.getItem('chat_link');
if (!chatLink) { if (!chatLink) {
let chats = localStorage.getItem('chats'); let chats = localStorage.getItem('chats');
@@ -232,6 +202,8 @@ const SiderBar = () => {
} }
} }
} }
setIsCollapsed(localStorage.getItem('default_collapse_sidebar') === 'true');
}, []); }, []);
return ( return (
@@ -239,7 +211,6 @@ const SiderBar = () => {
<Nav <Nav
style={{ maxWidth: 220, height: '100%' }} style={{ maxWidth: 220, height: '100%' }}
defaultIsCollapsed={ defaultIsCollapsed={
isMobile() ||
localStorage.getItem('default_collapse_sidebar') === 'true' localStorage.getItem('default_collapse_sidebar') === 'true'
} }
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
@@ -280,21 +251,15 @@ const SiderBar = () => {
}} }}
items={headerButtons} items={headerButtons}
onSelect={(key) => { onSelect={(key) => {
if (key.itemKey.toString().startsWith('chat')) {
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
} else {
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
}
setSelectedKeys([key.itemKey]); setSelectedKeys([key.itemKey]);
}} }}
footer={ footer={
<> <>
{isMobile() && (
<Switch
checkedText='🌞'
size={'small'}
checked={theme === 'dark'}
uncheckedText='🌙'
onChange={(checked) => {
setTheme(checked);
}}
/>
)}
</> </>
} }
> >

View File

@@ -23,67 +23,66 @@ import {
import { IconTreeTriangleDown } from '@douyinfe/semi-icons'; import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
import EditToken from '../pages/Token/EditToken'; import EditToken from '../pages/Token/EditToken';
import { useTranslation } from 'react-i18next';
function renderTimestamp(timestamp) { function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}</>; return <>{timestamp2string(timestamp)}</>;
} }
function renderStatus(status, model_limits_enabled = false) {
switch (status) {
case 1:
if (model_limits_enabled) {
return (
<Tag color='green' size='large'>
已启用限制模型
</Tag>
);
} else {
return (
<Tag color='green' size='large'>
已启用
</Tag>
);
}
case 2:
return (
<Tag color='red' size='large'>
{' '}
已禁用{' '}
</Tag>
);
case 3:
return (
<Tag color='yellow' size='large'>
{' '}
已过期{' '}
</Tag>
);
case 4:
return (
<Tag color='grey' size='large'>
{' '}
已耗尽{' '}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
{' '}
未知状态{' '}
</Tag>
);
}
}
const TokensTable = () => { const TokensTable = () => {
const { t } = useTranslation();
const renderStatus = (status, model_limits_enabled = false) => {
switch (status) {
case 1:
if (model_limits_enabled) {
return (
<Tag color='green' size='large'>
{t('已启用:限制模型')}
</Tag>
);
} else {
return (
<Tag color='green' size='large'>
{t('已启用')}
</Tag>
);
}
case 2:
return (
<Tag color='red' size='large'>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag color='yellow' size='large'>
{t('已过期')}
</Tag>
);
case 4:
return (
<Tag color='grey' size='large'>
{t('已耗尽')}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
{t('未知状态')}
</Tag>
);
}
};
const columns = [ const columns = [
{ {
title: '名称', title: t('名称'),
dataIndex: 'name', dataIndex: 'name',
}, },
{ {
title: '状态', title: t('状态'),
dataIndex: 'status', dataIndex: 'status',
key: 'status', key: 'status',
render: (text, record, index) => { render: (text, record, index) => {
@@ -96,21 +95,21 @@ const TokensTable = () => {
}, },
}, },
{ {
title: '已用额度', title: t('已用额度'),
dataIndex: 'used_quota', dataIndex: 'used_quota',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{renderQuota(parseInt(text))}</div>; return <div>{renderQuota(parseInt(text))}</div>;
}, },
}, },
{ {
title: '剩余额度', title: t('剩余额度'),
dataIndex: 'remain_quota', dataIndex: 'remain_quota',
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
{record.unlimited_quota ? ( {record.unlimited_quota ? (
<Tag size={'large'} color={'white'}> <Tag size={'large'} color={'white'}>
无限制 {t('无限制')}
</Tag> </Tag>
) : ( ) : (
<Tag size={'large'} color={'light-blue'}> <Tag size={'large'} color={'light-blue'}>
@@ -122,19 +121,19 @@ const TokensTable = () => {
}, },
}, },
{ {
title: '创建时间', title: t('创建时间'),
dataIndex: 'created_time', dataIndex: 'created_time',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{renderTimestamp(text)}</div>; return <div>{renderTimestamp(text)}</div>;
}, },
}, },
{ {
title: '过期时间', title: t('过期时间'),
dataIndex: 'expired_time', dataIndex: 'expired_time',
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
{record.expired_time === -1 ? '永不过期' : renderTimestamp(text)} {record.expired_time === -1 ? t('永不过期') : renderTimestamp(text)}
</div> </div>
); );
}, },
@@ -199,7 +198,7 @@ const TokensTable = () => {
} catch (e) { } catch (e) {
console.log(e); console.log(e);
showError('聊天链接配置错误,请联系管理员'); showError(t('聊天链接配置错误,请联系管理员'));
} }
} }
return ( return (
@@ -210,7 +209,7 @@ const TokensTable = () => {
position='top' position='top'
> >
<Button theme='light' type='tertiary' style={{ marginRight: 1 }}> <Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
查看 {t('查看')}
</Button> </Button>
</Popover> </Popover>
<Button <Button
@@ -221,24 +220,24 @@ const TokensTable = () => {
await copyText('sk-' + record.key); await copyText('sk-' + record.key);
}} }}
> >
复制 {t('复制')}
</Button> </Button>
<SplitButtonGroup <SplitButtonGroup
style={{ marginRight: 1 }} style={{ marginRight: 1 }}
aria-label='项目操作按钮组' aria-label={t('项目操作按钮组')}
> >
<Button <Button
theme='light' theme='light'
style={{ color: 'rgba(var(--semi-teal-7), 1)' }} style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
onClick={() => { onClick={() => {
if (chatsArray.length === 0) { if (chatsArray.length === 0) {
showError('请联系管理员配置聊天链接'); showError(t('请联系管理员配置聊天链接'));
} else { } else {
onOpenLink('default', chats[0][Object.keys(chats[0])[0]], record); onOpenLink('default', chats[0][Object.keys(chats[0])[0]], record);
} }
}} }}
> >
聊天 {t('聊天')}
</Button> </Button>
<Dropdown <Dropdown
trigger='click' trigger='click'
@@ -256,8 +255,8 @@ const TokensTable = () => {
</Dropdown> </Dropdown>
</SplitButtonGroup> </SplitButtonGroup>
<Popconfirm <Popconfirm
title='确定是否要删除此令牌?' title={t('确定是否要删除此令牌?')}
content='此修改将不可逆' content={t('此修改将不可逆')}
okType={'danger'} okType={'danger'}
position={'left'} position={'left'}
onConfirm={() => { onConfirm={() => {
@@ -267,7 +266,7 @@ const TokensTable = () => {
}} }}
> >
<Button theme='light' type='danger' style={{ marginRight: 1 }}> <Button theme='light' type='danger' style={{ marginRight: 1 }}>
删除 {t('删除')}
</Button> </Button>
</Popconfirm> </Popconfirm>
{record.status === 1 ? ( {record.status === 1 ? (
@@ -279,7 +278,7 @@ const TokensTable = () => {
manageToken(record.id, 'disable', record); manageToken(record.id, 'disable', record);
}} }}
> >
禁用 {t('禁用')}
</Button> </Button>
) : ( ) : (
<Button <Button
@@ -290,7 +289,7 @@ const TokensTable = () => {
manageToken(record.id, 'enable', record); manageToken(record.id, 'enable', record);
}} }}
> >
启用 {t('启用')}
</Button> </Button>
)} )}
<Button <Button
@@ -302,7 +301,7 @@ const TokensTable = () => {
setShowEdit(true); setShowEdit(true);
}} }}
> >
编辑 {t('编辑')}
</Button> </Button>
</div> </div>
); );
@@ -371,10 +370,10 @@ const TokensTable = () => {
const copyText = async (text) => { const copyText = async (text) => {
if (await copy(text)) { if (await copy(text)) {
showSuccess('已复制到剪贴板!'); showSuccess(t('已复制到剪贴板!'));
} else { } else {
Modal.error({ Modal.error({
title: '无法复制到剪贴板,请手动复制', title: t('无法复制到剪贴板,请手动复制'),
content: text, content: text,
size: 'large', size: 'large',
}); });
@@ -395,37 +394,6 @@ const TokensTable = () => {
let encodedServerAddress = encodeURIComponent(serverAddress); let encodedServerAddress = encodeURIComponent(serverAddress);
url = url.replaceAll('{address}', encodedServerAddress); url = url.replaceAll('{address}', encodedServerAddress);
url = url.replaceAll('{key}', 'sk-' + record.key); url = url.replaceAll('{key}', 'sk-' + record.key);
// console.log(url);
// const chatLink = localStorage.getItem('chat_link');
// const mjLink = localStorage.getItem('chat_link2');
// let defaultUrl;
//
// if (chatLink) {
// defaultUrl =
// chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
// }
// let url;
// switch (type) {
// case 'ama':
// url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
// break;
// case 'opencat':
// url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
// break;
// case 'lobe':
// url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}/v1"}}}`;
// break;
// case 'next-mj':
// url =
// mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
// break;
// default:
// if (!chatLink) {
// showError('管理员未设置聊天链接');
// return;
// }
// url = defaultUrl;
// }
window.open(url, '_blank'); window.open(url, '_blank');
}; };
@@ -571,29 +539,29 @@ const TokensTable = () => {
> >
<Form.Input <Form.Input
field='keyword' field='keyword'
label='搜索关键字' label={t('搜索关键字')}
placeholder='令牌名称' placeholder={t('令牌名称')}
value={searchKeyword} value={searchKeyword}
loading={searching} loading={searching}
onChange={handleKeywordChange} onChange={handleKeywordChange}
/> />
<Form.Input <Form.Input
field='token' field='token'
label='Key' label={t('密钥')}
placeholder='密钥' placeholder={t('密钥')}
value={searchToken} value={searchToken}
loading={searching} loading={searching}
onChange={handleSearchTokenChange} onChange={handleSearchTokenChange}
/> />
<Button <Button
label='查询' label={t('查询')}
type='primary' type='primary'
htmlType='submit' htmlType='submit'
className='btn-margin-right' className='btn-margin-right'
onClick={searchTokens} onClick={searchTokens}
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
> >
查询 {t('查询')}
</Button> </Button>
</Form> </Form>
<Divider style={{margin:'15px 0'}}/> <Divider style={{margin:'15px 0'}}/>
@@ -609,14 +577,14 @@ const TokensTable = () => {
setShowEdit(true); setShowEdit(true);
}} }}
> >
添加令牌 {t('添加令牌')}
</Button> </Button>
<Button <Button
label='复制所选令牌' label={t('复制所选令牌')}
type='warning' type='warning'
onClick={async () => { onClick={async () => {
if (selectedKeys.length === 0) { if (selectedKeys.length === 0) {
showError('请至少选择一个令牌!'); showError(t('请至少选择一个令牌!'));
return; return;
} }
let keys = ''; let keys = '';
@@ -627,7 +595,7 @@ const TokensTable = () => {
await copyText(keys); await copyText(keys);
}} }}
> >
复制所选令牌到剪贴板 {t('复制所选令牌到剪贴板')}
</Button> </Button>
</div> </div>
@@ -642,7 +610,11 @@ const TokensTable = () => {
showSizeChanger: true, showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100], pageSizeOptions: [10, 20, 50, 100],
formatPageText: (page) => formatPageText: (page) =>
`${page.currentStart} - ${page.currentEnd} 条,共 ${tokens.length}`, t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: tokens.length
}),
onPageSizeChange: (size) => { onPageSizeChange: (size) => {
setPageSize(size); setPageSize(size);
setActivePage(1); setActivePage(1);

View File

@@ -13,67 +13,69 @@ import { ITEMS_PER_PAGE } from '../constants';
import { renderGroup, renderNumber, renderQuota } from '../helpers/render'; import { renderGroup, renderNumber, renderQuota } from '../helpers/render';
import AddUser from '../pages/User/AddUser'; import AddUser from '../pages/User/AddUser';
import EditUser from '../pages/User/EditUser'; import EditUser from '../pages/User/EditUser';
import { useTranslation } from 'react-i18next';
function renderRole(role) {
switch (role) {
case 1:
return <Tag size='large'>普通用户</Tag>;
case 10:
return (
<Tag color='yellow' size='large'>
管理员
</Tag>
);
case 100:
return (
<Tag color='orange' size='large'>
超级管理员
</Tag>
);
default:
return (
<Tag color='red' size='large'>
未知身份
</Tag>
);
}
}
const UsersTable = () => { const UsersTable = () => {
const { t } = useTranslation();
function renderRole(role) {
switch (role) {
case 1:
return <Tag size='large'>{t('普通用户')}</Tag>;
case 10:
return (
<Tag color='yellow' size='large'>
{t('管理员')}
</Tag>
);
case 100:
return (
<Tag color='orange' size='large'>
{t('超级管理员')}
</Tag>
);
default:
return (
<Tag color='red' size='large'>
{t('未知身份')}
</Tag>
);
}
}
const columns = [ const columns = [
{ {
title: 'ID', title: 'ID',
dataIndex: 'id', dataIndex: 'id',
}, },
{ {
title: '用户名', title: t('用户名'),
dataIndex: 'username', dataIndex: 'username',
}, },
{ {
title: '分组', title: t('分组'),
dataIndex: 'group', dataIndex: 'group',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{renderGroup(text)}</div>; return <div>{renderGroup(text)}</div>;
}, },
}, },
{ {
title: '统计信息', title: t('统计信息'),
dataIndex: 'info', dataIndex: 'info',
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
<Space spacing={1}> <Space spacing={1}>
<Tooltip content={'剩余额度'}> <Tooltip content={t('剩余额度')}>
<Tag color='white' size='large'> <Tag color='white' size='large'>
{renderQuota(record.quota)} {renderQuota(record.quota)}
</Tag> </Tag>
</Tooltip> </Tooltip>
<Tooltip content={'已用额度'}> <Tooltip content={t('已用额度')}>
<Tag color='white' size='large'> <Tag color='white' size='large'>
{renderQuota(record.used_quota)} {renderQuota(record.used_quota)}
</Tag> </Tag>
</Tooltip> </Tooltip>
<Tooltip content={'调用次数'}> <Tooltip content={t('调用次数')}>
<Tag color='white' size='large'> <Tag color='white' size='large'>
{renderNumber(record.request_count)} {renderNumber(record.request_count)}
</Tag> </Tag>
@@ -84,26 +86,26 @@ const UsersTable = () => {
}, },
}, },
{ {
title: '邀请信息', title: t('邀请信息'),
dataIndex: 'invite', dataIndex: 'invite',
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
<Space spacing={1}> <Space spacing={1}>
<Tooltip content={'邀请人数'}> <Tooltip content={t('邀请人数')}>
<Tag color='white' size='large'> <Tag color='white' size='large'>
{renderNumber(record.aff_count)} {renderNumber(record.aff_count)}
</Tag> </Tag>
</Tooltip> </Tooltip>
<Tooltip content={'邀请总收益'}> <Tooltip content={t('邀请总收益')}>
<Tag color='white' size='large'> <Tag color='white' size='large'>
{renderQuota(record.aff_history_quota)} {renderQuota(record.aff_history_quota)}
</Tag> </Tag>
</Tooltip> </Tooltip>
<Tooltip content={'邀请人ID'}> <Tooltip content={t('邀请人ID')}>
{record.inviter_id === 0 ? ( {record.inviter_id === 0 ? (
<Tag color='white' size='large'> <Tag color='white' size='large'>
{t('无')}
</Tag> </Tag>
) : ( ) : (
<Tag color='white' size='large'> <Tag color='white' size='large'>
@@ -117,20 +119,20 @@ const UsersTable = () => {
}, },
}, },
{ {
title: '角色', title: t('角色'),
dataIndex: 'role', dataIndex: 'role',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{renderRole(text)}</div>; return <div>{renderRole(text)}</div>;
}, },
}, },
{ {
title: '状态', title: t('状态'),
dataIndex: 'status', dataIndex: 'status',
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
{record.DeletedAt !== null ? ( {record.DeletedAt !== null ? (
<Tag color='red'>已注销</Tag> <Tag color='red'>{t('已注销')}</Tag>
) : ( ) : (
renderStatus(text) renderStatus(text)
)} )}
@@ -148,29 +150,25 @@ const UsersTable = () => {
) : ( ) : (
<> <>
<Popconfirm <Popconfirm
title='确定?' title={t('确定?')}
okType={'warning'} okType={'warning'}
onConfirm={() => { onConfirm={() => {
manageUser(record.id, 'promote', record); manageUser(record.id, 'promote', record);
}} }}
> >
<Button theme='light' type='warning' style={{ marginRight: 1 }}> <Button theme='light' type='warning' style={{ marginRight: 1 }}>
提升 {t('提升')}
</Button> </Button>
</Popconfirm> </Popconfirm>
<Popconfirm <Popconfirm
title='确定?' title={t('确定?')}
okType={'warning'} okType={'warning'}
onConfirm={() => { onConfirm={() => {
manageUser(record.id, 'demote', record); manageUser(record.id, 'demote', record);
}} }}
> >
<Button <Button theme='light' type='secondary' style={{ marginRight: 1 }}>
theme='light' {t('降级')}
type='secondary'
style={{ marginRight: 1 }}
>
降级
</Button> </Button>
</Popconfirm> </Popconfirm>
{record.status === 1 ? ( {record.status === 1 ? (
@@ -182,7 +180,7 @@ const UsersTable = () => {
manageUser(record.id, 'disable', record); manageUser(record.id, 'disable', record);
}} }}
> >
禁用 {t('禁用')}
</Button> </Button>
) : ( ) : (
<Button <Button
@@ -194,7 +192,7 @@ const UsersTable = () => {
}} }}
disabled={record.status === 3} disabled={record.status === 3}
> >
启用 {t('启用')}
</Button> </Button>
)} )}
<Button <Button
@@ -206,11 +204,11 @@ const UsersTable = () => {
setShowEditUser(true); setShowEditUser(true);
}} }}
> >
编辑 {t('编辑')}
</Button> </Button>
<Popconfirm <Popconfirm
title='确定是否要注销此用户?' title={t('确定是否要注销此用户?')}
content='相当于删除用户,此修改将不可逆' content={t('相当于删除用户,此修改将不可逆')}
okType={'danger'} okType={'danger'}
position={'left'} position={'left'}
onConfirm={() => { onConfirm={() => {
@@ -220,7 +218,7 @@ const UsersTable = () => {
}} }}
> >
<Button theme='light' type='danger' style={{ marginRight: 1 }}> <Button theme='light' type='danger' style={{ marginRight: 1 }}>
注销 {t('注销')}
</Button> </Button>
</Popconfirm> </Popconfirm>
</> </>
@@ -327,17 +325,17 @@ const UsersTable = () => {
const renderStatus = (status) => { const renderStatus = (status) => {
switch (status) { switch (status) {
case 1: case 1:
return <Tag size='large'>已激活</Tag>; return <Tag size='large'>{t('已激活')}</Tag>;
case 2: case 2:
return ( return (
<Tag size='large' color='red'> <Tag size='large' color='red'>
已封禁 {t('已封禁')}
</Tag> </Tag>
); );
default: default:
return ( return (
<Tag size='large' color='grey'> <Tag size='large' color='grey'>
未知状态 {t('未知状态')}
</Tag> </Tag>
); );
} }
@@ -452,41 +450,41 @@ const UsersTable = () => {
> >
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<Space> <Space>
<Form.Input <Form.Input
label='搜索关键字' label={t('搜索关键字')}
icon='search' icon='search'
field='keyword' field='keyword'
iconPosition='left' iconPosition='left'
placeholder='搜索用户的 ID用户名显示名称以及邮箱地址 ...' placeholder={t('搜索用户的 ID用户名显示名称以及邮箱地址 ...')}
value={searchKeyword} value={searchKeyword}
loading={searching} loading={searching}
onChange={(value) => handleKeywordChange(value)} onChange={(value) => handleKeywordChange(value)}
/> />
<Form.Select <Form.Select
field='group' field='group'
label='分组' label={t('分组')}
optionList={groupOptions} optionList={groupOptions}
onChange={(value) => { onChange={(value) => {
setSearchGroup(value); setSearchGroup(value);
searchUsers(searchKeyword, value); searchUsers(searchKeyword, value);
}} }}
/> />
<Button
label='查询'
type='primary'
htmlType='submit'
className='btn-margin-right'
>
查询
</Button>
<Button <Button
theme='light' label={t('查询')}
type='primary' type='primary'
onClick={() => { htmlType='submit'
setShowAddUser(true); className='btn-margin-right'
}}
> >
添加用户 {t('查询')}
</Button>
<Button
theme='light'
type='primary'
onClick={() => {
setShowAddUser(true);
}}
>
{t('添加用户')}
</Button> </Button>
</Space> </Space>
</div> </div>
@@ -496,6 +494,12 @@ const UsersTable = () => {
columns={columns} columns={columns}
dataSource={pageData} dataSource={pageData}
pagination={{ pagination={{
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: users.length
}),
currentPage: activePage, currentPage: activePage,
pageSize: ITEMS_PER_PAGE, pageSize: ITEMS_PER_PAGE,
total: userCount, total: userCount,

View File

@@ -0,0 +1,80 @@
// contexts/User/index.jsx
import React, { useState, useEffect } from 'react';
import { isMobile } from '../../helpers/index.js';
export const StyleContext = React.createContext({
dispatch: () => null,
});
export const StyleProvider = ({ children }) => {
const [state, setState] = useState({
isMobile: false,
showSider: false,
shouldInnerPadding: false,
});
const dispatch = (action) => {
if ('type' in action) {
switch (action.type) {
case 'TOGGLE_SIDER':
setState(prev => ({ ...prev, showSider: !prev.showSider }));
break;
case 'SET_SIDER':
setState(prev => ({ ...prev, showSider: action.payload }));
break;
case 'SET_MOBILE':
setState(prev => ({ ...prev, isMobile: action.payload }));
break;
case 'SET_INNER_PADDING':
setState(prev => ({ ...prev, shouldInnerPadding: action.payload }));
break;
default:
setState(prev => ({ ...prev, ...action }));
}
} else {
setState(prev => ({ ...prev, ...action }));
}
};
useEffect(() => {
const updateIsMobile = () => {
dispatch({ type: 'SET_MOBILE', payload: isMobile() });
};
updateIsMobile();
const updateShowSider = () => {
// check pathname
const pathname = window.location.pathname;
if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) {
dispatch({ type: 'SET_SIDER', payload: false });
dispatch({ type: 'SET_INNER_PADDING', payload: false });
} else {
dispatch({ type: 'SET_SIDER', payload: true });
dispatch({ type: 'SET_INNER_PADDING', payload: true });
}
if (isMobile()) {
dispatch({ type: 'SET_SIDER', payload: false });
}
};
updateShowSider()
// Optionally, add event listeners to handle window resize
window.addEventListener('resize', updateIsMobile);
// Cleanup event listener on component unmount
return () => {
window.removeEventListener('resize', updateIsMobile);
};
}, []);
return (
<StyleContext.Provider value={[state, dispatch]}>
{children}
</StyleContext.Provider>
);
};

View File

@@ -1,3 +1,4 @@
import i18next from 'i18next';
import { Tag } from '@douyinfe/semi-ui'; import { Tag } from '@douyinfe/semi-ui';
export function renderText(text, limit) { export function renderText(text, limit) {
@@ -16,7 +17,7 @@ export function renderGroup(group) {
if (group === '') { if (group === '') {
return ( return (
<Tag size='large' key='default' color='orange'> <Tag size='large' key='default' color='orange'>
用户分组 {i18next.t('用户分组')}
</Tag> </Tag>
); );
} }
@@ -144,14 +145,16 @@ export function renderModelPrice(
completionRatio, completionRatio,
groupRatio, groupRatio,
) { ) {
// 1 ratio = $0.002 / 1K tokens
if (modelPrice !== -1) { if (modelPrice !== -1) {
return '模型价格:$' + modelPrice + ' * 分组倍率:' + groupRatio + ' = $' + modelPrice * groupRatio; return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', {
price: modelPrice,
ratio: groupRatio,
total: modelPrice * groupRatio
});
} else { } else {
if (completionRatio === undefined) { if (completionRatio === undefined) {
completionRatio = 0; completionRatio = 0;
} }
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
let inputRatioPrice = modelRatio * 2.0; let inputRatioPrice = modelRatio * 2.0;
let completionRatioPrice = modelRatio * 2.0 * completionRatio; let completionRatioPrice = modelRatio * 2.0 * completionRatio;
let price = let price =
@@ -160,21 +163,52 @@ export function renderModelPrice(
return ( return (
<> <>
<article> <article>
<p>提示${inputRatioPrice} * {groupRatio} = ${inputRatioPrice * groupRatio} / 1M tokens</p> <p>{i18next.t('提示:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
<p>补全${completionRatioPrice} * {groupRatio} = ${completionRatioPrice * groupRatio} / 1M tokens</p> price: inputRatioPrice,
ratio: groupRatio,
total: inputRatioPrice * groupRatio
})}</p>
<p>{i18next.t('补全:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
price: completionRatioPrice,
ratio: groupRatio,
total: completionRatioPrice * groupRatio
})}</p>
<p></p> <p></p>
<p> <p>
提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '} {i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
{completionTokens} tokens / 1M tokens * ${completionRatioPrice} * 分组 {groupRatio} = input: inputTokens,
${price.toFixed(6)} price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice,
ratio: groupRatio,
total: price.toFixed(6)
})}
</p> </p>
<p>仅供参考以实际扣费为准</p> <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
</article> </article>
</> </>
); );
} }
} }
export function renderModelPriceSimple(
modelRatio,
modelPrice = -1,
groupRatio,
) {
if (modelPrice !== -1) {
return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
price: modelPrice,
ratio: groupRatio
});
} else {
return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
ratio: modelRatio,
groupRatio: groupRatio
});
}
}
export function renderAudioModelPrice( export function renderAudioModelPrice(
inputTokens, inputTokens,
completionTokens, completionTokens,
@@ -211,8 +245,12 @@ export function renderAudioModelPrice(
<p>音频补全${inputRatioPrice} * {groupRatio} * {audioRatio} * {audioCompletionRatio} = ${inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio} / 1M tokens</p> <p>音频补全${inputRatioPrice} * {groupRatio} * {audioRatio} * {audioCompletionRatio} = ${inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio} / 1M tokens</p>
<p></p> <p></p>
<p> <p>
提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '} {i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +', {
{completionTokens} tokens / 1M tokens * ${completionRatioPrice} + input: inputTokens,
price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice
})}
</p> </p>
<p> <p>
音频提示 {audioInputTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} + 音频补全 {audioCompletionTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} * {audioCompletionRatio} 音频提示 {audioInputTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} + 音频补全 {audioCompletionTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} * {audioCompletionRatio}
@@ -232,7 +270,7 @@ export function renderQuotaWithPrompt(quota, digits) {
let displayInCurrency = localStorage.getItem('display_in_currency'); let displayInCurrency = localStorage.getItem('display_in_currency');
displayInCurrency = displayInCurrency === 'true'; displayInCurrency = displayInCurrency === 'true';
if (displayInCurrency) { if (displayInCurrency) {
return `(等价金额:${renderQuota(quota, digits)}`; return '|' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + '';
} }
return ''; return '';
} }
@@ -255,6 +293,44 @@ const colors = [
'yellow', 'yellow',
]; ];
// 基础10色色板 (N ≤ 10)
const baseColors = [
'#1664FF', // 主色
'#1AC6FF',
'#FF8A00',
'#3CC780',
'#7442D4',
'#FFC400',
'#304D77',
'#B48DEB',
'#009488',
'#FF7DDA'
];
// 扩展20色色板 (10 < N ≤ 20)
const extendedColors = [
'#1664FF',
'#B2CFFF',
'#1AC6FF',
'#94EFFF',
'#FF8A00',
'#FFCE7A',
'#3CC780',
'#B9EDCD',
'#7442D4',
'#DDC5FA',
'#FFC400',
'#FAE878',
'#304D77',
'#8B959E',
'#B48DEB',
'#EFE3FF',
'#009488',
'#59BAA8',
'#FF7DDA',
'#FFCFEE'
];
export const modelColorMap = { export const modelColorMap = {
'dall-e': 'rgb(147,112,219)', // 深紫色 'dall-e': 'rgb(147,112,219)', // 深紫色
// 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调 // 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
@@ -264,7 +340,7 @@ export const modelColorMap = {
'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色 'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃 'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃<EFBFBD><EFBFBD><EFBFBD>
'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
'gpt-4': 'rgb(135,206,235)', // 天蓝色 'gpt-4': 'rgb(135,206,235)', // 天蓝色
// 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色 // 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
@@ -287,7 +363,7 @@ export const modelColorMap = {
'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红 'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别) 'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
'text-moderation-latest': 'rgb(255,130,171)', // 强粉色 'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(Babbage相同表示同一类功能 'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(<EFBFBD><EFBFBD><EFBFBD>Babbage相同表示同一类功能
'tts-1': 'rgb(255,140,0)', // 深橙色 'tts-1': 'rgb(255,140,0)', // 深橙色
'tts-1-1106': 'rgb(255,165,0)', // 橙色 'tts-1-1106': 'rgb(255,165,0)', // 橙色
'tts-1-hd': 'rgb(255,215,0)', // 金色 'tts-1-hd': 'rgb(255,215,0)', // 金色
@@ -299,14 +375,33 @@ export const modelColorMap = {
'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别) 'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
}; };
export function modelToColor(modelName) {
// 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
if (modelColorMap[modelName]) {
return modelColorMap[modelName];
}
// 2. 生成一个稳定的数字作为索引
let hash = 0;
for (let i = 0; i < modelName.length; i++) {
hash = ((hash << 5) - hash) + modelName.charCodeAt(i);
hash = hash & hash; // Convert to 32-bit integer
}
hash = Math.abs(hash);
// 3. 根据模型名称长度选择不同的色板
const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
// 4. 使用hash值选择颜色
const index = hash % colorPalette.length;
return colorPalette[index];
}
export function stringToColor(str) { export function stringToColor(str) {
let sum = 0; let sum = 0;
// 对字符串中的每个字符进行操作
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
// 将字符的ASCII值加到sum中
sum += str.charCodeAt(i); sum += str.charCodeAt(i);
} }
// 使用模运算得到个位数
let i = sum % colors.length; let i = sum % colors.length;
return colors[i]; return colors[i];
} }

26
web/src/i18n/i18n.js Normal file
View File

@@ -0,0 +1,26 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import enTranslation from './locales/en.json';
import zhTranslation from './locales/zh.json';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: {
translation: enTranslation
},
zh: {
translation: zhTranslation
}
},
fallbackLng: 'zh',
interpolation: {
escapeValue: false
}
});
export default i18n;

File diff suppressed because it is too large Load Diff

1235
web/src/i18n/locales/en.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"首页": "首页",
"控制台": "控制台",
"定价": "定价",
"关于": "关于",
"登录": "登录",
"注册": "注册",
"退出": "退出",
"语言": "语言",
"展开侧边栏": "展开侧边栏",
"关闭侧边栏": "关闭侧边栏",
"注销成功!": "注销成功!"
}

View File

@@ -17,7 +17,36 @@ body {
flex-direction: column; flex-direction: column;
} }
#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li > span{
font-weight: 600 !important;
}
.semi-descriptions-double-small .semi-descriptions-item {
padding-right: 30px;
}
@media only screen and (max-width: 767px) { @media only screen and (max-width: 767px) {
/*.semi-navigation-sub-wrap .semi-navigation-sub-title, .semi-navigation-item {*/
/* padding: 0 0;*/
/*}*/
.topnav .semi-navigation-list-wrapper {
max-width: calc(55vw - 20px);
overflow-x: auto;
scrollbar-width: none;
}
#root > section > header > section > div > div > div > div.semi-navigation-footer > div > a > li {
padding: 0 0;
}
#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li {
padding: 0 5px;
}
#root > section > header > section > div > div > div > div.semi-navigation-footer > div:nth-child(1) > a > li {
padding: 0 5px;
}
.semi-navigation-footer {
padding-left: 0;
padding-right: 0;
}
.semi-table-tbody, .semi-table-tbody,
.semi-table-row, .semi-table-row,
.semi-table-row-cell { .semi-table-row-cell {
@@ -39,6 +68,10 @@ body {
row-gap: 3px; row-gap: 3px;
column-gap: 10px; column-gap: 10px;
} }
.semi-navigation-horizontal .semi-navigation-header {
margin-right: 0;
}
} }
.semi-table-tbody > .semi-table-row > .semi-table-row-cell { .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
@@ -83,11 +116,6 @@ code {
monospace; monospace;
} }
.semi-navigation-vertical {
/*display: flex;*/
/*flex-direction: column;*/
}
.semi-navigation-item { .semi-navigation-item {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@@ -13,6 +13,9 @@ import { Layout } from '@douyinfe/semi-ui';
import SiderBar from './components/SiderBar'; import SiderBar from './components/SiderBar';
import { ThemeProvider } from './context/Theme'; import { ThemeProvider } from './context/Theme';
import FooterBar from './components/Footer'; import FooterBar from './components/Footer';
import { StyleProvider } from './context/Style/index.js';
import PageLayout from './components/PageLayout.js';
import './i18n/i18n.js';
// initialization // initialization
@@ -24,27 +27,9 @@ root.render(
<UserProvider> <UserProvider>
<BrowserRouter> <BrowserRouter>
<ThemeProvider> <ThemeProvider>
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}> <StyleProvider>
<Header> <PageLayout/>
<HeaderBar /> </StyleProvider>
</Header>
<Layout style={{ flex: 1, overflow: 'hidden' }}>
<Sider>
<SiderBar />
</Sider>
<Layout>
<Content
style={{ overflowY: 'auto', padding: '24px' }}
>
<App />
</Content>
<Layout.Footer>
<FooterBar></FooterBar>
</Layout.Footer>
</Layout>
</Layout>
<ToastContainer />
</Layout>
</ThemeProvider> </ThemeProvider>
</BrowserRouter> </BrowserRouter>
</UserProvider> </UserProvider>

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { import {
API, API,
isMobile, isMobile,
@@ -61,6 +62,7 @@ function type2secretPrompt(type) {
} }
const EditChannel = (props) => { const EditChannel = (props) => {
const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const channelId = props.editingChannel.id; const channelId = props.editingChannel.id;
const isEdit = channelId !== undefined; const isEdit = channelId !== undefined;
@@ -192,7 +194,7 @@ const EditChannel = (props) => {
const fetchUpstreamModelList = async (name) => { const fetchUpstreamModelList = async (name) => {
if (inputs['type'] !== 1) { if (inputs['type'] !== 1) {
showError('仅支持 OpenAI 接口格式'); showError(t('仅支持 OpenAI 接口格式'));
return; return;
} }
setLoading(true); setLoading(true);
@@ -207,7 +209,7 @@ const EditChannel = (props) => {
} }
} else { } else {
if (!inputs?.['key']) { if (!inputs?.['key']) {
showError('请填写密钥'); showError(t('请填写密钥'));
err = true; err = true;
} else { } else {
try { try {
@@ -220,7 +222,7 @@ const EditChannel = (props) => {
'Authorization': `Bearer ${key}` 'Authorization': `Bearer ${key}`
} }
}); });
if (res.data && res.data?.success) { if (res.data) {
models.push(...res.data.data.map((model) => model.id)); models.push(...res.data.data.map((model) => model.id));
} else { } else {
err = true; err = true;
@@ -232,9 +234,9 @@ const EditChannel = (props) => {
} }
if (!err) { if (!err) {
handleInputChange(name, Array.from(new Set(models))); handleInputChange(name, Array.from(new Set(models)));
showSuccess('获取模型列表成功'); showSuccess(t('获取模型列表成功'));
} else { } else {
showError('获取模型列表失败'); showError(t('获取模型列表失败'));
} }
setLoading(false); setLoading(false);
}; };
@@ -305,15 +307,15 @@ const EditChannel = (props) => {
const submit = async () => { const submit = async () => {
if (!isEdit && (inputs.name === '' || inputs.key === '')) { if (!isEdit && (inputs.name === '' || inputs.key === '')) {
showInfo('请填写渠道名称和渠道密钥!'); showInfo(t('请填写渠道名称和渠道密钥!'));
return; return;
} }
if (inputs.models.length === 0) { if (inputs.models.length === 0) {
showInfo('请至少选择一个模型!'); showInfo(t('请至少选择一个模型!'));
return; return;
} }
if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) { if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
showInfo('模型映射必须是合法的 JSON 格式!'); showInfo(t('模型映射必须是合法的 JSON 格式!'));
return; return;
} }
let localInputs = { ...inputs }; let localInputs = { ...inputs };
@@ -331,7 +333,7 @@ const EditChannel = (props) => {
} }
let res; let res;
if (!Array.isArray(localInputs.models)) { if (!Array.isArray(localInputs.models)) {
showError('提交失败,请勿重复提交!'); showError(t('提交失败,请勿重复提交!'));
handleCancel(); handleCancel();
return; return;
} }
@@ -349,9 +351,9 @@ const EditChannel = (props) => {
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
if (isEdit) { if (isEdit) {
showSuccess('渠道更新成功!'); showSuccess(t('渠道更新成功!'));
} else { } else {
showSuccess('渠道创建成功!'); showSuccess(t('渠道创建成功!'));
setInputs(originInputs); setInputs(originInputs);
} }
props.refresh(); props.refresh();
@@ -363,7 +365,6 @@ const EditChannel = (props) => {
const addCustomModels = () => { const addCustomModels = () => {
if (customModel.trim() === '') return; if (customModel.trim() === '') return;
// 使用逗号分隔字符串,然后去除每个模型名称前后的空格
const modelArray = customModel.split(',').map((model) => model.trim()); const modelArray = customModel.split(',').map((model) => model.trim());
let localModels = [...inputs.models]; let localModels = [...inputs.models];
@@ -371,24 +372,21 @@ const EditChannel = (props) => {
let hasError = false; let hasError = false;
modelArray.forEach((model) => { modelArray.forEach((model) => {
// 检查模型是否已存在,且模型名称非空
if (model && !localModels.includes(model)) { if (model && !localModels.includes(model)) {
localModels.push(model); // 添加到模型列表 localModels.push(model);
localModelOptions.push({ localModelOptions.push({
// 添加到下拉选项
key: model, key: model,
text: model, text: model,
value: model value: model
}); });
} else if (model) { } else if (model) {
showError('某些模型已存在!'); showError(t('某些模型已存在!'));
hasError = true; hasError = true;
} }
}); });
if (hasError) return; // 如果有错误则终止操作 if (hasError) return;
// 更新状态值
setModelOptions(localModelOptions); setModelOptions(localModelOptions);
setCustomModel(''); setCustomModel('');
handleInputChange('models', localModels); handleInputChange('models', localModels);
@@ -401,7 +399,7 @@ const EditChannel = (props) => {
maskClosable={false} maskClosable={false}
placement={isEdit ? 'right' : 'left'} placement={isEdit ? 'right' : 'left'}
title={ title={
<Title level={3}>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Title> <Title level={3}>{isEdit ? t('更新渠道信息') : t('创建新的渠道')}</Title>
} }
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
@@ -410,7 +408,7 @@ const EditChannel = (props) => {
<div style={{ display: 'flex', justifyContent: 'flex-end' }}> <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space> <Space>
<Button theme="solid" size={'large'} onClick={submit}> <Button theme="solid" size={'large'} onClick={submit}>
提交 {t('提交')}
</Button> </Button>
<Button <Button
theme="solid" theme="solid"
@@ -418,7 +416,7 @@ const EditChannel = (props) => {
type={'tertiary'} type={'tertiary'}
onClick={handleCancel} onClick={handleCancel}
> >
取消 {t('取消')}
</Button> </Button>
</Space> </Space>
</div> </div>
@@ -429,7 +427,7 @@ const EditChannel = (props) => {
> >
<Spin spinning={loading}> <Spin spinning={loading}>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>类型</Typography.Text> <Typography.Text strong>{t('类型')}</Typography.Text>
</div> </div>
<Select <Select
name="type" name="type"
@@ -444,20 +442,7 @@ const EditChannel = (props) => {
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Banner <Banner
type={'warning'} type={'warning'}
description={ description={t('注意,模型部署名称必须和模型名称保持一致,因为 One API 会把请求体中的 model 参数替换为你的部署名称(模型名称中的点会被剔除)')}
<>
注意<strong>模型部署名称必须和模型名称保持一致</strong>
因为 One API 会把请求体中的 model
参数替换为你的部署名称模型名称中的点会被剔除
<a
target="_blank"
href="https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271"
>
图片演示
</a>
</>
}
></Banner> ></Banner>
</div> </div>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
@@ -468,9 +453,7 @@ const EditChannel = (props) => {
<Input <Input
label="AZURE_OPENAI_ENDPOINT" label="AZURE_OPENAI_ENDPOINT"
name="azure_base_url" name="azure_base_url"
placeholder={ placeholder={t('请输入 AZURE_OPENAI_ENDPOINT例如https://docs-test-001.openai.azure.com')}
'请输入 AZURE_OPENAI_ENDPOINT例如https://docs-test-001.openai.azure.com'
}
onChange={(value) => { onChange={(value) => {
handleInputChange('base_url', value); handleInputChange('base_url', value);
}} }}
@@ -478,14 +461,12 @@ const EditChannel = (props) => {
autoComplete="new-password" autoComplete="new-password"
/> />
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>默认 API 版本</Typography.Text> <Typography.Text strong>{t('默认 API 版本')}</Typography.Text>
</div> </div>
<Input <Input
label="默认 API 版本" label={t('默认 API 版本')}
name="azure_other" name="azure_other"
placeholder={ placeholder={t('请输入默认 API 版本例如2023-06-01-preview该配置可以被实际的请求查询参数所覆盖')}
'请输入默认 API 版本例如2023-06-01-preview该配置可以被实际的请求查询参数所覆盖'
}
onChange={(value) => { onChange={(value) => {
handleInputChange('other', value); handleInputChange('other', value);
}} }}
@@ -499,23 +480,17 @@ const EditChannel = (props) => {
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Banner <Banner
type={'warning'} type={'warning'}
description={ description={t('如果你对接的是上游One API或者New API等转发项目请使用OpenAI类型不要使用此类型除非你知道你在做什么。')}
<>
如果你对接的是上游One API或者New API等转发项目请使用OpenAI类型不要使用此类型除非你知道你在做什么
</>
}
></Banner> ></Banner>
</div> </div>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong> <Typography.Text strong>
完整的 Base URL支持变量{'{model}'} {t('完整的 Base URL支持变量{model}')}
</Typography.Text> </Typography.Text>
</div> </div>
<Input <Input
name="base_url" name="base_url"
placeholder={ placeholder={t('请输入完整的URL例如https://api.openai.com/v1/chat/completions')}
'请输入完整的URL例如https://api.openai.com/v1/chat/completions'
}
onChange={(value) => { onChange={(value) => {
handleInputChange('base_url', value); handleInputChange('base_url', value);
}} }}
@@ -527,12 +502,12 @@ const EditChannel = (props) => {
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && ( {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && (
<> <>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>代理</Typography.Text> <Typography.Text strong>{t('代理')}</Typography.Text>
</div> </div>
<Input <Input
label="代理" label={t('代理')}
name="base_url" name="base_url"
placeholder={'此项可选,用于通过代理站来进行 API 调用'} placeholder={t('此项可选,用于通过代理站来进行 API 调用')}
onChange={(value) => { onChange={(value) => {
handleInputChange('base_url', value); handleInputChange('base_url', value);
}} }}
@@ -544,13 +519,11 @@ const EditChannel = (props) => {
{inputs.type === 22 && ( {inputs.type === 22 && (
<> <>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>私有部署地址</Typography.Text> <Typography.Text strong>{t('私有部署地址')}</Typography.Text>
</div> </div>
<Input <Input
name="base_url" name="base_url"
placeholder={ placeholder={t('请输入私有部署地址格式为https://fastgpt.run/api/openapi')}
'请输入私有部署地址格式为https://fastgpt.run/api/openapi'
}
onChange={(value) => { onChange={(value) => {
handleInputChange('base_url', value); handleInputChange('base_url', value);
}} }}
@@ -563,14 +536,12 @@ const EditChannel = (props) => {
<> <>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong> <Typography.Text strong>
注意非Chat API请务必填写正确的API地址否则可能导致无法使用 {t('注意非Chat API请务必填写正确的API地址否则可能导致无法使用')}
</Typography.Text> </Typography.Text>
</div> </div>
<Input <Input
name="base_url" name="base_url"
placeholder={ placeholder={t('请输入到 /suno 前的路径通常就是域名例如https://api.example.com')}
'请输入到 /suno 前的路径通常就是域名例如https://api.example.com '
}
onChange={(value) => { onChange={(value) => {
handleInputChange('base_url', value); handleInputChange('base_url', value);
}} }}
@@ -580,12 +551,12 @@ const EditChannel = (props) => {
</> </>
)} )}
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>名称</Typography.Text> <Typography.Text strong>{t('名称')}</Typography.Text>
</div> </div>
<Input <Input
required required
name="name" name="name"
placeholder={'请为渠道命名'} placeholder={t('请为渠道命名')}
onChange={(value) => { onChange={(value) => {
handleInputChange('name', value); handleInputChange('name', value);
}} }}
@@ -593,16 +564,16 @@ const EditChannel = (props) => {
autoComplete="new-password" autoComplete="new-password"
/> />
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>分组</Typography.Text> <Typography.Text strong>{t('分组')}</Typography.Text>
</div> </div>
<Select <Select
placeholder={'请选择可以使用该渠道的分组'} placeholder={t('请选择可以使用该渠道的分组')}
name="groups" name="groups"
required required
multiple multiple
selection selection
allowAdditions allowAdditions
additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'} additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
onChange={(value) => { onChange={(value) => {
handleInputChange('groups', value); handleInputChange('groups', value);
}} }}
@@ -631,17 +602,15 @@ const EditChannel = (props) => {
{inputs.type === 41 && ( {inputs.type === 41 && (
<> <>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>部署地区</Typography.Text> <Typography.Text strong>{t('部署地区')}</Typography.Text>
</div> </div>
<TextArea <TextArea
name="other" name="other"
placeholder={ placeholder={t('请输入部署地区例如us-central1\n支持使用模型映射格式\n' +
'请输入部署地区例如us-central1\n支持使用模型映射格式\n' +
'{\n' + '{\n' +
' "default": "us-central1",\n' + ' "default": "us-central1",\n' +
' "claude-3-5-sonnet-20240620": "europe-west1"\n' + ' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
'}' '}')}
}
autosize={{ minRows: 2 }} autosize={{ minRows: 2 }}
onChange={(value) => { onChange={(value) => {
handleInputChange('other', value); handleInputChange('other', value);
@@ -662,7 +631,7 @@ const EditChannel = (props) => {
); );
}} }}
> >
填入模板 {t('填入模板')}
</Typography.Text> </Typography.Text>
</> </>
)} )}
@@ -702,7 +671,7 @@ const EditChannel = (props) => {
</> </>
)} )}
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>模型</Typography.Text> <Typography.Text strong>{t('模型')}</Typography.Text>
</div> </div>
<Select <Select
placeholder={'请选择该渠道所支持的模型'} placeholder={'请选择该渠道所支持的模型'}
@@ -710,6 +679,8 @@ const EditChannel = (props) => {
required required
multiple multiple
selection selection
filter
searchPosition='dropdown'
onChange={(value) => { onChange={(value) => {
handleInputChange('models', value); handleInputChange('models', value);
}} }}
@@ -725,7 +696,7 @@ const EditChannel = (props) => {
handleInputChange('models', basicModels); handleInputChange('models', basicModels);
}} }}
> >
填入相关模型 {t('填入相关模型')}
</Button> </Button>
<Button <Button
type="secondary" type="secondary"
@@ -733,16 +704,16 @@ const EditChannel = (props) => {
handleInputChange('models', fullModels); handleInputChange('models', fullModels);
}} }}
> >
填入所有模型 {t('填入所有模型')}
</Button> </Button>
<Tooltip content={fetchButtonTips}> <Tooltip content={t('新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出')}>
<Button <Button
type="tertiary" type="tertiary"
onClick={() => { onClick={() => {
fetchUpstreamModelList('models'); fetchUpstreamModelList('models');
}} }}
> >
获取模型列表 {t('获取模型列表')}
</Button> </Button>
</Tooltip> </Tooltip>
<Button <Button
@@ -751,16 +722,16 @@ const EditChannel = (props) => {
handleInputChange('models', []); handleInputChange('models', []);
}} }}
> >
清除所有模型 {t('清除所有模型')}
</Button> </Button>
</Space> </Space>
<Input <Input
addonAfter={ addonAfter={
<Button type="primary" onClick={addCustomModels}> <Button type="primary" onClick={addCustomModels}>
填入 {t('填入')}
</Button> </Button>
} }
placeholder="输入自定义模型名称" placeholder={t('输入自定义模型名称')}
value={customModel} value={customModel}
onChange={(value) => { onChange={(value) => {
setCustomModel(value.trim()); setCustomModel(value.trim());
@@ -768,10 +739,10 @@ const EditChannel = (props) => {
/> />
</div> </div>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>模型重定向</Typography.Text> <Typography.Text strong>{t('模型重定向')}</Typography.Text>
</div> </div>
<TextArea <TextArea
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`} placeholder={t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') + `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
name="model_mapping" name="model_mapping"
onChange={(value) => { onChange={(value) => {
handleInputChange('model_mapping', value); handleInputChange('model_mapping', value);
@@ -793,17 +764,17 @@ const EditChannel = (props) => {
); );
}} }}
> >
填入模板 {t('填入模板')}
</Typography.Text> </Typography.Text>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>密钥</Typography.Text> <Typography.Text strong>{t('密钥')}</Typography.Text>
</div> </div>
{batch ? ( {batch ? (
<TextArea <TextArea
label="密钥" label={t('密钥')}
name="key" name="key"
required required
placeholder={'请输入密钥,一行一个'} placeholder={t('请输入密钥,一行一个')}
onChange={(value) => { onChange={(value) => {
handleInputChange('key', value); handleInputChange('key', value);
}} }}
@@ -815,7 +786,7 @@ const EditChannel = (props) => {
<> <>
{inputs.type === 41 ? ( {inputs.type === 41 ? (
<TextArea <TextArea
label="鉴权json" label={t('鉴权json')}
name="key" name="key"
required required
placeholder={'{\n' + placeholder={'{\n' +
@@ -840,18 +811,17 @@ const EditChannel = (props) => {
/> />
) : ( ) : (
<Input <Input
label="密钥" label={t('密钥')}
name="key" name="key"
required required
placeholder={type2secretPrompt(inputs.type)} placeholder={t(type2secretPrompt(inputs.type))}
onChange={(value) => { onChange={(value) => {
handleInputChange('key', value); handleInputChange('key', value);
}} }}
value={inputs.key} value={inputs.key}
autoComplete="new-password" autoComplete="new-password"
/> />
) )}
}
</> </>
)} )}
{!isEdit && ( {!isEdit && (
@@ -859,23 +829,23 @@ const EditChannel = (props) => {
<Space> <Space>
<Checkbox <Checkbox
checked={batch} checked={batch}
label="批量创建" label={t('批量创建')}
name="batch" name="batch"
onChange={() => setBatch(!batch)} onChange={() => setBatch(!batch)}
/> />
<Typography.Text strong>批量创建</Typography.Text> <Typography.Text strong>{t('批量创建')}</Typography.Text>
</Space> </Space>
</div> </div>
)} )}
{inputs.type === 1 && ( {inputs.type === 1 && (
<> <>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>组织</Typography.Text> <Typography.Text strong>{t('组织')}</Typography.Text>
</div> </div>
<Input <Input
label="组织,可选,不填则为默认组织" label={t('组织,可选,不填则为默认组织')}
name="openai_organization" name="openai_organization"
placeholder="请输入组织org-xxx" placeholder={t('请输入组织org-xxx')}
onChange={(value) => { onChange={(value) => {
handleInputChange('openai_organization', value); handleInputChange('openai_organization', value);
}} }}
@@ -884,11 +854,11 @@ const EditChannel = (props) => {
</> </>
)} )}
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>默认测试模型</Typography.Text> <Typography.Text strong>{t('默认测试模型')}</Typography.Text>
</div> </div>
<Input <Input
name="test_model" name="test_model"
placeholder="不填则为模型列表第一个" placeholder={t('不填则为模型列表第一个')}
onChange={(value) => { onChange={(value) => {
handleInputChange('test_model', value); handleInputChange('test_model', value);
}} }}
@@ -902,20 +872,20 @@ const EditChannel = (props) => {
onChange={() => { onChange={() => {
setAutoBan(!autoBan); setAutoBan(!autoBan);
}} }}
// onChange={handleInputChange}
/> />
<Typography.Text strong> <Typography.Text strong>
是否自动禁用仅当自动禁用开启时有效关闭后不会自动禁用该渠道 {t('是否自动禁用仅当自动禁用开启时有效关闭后不会自动禁用该渠道:')}
</Typography.Text> </Typography.Text>
</Space> </Space>
</div> </div>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong> <Typography.Text strong>
状态码复写仅影响本地判断不修改返回到上游的状态码 {t('状态码复写(仅影响本地判断不修改返回到上游的状态码)')}
</Typography.Text> </Typography.Text>
</div> </div>
<TextArea <TextArea
placeholder={`此项可选用于复写返回的状态码比如将claude渠道的400错误复写为500用于重试请勿滥用该功能例如\n${JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}`} placeholder={t('此项可选用于复写返回的状态码比如将claude渠道的400错误复写为500用于重试请勿滥用该功能例如') +
'\n' + JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}
name="status_code_mapping" name="status_code_mapping"
onChange={(value) => { onChange={(value) => {
handleInputChange('status_code_mapping', value); handleInputChange('status_code_mapping', value);
@@ -937,17 +907,17 @@ const EditChannel = (props) => {
); );
}} }}
> >
填入模板 {t('填入模板')}
</Typography.Text> </Typography.Text>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong> <Typography.Text strong>
渠道标签 {t('渠道标签')}
</Typography.Text> </Typography.Text>
</div> </div>
<Input <Input
label="渠道标签" label={t('渠道标签')}
name="tag" name="tag"
placeholder={'渠道标签'} placeholder={t('渠道标签')}
onChange={(value) => { onChange={(value) => {
handleInputChange('tag', value); handleInputChange('tag', value);
}} }}
@@ -956,34 +926,80 @@ const EditChannel = (props) => {
/> />
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong> <Typography.Text strong>
渠道优先级 {t('渠道优先级')}
</Typography.Text> </Typography.Text>
</div> </div>
<Input <Input
label="渠道优先级" label={t('渠道优先级')}
name="priority" name="priority"
placeholder={'渠道优先级'} placeholder={t('渠道优先级')}
onChange={(value) => { onChange={(value) => {
handleInputChange('priority', parseInt(value)); const number = parseInt(value);
if (isNaN(number)) {
handleInputChange('priority', value);
} else {
handleInputChange('priority', number);
}
}} }}
value={inputs.priority} value={inputs.priority}
autoComplete="new-password" autoComplete="new-password"
/> />
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong> <Typography.Text strong>
渠道权重 {t('渠道权重')}
</Typography.Text> </Typography.Text>
</div> </div>
<Input <Input
label="渠道权重" label={t('渠道权重')}
name="weight" name="weight"
placeholder={'渠道权重'} placeholder={t('渠道权重')}
onChange={(value) => { onChange={(value) => {
handleInputChange('weight', parseInt(value)); const number = parseInt(value);
if (isNaN(number)) {
handleInputChange('weight', value);
} else {
handleInputChange('weight', number);
}
}} }}
value={inputs.weight} value={inputs.weight}
autoComplete="new-password" autoComplete="new-password"
/> />
{inputs.type === 8 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>
{t('渠道额外设置')}
</Typography.Text>
</div>
<TextArea
placeholder={t('此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:') + '\n{\n "force_format": true\n}'}
name="setting"
onChange={(value) => {
handleInputChange('setting', value);
}}
autosize
value={inputs.setting}
autoComplete="new-password"
/>
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer'
}}
onClick={() => {
handleInputChange(
'setting',
JSON.stringify({
force_format: true
}, null, 2)
);
}}
>
{t('填入模板')}
</Typography.Text>
</>
)}
</Spin> </Spin>
</SideSheet> </SideSheet>
</> </>

View File

@@ -16,6 +16,7 @@ const EditTagModal = (props) => {
const [groupOptions, setGroupOptions] = useState([]); const [groupOptions, setGroupOptions] = useState([]);
const [basicModels, setBasicModels] = useState([]); const [basicModels, setBasicModels] = useState([]);
const [fullModels, setFullModels] = useState([]); const [fullModels, setFullModels] = useState([]);
const [customModel, setCustomModel] = useState('');
const originInputs = { const originInputs = {
tag: '', tag: '',
new_tag: null, new_tag: null,
@@ -183,6 +184,40 @@ const EditTagModal = (props) => {
fetchGroups().then(); fetchGroups().then();
}, [visible]); }, [visible]);
const addCustomModels = () => {
if (customModel.trim() === '') return;
// 使用逗号分隔字符串,然后去除每个模型名称前后的空格
const modelArray = customModel.split(',').map((model) => model.trim());
let localModels = [...inputs.models];
let localModelOptions = [...modelOptions];
let hasError = false;
modelArray.forEach((model) => {
// 检查模型是否已存在,且模型名称非空
if (model && !localModels.includes(model)) {
localModels.push(model); // 添加到模型列表
localModelOptions.push({
// 添加到下拉选项
key: model,
text: model,
value: model
});
} else if (model) {
showError('某些模型已存在!');
hasError = true;
}
});
if (hasError) return; // 如果有错误则终止操作
// 更新状态值
setModelOptions(localModelOptions);
setCustomModel('');
handleInputChange('models', localModels);
};
return ( return (
<SideSheet <SideSheet
title="编辑标签" title="编辑标签"
@@ -209,7 +244,7 @@ const EditTagModal = (props) => {
</div> </div>
<Spin spinning={loading}> <Spin spinning={loading}>
<TextInput <TextInput
label="标签,留空则不更改" label="标签,留空则解散标签"
name="newTag" name="newTag"
value={inputs.new_tag} value={inputs.new_tag}
onChange={(value) => setInputs({ ...inputs, new_tag: value })} onChange={(value) => setInputs({ ...inputs, new_tag: value })}
@@ -224,6 +259,8 @@ const EditTagModal = (props) => {
required required
multiple multiple
selection selection
filter
searchPosition='dropdown'
onChange={(value) => { onChange={(value) => {
handleInputChange('models', value); handleInputChange('models', value);
}} }}
@@ -231,6 +268,18 @@ const EditTagModal = (props) => {
autoComplete="new-password" autoComplete="new-password"
optionList={modelOptions} optionList={modelOptions}
/> />
<Input
addonAfter={
<Button type="primary" onClick={addCustomModels}>
填入
</Button>
}
placeholder="输入自定义模型名称"
value={customModel}
onChange={(value) => {
setCustomModel(value.trim());
}}
/>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>分组留空则不更改</Typography.Text> <Typography.Text strong>分组留空则不更改</Typography.Text>
</div> </div>

View File

@@ -1,18 +1,22 @@
import React from 'react'; import React from 'react';
import ChannelsTable from '../../components/ChannelsTable'; import ChannelsTable from '../../components/ChannelsTable';
import { Layout } from '@douyinfe/semi-ui'; import { Layout } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const File = () => ( const File = () => {
<> const { t } = useTranslation();
<Layout> return (
<Layout.Header> <>
<h3>管理渠道</h3> <Layout>
<Layout.Header>
<h3>{t('管理渠道')}</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<ChannelsTable /> <ChannelsTable />
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</> </>
); );
};
export default File; export default File;

View File

@@ -1,8 +1,8 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useContext, useEffect, useRef, useState } from 'react';
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme'; import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import { Button, Col, Form, Layout, Row, Spin } from '@douyinfe/semi-ui'; import { Button, Card, Col, Descriptions, Form, Layout, Row, Spin, Tabs } from '@douyinfe/semi-ui';
import VChart from '@visactor/vchart'; import { VChart } from "@visactor/react-vchart";
import { import {
API, API,
isAdmin, isAdmin,
@@ -17,11 +17,18 @@ import {
renderQuota, renderQuota,
renderQuotaNumberWithDigit, renderQuotaNumberWithDigit,
stringToColor, stringToColor,
modelToColor,
} from '../../helpers/render'; } from '../../helpers/render';
import { UserContext } from '../../context/User/index.js';
import { StyleContext } from '../../context/Style/index.js';
import { useTranslation } from 'react-i18next';
const Detail = (props) => { const Detail = (props) => {
const { t } = useTranslation();
const formRef = useRef(); const formRef = useRef();
let now = new Date(); let now = new Date();
const [userState, userDispatch] = useContext(UserContext);
const [styleState, styleDispatch] = useContext(StyleContext);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
username: '', username: '',
token_name: '', token_name: '',
@@ -40,32 +47,76 @@ const Detail = (props) => {
inputs; inputs;
const isAdminUser = isAdmin(); const isAdminUser = isAdmin();
const initialized = useRef(false); const initialized = useRef(false);
const [modelDataChart, setModelDataChart] = useState(null);
const [modelDataPieChart, setModelDataPieChart] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [quotaData, setQuotaData] = useState([]); const [quotaData, setQuotaData] = useState([]);
const [consumeQuota, setConsumeQuota] = useState(0); const [consumeQuota, setConsumeQuota] = useState(0);
const [consumeTokens, setConsumeTokens] = useState(0);
const [times, setTimes] = useState(0); const [times, setTimes] = useState(0);
const [dataExportDefaultTime, setDataExportDefaultTime] = useState( const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
localStorage.getItem('data_export_default_time') || 'hour', localStorage.getItem('data_export_default_time') || 'hour',
); );
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
const handleInputChange = (value, name) => { const [lineData, setLineData] = useState([]);
if (name === 'data_export_default_time') { const [spec_pie, setSpecPie] = useState({
setDataExportDefaultTime(value); type: 'pie',
return; data: [{
} id: 'id0',
setInputs((inputs) => ({ ...inputs, [name]: value })); values: pieData
}; }],
outerRadius: 0.8,
const spec_line = { innerRadius: 0.5,
type: 'bar', padAngle: 0.6,
data: [ valueField: 'value',
{ categoryField: 'type',
id: 'barData', pie: {
values: [], style: {
cornerRadius: 10,
}, },
], state: {
hover: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
selected: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
},
},
title: {
visible: true,
text: t('模型调用次数占比'),
subtext: `${t('总计')}${renderNumber(times)}`,
},
legends: {
visible: true,
orient: 'left',
},
label: {
visible: true,
},
tooltip: {
mark: {
content: [
{
key: (datum) => datum['type'],
value: (datum) => renderNumber(datum['value']),
},
],
},
},
color: {
specified: modelColorMap,
},
});
const [spec_line, setSpecLine] = useState({
type: 'bar',
data: [{
id: 'barData',
values: lineData
}],
xField: 'Time', xField: 'Time',
yField: 'Usage', yField: 'Usage',
seriesField: 'Model', seriesField: 'Model',
@@ -76,11 +127,10 @@ const Detail = (props) => {
}, },
title: { title: {
visible: true, visible: true,
text: '模型消耗分布', text: t('模型消耗分布'),
subtext: '0', subtext: `${t('总计')}${renderQuota(consumeQuota, 2)}`,
}, },
bar: { bar: {
// The state style of bar
state: { state: {
hover: { hover: {
stroke: '#000', stroke: '#000',
@@ -106,9 +156,7 @@ const Detail = (props) => {
}, },
], ],
updateContent: (array) => { updateContent: (array) => {
// sort by value
array.sort((a, b) => b.value - a.value); array.sort((a, b) => b.value - a.value);
// add $
let sum = 0; let sum = 0;
for (let i = 0; i < array.length; i++) { for (let i = 0; i < array.length; i++) {
sum += parseFloat(array[i].value); sum += parseFloat(array[i].value);
@@ -117,9 +165,8 @@ const Detail = (props) => {
4, 4,
); );
} }
// add to first
array.unshift({ array.unshift({
key: '总计', key: t('总计'),
value: renderQuotaNumberWithDigit(sum, 4), value: renderQuotaNumberWithDigit(sum, 4),
}); });
return array; return array;
@@ -129,196 +176,197 @@ const Detail = (props) => {
color: { color: {
specified: modelColorMap, specified: modelColorMap,
}, },
});
// 添加一个新的状态来存储模型-颜色映射
const [modelColors, setModelColors] = useState({});
const handleInputChange = (value, name) => {
if (name === 'data_export_default_time') {
setDataExportDefaultTime(value);
return;
}
setInputs((inputs) => ({ ...inputs, [name]: value }));
}; };
const spec_pie = { const loadQuotaData = async () => {
type: 'pie',
data: [
{
id: 'id0',
values: [{ type: 'null', value: '0' }],
},
],
outerRadius: 0.8,
innerRadius: 0.5,
padAngle: 0.6,
valueField: 'value',
categoryField: 'type',
pie: {
style: {
cornerRadius: 10,
},
state: {
hover: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
selected: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
},
},
title: {
visible: true,
text: '模型调用次数占比',
},
legends: {
visible: true,
orient: 'left',
},
label: {
visible: true,
},
tooltip: {
mark: {
content: [
{
key: (datum) => datum['type'],
value: (datum) => renderNumber(datum['value']),
},
],
},
},
color: {
specified: modelColorMap,
},
};
const loadQuotaData = async (lineChart, pieChart) => {
setLoading(true); setLoading(true);
try {
let url = ''; let url = '';
let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000; let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) { if (isAdminUser) {
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`; url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
} else { } else {
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`; url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
} }
const res = await API.get(url); const res = await API.get(url);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
setQuotaData(data); setQuotaData(data);
if (data.length === 0) { if (data.length === 0) {
data.push({ data.push({
count: 0, count: 0,
model_name: '无数据', model_name: '无数据',
quota: 0, quota: 0,
created_at: now.getTime() / 1000, created_at: now.getTime() / 1000,
});
}
// 根据dataExportDefaultTime重制时间粒度
let timeGranularity = 3600;
if (dataExportDefaultTime === 'day') {
timeGranularity = 86400;
} else if (dataExportDefaultTime === 'week') {
timeGranularity = 604800;
}
// sort created_at
data.sort((a, b) => a.created_at - b.created_at);
data.forEach((item) => {
item['created_at'] =
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
}); });
updateChartData(data);
} else {
showError(message);
} }
// 根据dataExportDefaultTime重制时间粒度 } finally {
let timeGranularity = 3600; setLoading(false);
if (dataExportDefaultTime === 'day') {
timeGranularity = 86400;
} else if (dataExportDefaultTime === 'week') {
timeGranularity = 604800;
}
// sort created_at
data.sort((a, b) => a.created_at - b.created_at);
data.forEach((item) => {
item['created_at'] =
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
});
updateChart(lineChart, pieChart, data);
} else {
showError(message);
} }
setLoading(false);
}; };
const refresh = async () => { const refresh = async () => {
await loadQuotaData(modelDataChart, modelDataPieChart); await loadQuotaData();
}; };
const initChart = async () => { const initChart = async () => {
let lineChart = modelDataChart; await loadQuotaData();
if (!modelDataChart) {
lineChart = new VChart(spec_line, { dom: 'model_data' });
setModelDataChart(lineChart);
lineChart.renderAsync();
}
let pieChart = modelDataPieChart;
if (!modelDataPieChart) {
pieChart = new VChart(spec_pie, { dom: 'model_pie' });
setModelDataPieChart(pieChart);
pieChart.renderAsync();
}
console.log('init vchart');
await loadQuotaData(lineChart, pieChart);
}; };
const updateChart = (lineChart, pieChart, data) => { const updateChartData = (data) => {
if (isAdminUser) { let newPieData = [];
// 将所有用户合并 let newLineData = [];
} let totalQuota = 0;
let pieData = []; let totalTimes = 0;
let lineData = []; let uniqueModels = new Set();
let consumeQuota = 0; let totalTokens = 0;
let times = 0;
for (let i = 0; i < data.length; i++) { // 收集所有唯一的模型名称和时间点
const item = data[i]; let uniqueTimes = new Set();
consumeQuota += item.quota; data.forEach(item => {
times += item.count; uniqueModels.add(item.model_name);
// 合并model_name uniqueTimes.add(timestamp2string1(item.created_at, dataExportDefaultTime));
let pieItem = pieData.find((it) => it.type === item.model_name); totalTokens += item.token_used;
});
// 处理颜色映射
const newModelColors = {};
Array.from(uniqueModels).forEach((modelName) => {
newModelColors[modelName] = modelColorMap[modelName] ||
modelColors[modelName] ||
modelToColor(modelName);
});
setModelColors(newModelColors);
// 处理饼图数据
for (let item of data) {
totalQuota += item.quota;
totalTimes += item.count;
let pieItem = newPieData.find((it) => it.type === item.model_name);
if (pieItem) { if (pieItem) {
pieItem.value += item.count; pieItem.value += item.count;
} else { } else {
pieData.push({ newPieData.push({
type: item.model_name, type: item.model_name,
value: item.count, value: item.count,
}); });
} }
// 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
// 转换日期格式
let createTime = timestamp2string1(
item.created_at,
dataExportDefaultTime,
);
let lineItem = lineData.find(
(it) => it.Time === createTime && it.Model === item.model_name,
);
if (lineItem) {
lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
} else {
lineData.push({
Time: createTime,
Model: item.model_name,
Usage: parseFloat(getQuotaWithUnit(item.quota)),
});
}
} }
setConsumeQuota(consumeQuota);
setTimes(times);
// sort by count // 处理柱状图数据
pieData.sort((a, b) => b.value - a.value); let timePoints = Array.from(uniqueTimes);
spec_pie.title.subtext = `总计:${renderNumber(times)}`; if (timePoints.length < 7) {
spec_pie.data[0].values = pieData; // 根据时间粒度生成合适的时间点
const generateTimePoints = () => {
let lastTime = Math.max(...data.map(item => item.created_at));
let points = [];
let interval = dataExportDefaultTime === 'hour' ? 3600
: dataExportDefaultTime === 'day' ? 86400
: 604800;
spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`; for (let i = 0; i < 7; i++) {
spec_line.data[0].values = lineData; points.push(timestamp2string1(lastTime - (i * interval), dataExportDefaultTime));
pieChart.updateSpec(spec_pie); }
lineChart.updateSpec(spec_line); return points.reverse();
};
// pieChart.updateData('id0', pieData); timePoints = generateTimePoints();
// lineChart.updateData('barData', lineData); }
pieChart.reLayout();
lineChart.reLayout(); // 为每个时间点和模型生成数据
timePoints.forEach(time => {
Array.from(uniqueModels).forEach(model => {
let existingData = data.find(item =>
timestamp2string1(item.created_at, dataExportDefaultTime) === time &&
item.model_name === model
);
newLineData.push({
Time: time,
Model: model,
Usage: existingData ? parseFloat(getQuotaWithUnit(existingData.quota)) : 0
});
});
});
// 排序
newPieData.sort((a, b) => b.value - a.value);
newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
// 更新图表配置和数据
setSpecPie(prev => ({
...prev,
data: [{ id: 'id0', values: newPieData }],
title: {
...prev.title,
subtext: `${t('总计')}${renderNumber(totalTimes)}`
},
color: {
specified: newModelColors
}
}));
setSpecLine(prev => ({
...prev,
data: [{ id: 'barData', values: newLineData }],
title: {
...prev.title,
subtext: `${t('总计')}${renderQuota(totalQuota, 2)}`
},
color: {
specified: newModelColors
}
}));
setPieData(newPieData);
setLineData(newLineData);
setConsumeQuota(totalQuota);
setTimes(totalTimes);
setConsumeTokens(totalTokens);
};
const getUserData = async () => {
let res = await API.get(`/api/user/self`);
const {success, message, data} = res.data;
if (success) {
userDispatch({type: 'login', payload: data});
} else {
showError(message);
}
}; };
useEffect(() => { useEffect(() => {
// setDataExportDefaultTime(localStorage.getItem('data_export_default_time')); getUserData()
// if (dataExportDefaultTime === 'day') {
// // 设置开始时间为7天前
// let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
// inputs.start_timestamp = st;
// formRef.current.formApi.setValue('start_timestamp', st);
// }
if (!initialized.current) { if (!initialized.current) {
initVChartSemiTheme({ initVChartSemiTheme({
isWatchingThemeSwitch: true, isWatchingThemeSwitch: true,
@@ -332,14 +380,14 @@ const Detail = (props) => {
<> <>
<Layout> <Layout>
<Layout.Header> <Layout.Header>
<h3>数据看板</h3> <h3>{t('数据看板')}</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<Form ref={formRef} layout='horizontal' style={{ marginTop: 10 }}> <Form ref={formRef} layout='horizontal' style={{ marginTop: 10 }}>
<> <>
<Form.DatePicker <Form.DatePicker
field='start_timestamp' field='start_timestamp'
label='起始时间' label={t('起始时间')}
style={{ width: 272 }} style={{ width: 272 }}
initValue={start_timestamp} initValue={start_timestamp}
value={start_timestamp} value={start_timestamp}
@@ -352,7 +400,7 @@ const Detail = (props) => {
<Form.DatePicker <Form.DatePicker
field='end_timestamp' field='end_timestamp'
fluid fluid
label='结束时间' label={t('结束时间')}
style={{ width: 272 }} style={{ width: 272 }}
initValue={end_timestamp} initValue={end_timestamp}
value={end_timestamp} value={end_timestamp}
@@ -362,15 +410,15 @@ const Detail = (props) => {
/> />
<Form.Select <Form.Select
field='data_export_default_time' field='data_export_default_time'
label='时间粒度' label={t('时间粒度')}
style={{ width: 176 }} style={{ width: 176 }}
initValue={dataExportDefaultTime} initValue={dataExportDefaultTime}
placeholder={'时间粒度'} placeholder={t('时间粒度')}
name='data_export_default_time' name='data_export_default_time'
optionList={[ optionList={[
{ label: '小时', value: 'hour' }, { label: t('小时'), value: 'hour' },
{ label: '天', value: 'day' }, { label: t('天'), value: 'day' },
{ label: '周', value: 'week' }, { label: t('周'), value: 'week' },
]} ]}
onChange={(value) => onChange={(value) =>
handleInputChange(value, 'data_export_default_time') handleInputChange(value, 'data_export_default_time')
@@ -380,42 +428,102 @@ const Detail = (props) => {
<> <>
<Form.Input <Form.Input
field='username' field='username'
label='用户名称' label={t('用户名称')}
style={{ width: 176 }} style={{ width: 176 }}
value={username} value={username}
placeholder={'可选值'} placeholder={t('可选值')}
name='username' name='username'
onChange={(value) => handleInputChange(value, 'username')} onChange={(value) => handleInputChange(value, 'username')}
/> />
</> </>
)} )}
<Button
label={t('查询')}
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={refresh}
loading={loading}
style={{ marginTop: 24 }}
>
{t('查询')}
</Button>
<Form.Section> <Form.Section>
<Button
label='查询'
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={refresh}
loading={loading}
>
查询
</Button>
</Form.Section> </Form.Section>
</> </>
</Form> </Form>
<Spin spinning={loading}> <Spin spinning={loading}>
<div style={{ height: 500 }}> <Row gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }} style={{marginTop: 20}} type="flex" justify="space-between">
<div <Col span={styleState.isMobile?24:8}>
id='model_pie' <Card className='panel-desc-card'>
style={{ width: '100%', minWidth: 100 }} <Descriptions row size="small">
></div> <Descriptions.Item itemKey={t('当前余额')}>
</div> {renderQuota(userState?.user?.quota)}
<div style={{ height: 500 }}> </Descriptions.Item>
<div <Descriptions.Item itemKey={t('历史消耗')}>
id='model_data' {renderQuota(userState?.user?.used_quota)}
style={{ width: '100%', minWidth: 100 }} </Descriptions.Item>
></div> <Descriptions.Item itemKey={t('请求次数')}>
</div> {userState.user?.request_count}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={styleState.isMobile?24:8}>
<Card>
<Descriptions row size="small">
<Descriptions.Item itemKey={t('统计额度')}>
{renderQuota(consumeQuota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('统计Tokens')}>
{consumeTokens}
</Descriptions.Item>
<Descriptions.Item itemKey={t('统计次数')}>
{times}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={styleState.isMobile ? 24 : 8}>
<Card>
<Descriptions row size='small'>
<Descriptions.Item itemKey={t('平均RPM')}>
{(times /
((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) /
60000)).toFixed(3)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('平均TPM')}>
{(consumeTokens /
((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) /
60000)).toFixed(3)}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
</Row>
<Card style={{marginTop: 20}}>
<Tabs type="line" defaultActiveKey="1">
<Tabs.TabPane tab={t('消耗分布')} itemKey="1">
<div style={{ height: 500 }}>
<VChart
spec={spec_line}
option={{ mode: "desktop-browser" }}
/>
</div>
</Tabs.TabPane>
<Tabs.TabPane tab={t('调用次数分布')} itemKey="2">
<div style={{ height: 500 }}>
<VChart
spec={spec_pie}
option={{ mode: "desktop-browser" }}
/>
</div>
</Tabs.TabPane>
</Tabs>
</Card>
</Spin> </Spin>
</Layout.Content> </Layout.Content>
</Layout> </Layout>

View File

@@ -3,11 +3,15 @@ import { Card, Col, Row } from '@douyinfe/semi-ui';
import { API, showError, showNotice, timestamp2string } from '../../helpers'; import { API, showError, showNotice, timestamp2string } from '../../helpers';
import { StatusContext } from '../../context/Status'; import { StatusContext } from '../../context/Status';
import { marked } from 'marked'; import { marked } from 'marked';
import { StyleContext } from '../../context/Style/index.js';
import { useTranslation } from 'react-i18next';
const Home = () => { const Home = () => {
const { t } = useTranslation();
const [statusState] = useContext(StatusContext); const [statusState] = useContext(StatusContext);
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false); const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
const [homePageContent, setHomePageContent] = useState(''); const [homePageContent, setHomePageContent] = useState('');
const [styleState, styleDispatch] = useContext(StyleContext);
const displayNotice = async () => { const displayNotice = async () => {
const res = await API.get('/api/notice'); const res = await API.get('/api/notice');
@@ -50,7 +54,8 @@ const Home = () => {
useEffect(() => { useEffect(() => {
displayNotice().then(); displayNotice().then();
displayHomePageContent().then(); displayHomePageContent().then();
}, []); });
return ( return (
<> <>
{homePageContentLoaded && homePageContent === '' ? ( {homePageContentLoaded && homePageContent === '' ? (
@@ -58,13 +63,13 @@ const Home = () => {
<Card <Card
bordered={false} bordered={false}
headerLine={false} headerLine={false}
title='系统状况' title={t('系统状况')}
bodyStyle={{ padding: '10px 20px' }} bodyStyle={{ padding: '10px 20px' }}
> >
<Row gutter={16}> <Row gutter={16}>
<Col span={12}> <Col span={12}>
<Card <Card
title='系统信息' title={t('系统信息')}
headerExtraContent={ headerExtraContent={
<span <span
style={{ style={{
@@ -72,19 +77,19 @@ const Home = () => {
color: 'var(--semi-color-text-1)', color: 'var(--semi-color-text-1)',
}} }}
> >
系统信息总览 {t('系统信息总览')}
</span> </span>
} }
> >
<p>名称{statusState?.status?.system_name}</p> <p>{t('名称')}{statusState?.status?.system_name}</p>
<p> <p>
版本 {t('版本')}
{statusState?.status?.version {statusState?.status?.version
? statusState?.status?.version ? statusState?.status?.version
: 'unknown'} : 'unknown'}
</p> </p>
<p> <p>
源码 {t('源码')}
<a <a
href='https://github.com/Calcium-Ion/new-api' href='https://github.com/Calcium-Ion/new-api'
target='_blank' target='_blank'
@@ -94,7 +99,7 @@ const Home = () => {
</a> </a>
</p> </p>
<p> <p>
协议 {t('协议')}
<a <a
href='https://www.apache.org/licenses/LICENSE-2.0' href='https://www.apache.org/licenses/LICENSE-2.0'
target='_blank' target='_blank'
@@ -103,12 +108,12 @@ const Home = () => {
Apache-2.0 License Apache-2.0 License
</a> </a>
</p> </p>
<p>启动时间{getStartTimeString()}</p> <p>{t('启动时间')}{getStartTimeString()}</p>
</Card> </Card>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Card <Card
title='系统配置' title={t('系统配置')}
headerExtraContent={ headerExtraContent={
<span <span
style={{ style={{
@@ -116,45 +121,45 @@ const Home = () => {
color: 'var(--semi-color-text-1)', color: 'var(--semi-color-text-1)',
}} }}
> >
系统配置总览 {t('系统配置总览')}
</span> </span>
} }
> >
<p> <p>
邮箱验证 {t('邮箱验证')}
{statusState?.status?.email_verification === true {statusState?.status?.email_verification === true
? '已启用' ? t('已启用')
: '未启用'} : t('未启用')}
</p> </p>
<p> <p>
GitHub 身份验证 {t('GitHub 身份验证')}
{statusState?.status?.github_oauth === true {statusState?.status?.github_oauth === true
? '已启用' ? t('已启用')
: '未启用'} : t('未启用')}
</p> </p>
<p> <p>
微信身份验证 {t('微信身份验证')}
{statusState?.status?.wechat_login === true {statusState?.status?.wechat_login === true
? '已启用' ? t('已启用')
: '未启用'} : t('未启用')}
</p> </p>
<p> <p>
Turnstile 用户校验 {t('Turnstile 用户校验')}
{statusState?.status?.turnstile_check === true {statusState?.status?.turnstile_check === true
? '已启用' ? t('已启用')
: '未启用'} : t('未启用')}
</p> </p>
<p> <p>
Telegram 身份验证 {t('Telegram 身份验证')}
{statusState?.status?.telegram_oauth === true {statusState?.status?.telegram_oauth === true
? '已启用' ? t('已启用')
: '未启用'} : t('未启用')}
</p> </p>
<p> <p>
Linux DO 身份验证 {t('Linux DO 身份验证')}
{statusState?.status?.linuxdo_oauth === true {statusState?.status?.linuxdo_oauth === true
? '已启用' ? t('已启用')
: '未启用'} : t('未启用')}
</p> </p>
</Card> </Card>
</Col> </Col>

View File

@@ -1,24 +1,27 @@
import React, { useCallback, useContext, useEffect, useState } from 'react'; import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { UserContext } from '../context/User'; import { UserContext } from '../../context/User/index.js';
import { API, getUserIdFromLocalStorage, showError } from '../helpers'; import { API, getUserIdFromLocalStorage, showError } from '../../helpers/index.js';
import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography } from '@douyinfe/semi-ui'; import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography, Button } from '@douyinfe/semi-ui';
import { SSE } from 'sse'; import { SSE } from 'sse';
import { IconSetting } from '@douyinfe/semi-icons';
import { StyleContext } from '../../context/Style/index.js';
import { useTranslation } from 'react-i18next';
const defaultMessage = [ const roleInfo = {
{ user: {
role: 'user', name: 'User',
id: '2', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
createAt: 1715676751919,
content: "你好",
}, },
{ assistant: {
role: 'assistant', name: 'Assistant',
id: '3', avatar: 'logo.png'
createAt: 1715676751919, },
content: "你好,请问有什么可以帮助您的吗?", system: {
name: 'System',
avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
} }
]; }
let id = 4; let id = 4;
function getId() { function getId() {
@@ -26,6 +29,23 @@ function getId() {
} }
const Playground = () => { const Playground = () => {
const { t } = useTranslation();
const defaultMessage = [
{
role: 'user',
id: '2',
createAt: 1715676751919,
content: t('你好'),
},
{
role: 'assistant',
id: '3',
createAt: 1715676751919,
content: t('你好,请问有什么可以帮助您的吗?'),
}
];
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
model: 'gpt-4o-mini', model: 'gpt-4o-mini',
group: '', group: '',
@@ -39,6 +59,8 @@ const Playground = () => {
const [message, setMessage] = useState(defaultMessage); const [message, setMessage] = useState(defaultMessage);
const [models, setModels] = useState([]); const [models, setModels] = useState([]);
const [groups, setGroups] = useState([]); const [groups, setGroups] = useState([]);
const [showSettings, setShowSettings] = useState(true);
const [styleState, styleDispatch] = useContext(StyleContext);
const handleInputChange = (name, value) => { const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
@@ -46,7 +68,7 @@ const Playground = () => {
useEffect(() => { useEffect(() => {
if (searchParams.get('expired')) { if (searchParams.get('expired')) {
showError('未登录或登录已过期,请重新登录!'); showError(t('未登录或登录已过期,请重新登录!'));
} }
let status = localStorage.getItem('status'); let status = localStorage.getItem('status');
if (status) { if (status) {
@@ -67,7 +89,7 @@ const Playground = () => {
})); }));
setModels(localModelOptions); setModels(localModelOptions);
} else { } else {
showError(message); showError(t(message));
} }
}; };
@@ -75,31 +97,33 @@ const Playground = () => {
let res = await API.get(`/api/user/self/groups`); let res = await API.get(`/api/user/self/groups`);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
// return data is a map, key is group name, value is group description
// label is group description, value is group name
let localGroupOptions = Object.keys(data).map((group) => ({ let localGroupOptions = Object.keys(data).map((group) => ({
label: data[group], label: data[group],
value: group, value: group,
})); }));
// handleInputChange('group', localGroupOptions[0].value);
if (localGroupOptions.length > 0) { if (localGroupOptions.length === 0) {
// set default group at first
localGroupOptions.unshift({
label: '用户分组',
value: '',
});
} else {
localGroupOptions = [{ localGroupOptions = [{
label: '用户分组', label: t('用户分组'),
value: '', value: '',
}]; }];
setGroups(localGroupOptions); } else {
const localUser = JSON.parse(localStorage.getItem('user'));
const userGroup = (userState.user && userState.user.group) || (localUser && localUser.group);
if (userGroup) {
const userGroupIndex = localGroupOptions.findIndex(g => g.value === userGroup);
if (userGroupIndex > -1) {
const userGroupOption = localGroupOptions.splice(userGroupIndex, 1)[0];
localGroupOptions.unshift(userGroupOption);
}
}
} }
setGroups(localGroupOptions); setGroups(localGroupOptions);
handleInputChange('group', localGroupOptions[0].value); handleInputChange('group', localGroupOptions[0].value);
} else { } else {
showError(message); showError(t(message));
} }
}; };
@@ -242,94 +266,147 @@ const Playground = () => {
}) })
}, []); }, []);
const SettingsToggle = () => {
if (!styleState.isMobile) return null;
return (
<Button
icon={<IconSetting />}
style={{
position: 'absolute',
left: showSettings ? -10 : -20,
top: '50%',
transform: 'translateY(-50%)',
zIndex: 1000,
width: 40,
height: 40,
borderRadius: '0 20px 20px 0',
padding: 0,
boxShadow: '2px 0 8px rgba(0, 0, 0, 0.15)',
}}
onClick={() => setShowSettings(!showSettings)}
theme="solid"
type="primary"
/>
);
};
function CustomInputRender(props) {
const { detailProps } = props;
const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = detailProps;
return <div style={{margin: '8px 16px', display: 'flex', flexDirection:'row',
alignItems: 'flex-end', borderRadius: 16,padding: 10, border: '1px solid var(--semi-color-border)'}}
onClick={onClick}
>
{/*{uploadNode}*/}
{inputNode}
{sendNode}
</div>
}
const renderInputArea = useCallback((props) => {
return (<CustomInputRender {...props} />)
}, []);
return ( return (
<Layout style={{height: '100%'}}> <Layout style={{height: '100%'}}>
<Layout.Sider> {(showSettings || !styleState.isMobile) && (
<Card style={commonOuterStyle}> <Layout.Sider style={{ display: styleState.isMobile ? 'block' : 'initial' }}>
<div style={{ marginTop: 10 }}> <Card style={commonOuterStyle}>
<Typography.Text strong>分组</Typography.Text> <div style={{ marginTop: 10 }}>
</div> <Typography.Text strong>{t('分组')}</Typography.Text>
<Select </div>
placeholder={'请选择分组'} <Select
name='group' placeholder={t('请选择分组')}
required name='group'
selection required
onChange={(value) => { selection
handleInputChange('group', value); onChange={(value) => {
}} handleInputChange('group', value);
value={inputs.group} }}
autoComplete='new-password' value={inputs.group}
optionList={groups} autoComplete='new-password'
/> optionList={groups.map((group) => ({
<div style={{ marginTop: 10 }}> ...group,
<Typography.Text strong>模型</Typography.Text> label: styleState.isMobile && group.label.length > 16
</div> ? group.label.substring(0, 16) + '...'
<Select : group.label,
placeholder={'请选择模型'} }))}
name='model' />
required <div style={{ marginTop: 10 }}>
selection <Typography.Text strong>{t('模型')}</Typography.Text>
filter </div>
onChange={(value) => { <Select
handleInputChange('model', value); placeholder={t('请选择模型')}
}} name='model'
value={inputs.model} required
autoComplete='new-password' selection
optionList={models} searchPosition='dropdown'
/> filter
<div style={{ marginTop: 10 }}> onChange={(value) => {
<Typography.Text strong>Temperature</Typography.Text> handleInputChange('model', value);
</div> }}
<Slider value={inputs.model}
step={0.1} autoComplete='new-password'
min={0.1} optionList={models}
max={1} />
value={inputs.temperature} <div style={{ marginTop: 10 }}>
onChange={(value) => { <Typography.Text strong>Temperature</Typography.Text>
handleInputChange('temperature', value); </div>
}} <Slider
/> step={0.1}
<div style={{ marginTop: 10 }}> min={0.1}
<Typography.Text strong>MaxTokens</Typography.Text> max={1}
</div> value={inputs.temperature}
<Input onChange={(value) => {
placeholder='MaxTokens' handleInputChange('temperature', value);
name='max_tokens' }}
required />
autoComplete='new-password' <div style={{ marginTop: 10 }}>
defaultValue={0} <Typography.Text strong>MaxTokens</Typography.Text>
value={inputs.max_tokens} </div>
onChange={(value) => { <Input
handleInputChange('max_tokens', value); placeholder='MaxTokens'
}} name='max_tokens'
/> required
autoComplete='new-password'
defaultValue={0}
value={inputs.max_tokens}
onChange={(value) => {
handleInputChange('max_tokens', value);
}}
/>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>System</Typography.Text> <Typography.Text strong>System</Typography.Text>
</div> </div>
<TextArea <TextArea
placeholder='System Prompt' placeholder='System Prompt'
name='system' name='system'
required required
autoComplete='new-password' autoComplete='new-password'
autosize autosize
defaultValue={systemPrompt} defaultValue={systemPrompt}
// value={systemPrompt} // value={systemPrompt}
onChange={(value) => { onChange={(value) => {
setSystemPrompt(value); setSystemPrompt(value);
}} }}
/> />
</Card> </Card>
</Layout.Sider> </Layout.Sider>
)}
<Layout.Content> <Layout.Content>
<div style={{height: '100%'}}> <div style={{height: '100%', position: 'relative'}}>
<SettingsToggle />
<Chat <Chat
chatBoxRenderConfig={{ chatBoxRenderConfig={{
renderChatBoxAction: () => { renderChatBoxAction: () => {
return <div></div> return <div></div>
} }
}} }}
renderInputArea={renderInputArea}
roleConfig={roleInfo}
style={commonOuterStyle} style={commonOuterStyle}
chats={message} chats={message}
onMessageSend={onMessageSend} onMessageSend={onMessageSend}

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { import {
API, API,
downloadTextAsFile, downloadTextAsFile,
@@ -7,7 +8,7 @@ import {
showError, showError,
showSuccess, showSuccess,
} from '../../helpers'; } from '../../helpers';
import { renderQuotaWithPrompt } from '../../helpers/render'; import { getQuotaPerUnit, renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
import { import {
AutoComplete, AutoComplete,
Button, Button,
@@ -22,6 +23,7 @@ import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import { Divider } from 'semantic-ui-react'; import { Divider } from 'semantic-ui-react';
const EditRedemption = (props) => { const EditRedemption = (props) => {
const { t } = useTranslation();
const isEdit = props.editingRedemption.id !== undefined; const isEdit = props.editingRedemption.id !== undefined;
const [loading, setLoading] = useState(isEdit); const [loading, setLoading] = useState(isEdit);
@@ -66,11 +68,16 @@ const EditRedemption = (props) => {
}, [props.editingRedemption.id]); }, [props.editingRedemption.id]);
const submit = async () => { const submit = async () => {
if (!isEdit && inputs.name === '') return; let name = inputs.name;
if (!isEdit && inputs.name === '') {
// set default name
name = t('新建兑换码') + ' ' + renderQuota(quota);
}
setLoading(true); setLoading(true);
let localInputs = inputs; let localInputs = inputs;
localInputs.count = parseInt(localInputs.count); localInputs.count = parseInt(localInputs.count);
localInputs.quota = parseInt(localInputs.quota); localInputs.quota = parseInt(localInputs.quota);
localInputs.name = name;
let res; let res;
if (isEdit) { if (isEdit) {
res = await API.put(`/api/redemption/`, { res = await API.put(`/api/redemption/`, {
@@ -85,11 +92,11 @@ const EditRedemption = (props) => {
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
if (isEdit) { if (isEdit) {
showSuccess('兑换码更新成功!'); showSuccess(t('兑换码更新成功!'));
props.refresh(); props.refresh();
props.handleClose(); props.handleClose();
} else { } else {
showSuccess('兑换码创建成功!'); showSuccess(t('兑换码创建成功!'));
setInputs(originInputs); setInputs(originInputs);
props.refresh(); props.refresh();
props.handleClose(); props.handleClose();
@@ -102,13 +109,12 @@ const EditRedemption = (props) => {
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
text += data[i] + '\n'; text += data[i] + '\n';
} }
// downloadTextAsFile(text, `${inputs.name}.txt`);
Modal.confirm({ Modal.confirm({
title: '兑换码创建成功', title: t('兑换码创建成功'),
content: ( content: (
<div> <div>
<p>兑换码创建成功是否下载兑换码</p> <p>{t('兑换码创建成功是否下载兑换码?')}</p>
<p>兑换码将以文本文件的形式下载文件名为兑换码的名称</p> <p>{t('兑换码将以文本文件的形式下载文件名为兑换码的名称。')}</p>
</div> </div>
), ),
onOk: () => { onOk: () => {
@@ -125,7 +131,7 @@ const EditRedemption = (props) => {
placement={isEdit ? 'right' : 'left'} placement={isEdit ? 'right' : 'left'}
title={ title={
<Title level={3}> <Title level={3}>
{isEdit ? '更新兑换码信息' : '创建新的兑换码'} {isEdit ? t('更新兑换码信息') : t('创建新的兑换码')}
</Title> </Title>
} }
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
@@ -135,7 +141,7 @@ const EditRedemption = (props) => {
<div style={{ display: 'flex', justifyContent: 'flex-end' }}> <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space> <Space>
<Button theme='solid' size={'large'} onClick={submit}> <Button theme='solid' size={'large'} onClick={submit}>
提交 {t('提交')}
</Button> </Button>
<Button <Button
theme='solid' theme='solid'
@@ -143,7 +149,7 @@ const EditRedemption = (props) => {
type={'tertiary'} type={'tertiary'}
onClick={handleCancel} onClick={handleCancel}
> >
取消 {t('取消')}
</Button> </Button>
</Space> </Space>
</div> </div>
@@ -155,9 +161,9 @@ const EditRedemption = (props) => {
<Spin spinning={loading}> <Spin spinning={loading}>
<Input <Input
style={{ marginTop: 20 }} style={{ marginTop: 20 }}
label='名称' label={t('名称')}
name='name' name='name'
placeholder={'请输入名称'} placeholder={t('请输入名称')}
onChange={(value) => handleInputChange('name', value)} onChange={(value) => handleInputChange('name', value)}
value={name} value={name}
autoComplete='new-password' autoComplete='new-password'
@@ -165,12 +171,12 @@ const EditRedemption = (props) => {
/> />
<Divider /> <Divider />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>{`额度${renderQuotaWithPrompt(quota)}`}</Typography.Text> <Typography.Text>{t('额度') + renderQuotaWithPrompt(quota)}</Typography.Text>
</div> </div>
<AutoComplete <AutoComplete
style={{ marginTop: 8 }} style={{ marginTop: 8 }}
name='quota' name='quota'
placeholder={'请输入额度'} placeholder={t('请输入额度')}
onChange={(value) => handleInputChange('quota', value)} onChange={(value) => handleInputChange('quota', value)}
value={quota} value={quota}
autoComplete='new-password' autoComplete='new-password'
@@ -188,12 +194,12 @@ const EditRedemption = (props) => {
{!isEdit && ( {!isEdit && (
<> <>
<Divider /> <Divider />
<Typography.Text>生成数量</Typography.Text> <Typography.Text>{t('生成数量')}</Typography.Text>
<Input <Input
style={{ marginTop: 8 }} style={{ marginTop: 8 }}
label='生成数量' label={t('生成数量')}
name='count' name='count'
placeholder={'请输入生成数量'} placeholder={t('请输入生成数量')}
onChange={(value) => handleInputChange('count', value)} onChange={(value) => handleInputChange('count', value)}
value={count} value={count}
autoComplete='new-password' autoComplete='new-password'

View File

@@ -1,12 +1,15 @@
import React from 'react'; import React from 'react';
import RedemptionsTable from '../../components/RedemptionsTable'; import RedemptionsTable from '../../components/RedemptionsTable';
import { Layout } from '@douyinfe/semi-ui'; import { Layout } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const Redemption = () => ( const Redemption = () => {
<> const { t } = useTranslation();
<Layout> return (
<Layout.Header> <>
<h3>管理兑换码</h3> <Layout>
<Layout.Header>
<h3>{t('管理兑换码')}</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<RedemptionsTable /> <RedemptionsTable />
@@ -14,5 +17,6 @@ const Redemption = () => (
</Layout> </Layout>
</> </>
); );
}
export default Redemption; export default Redemption;

View File

@@ -0,0 +1,131 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
verifyJSON,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function GroupRatioSettings(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
GroupRatio: '',
UserUsableGroups: ''
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
async function onSubmit() {
try {
await refForm.current.validate().then(() => {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
const value = typeof inputs[item.key] === 'boolean'
? String(inputs[item.key])
: inputs[item.key];
return API.put('/api/option/', { key: item.key, value });
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (res.includes(undefined)) {
return showError(requestQueue.length > 1 ? t('部分保存失败,请重试') : t('保存失败'));
}
for (let i = 0; i < res.length; i++) {
if (!res[i].data.success) {
return showError(res[i].data.message);
}
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch(error => {
console.error('Unexpected error:', error);
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
}).catch(() => {
showError(t('请检查输入'));
});
} catch (error) {
showError(t('请检查输入'));
console.error(error);
}
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={t('分组设置')}>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={t('分组倍率')}
placeholder={t('为一个 JSON 文本,键为分组名称,值为倍率')}
field={'GroupRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: t('不是合法的 JSON 字符串')
}
]}
onChange={(value) => setInputs({ ...inputs, GroupRatio: value })}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={t('用户可选分组')}
placeholder={t('为一个 JSON 文本,键为分组名称,值为分组描述')}
field={'UserUsableGroups'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: t('不是合法的 JSON 字符串')
}
]}
onChange={(value) => setInputs({ ...inputs, UserUsableGroups: value })}
/>
</Col>
</Row>
</Form.Section>
</Form>
<Button onClick={onSubmit}>{t('保存分组倍率设置')}</Button>
</Spin>
);
}

View File

@@ -0,0 +1,178 @@
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Popconfirm, Row, Space, Spin } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
verifyJSON,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function ModelRatioSettings(props) {
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
ModelPrice: '',
ModelRatio: '',
CompletionRatio: '',
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
const { t } = useTranslation();
async function onSubmit() {
try {
await refForm.current.validate().then(() => {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
const value = typeof inputs[item.key] === 'boolean'
? String(inputs[item.key])
: inputs[item.key];
return API.put('/api/option/', { key: item.key, value });
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (res.includes(undefined)) {
return showError(requestQueue.length > 1 ? t('部分保存失败,请重试') : t('保存失败'));
}
for (let i = 0; i < res.length; i++) {
if (!res[i].data.success) {
return showError(res[i].data.message);
}
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch(error => {
console.error('Unexpected error:', error);
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
}).catch(() => {
showError(t('请检查输入'));
});
} catch (error) {
showError(t('请检查输入'));
console.error(error);
}
}
async function resetModelRatio() {
try {
let res = await API.post(`/api/option/rest_model_ratio`);
if (res.data.success) {
showSuccess(res.data.message);
props.refresh();
} else {
showError(res.data.message);
}
} catch (error) {
showError(error);
}
}
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
if (Object.keys(inputs).includes(key)) {
currentInputs[key] = props.options[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
}, [props.options]);
return (
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={t('模型固定价格')}
extraText={t('一次调用消耗多少刀,优先级大于模型倍率')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1一次消耗0.1刀')}
field={'ModelPrice'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串'
}
]}
onChange={(value) => setInputs({ ...inputs, ModelPrice: value })}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={t('模型倍率')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率')}
field={'ModelRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串'
}
]}
onChange={(value) => setInputs({ ...inputs, ModelRatio: value })}
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={t('模型补全倍率(仅对自定义模型有效)')}
extraText={t('仅对自定义模型有效')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率')}
field={'CompletionRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串'
}
]}
onChange={(value) => setInputs({ ...inputs, CompletionRatio: value })}
/>
</Col>
</Row>
</Form.Section>
</Form>
<Space>
<Button onClick={onSubmit}>{t('保存模型倍率设置')}</Button>
<Popconfirm
title={t('确定重置模型倍率吗?')}
content={t('此修改将不可逆')}
okType={'danger'}
position={'top'}
onConfirm={resetModelRatio}
>
<Button type={'danger'}>{t('重置模型倍率')}</Button>
</Popconfirm>
</Space>
</Spin>
);
}

View File

@@ -0,0 +1,319 @@
// ModelSettingsVisualEditor.js
import React, { useEffect, useState } from 'react';
import { Table, Button, Input, Modal, Form, Space } from '@douyinfe/semi-ui';
import { IconDelete, IconPlus, IconSearch, IconSave } from '@douyinfe/semi-icons';
import { showError, showSuccess } from '../../../helpers';
import { API } from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function ModelSettingsVisualEditor(props) {
const { t } = useTranslation();
const [models, setModels] = useState([]);
const [visible, setVisible] = useState(false);
const [currentModel, setCurrentModel] = useState(null);
const [searchText, setSearchText] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(false);
const pageSize = 10;
useEffect(() => {
try {
const modelPrice = JSON.parse(props.options.ModelPrice || '{}');
const modelRatio = JSON.parse(props.options.ModelRatio || '{}');
const completionRatio = JSON.parse(props.options.CompletionRatio || '{}');
// 合并所有模型名称
const modelNames = new Set([
...Object.keys(modelPrice),
...Object.keys(modelRatio),
...Object.keys(completionRatio)
]);
const modelData = Array.from(modelNames).map(name => ({
name,
price: modelPrice[name] === undefined ? '' : modelPrice[name],
ratio: modelRatio[name] === undefined ? '' : modelRatio[name],
completionRatio: completionRatio[name] === undefined ? '' : completionRatio[name]
}));
setModels(modelData);
} catch (error) {
console.error('JSON解析错误:', error);
}
}, [props.options]);
// 首先声明分页相关的工具函数
const getPagedData = (data, currentPage, pageSize) => {
const start = (currentPage - 1) * pageSize;
const end = start + pageSize;
return data.slice(start, end);
};
// 在 return 语句之前,先处理过滤和分页逻辑
const filteredModels = models.filter(model =>
searchText ? model.name.toLowerCase().includes(searchText.toLowerCase()) : true
);
// 然后基于过滤后的数据计算分页数据
const pagedData = getPagedData(filteredModels, currentPage, pageSize);
const SubmitData = async () => {
setLoading(true);
const output = {
ModelPrice: {},
ModelRatio: {},
CompletionRatio: {}
};
let currentConvertModelName = '';
try {
// 数据转换
models.forEach(model => {
currentConvertModelName = model.name;
if (model.price !== '') {
// 如果价格不为空,则转换为浮点数,忽略倍率参数
output.ModelPrice[model.name] = parseFloat(model.price)
} else {
if (model.ratio !== '') output.ModelRatio[model.name] = parseFloat(model.ratio);
if (model.completionRatio != '') output.CompletionRatio[model.name] = parseFloat(model.completionRatio);
}
});
// 准备API请求数组
const finalOutput = {
ModelPrice: JSON.stringify(output.ModelPrice, null, 2),
ModelRatio: JSON.stringify(output.ModelRatio, null, 2),
CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2)
};
const requestQueue = Object.entries(finalOutput).map(([key, value]) => {
return API.put('/api/option/', {
key,
value
});
});
// 批量处理请求
const results = await Promise.all(requestQueue);
// 验证结果
if (requestQueue.length === 1) {
if (results.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (results.includes(undefined)) {
return showError('部分保存失败,请重试');
}
}
// 检查每个请求的结果
for (const res of results) {
if (!res.data.success) {
return showError(res.data.message);
}
}
showSuccess('保存成功');
props.refresh();
} catch (error) {
console.error('保存失败:', error);
showError('保存失败,请重试');
} finally {
setLoading(false);
}
};
const columns = [
{
title: t('模型名称'),
dataIndex: 'name',
key: 'name',
},
{
title: t('模型固定价格'),
dataIndex: 'price',
key: 'price',
render: (text, record) => (
<Input
value={text}
placeholder={t('按量计费')}
onChange={value => updateModel(record.name, 'price', value)}
/>
)
},
{
title: t('模型倍率'),
dataIndex: 'ratio',
key: 'ratio',
render: (text, record) => (
<Input
value={text}
placeholder={record.price !== '' ? t('模型倍率') : t('默认补全倍率')}
disabled={record.price !== ''}
onChange={value => updateModel(record.name, 'ratio', value)}
/>
)
},
{
title: t('补全倍率'),
dataIndex: 'completionRatio',
key: 'completionRatio',
render: (text, record) => (
<Input
value={text}
placeholder={record.price !== '' ? t('补全倍率') : t('默认补全倍率')}
disabled={record.price !== ''}
onChange={value => updateModel(record.name, 'completionRatio', value)}
/>
)
},
{
title: t('操作'),
key: 'action',
render: (_, record) => (
<Button
icon={<IconDelete />}
type="danger"
onClick={() => deleteModel(record.name)}
/>
)
}
];
const updateModel = (name, field, value) => {
if (isNaN(value)) {
showError('请输入数字');
return;
}
setModels(prev =>
prev.map(model =>
model.name === name
? { ...model, [field]: value }
: model
)
);
};
const deleteModel = (name) => {
setModels(prev => prev.filter(model => model.name !== name));
};
const addModel = (values) => {
// 检查模型名称是否存在, 如果存在则拒绝添加
if (models.some(model => model.name === values.name)) {
showError('模型名称已存在');
return;
}
// 不允许同时添加固定价格和倍率
if (values.price !== '' && (values.ratio !== '' || values.completionRatio !== '')) {
showError('固定价格和倍率不能同时存在');
return;
}
setModels(prev => [{
name: values.name,
price: values.price || '',
ratio: values.ratio || '',
completionRatio: values.completionRatio || ''
}, ...prev]);
setVisible(false);
showSuccess('添加成功');
};
return (
<>
<Space vertical align="start" style={{ width: '100%' }}>
<Space>
<Button icon={<IconPlus />} onClick={() => setVisible(true)}>
{t('添加模型')}
</Button>
<Button type="primary" icon={<IconSave />} onClick={SubmitData}>
{t('应用更改')}
</Button>
<Input
prefix={<IconSearch />}
placeholder={t('搜索模型名称')}
value={searchText}
onChange={value => {
setSearchText(value)
setCurrentPage(1);
}}
style={{ width: 200 }}
/>
</Space>
<Table
columns={columns}
dataSource={pagedData}
pagination={{
currentPage: currentPage,
pageSize: pageSize,
total: filteredModels.length,
onPageChange: page => setCurrentPage(page),
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: filteredModels.length
}),
showTotal: true,
showSizeChanger: false
}}
/>
</Space>
<Modal
title={t('添加模型')}
visible={visible}
onCancel={() => setVisible(false)}
onOk={() => {
currentModel && addModel(currentModel);
}}
>
<Form>
<Form.Input
field="name"
label={t('模型名称')}
placeholder="strawberry"
required
onChange={value => setCurrentModel(prev => ({ ...prev, name: value }))}
/>
<Form.Switch
field="priceMode"
label={<>{t('定价模式')}{currentModel?.priceMode ? t("固定价格") : t("倍率模式")}</>}
onChange={checked => {
setCurrentModel(prev => ({
...prev,
price: '',
ratio: '',
completionRatio: '',
priceMode: checked
}));
}}
/>
{currentModel?.priceMode ? (
<Form.Input
field="price"
label={t('固定价格(每次)')}
placeholder={t('输入每次价格')}
onChange={value => setCurrentModel(prev => ({ ...prev, price: value }))}
/>
) : (
<>
<Form.Input
field="ratio"
label={t('模型倍率')}
placeholder={t('输入模型倍率')}
onChange={value => setCurrentModel(prev => ({ ...prev, ratio: value }))}
/>
<Form.Input
field="completionRatio"
label={t('补全倍率')}
placeholder={t('输入补全价格')}
onChange={value => setCurrentModel(prev => ({ ...prev, completionRatio: value }))}
/>
</>
)}
</Form>
</Modal>
</>
);
}

View File

@@ -9,8 +9,10 @@ import {
verifyJSON, verifyJSON,
verifyJSONPromise verifyJSONPromise
} from '../../../helpers'; } from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function SettingsChats(props) { export default function SettingsChats(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
Chats: "[]", Chats: "[]",
@@ -24,7 +26,7 @@ export default function SettingsChats(props) {
await refForm.current.validate().then(() => { await refForm.current.validate().then(() => {
console.log('Validation passed'); console.log('Validation passed');
const updateArray = compareObjects(inputs, inputsRow); const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么'); if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => { const requestQueue = updateArray.map((item) => {
let value = ''; let value = '';
if (typeof inputs[item.key] === 'boolean') { if (typeof inputs[item.key] === 'boolean') {
@@ -44,23 +46,23 @@ export default function SettingsChats(props) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) if (res.includes(undefined))
return showError('部分保存失败,请重试'); return showError(t('部分保存失败,请重试'));
} }
showSuccess('保存成功'); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
}) })
.catch(() => { .catch(() => {
showError('保存失败,请重试'); showError(t('保存失败,请重试'));
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
}); });
}).catch((error) => { }).catch((error) => {
console.error('Validation failed:', error); console.error('Validation failed:', error);
showError('请检查输入'); showError(t('请检查输入'));
}); });
} catch (error) { } catch (error) {
showError('请检查输入'); showError(t('请检查输入'));
console.error(error); console.error(error);
} }
} }
@@ -104,19 +106,19 @@ export default function SettingsChats(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)} getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }} style={{ marginBottom: 15 }}
> >
<Form.Section text={'令牌聊天设置'}> <Form.Section text={t('令牌聊天设置')}>
<Banner <Banner
type='warning' type='warning'
description={'必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能'} description={t('必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能')}
/> />
<Banner <Banner
type='info' type='info'
description={'链接中的{key}将自动替换为sk-xxxx{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1'} description={t('链接中的{key}将自动替换为sk-xxxx{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1')}
/> />
<Form.TextArea <Form.TextArea
label={'聊天配置'} label={t('聊天配置')}
extraText={''} extraText={''}
placeholder={'为一个 JSON 文本'} placeholder={t('为一个 JSON 文本')}
field={'Chats'} field={'Chats'}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur' trigger='blur'
@@ -126,7 +128,7 @@ export default function SettingsChats(props) {
validator: (rule, value) => { validator: (rule, value) => {
return verifyJSON(value); return verifyJSON(value);
}, },
message: '不是合法的 JSON 字符串' message: t('不是合法的 JSON 字符串')
} }
]} ]}
onChange={(value) => onChange={(value) =>
@@ -140,7 +142,7 @@ export default function SettingsChats(props) {
</Form> </Form>
<Space> <Space>
<Button onClick={onSubmit}> <Button onClick={onSubmit}>
保存聊天设置 {t('保存聊天设置')}
</Button> </Button>
</Space> </Space>
</Spin> </Spin>

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui'; import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
import { import {
compareObjects, compareObjects,
API, API,
@@ -9,6 +10,7 @@ import {
} from '../../../helpers'; } from '../../../helpers';
export default function SettingsCreditLimit(props) { export default function SettingsCreditLimit(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
QuotaForNewUser: '', QuotaForNewUser: '',
@@ -21,7 +23,7 @@ export default function SettingsCreditLimit(props) {
function onSubmit() { function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow); const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么'); if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => { const requestQueue = updateArray.map((item) => {
let value = ''; let value = '';
if (typeof inputs[item.key] === 'boolean') { if (typeof inputs[item.key] === 'boolean') {
@@ -40,13 +42,13 @@ export default function SettingsCreditLimit(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试'); if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
} }
showSuccess('保存成功'); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
}) })
.catch(() => { .catch(() => {
showError('保存失败,请重试'); showError(t('保存失败,请重试'));
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
@@ -72,11 +74,11 @@ export default function SettingsCreditLimit(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)} getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }} style={{ marginBottom: 15 }}
> >
<Form.Section text={'额度设置'}> <Form.Section text={t('额度设置')}>
<Row gutter={16}> <Row gutter={16}>
<Col span={6}> <Col span={6}>
<Form.InputNumber <Form.InputNumber
label={'新用户初始额度'} label={t('新用户初始额度')}
field={'QuotaForNewUser'} field={'QuotaForNewUser'}
step={1} step={1}
min={0} min={0}
@@ -92,12 +94,12 @@ export default function SettingsCreditLimit(props) {
</Col> </Col>
<Col span={6}> <Col span={6}>
<Form.InputNumber <Form.InputNumber
label={'请求预扣费额度'} label={t('请求预扣费额度')}
field={'PreConsumedQuota'} field={'PreConsumedQuota'}
step={1} step={1}
min={0} min={0}
suffix={'Token'} suffix={'Token'}
extraText={'请求结束后多退少补'} extraText={t('请求结束后多退少补')}
placeholder={''} placeholder={''}
onChange={(value) => onChange={(value) =>
setInputs({ setInputs({
@@ -109,13 +111,13 @@ export default function SettingsCreditLimit(props) {
</Col> </Col>
<Col span={6}> <Col span={6}>
<Form.InputNumber <Form.InputNumber
label={'邀请新用户奖励额度'} label={t('邀请新用户奖励额度')}
field={'QuotaForInviter'} field={'QuotaForInviter'}
step={1} step={1}
min={0} min={0}
suffix={'Token'} suffix={'Token'}
extraText={''} extraText={''}
placeholder={'例如2000'} placeholder={t('例如2000')}
onChange={(value) => onChange={(value) =>
setInputs({ setInputs({
...inputs, ...inputs,
@@ -126,13 +128,13 @@ export default function SettingsCreditLimit(props) {
</Col> </Col>
<Col span={6}> <Col span={6}>
<Form.InputNumber <Form.InputNumber
label={'新用户使用邀请码奖励额度'} label={t('新用户使用邀请码奖励额度')}
field={'QuotaForInvitee'} field={'QuotaForInvitee'}
step={1} step={1}
min={0} min={0}
suffix={'Token'} suffix={'Token'}
extraText={''} extraText={''}
placeholder={'例如1000'} placeholder={t('例如1000')}
onChange={(value) => onChange={(value) =>
setInputs({ setInputs({
...inputs, ...inputs,
@@ -145,7 +147,7 @@ export default function SettingsCreditLimit(props) {
<Row> <Row>
<Button size='default' onClick={onSubmit}> <Button size='default' onClick={onSubmit}>
保存额度设置 {t('保存额度设置')}
</Button> </Button>
</Row> </Row>
</Form.Section> </Form.Section>

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin, Tag } from '@douyinfe/semi-ui'; import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
import { import {
compareObjects, compareObjects,
API, API,
@@ -7,12 +7,15 @@ import {
showSuccess, showSuccess,
showWarning, showWarning,
} from '../../../helpers'; } from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function DataDashboard(props) { export default function DataDashboard(props) {
const { t } = useTranslation();
const optionsDataExportDefaultTime = [ const optionsDataExportDefaultTime = [
{ key: 'hour', label: '小时', value: 'hour' }, { key: 'hour', label: t('小时'), value: 'hour' },
{ key: 'day', label: '天', value: 'day' }, { key: 'day', label: t('天'), value: 'day' },
{ key: 'week', label: '周', value: 'week' }, { key: 'week', label: t('周'), value: 'week' },
]; ];
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
@@ -25,7 +28,7 @@ export default function DataDashboard(props) {
function onSubmit() { function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow); const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么'); if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => { const requestQueue = updateArray.map((item) => {
let value = ''; let value = '';
if (typeof inputs[item.key] === 'boolean') { if (typeof inputs[item.key] === 'boolean') {
@@ -44,13 +47,13 @@ export default function DataDashboard(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试'); if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
} }
showSuccess('保存成功'); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
}) })
.catch(() => { .catch(() => {
showError('保存失败,请重试'); showError(t('保存失败,请重试'));
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
@@ -81,12 +84,12 @@ export default function DataDashboard(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)} getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }} style={{ marginBottom: 15 }}
> >
<Form.Section text={'数据看板设置'}> <Form.Section text={t('数据看板设置')}>
<Row gutter={16}> <Row gutter={16}>
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'DataExportEnabled'} field={'DataExportEnabled'}
label={'启用数据看板(实验性)'} label={t('启用数据看板(实验性)')}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -102,12 +105,12 @@ export default function DataDashboard(props) {
<Row> <Row>
<Col span={8}> <Col span={8}>
<Form.InputNumber <Form.InputNumber
label={'数据看板更新间隔 '} label={t('数据看板更新间隔')}
step={1} step={1}
min={1} min={1}
suffix={'分钟'} suffix={t('分钟')}
extraText={'设置过短会影响数据库性能'} extraText={t('设置过短会影响数据库性能')}
placeholder={'数据看板更新间隔'} placeholder={t('数据看板更新间隔')}
field={'DataExportInterval'} field={'DataExportInterval'}
onChange={(value) => onChange={(value) =>
setInputs({ setInputs({
@@ -119,11 +122,11 @@ export default function DataDashboard(props) {
</Col> </Col>
<Col span={8}> <Col span={8}>
<Form.Select <Form.Select
label='数据看板默认时间粒度' label={t('数据看板默认时间粒度')}
optionList={optionsDataExportDefaultTime} optionList={optionsDataExportDefaultTime}
field={'DataExportDefaultTime'} field={'DataExportDefaultTime'}
extraText={'仅修改展示粒度,统计精确到小时'} extraText={t('仅修改展示粒度,统计精确到小时')}
placeholder={'数据看板默认时间粒度'} placeholder={t('数据看板默认时间粒度')}
style={{ width: 180 }} style={{ width: 180 }}
onChange={(value) => onChange={(value) =>
setInputs({ setInputs({
@@ -136,7 +139,7 @@ export default function DataDashboard(props) {
</Row> </Row>
<Row> <Row>
<Button size='default' onClick={onSubmit}> <Button size='default' onClick={onSubmit}>
保存数据看板设置 {t('保存数据看板设置')}
</Button> </Button>
</Row> </Row>
</Form.Section> </Form.Section>

View File

@@ -7,8 +7,10 @@ import {
showSuccess, showSuccess,
showWarning, showWarning,
} from '../../../helpers'; } from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function SettingsDrawing(props) { export default function SettingsDrawing(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
DrawingEnabled: false, DrawingEnabled: false,
@@ -23,7 +25,7 @@ export default function SettingsDrawing(props) {
function onSubmit() { function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow); const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么'); if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => { const requestQueue = updateArray.map((item) => {
let value = ''; let value = '';
if (typeof inputs[item.key] === 'boolean') { if (typeof inputs[item.key] === 'boolean') {
@@ -42,13 +44,13 @@ export default function SettingsDrawing(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试'); if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
} }
showSuccess('保存成功'); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
}) })
.catch(() => { .catch(() => {
showError('保存失败,请重试'); showError(t('保存失败,请重试'));
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
@@ -67,6 +69,7 @@ export default function SettingsDrawing(props) {
refForm.current.setValues(currentInputs); refForm.current.setValues(currentInputs);
localStorage.setItem('mj_notify_enabled', String(inputs.MjNotifyEnabled)); localStorage.setItem('mj_notify_enabled', String(inputs.MjNotifyEnabled));
}, [props.options]); }, [props.options]);
return ( return (
<> <>
<Spin spinning={loading}> <Spin spinning={loading}>
@@ -75,12 +78,12 @@ export default function SettingsDrawing(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)} getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }} style={{ marginBottom: 15 }}
> >
<Form.Section text={'绘图设置'}> <Form.Section text={t('绘图设置')}>
<Row gutter={16}> <Row gutter={16}>
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'DrawingEnabled'} field={'DrawingEnabled'}
label={'启用绘图功能'} label={t('启用绘图功能')}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -95,7 +98,7 @@ export default function SettingsDrawing(props) {
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'MjNotifyEnabled'} field={'MjNotifyEnabled'}
label={'允许回调(会泄露服务器 IP 地址)'} label={t('允许回调(会泄露服务器 IP 地址)')}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -110,7 +113,7 @@ export default function SettingsDrawing(props) {
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'MjAccountFilterEnabled'} field={'MjAccountFilterEnabled'}
label={'允许 AccountFilter 参数'} label={t('允许 AccountFilter 参数')}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -125,7 +128,7 @@ export default function SettingsDrawing(props) {
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'MjForwardUrlEnabled'} field={'MjForwardUrlEnabled'}
label={'开启之后将上游地址替换为服务器地址'} label={t('开启之后将上游地址替换为服务器地址')}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -142,8 +145,8 @@ export default function SettingsDrawing(props) {
field={'MjModeClearEnabled'} field={'MjModeClearEnabled'}
label={ label={
<> <>
开启之后会清除用户提示词中的 <Tag>--fast</Tag> {t('开启之后会清除用户提示词中的')} <Tag>--fast</Tag>
<Tag>--relax</Tag> <Tag>--turbo</Tag> <Tag>--relax</Tag> {t('')} <Tag>--turbo</Tag> {t('')}
</> </>
} }
size='default' size='default'
@@ -160,11 +163,7 @@ export default function SettingsDrawing(props) {
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'MjActionCheckSuccessEnabled'} field={'MjActionCheckSuccessEnabled'}
label={ label={t('检测必须等待绘图成功才能进行放大等操作')}
<>
检测必须等待绘图成功才能进行放大等操作
</>
}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -179,7 +178,7 @@ export default function SettingsDrawing(props) {
</Row> </Row>
<Row> <Row>
<Button size='default' onClick={onSubmit}> <Button size='default' onClick={onSubmit}>
保存绘图设置 {t('保存绘图设置')}
</Button> </Button>
</Row> </Row>
</Form.Section> </Form.Section>

View File

@@ -7,8 +7,10 @@ import {
showSuccess, showSuccess,
showWarning, showWarning,
} from '../../../helpers'; } from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function GeneralSettings(props) { export default function GeneralSettings(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
TopUpLink: '', TopUpLink: '',
@@ -22,13 +24,15 @@ export default function GeneralSettings(props) {
}); });
const refForm = useRef(); const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs); const [inputsRow, setInputsRow] = useState(inputs);
function onChange(value, e) { function onChange(value, e) {
const name = e.target.id; const name = e.target.id;
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
} }
function onSubmit() { function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow); const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么'); if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => { const requestQueue = updateArray.map((item) => {
let value = ''; let value = '';
if (typeof inputs[item.key] === 'boolean') { if (typeof inputs[item.key] === 'boolean') {
@@ -47,13 +51,13 @@ export default function GeneralSettings(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试'); if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
} }
showSuccess('保存成功'); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
}) })
.catch(() => { .catch(() => {
showError('保存失败,请重试'); showError(t('保存失败,请重试'));
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
@@ -71,26 +75,27 @@ export default function GeneralSettings(props) {
setInputsRow(structuredClone(currentInputs)); setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs); refForm.current.setValues(currentInputs);
}, [props.options]); }, [props.options]);
return ( return (
<> <>
<Spin spinning={loading}> <Spin spinning={loading}>
<Banner <Banner
type='warning' type='warning'
description={'聊天链接功能已经弃用,请使用下方聊天设置功能'} description={t('聊天链接功能已经弃用,请使用下方聊天设置功能')}
/> />
<Form <Form
values={inputs} values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)} getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }} style={{ marginBottom: 15 }}
> >
<Form.Section text={'通用设置'}> <Form.Section text={t('通用设置')}>
<Row gutter={16}> <Row gutter={16}>
<Col span={8}> <Col span={8}>
<Form.Input <Form.Input
field={'TopUpLink'} field={'TopUpLink'}
label={'充值链接'} label={t('充值链接')}
initValue={''} initValue={''}
placeholder={'例如发卡网站的购买链接'} placeholder={t('例如发卡网站的购买链接')}
onChange={onChange} onChange={onChange}
showClear showClear
/> />
@@ -98,9 +103,9 @@ export default function GeneralSettings(props) {
<Col span={8}> <Col span={8}>
<Form.Input <Form.Input
field={'ChatLink'} field={'ChatLink'}
label={'默认聊天页面链接'} label={t('默认聊天页面链接')}
initValue={''} initValue={''}
placeholder='例如 ChatGPT Next Web 的部署地址' placeholder={t('例如 ChatGPT Next Web 的部署地址')}
onChange={onChange} onChange={onChange}
showClear showClear
/> />
@@ -108,9 +113,9 @@ export default function GeneralSettings(props) {
<Col span={8}> <Col span={8}>
<Form.Input <Form.Input
field={'ChatLink2'} field={'ChatLink2'}
label={'聊天页面 2 链接'} label={t('聊天页面 2 链接')}
initValue={''} initValue={''}
placeholder='例如 ChatGPT Next Web 的部署地址' placeholder={t('例如 ChatGPT Next Web 的部署地址')}
onChange={onChange} onChange={onChange}
showClear showClear
/> />
@@ -118,9 +123,9 @@ export default function GeneralSettings(props) {
<Col span={8}> <Col span={8}>
<Form.Input <Form.Input
field={'QuotaPerUnit'} field={'QuotaPerUnit'}
label={'单位美元额度'} label={t('单位美元额度')}
initValue={''} initValue={''}
placeholder='一单位货币能兑换的额度' placeholder={t('一单位货币能兑换的额度')}
onChange={onChange} onChange={onChange}
showClear showClear
/> />
@@ -128,9 +133,9 @@ export default function GeneralSettings(props) {
<Col span={8}> <Col span={8}>
<Form.Input <Form.Input
field={'RetryTimes'} field={'RetryTimes'}
label={'失败重试次数'} label={t('失败重试次数')}
initValue={''} initValue={''}
placeholder='失败重试次数' placeholder={t('失败重试次数')}
onChange={onChange} onChange={onChange}
showClear showClear
/> />
@@ -140,7 +145,7 @@ export default function GeneralSettings(props) {
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'DisplayInCurrencyEnabled'} field={'DisplayInCurrencyEnabled'}
label={'以货币形式显示额度'} label={t('以货币形式显示额度')}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -155,7 +160,7 @@ export default function GeneralSettings(props) {
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'DisplayTokenStatEnabled'} field={'DisplayTokenStatEnabled'}
label={'Billing 相关 API 显示令牌额度而非用户额度'} label={t('额度查询接口返回令牌额度而非用户额度')}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -170,7 +175,7 @@ export default function GeneralSettings(props) {
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'DefaultCollapseSidebar'} field={'DefaultCollapseSidebar'}
label={'默认折叠侧边栏'} label={t('默认折叠侧边栏')}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -185,7 +190,7 @@ export default function GeneralSettings(props) {
</Row> </Row>
<Row> <Row>
<Button size='default' onClick={onSubmit}> <Button size='default' onClick={onSubmit}>
保存通用设置 {t('保存通用设置')}
</Button> </Button>
</Row> </Row>
</Form.Section> </Form.Section>

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin, DatePicker } from '@douyinfe/semi-ui'; import { Button, Col, Form, Row, Spin, DatePicker } from '@douyinfe/semi-ui';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useTranslation } from 'react-i18next';
import { import {
compareObjects, compareObjects,
API, API,
@@ -10,6 +11,7 @@ import {
} from '../../../helpers'; } from '../../../helpers';
export default function SettingsLog(props) { export default function SettingsLog(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingCleanHistoryLog, setLoadingCleanHistoryLog] = useState(false); const [loadingCleanHistoryLog, setLoadingCleanHistoryLog] = useState(false);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
@@ -24,7 +26,7 @@ export default function SettingsLog(props) {
(item) => item.key !== 'historyTimestamp', (item) => item.key !== 'historyTimestamp',
); );
if (!updateArray.length) return showWarning('你似乎并没有修改什么'); if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => { const requestQueue = updateArray.map((item) => {
let value = ''; let value = '';
if (typeof inputs[item.key] === 'boolean') { if (typeof inputs[item.key] === 'boolean') {
@@ -43,13 +45,13 @@ export default function SettingsLog(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试'); if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
} }
showSuccess('保存成功'); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
}) })
.catch(() => { .catch(() => {
showError('保存失败,请重试'); showError(t('保存失败,请重试'));
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
@@ -58,16 +60,16 @@ export default function SettingsLog(props) {
async function onCleanHistoryLog() { async function onCleanHistoryLog() {
try { try {
setLoadingCleanHistoryLog(true); setLoadingCleanHistoryLog(true);
if (!inputs.historyTimestamp) throw new Error('请选择日志记录时间'); if (!inputs.historyTimestamp) throw new Error(t('请选择日志记录时间'));
const res = await API.delete( const res = await API.delete(
`/api/log/?target_timestamp=${Date.parse(inputs.historyTimestamp) / 1000}`, `/api/log/?target_timestamp=${Date.parse(inputs.historyTimestamp) / 1000}`,
); );
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
showSuccess(`${data} 条日志已清理!`); showSuccess(`${data} ${t('条日志已清理!')}`);
return; return;
} else { } else {
throw new Error('日志清理失败:' + message); throw new Error(t('日志清理失败:') + message);
} }
} catch (error) { } catch (error) {
showError(error.message); showError(error.message);
@@ -96,12 +98,12 @@ export default function SettingsLog(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)} getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }} style={{ marginBottom: 15 }}
> >
<Form.Section text={'日志设置'}> <Form.Section text={t('日志设置')}>
<Row gutter={16}> <Row gutter={16}>
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'LogConsumeEnabled'} field={'LogConsumeEnabled'}
label={'启用额度消费日志记录'} label={t('启用额度消费日志记录')}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -116,7 +118,7 @@ export default function SettingsLog(props) {
<Col span={8}> <Col span={8}>
<Spin spinning={loadingCleanHistoryLog}> <Spin spinning={loadingCleanHistoryLog}>
<Form.DatePicker <Form.DatePicker
label='日志记录时间' label={t('日志记录时间')}
field={'historyTimestamp'} field={'historyTimestamp'}
type='dateTime' type='dateTime'
inputReadOnly={true} inputReadOnly={true}
@@ -128,7 +130,7 @@ export default function SettingsLog(props) {
}} }}
/> />
<Button size='default' onClick={onCleanHistoryLog}> <Button size='default' onClick={onCleanHistoryLog}>
清除历史日志 {t('清除历史日志')}
</Button> </Button>
</Spin> </Spin>
</Col> </Col>
@@ -136,7 +138,7 @@ export default function SettingsLog(props) {
<Row> <Row>
<Button size='default' onClick={onSubmit}> <Button size='default' onClick={onSubmit}>
保存日志设置 {t('保存日志设置')}
</Button> </Button>
</Row> </Row>
</Form.Section> </Form.Section>

View File

@@ -7,8 +7,10 @@ import {
showSuccess, showSuccess,
showWarning, showWarning,
} from '../../../helpers'; } from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function SettingsMonitoring(props) { export default function SettingsMonitoring(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
ChannelDisableThreshold: '', ChannelDisableThreshold: '',
@@ -21,7 +23,7 @@ export default function SettingsMonitoring(props) {
function onSubmit() { function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow); const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么'); if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => { const requestQueue = updateArray.map((item) => {
let value = ''; let value = '';
if (typeof inputs[item.key] === 'boolean') { if (typeof inputs[item.key] === 'boolean') {
@@ -40,13 +42,13 @@ export default function SettingsMonitoring(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试'); if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
} }
showSuccess('保存成功'); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
}) })
.catch(() => { .catch(() => {
showError('保存失败,请重试'); showError(t('保存失败,请重试'));
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
@@ -64,6 +66,7 @@ export default function SettingsMonitoring(props) {
setInputsRow(structuredClone(currentInputs)); setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs); refForm.current.setValues(currentInputs);
}, [props.options]); }, [props.options]);
return ( return (
<> <>
<Spin spinning={loading}> <Spin spinning={loading}>
@@ -72,15 +75,15 @@ export default function SettingsMonitoring(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)} getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }} style={{ marginBottom: 15 }}
> >
<Form.Section text={'监控设置'}> <Form.Section text={t('监控设置')}>
<Row gutter={16}> <Row gutter={16}>
<Col span={8}> <Col span={8}>
<Form.InputNumber <Form.InputNumber
label={'最长响应时间'} label={t('最长响应时间')}
step={1} step={1}
min={0} min={0}
suffix={'秒'} suffix={t('秒')}
extraText={'当运行通道全部测试时,超过此时间将自动禁用通道'} extraText={t('当运行通道全部测试时,超过此时间将自动禁用通道')}
placeholder={''} placeholder={''}
field={'ChannelDisableThreshold'} field={'ChannelDisableThreshold'}
onChange={(value) => onChange={(value) =>
@@ -93,11 +96,11 @@ export default function SettingsMonitoring(props) {
</Col> </Col>
<Col span={8}> <Col span={8}>
<Form.InputNumber <Form.InputNumber
label={'额度提醒阈值'} label={t('额度提醒阈值')}
step={1} step={1}
min={0} min={0}
suffix={'Token'} suffix={'Token'}
extraText={'低于此额度时将发送邮件提醒用户'} extraText={t('低于此额度时将发送邮件提醒用户')}
placeholder={''} placeholder={''}
field={'QuotaRemindThreshold'} field={'QuotaRemindThreshold'}
onChange={(value) => onChange={(value) =>
@@ -113,7 +116,7 @@ export default function SettingsMonitoring(props) {
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'AutomaticDisableChannelEnabled'} field={'AutomaticDisableChannelEnabled'}
label={'失败时自动禁用通道'} label={t('失败时自动禁用通道')}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -128,7 +131,7 @@ export default function SettingsMonitoring(props) {
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'AutomaticEnableChannelEnabled'} field={'AutomaticEnableChannelEnabled'}
label={'成功时自动启用通道'} label={t('成功时自动启用通道')}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -143,7 +146,7 @@ export default function SettingsMonitoring(props) {
</Row> </Row>
<Row> <Row>
<Button size='default' onClick={onSubmit}> <Button size='default' onClick={onSubmit}>
保存监控设置 {t('保存监控设置')}
</Button> </Button>
</Row> </Row>
</Form.Section> </Form.Section>

View File

@@ -7,8 +7,10 @@ import {
showSuccess, showSuccess,
showWarning, showWarning,
} from '../../../helpers'; } from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function SettingsSensitiveWords(props) { export default function SettingsSensitiveWords(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
CheckSensitiveEnabled: false, CheckSensitiveEnabled: false,
@@ -20,7 +22,7 @@ export default function SettingsSensitiveWords(props) {
function onSubmit() { function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow); const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning('你似乎并没有修改什么'); if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => { const requestQueue = updateArray.map((item) => {
let value = ''; let value = '';
if (typeof inputs[item.key] === 'boolean') { if (typeof inputs[item.key] === 'boolean') {
@@ -39,13 +41,13 @@ export default function SettingsSensitiveWords(props) {
if (requestQueue.length === 1) { if (requestQueue.length === 1) {
if (res.includes(undefined)) return; if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) { } else if (requestQueue.length > 1) {
if (res.includes(undefined)) return showError('部分保存失败,请重试'); if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
} }
showSuccess('保存成功'); showSuccess(t('保存成功'));
props.refresh(); props.refresh();
}) })
.catch(() => { .catch(() => {
showError('保存失败,请重试'); showError(t('保存失败,请重试'));
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
@@ -71,12 +73,12 @@ export default function SettingsSensitiveWords(props) {
getFormApi={(formAPI) => (refForm.current = formAPI)} getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }} style={{ marginBottom: 15 }}
> >
<Form.Section text={'屏蔽词过滤设置'}> <Form.Section text={t('屏蔽词过滤设置')}>
<Row gutter={16}> <Row gutter={16}>
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'CheckSensitiveEnabled'} field={'CheckSensitiveEnabled'}
label={'启用屏蔽词过滤功能'} label={t('启用屏蔽词过滤功能')}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -91,7 +93,7 @@ export default function SettingsSensitiveWords(props) {
<Col span={8}> <Col span={8}>
<Form.Switch <Form.Switch
field={'CheckSensitiveOnPromptEnabled'} field={'CheckSensitiveOnPromptEnabled'}
label={'启用 Prompt 检查'} label={t('启用 Prompt 检查')}
size='default' size='default'
checkedText='' checkedText=''
uncheckedText='' uncheckedText=''
@@ -107,9 +109,9 @@ export default function SettingsSensitiveWords(props) {
<Row> <Row>
<Col span={16}> <Col span={16}>
<Form.TextArea <Form.TextArea
label={'屏蔽词列表'} label={t('屏蔽词列表')}
extraText={'一行一个屏蔽词,不需要符号分割'} extraText={t('一行一个屏蔽词,不需要符号分割')}
placeholder={'一行一个屏蔽词,不需要符号分割'} placeholder={t('一行一个屏蔽词,不需要符号分割')}
field={'SensitiveWords'} field={'SensitiveWords'}
onChange={(value) => onChange={(value) =>
setInputs({ setInputs({
@@ -124,7 +126,7 @@ export default function SettingsSensitiveWords(props) {
</Row> </Row>
<Row> <Row>
<Button size='default' onClick={onSubmit}> <Button size='default' onClick={onSubmit}>
保存屏蔽词过滤设置 {t('保存屏蔽词过滤设置')}
</Button> </Button>
</Row> </Row>
</Form.Section> </Form.Section>

View File

@@ -1,19 +1,22 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Layout, TabPane, Tabs } from '@douyinfe/semi-ui'; import { Layout, TabPane, Tabs } from '@douyinfe/semi-ui';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import SystemSetting from '../../components/SystemSetting'; import SystemSetting from '../../components/SystemSetting';
import { isRoot } from '../../helpers'; import { isRoot } from '../../helpers';
import OtherSetting from '../../components/OtherSetting'; import OtherSetting from '../../components/OtherSetting';
import PersonalSetting from '../../components/PersonalSetting'; import PersonalSetting from '../../components/PersonalSetting';
import OperationSetting from '../../components/OperationSetting'; import OperationSetting from '../../components/OperationSetting';
const Setting = () => { const Setting = () => {
const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [tabActiveKey, setTabActiveKey] = useState('1'); const [tabActiveKey, setTabActiveKey] = useState('1');
let panes = [ let panes = [
{ {
tab: '个人设置', tab: t('个人设置'),
content: <PersonalSetting />, content: <PersonalSetting />,
itemKey: 'personal', itemKey: 'personal',
}, },
@@ -21,17 +24,17 @@ const Setting = () => {
if (isRoot()) { if (isRoot()) {
panes.push({ panes.push({
tab: '运营设置', tab: t('运营设置'),
content: <OperationSetting />, content: <OperationSetting />,
itemKey: 'operation', itemKey: 'operation',
}); });
panes.push({ panes.push({
tab: '系统设置', tab: t('系统设置'),
content: <SystemSetting />, content: <SystemSetting />,
itemKey: 'system', itemKey: 'system',
}); });
panes.push({ panes.push({
tab: '其他设置', tab: t('其他设置'),
content: <OtherSetting />, content: <OtherSetting />,
itemKey: 'other', itemKey: 'other',
}); });

View File

@@ -1,20 +1,24 @@
import React from 'react'; import React from 'react';
import TokensTable from '../../components/TokensTable'; import TokensTable from '../../components/TokensTable';
import { Banner, Layout } from '@douyinfe/semi-ui'; import { Banner, Layout } from '@douyinfe/semi-ui';
const Token = () => ( import { useTranslation } from 'react-i18next';
<> const Token = () => {
<Layout> const { t } = useTranslation();
<Layout.Header> return (
<>
<Layout>
<Layout.Header>
<Banner <Banner
type='warning' type='warning'
description='令牌无法精确控制使用额度,请勿直接将令牌分发给用户。' description={t('令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。')}
/> />
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<TokensTable /> <TokensTable />
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</> </>
); );
};
export default Token; export default Token;

View File

@@ -21,8 +21,10 @@ import {
import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import Text from '@douyinfe/semi-ui/lib/es/typography/text'; import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
const TopUp = () => { const TopUp = () => {
const { t } = useTranslation();
const [redemptionCode, setRedemptionCode] = useState(''); const [redemptionCode, setRedemptionCode] = useState('');
const [topUpCode, setTopUpCode] = useState(''); const [topUpCode, setTopUpCode] = useState('');
const [topUpCount, setTopUpCount] = useState(0); const [topUpCount, setTopUpCount] = useState(0);
@@ -38,7 +40,7 @@ const TopUp = () => {
const topUp = async () => { const topUp = async () => {
if (redemptionCode === '') { if (redemptionCode === '') {
showInfo('请输入兑换码!'); showInfo(t('请输入兑换码!'));
return; return;
} }
setIsSubmitting(true); setIsSubmitting(true);
@@ -48,10 +50,10 @@ const TopUp = () => {
}); });
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
showSuccess('兑换成功!'); showSuccess(t('兑换成功!'));
Modal.success({ Modal.success({
title: '兑换成功!', title: t('兑换成功!'),
content: '成功兑换额度:' + renderQuota(data), content: t('成功兑换额度:') + renderQuota(data),
centered: true, centered: true,
}); });
setUserQuota((quota) => { setUserQuota((quota) => {
@@ -62,7 +64,7 @@ const TopUp = () => {
showError(message); showError(message);
} }
} catch (err) { } catch (err) {
showError('请求失败'); showError(t('请求失败'));
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@@ -70,7 +72,7 @@ const TopUp = () => {
const openTopUpLink = () => { const openTopUpLink = () => {
if (!topUpLink) { if (!topUpLink) {
showError('超级管理员未设置充值链接!'); showError(t('超级管理员未设置充值链接!'));
return; return;
} }
window.open(topUpLink, '_blank'); window.open(topUpLink, '_blank');
@@ -78,12 +80,12 @@ const TopUp = () => {
const preTopUp = async (payment) => { const preTopUp = async (payment) => {
if (!enableOnlineTopUp) { if (!enableOnlineTopUp) {
showError('管理员未开启在线充值!'); showError(t('管理员未开启在线充值!'));
return; return;
} }
await getAmount(); await getAmount();
if (topUpCount < minTopUp) { if (topUpCount < minTopUp) {
showError('充值数量不能小于' + minTopUp); showError(t('充值数量不能小于') + minTopUp);
return; return;
} }
setPayWay(payment); setPayWay(payment);
@@ -174,7 +176,7 @@ const TopUp = () => {
const renderAmount = () => { const renderAmount = () => {
// console.log(amount); // console.log(amount);
return amount + '元'; return amount + ' ' + t('元');
}; };
const getAmount = async (value) => { const getAmount = async (value) => {
@@ -214,11 +216,11 @@ const TopUp = () => {
<div> <div>
<Layout> <Layout>
<Layout.Header> <Layout.Header>
<h3>我的钱包</h3> <h3>{t('我的钱包')}</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<Modal <Modal
title='确定要充值吗' title={t('确定要充值吗')}
visible={open} visible={open}
onOk={onlineTopUp} onOk={onlineTopUp}
onCancel={handleCancel} onCancel={handleCancel}
@@ -226,24 +228,24 @@ const TopUp = () => {
size={'small'} size={'small'}
centered={true} centered={true}
> >
<p>充值数量{topUpCount}</p> <p>{t('充值数量')}{topUpCount}</p>
<p>实付金额{renderAmount()}</p> <p>{t('实付金额')}{renderAmount()}</p>
<p>是否确认充值</p> <p>{t('是否确认充值?')}</p>
</Modal> </Modal>
<div <div
style={{ marginTop: 20, display: 'flex', justifyContent: 'center' }} style={{ marginTop: 20, display: 'flex', justifyContent: 'center' }}
> >
<Card style={{ width: '500px', padding: '20px' }}> <Card style={{ width: '500px', padding: '20px' }}>
<Title level={3} style={{ textAlign: 'center' }}> <Title level={3} style={{ textAlign: 'center' }}>
余额 {renderQuota(userQuota)} {t('余额')} {renderQuota(userQuota)}
</Title> </Title>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Divider>兑换余额</Divider> <Divider>{t('兑换余额')}</Divider>
<Form> <Form>
<Form.Input <Form.Input
field={'redemptionCode'} field={'redemptionCode'}
label={'兑换码'} label={t('兑换码')}
placeholder='兑换码' placeholder={t('兑换码')}
name='redemptionCode' name='redemptionCode'
value={redemptionCode} value={redemptionCode}
onChange={(value) => { onChange={(value) => {
@@ -257,7 +259,7 @@ const TopUp = () => {
theme={'solid'} theme={'solid'}
onClick={openTopUpLink} onClick={openTopUpLink}
> >
获取兑换码 {t('获取兑换码')}
</Button> </Button>
) : null} ) : null}
<Button <Button
@@ -266,21 +268,19 @@ const TopUp = () => {
onClick={topUp} onClick={topUp}
disabled={isSubmitting} disabled={isSubmitting}
> >
{isSubmitting ? '兑换中...' : '兑换'} {isSubmitting ? t('兑换中...') : t('兑换')}
</Button> </Button>
</Space> </Space>
</Form> </Form>
</div> </div>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Divider>在线充值</Divider> <Divider>{t('在线充值')}</Divider>
<Form> <Form>
<Form.Input <Form.Input
disabled={!enableOnlineTopUp} disabled={!enableOnlineTopUp}
field={'redemptionCount'} field={'redemptionCount'}
label={'实付金额:' + renderAmount()} label={t('实付金额:') + ' ' + renderAmount()}
placeholder={ placeholder={t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)}
'充值数量,最低 ' + renderQuotaWithAmount(minTopUp)
}
name='redemptionCount' name='redemptionCount'
type={'number'} type={'number'}
value={topUpCount} value={topUpCount}
@@ -300,7 +300,7 @@ const TopUp = () => {
preTopUp('zfb'); preTopUp('zfb');
}} }}
> >
支付宝 {t('支付宝')}
</Button> </Button>
<Button <Button
style={{ style={{
@@ -312,7 +312,7 @@ const TopUp = () => {
preTopUp('wx'); preTopUp('wx');
}} }}
> >
微信 {t('微信')}
</Button> </Button>
</Space> </Space>
</Form> </Form>

View File

@@ -14,6 +14,7 @@ import {
Spin, Spin,
Typography, Typography,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const EditUser = (props) => { const EditUser = (props) => {
const userId = props.editingUser.id; const userId = props.editingUser.id;
@@ -120,11 +121,13 @@ const EditUser = (props) => {
setIsModalOpen(true); setIsModalOpen(true);
}; };
const { t } = useTranslation();
return ( return (
<> <>
<SideSheet <SideSheet
placement={'right'} placement={'right'}
title={<Title level={3}>{'编辑用户'}</Title>} title={<Title level={3}>{t('编辑用户')}</Title>}
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
visible={props.visible} visible={props.visible}
@@ -132,7 +135,7 @@ const EditUser = (props) => {
<div style={{ display: 'flex', justifyContent: 'flex-end' }}> <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space> <Space>
<Button theme='solid' size={'large'} onClick={submit}> <Button theme='solid' size={'large'} onClick={submit}>
提交 {t('提交')}
</Button> </Button>
<Button <Button
theme='solid' theme='solid'
@@ -140,7 +143,7 @@ const EditUser = (props) => {
type={'tertiary'} type={'tertiary'}
onClick={handleCancel} onClick={handleCancel}
> >
取消 {t('取消')}
</Button> </Button>
</Space> </Space>
</div> </div>
@@ -151,35 +154,35 @@ const EditUser = (props) => {
> >
<Spin spinning={loading}> <Spin spinning={loading}>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>用户名</Typography.Text> <Typography.Text>{t('用户名')}</Typography.Text>
</div> </div>
<Input <Input
label='用户名' label={t('用户名')}
name='username' name='username'
placeholder={'请输入新的用户名'} placeholder={t('请输入新的用户名')}
onChange={(value) => handleInputChange('username', value)} onChange={(value) => handleInputChange('username', value)}
value={username} value={username}
autoComplete='new-password' autoComplete='new-password'
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>密码</Typography.Text> <Typography.Text>{t('密码')}</Typography.Text>
</div> </div>
<Input <Input
label='密码' label={t('密码')}
name='password' name='password'
type={'password'} type={'password'}
placeholder={'请输入新的密码,最短 8 位'} placeholder={t('请输入新的密码,最短 8 位')}
onChange={(value) => handleInputChange('password', value)} onChange={(value) => handleInputChange('password', value)}
value={password} value={password}
autoComplete='new-password' autoComplete='new-password'
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>显示名称</Typography.Text> <Typography.Text>{t('显示名称')}</Typography.Text>
</div> </div>
<Input <Input
label='显示名称' label={t('显示名称')}
name='display_name' name='display_name'
placeholder={'请输入新的显示名称'} placeholder={t('请输入新的显示名称')}
onChange={(value) => handleInputChange('display_name', value)} onChange={(value) => handleInputChange('display_name', value)}
value={display_name} value={display_name}
autoComplete='new-password' autoComplete='new-password'
@@ -187,76 +190,76 @@ const EditUser = (props) => {
{userId && ( {userId && (
<> <>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>分组</Typography.Text> <Typography.Text>{t('分组')}</Typography.Text>
</div> </div>
<Select <Select
placeholder={'请选择分组'} placeholder={t('请选择分组')}
name='group' name='group'
fluid fluid
search search
selection selection
allowAdditions allowAdditions
additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'} additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
onChange={(value) => handleInputChange('group', value)} onChange={(value) => handleInputChange('group', value)}
value={inputs.group} value={inputs.group}
autoComplete='new-password' autoComplete='new-password'
optionList={groupOptions} optionList={groupOptions}
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>{`剩余额度${renderQuotaWithPrompt(quota)}`}</Typography.Text> <Typography.Text>{`${t('剩余额度')}${renderQuotaWithPrompt(quota)}`}</Typography.Text>
</div> </div>
<Space> <Space>
<Input <Input
name='quota' name='quota'
placeholder={'请输入新的剩余额度'} placeholder={t('请输入新的剩余额度')}
onChange={(value) => handleInputChange('quota', value)} onChange={(value) => handleInputChange('quota', value)}
value={quota} value={quota}
type={'number'} type={'number'}
autoComplete='new-password' autoComplete='new-password'
/> />
<Button onClick={openAddQuotaModal}>添加额度</Button> <Button onClick={openAddQuotaModal}>{t('添加额度')}</Button>
</Space> </Space>
</> </>
)} )}
<Divider style={{ marginTop: 20 }}>以下信息不可修改</Divider> <Divider style={{ marginTop: 20 }}>{t('以下信息不可修改')}</Divider>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的 GitHub 账户</Typography.Text> <Typography.Text>{t('已绑定的 GitHub 账户')}</Typography.Text>
</div> </div>
<Input <Input
name='github_id' name='github_id'
value={github_id} value={github_id}
autoComplete='new-password' autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改' placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
readonly readonly
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的微信账户</Typography.Text> <Typography.Text>{t('已绑定的微信账户')}</Typography.Text>
</div> </div>
<Input <Input
name='wechat_id' name='wechat_id'
value={wechat_id} value={wechat_id}
autoComplete='new-password' autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改' placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
readonly readonly
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的邮箱账户</Typography.Text> <Typography.Text>{t('已绑定的邮箱账户')}</Typography.Text>
</div> </div>
<Input <Input
name='email' name='email'
value={email} value={email}
autoComplete='new-password' autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改' placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
readonly readonly
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的Telegram账户</Typography.Text> <Typography.Text>{t('已绑定的Telegram账户')}</Typography.Text>
</div> </div>
<Input <Input
name='telegram_id' name='telegram_id'
value={telegram_id} value={telegram_id}
autoComplete='new-password' autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改' placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
readonly readonly
/> />
</Spin> </Spin>
@@ -272,11 +275,11 @@ const EditUser = (props) => {
closable={null} closable={null}
> >
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>{`新额度${renderQuota(quota)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(quota + parseInt(addQuotaLocal))}`}</Typography.Text> <Typography.Text>{`${t('新额度')}${renderQuota(quota)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(quota + parseInt(addQuotaLocal))}`}</Typography.Text>
</div> </div>
<Input <Input
name='addQuotaLocal' name='addQuotaLocal'
placeholder={'需要添加的额度(支持负数)'} placeholder={t('需要添加的额度(支持负数)')}
onChange={(value) => { onChange={(value) => {
setAddQuotaLocal(value); setAddQuotaLocal(value);
}} }}

View File

@@ -1,18 +1,22 @@
import React from 'react'; import React from 'react';
import UsersTable from '../../components/UsersTable'; import UsersTable from '../../components/UsersTable';
import { Layout } from '@douyinfe/semi-ui'; import { Layout } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const User = () => ( const User = () => {
<> const { t } = useTranslation();
<Layout> return (
<Layout.Header> <>
<h3>管理用户</h3> <Layout>
<Layout.Header>
<h3>{t('管理用户')}</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<UsersTable /> <UsersTable />
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</> </>
); );
};
export default User; export default User;

View File

@@ -26,6 +26,7 @@ export default defineConfig({
esbuildOptions: { esbuildOptions: {
loader: { loader: {
'.js': 'jsx', '.js': 'jsx',
'.json': 'json',
}, },
}, },
}, },
@@ -45,6 +46,7 @@ export default defineConfig({
'react-toastify', 'react-toastify',
'react-turnstile', 'react-turnstile',
], ],
'i18n': ['i18next', 'react-i18next', 'i18next-browser-languagedetector'],
}, },
}, },
}, },