Merge remote-tracking branch 'origin/alpha' into alpha
This commit is contained in:
@@ -5,3 +5,4 @@
|
|||||||
.gitignore
|
.gitignore
|
||||||
Makefile
|
Makefile
|
||||||
docs
|
docs
|
||||||
|
.eslintcache
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ web/dist
|
|||||||
one-api
|
one-api
|
||||||
.DS_Store
|
.DS_Store
|
||||||
tiktoken_cache
|
tiktoken_cache
|
||||||
|
.eslintcache
|
||||||
240
LICENSE
240
LICENSE
@@ -1,201 +1,103 @@
|
|||||||
Apache License
|
# **New API 许可协议 (Licensing)**
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
本项目采用**基于使用场景的双重许可 (Usage-Based Dual Licensing)** 模式。
|
||||||
|
|
||||||
1. Definitions.
|
**核心原则:**
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
- **默认许可:** 本项目默认在 **GNU Affero 通用公共许可证 v3.0 (AGPLv3)** 下提供。任何用户在遵守 AGPLv3 条款和下述附加限制的前提下,均可免费使用。
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
- **商业许可:** 在特定商业场景下,或当您希望获得 AGPLv3 之外的权利时,**必须**获取**商业许可证 (Commercial License)**。
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
---
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
## **1. 开源许可证 (Open Source License): AGPLv3 - 适用于基础使用**
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
- 在遵守 **AGPLv3** 条款的前提下,您可以自由地使用、修改和分发 New API。AGPLv3 的完整文本可以访问 [https://www.gnu.org/licenses/agpl-3.0.html](https://www.gnu.org/licenses/agpl-3.0.html) 获取。
|
||||||
exercising permissions granted by this License.
|
- **核心义务:** AGPLv3 的一个关键要求是,如果您修改了 New API 并通过网络提供服务 (SaaS),或者分发了修改后的版本,您必须以 AGPLv3 许可证向所有用户提供相应的**完整源代码**。
|
||||||
|
- **附加限制 (重要):** 在仅使用 AGPLv3 开源许可证的情况下,您**必须**完整保留项目代码中原有的品牌标识、LOGO 及版权声明信息。**禁止以任何形式修改、移除或遮盖**这些信息。如需移除,必须获取商业许可证。
|
||||||
|
- 使用前请务必仔细阅读并理解 AGPLv3 的所有条款及上述附加限制。
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
## **2. 商业许可证 (Commercial License) - 适用于高级场景及闭源需求**
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
在以下任一情况下,您**必须**联系我们获取并签署一份商业许可证,才能合法使用 New API:
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
- **场景一:移除品牌和版权信息**
|
||||||
Object form, made available under the License, as indicated by a
|
您希望在您的产品或服务中移除 New API 的 LOGO、UI界面中的版权声明或其他品牌标识。
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
- **场景二:规避 AGPLv3 开源义务**
|
||||||
form, that is based on (or derived from) the Work and for which the
|
您基于 New API 进行了修改,并希望:
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
- 通过网络提供服务(SaaS),但**不希望**向您的服务用户公开您修改后的源代码。
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
- 分发一个集成了 New API 的软件产品,但**不希望**以 AGPLv3 许可证发布您的产品或公开源代码。
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
- **场景三:企业政策与集成需求**
|
||||||
the original version of the Work and any modifications or additions
|
- 您所在公司的政策、客户合同或项目要求不允许使用 AGPLv3 许可的软件。
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
- 您需要进行 OEM 集成,将 New API 作为您闭源商业产品的一部分进行再分发。
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
- **场景四:需要商业支持与保障**
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
您需要 AGPLv3 未提供的商业保障,如官方技术支持等。
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
**获取商业许可:**
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
请通过电子邮件 **support@quantumnous.com** 联系 New API 团队洽谈商业授权事宜。
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
## **3. 贡献 (Contributions)**
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
- 我们欢迎社区对 New API 的贡献。所有向本项目提交的贡献(例如通过 Pull Request)都将被视为在 **AGPLv3** 许可证下提供。
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
- 通过向本项目提交贡献,即表示您同意您的代码以 AGPLv3 许可证授权给本项目及所有后续使用者(无论这些使用者最终遵循 AGPLv3 还是商业许可)。
|
||||||
modifications, and in Source or Object form, provided that You
|
- 您也理解并同意,您的贡献可能会被包含在根据商业许可证分发的 New API 版本中。
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
## **4. 其他条款 (Other Terms)**
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
- 关于商业许可证的具体条款、条件和价格,以双方签署的正式商业许可协议为准。
|
||||||
stating that You changed the files; and
|
- 项目维护者保留根据需要更新本许可政策的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
---
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
# **New API Licensing**
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
This project uses a **Usage-Based Dual Licensing** model.
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
**Core Principles:**
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
- **Default License:** This project is available by default under the **GNU Affero General Public License v3.0 (AGPLv3)**. Any user may use it free of charge, provided they comply with both the AGPLv3 terms and the additional restrictions listed below.
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
- **Commercial License:** For specific commercial scenarios, or if you require rights beyond those granted by AGPLv3, you **must** obtain a **Commercial License**.
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
---
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
## **1. Open Source License: AGPLv3 – For Basic Usage**
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
- Under the terms of the **AGPLv3**, you are free to use, modify, and distribute New API. The complete AGPLv3 license text can be viewed at [https://www.gnu.org/licenses/agpl-3.0.html](https://www.gnu.org/licenses/agpl-3.0.html).
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
- **Core Obligation:** A key AGPLv3 requirement is that if you modify New API and provide it as a network service (SaaS), or distribute a modified version, you must make the **complete corresponding source code** available to all users under the AGPLv3 license.
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
- **Additional Restriction (Important):** When using only the AGPLv3 open-source license, you **must** retain all original branding, logos, and copyright statements within the project’s code. **You are strictly prohibited from modifying, removing, or concealing** any such information. If you wish to remove this, you must obtain a Commercial License.
|
||||||
or other liability obligations and/or rights consistent with this
|
- Please read and ensure that you fully understand all AGPLv3 terms and the above additional restriction before use.
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
## **2. Commercial License – For Advanced Scenarios & Closed Source Needs**
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
You **must** contact us to obtain and sign a Commercial License in any of the following scenarios in order to legally use New API:
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
- **Scenario 1: Removal of Branding and Copyright**
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
You wish to remove the New API logo, copyright statement, or other branding elements from your product or service.
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
- **Scenario 2: Avoidance of AGPLv3 Open Source Obligations**
|
||||||
|
You have modified New API and wish to:
|
||||||
|
- Offer it as a network service (SaaS) **without** disclosing your modifications' source code to your users.
|
||||||
|
- Distribute a software product integrated with New API **without** releasing your product under AGPLv3 or open-sourcing the code.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
- **Scenario 3: Enterprise Policy & Integration Needs**
|
||||||
you may not use this file except in compliance with the License.
|
- Your organization’s policies, client contracts, or project requirements prohibit the use of AGPLv3-licensed software.
|
||||||
You may obtain a copy of the License at
|
- You require OEM integration and need to redistribute New API as part of your closed-source commercial product.
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
- **Scenario 4: Commercial Support and Assurances**
|
||||||
|
You require commercial assurances not provided by AGPLv3, such as official technical support.
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
**Obtaining a Commercial License:**
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
Please contact the New API team via email at **support@quantumnous.com** to discuss commercial licensing.
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
## **3. Contributions**
|
||||||
limitations under the License.
|
|
||||||
|
- We welcome community contributions to New API. All contributions (e.g., via Pull Request) are deemed to be provided under the **AGPLv3** license.
|
||||||
|
- By submitting a contribution, you agree that your code is licensed to this project and all downstream users under the AGPLv3 license (regardless of whether those users ultimately operate under AGPLv3 or a Commercial License).
|
||||||
|
- You also acknowledge and agree that your contribution may be included in New API releases distributed under a Commercial License.
|
||||||
|
|
||||||
|
## **4. Other Terms**
|
||||||
|
|
||||||
|
- The specific terms, conditions, and pricing of the Commercial License are governed by the formal commercial license agreement executed by both parties.
|
||||||
|
- Project maintainers reserve the right to update this licensing policy as needed. Updates will be communicated via official project channels (e.g., repository, official website).
|
||||||
|
|||||||
18
README.en.md
18
README.en.md
@@ -189,6 +189,24 @@ If you have any questions, please refer to [Help and Support](https://docs.newap
|
|||||||
- [Issue Feedback](https://docs.newapi.pro/support/feedback-issues)
|
- [Issue Feedback](https://docs.newapi.pro/support/feedback-issues)
|
||||||
- [FAQ](https://docs.newapi.pro/support/faq)
|
- [FAQ](https://docs.newapi.pro/support/faq)
|
||||||
|
|
||||||
|
## 🤝 Trusted Partners
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.cherry-ai.com/" target="_blank"><img
|
||||||
|
src="./docs/images/cherry-studio.svg" alt="Cherry Studio" height="58"
|
||||||
|
/></a>
|
||||||
|
|
||||||
|
<a href="https://bda.pku.edu.cn/" target="_blank"><img
|
||||||
|
src="./docs/images/pku.png" alt="Peking University" height="58"
|
||||||
|
/></a>
|
||||||
|
|
||||||
|
<a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target="_blank"><img
|
||||||
|
src="./docs/images/ucloud.svg" alt="UCloud" height="58"
|
||||||
|
/></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center"><em>No particular order</em></p>
|
||||||
|
|
||||||
## 🌟 Star History
|
## 🌟 Star History
|
||||||
|
|
||||||
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -188,6 +188,24 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
|
|||||||
- [反馈问题](https://docs.newapi.pro/support/feedback-issues)
|
- [反馈问题](https://docs.newapi.pro/support/feedback-issues)
|
||||||
- [常见问题](https://docs.newapi.pro/support/faq)
|
- [常见问题](https://docs.newapi.pro/support/faq)
|
||||||
|
|
||||||
|
## 🤝 我们信任的合作伙伴
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.cherry-ai.com/" target="_blank"><img
|
||||||
|
src="./docs/images/cherry-studio.svg" alt="Cherry Studio" height="58"
|
||||||
|
/></a>
|
||||||
|
|
||||||
|
<a href="https://bda.pku.edu.cn/" target="_blank"><img
|
||||||
|
src="./docs/images/pku.png" alt="北京大学" height="58"
|
||||||
|
/></a>
|
||||||
|
|
||||||
|
<a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target="_blank"><img
|
||||||
|
src="./docs/images/ucloud.svg" alt="UCloud 优刻得" height="58"
|
||||||
|
/></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center"><em>排名不分先后</em></p>
|
||||||
|
|
||||||
## 🌟 Star History
|
## 🌟 Star History
|
||||||
|
|
||||||
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
||||||
|
|||||||
@@ -114,3 +114,23 @@ type KlingImage2VideoRequest struct {
|
|||||||
CallbackURL string `json:"callback_url,omitempty" example:"https://your.domain/callback"`
|
CallbackURL string `json:"callback_url,omitempty" example:"https://your.domain/callback"`
|
||||||
ExternalTaskId string `json:"external_task_id,omitempty" example:"custom-task-002"`
|
ExternalTaskId string `json:"external_task_id,omitempty" example:"custom-task-002"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KlingImage2videoTaskId godoc
|
||||||
|
// @Summary 可灵任务查询--图生视频
|
||||||
|
// @Description Query the status and result of a Kling video generation task by task ID
|
||||||
|
// @Tags Origin
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param task_id path string true "Task ID"
|
||||||
|
// @Router /kling/v1/videos/image2video/{task_id} [get]
|
||||||
|
func KlingImage2videoTaskId(c *gin.Context) {}
|
||||||
|
|
||||||
|
// KlingText2videoTaskId godoc
|
||||||
|
// @Summary 可灵任务查询--文生视频
|
||||||
|
// @Description Query the status and result of a Kling text-to-video generation task by task ID
|
||||||
|
// @Tags Origin
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param task_id path string true "Task ID"
|
||||||
|
// @Router /kling/v1/videos/text2video/{task_id} [get]
|
||||||
|
func KlingText2videoTaskId(c *gin.Context) {}
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/constant"
|
"one-api/constant"
|
||||||
|
"one-api/dto"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/relay"
|
"one-api/relay"
|
||||||
"one-api/relay/channel"
|
"one-api/relay/channel"
|
||||||
|
relaycommon "one-api/relay/common"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,13 +80,21 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
|
|||||||
return fmt.Errorf("readAll failed for task %s: %w", taskId, err)
|
return fmt.Errorf("readAll failed for task %s: %w", taskId, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
taskResult, err := adaptor.ParseTaskResult(responseBody)
|
taskResult := &relaycommon.TaskInfo{}
|
||||||
if err != nil {
|
// try parse as New API response format
|
||||||
|
var responseItems dto.TaskResponse[model.Task]
|
||||||
|
if err = json.Unmarshal(responseBody, &responseItems); err == nil {
|
||||||
|
t := responseItems.Data
|
||||||
|
taskResult.TaskID = t.TaskID
|
||||||
|
taskResult.Status = string(t.Status)
|
||||||
|
taskResult.Url = t.FailReason
|
||||||
|
taskResult.Progress = t.Progress
|
||||||
|
taskResult.Reason = t.FailReason
|
||||||
|
} else if taskResult, err = adaptor.ParseTaskResult(responseBody); err != nil {
|
||||||
return fmt.Errorf("parseTaskResult failed for task %s: %w", taskId, err)
|
return fmt.Errorf("parseTaskResult failed for task %s: %w", taskId, err)
|
||||||
|
} else {
|
||||||
|
task.Data = responseBody
|
||||||
}
|
}
|
||||||
//if taskResult.Code != 0 {
|
|
||||||
// return fmt.Errorf("video task fetch failed for task %s", taskId)
|
|
||||||
//}
|
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
if taskResult.Status == "" {
|
if taskResult.Status == "" {
|
||||||
@@ -128,8 +139,6 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
|
|||||||
if taskResult.Progress != "" {
|
if taskResult.Progress != "" {
|
||||||
task.Progress = taskResult.Progress
|
task.Progress = taskResult.Progress
|
||||||
}
|
}
|
||||||
|
|
||||||
task.Data = responseBody
|
|
||||||
if err := task.Update(); err != nil {
|
if err := task.Update(); err != nil {
|
||||||
common.SysError("UpdateVideoTask task error: " + err.Error())
|
common.SysError("UpdateVideoTask task error: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
55
docs/images/cherry-studio.svg
Normal file
55
docs/images/cherry-studio.svg
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.45 66.73">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #ea5e5d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #23af69;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #ea5756;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="_图层_1-2" data-name="图层_1">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-1" d="M16.72,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
|
||||||
|
<path class="cls-1" d="M32.05,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
|
||||||
|
</g>
|
||||||
|
<path class="cls-2" d="M32.05,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M93.93,24.6l.55-.39c.69-.4,1.17-.61,1.46-.61.63,0,1.3.57,2.03,1.7.44.71.67,1.27.67,1.7s-.14.78-.41,1.06c-.27.28-.59.54-.96.76-.36.22-.71.43-1.05.64-.33.2-1.02.47-2.05.79-1.03.32-2.03.49-2.99.49s-1.93-.13-2.91-.38c-.98-.25-1.99-.68-3.03-1.27-1.04-.6-1.98-1.32-2.81-2.18-.83-.86-1.51-1.96-2.05-3.31-.54-1.35-.8-2.81-.8-4.38s.26-3.01.79-4.29c.53-1.28,1.2-2.35,2.02-3.19.82-.84,1.75-1.54,2.81-2.11,1.98-1.09,3.97-1.64,5.98-1.64.95,0,1.92.15,2.9.44.98.29,1.72.59,2.23.9l.73.42c.36.22.65.4.85.55.53.42.79.91.79,1.44s-.21,1.1-.64,1.68c-.79,1.09-1.5,1.64-2.12,1.64-.36,0-.88-.22-1.55-.67-.85-.69-1.98-1.03-3.4-1.03-1.31,0-2.61.46-3.88,1.36-.61.44-1.11,1.07-1.52,1.88-.4.81-.61,1.72-.61,2.75s.2,1.94.61,2.75c.4.81.92,1.45,1.55,1.91,1.23.89,2.52,1.34,3.85,1.34.63,0,1.22-.08,1.77-.24.56-.16.96-.32,1.2-.49Z"/>
|
||||||
|
<path class="cls-3" d="M114.38,9.07c.16-.3.43-.52.82-.64.38-.12.87-.18,1.46-.18s1.05.05,1.4.15c.34.1.61.22.79.36.18.14.32.34.42.61.1.34.15.87.15,1.58v16.84c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58v-6.16h-8.04v6.19c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V10.92c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v6.19h8.04v-6.22c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8Z"/>
|
||||||
|
<path class="cls-3" d="M127.21,25.1h9.34c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-12.01c-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55V10.9c0-1.03.19-1.73.58-2.11.38-.37,1.11-.56,2.18-.56h11.95c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-9.31v3.06h6.01c.46,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.38,2.25-1.15,2.49-.34.12-.87.18-1.58.18h-5.95v3.06Z"/>
|
||||||
|
<path class="cls-3" d="M196.96,8.79c.99.69,1.49,1.35,1.49,2,0,.38-.23.92-.7,1.61l-6.55,9.8v5.79c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.16.3-.43.52-.82.64-.38.12-.9.18-1.55.18s-1.16-.06-1.55-.18c-.38-.12-.66-.34-.82-.65-.16-.31-.26-.59-.29-.82-.03-.23-.05-.59-.05-1.08v-5.73l-6.55-9.8c-.47-.69-.7-1.22-.7-1.61,0-.65.44-1.27,1.33-1.87.89-.6,1.53-.9,1.91-.9s.69.08.91.24c.34.22.71.64,1.09,1.24l4.7,7.52,4.7-7.52c.38-.61.72-1.01,1-1.2s.61-.29.99-.29.97.25,1.77.76Z"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M81.93,56.63c-.53-.65-.79-1.23-.79-1.74s.43-1.2,1.3-2.05c.51-.49,1.04-.73,1.61-.73s1.36.51,2.37,1.52c.28.34.69.67,1.21.99.53.31,1.01.47,1.46.47,1.88,0,2.82-.77,2.82-2.31,0-.46-.26-.85-.77-1.17-.52-.31-1.16-.54-1.93-.68-.77-.14-1.6-.37-2.49-.68-.89-.31-1.72-.68-2.49-1.11-.77-.42-1.41-1.1-1.93-2.02-.52-.92-.77-2.03-.77-3.32,0-1.78.66-3.33,1.99-4.66s3.13-1.99,5.42-1.99c1.21,0,2.32.16,3.32.47,1,.31,1.69.63,2.08.96l.76.58c.63.59.94,1.08.94,1.49s-.24.96-.73,1.67c-.69,1.01-1.4,1.52-2.12,1.52-.42,0-.95-.2-1.58-.61-.06-.04-.18-.14-.35-.3-.17-.16-.33-.29-.47-.39-.42-.26-.97-.39-1.62-.39s-1.2.16-1.64.47c-.43.31-.65.75-.65,1.3s.26,1.01.77,1.35c.52.34,1.16.58,1.93.7.77.12,1.61.31,2.52.56.91.25,1.75.56,2.52.93.77.36,1.41,1,1.93,1.9.52.9.77,2.01.77,3.32s-.26,2.47-.79,3.47c-.53,1-1.21,1.77-2.06,2.32-1.64,1.07-3.39,1.61-5.25,1.61-.95,0-1.85-.12-2.7-.35-.85-.23-1.54-.52-2.06-.86-1.07-.65-1.82-1.27-2.24-1.88l-.27-.33Z"/>
|
||||||
|
<path class="cls-3" d="M100.74,37.49h16.87c.65,0,1.12.08,1.43.23.3.15.51.39.61.71.1.32.15.75.15,1.27s-.05.95-.15,1.26c-.1.31-.27.53-.52.65-.36.18-.88.27-1.55.27h-5.79v15.26c0,.47-.02.81-.05,1.03s-.12.48-.27.77c-.15.29-.42.5-.8.62-.38.12-.89.18-1.52.18s-1.13-.06-1.5-.18c-.37-.12-.64-.33-.79-.62-.15-.29-.24-.56-.27-.79-.03-.23-.05-.58-.05-1.05v-15.23h-5.82c-.65,0-1.12-.08-1.43-.23-.3-.15-.51-.39-.61-.71-.1-.32-.15-.75-.15-1.27s.05-.95.15-1.26c.1-.31.27-.53.52-.65.36-.18.88-.27,1.55-.27Z"/>
|
||||||
|
<path class="cls-3" d="M135.99,38.34c.2-.32.5-.55.88-.67.38-.12.86-.18,1.44-.18s1.04.05,1.38.15c.34.1.61.22.79.36.18.14.31.35.39.64.12.34.18.87.18,1.58v9.16c0,2.67-.83,5.1-2.49,7.28-.81,1.03-1.85,1.87-3.12,2.5s-2.68.96-4.23.96-2.95-.32-4.22-.97c-1.26-.65-2.29-1.5-3.08-2.55-1.64-2.14-2.46-4.57-2.46-7.28v-9.13c0-.49.02-.84.05-1.08.03-.23.13-.5.29-.8.16-.3.43-.52.82-.64.38-.12.9-.18,1.55-.18s1.16.06,1.55.18c.38.12.65.33.79.64.24.47.36,1.1.36,1.91v9.1c0,1.23.3,2.41.91,3.52.3.57.76,1.02,1.37,1.36.61.34,1.32.52,2.15.52,1.48,0,2.58-.55,3.31-1.64.73-1.09,1.09-2.36,1.09-3.79v-9.28c0-.79.1-1.34.3-1.67Z"/>
|
||||||
|
<path class="cls-3" d="M146.18,37.49l5.61.03c2.93,0,5.51,1.06,7.74,3.17,2.22,2.11,3.34,4.71,3.34,7.8s-1.09,5.73-3.26,7.93c-2.17,2.2-4.81,3.31-7.9,3.31h-5.55c-1.23,0-2-.25-2.31-.76-.24-.42-.36-1.07-.36-1.94v-16.87c0-.49.02-.84.05-1.06s.13-.49.29-.79c.28-.55,1.07-.82,2.37-.82ZM151.79,54.35c1.46,0,2.77-.54,3.94-1.62,1.17-1.08,1.76-2.44,1.76-4.08s-.57-3.01-1.71-4.11c-1.14-1.1-2.48-1.65-4.02-1.65h-2.91v11.47h2.94Z"/>
|
||||||
|
<path class="cls-3" d="M164.84,40.19c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v16.87c0,.49-.02.84-.05,1.06s-.13.49-.29.79c-.28.55-1.07.82-2.37.82-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55v-16.87Z"/>
|
||||||
|
<path class="cls-3" d="M183.07,37.24c2.99,0,5.59,1.08,7.8,3.25,2.2,2.16,3.31,4.85,3.31,8.05s-1.05,5.94-3.16,8.19c-2.1,2.26-4.69,3.38-7.77,3.38s-5.69-1.11-7.84-3.34c-2.15-2.22-3.23-4.87-3.23-7.95,0-1.68.3-3.25.91-4.72.61-1.47,1.42-2.7,2.43-3.69,1.01-.99,2.17-1.77,3.49-2.34,1.31-.57,2.67-.85,4.07-.85ZM177.55,48.68c0,1.8.58,3.26,1.74,4.38,1.16,1.12,2.46,1.68,3.9,1.68s2.73-.55,3.88-1.64c1.15-1.09,1.73-2.56,1.73-4.4s-.58-3.32-1.74-4.43c-1.16-1.11-2.46-1.67-3.9-1.67s-2.73.56-3.88,1.68c-1.15,1.12-1.73,2.58-1.73,4.38Z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M176.92,11.06c-.03-.23-.13-.5-.29-.8-.28-.55-1.07-.82-2.37-.82h-6.55c-1.78,0-3.51.65-5.19,1.94-.81.63-1.48,1.48-2,2.55-.53,1.07-.79,2.27-.79,3.58,0,2.29.76,4.17,2.28,5.64-.44,1.07-1.13,2.66-2.06,4.76-.3.73-.45,1.25-.45,1.58,0,.77.63,1.42,1.88,1.94.65.28,1.17.43,1.56.43s.72-.1.97-.29c.25-.19.44-.39.56-.59.2-.38.99-2.21,2.37-5.49l.94.06h3.82v3.43c0,.47.02.81.05,1.05.03.23.13.5.29.8.28.55,1.07.82,2.37.82,1.42,0,2.25-.37,2.49-1.12.12-.34.18-.87.18-1.58V12.11c0-.46-.02-.81-.05-1.05ZM172.81,19.44c-.09.14-.48.77-1.24.91-.2.04-.37.03-.48.02-.02.14-.04.26-.06.38-.16.83-.38,1.05-.57,1.07-.29.05-.51-.35-.93-.9-.23.01-.46.02-.69.02-.51,0-1.01-.03-1.49-.09-.25-.03-.5-.07-.74-.11-1.18-.32-2.03-1.27-2.03-2.4v-1.37c0-1.13.86-2.08,2.03-2.4.24-.04.49-.08.74-.11.48-.06.98-.09,1.49-.09s1.01.03,1.49.09c.25.03.5.07.74.11.6.16,1.12.49,1.49.93.34.41.55.92.55,1.47v1.37c0,.23-.01.66-.29,1.1Z"/>
|
||||||
|
<circle class="cls-2" cx="167.24" cy="17.67" r=".49"/>
|
||||||
|
<circle class="cls-2" cx="168.88" cy="17.71" r=".49"/>
|
||||||
|
<circle class="cls-2" cx="170.59" cy="17.71" r=".49"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-3" d="M141.01,8.24c.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82h6.55c1.78,0,3.51.65,5.19,1.94.81.63,1.48,1.48,2,2.55.53,1.07.79,2.27.79,3.58,0,2.29-.76,4.17-2.28,5.64.44,1.07,1.13,2.66,2.06,4.76.3.73.45,1.25.45,1.58,0,.77-.63,1.42-1.88,1.94-.65.28-1.17.43-1.56.43s-.72-.1-.97-.29c-.25-.19-.44-.39-.56-.59-.2-.38-.99-2.21-2.37-5.49l-.94.06h-3.82v3.43c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V9.28c0-.46.02-.81.05-1.05ZM145.12,16.62c.09.14.48.77,1.24.91.2.04.37.03.48.02.02.14.04.26.06.38.16.83.38,1.05.57,1.07.29.05.51-.35.93-.9.23.01.46.02.69.02.51,0,1.01-.03,1.49-.09.25-.03.5-.07.74-.11,1.18-.32,2.03-1.27,2.03-2.4v-1.37c0-1.13-.86-2.08-2.03-2.4-.24-.04-.49-.08-.74-.11-.48-.06-.98-.09-1.49-.09s-1.01.03-1.49.09c-.25.03-.5.07-.74.11-.6.16-1.12.49-1.49.93-.34.41-.55.92-.55,1.47v1.37c0,.23.01.66.29,1.1Z"/>
|
||||||
|
<circle class="cls-2" cx="150.69" cy="14.84" r=".49"/>
|
||||||
|
<circle class="cls-2" cx="149.05" cy="14.89" r=".49"/>
|
||||||
|
<circle class="cls-2" cx="147.35" cy="14.89" r=".49"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 9.5 KiB |
BIN
docs/images/pku.png
Normal file
BIN
docs/images/pku.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
1
docs/images/ucloud.svg
Normal file
1
docs/images/ucloud.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.9 KiB |
@@ -70,7 +70,7 @@
|
|||||||
"关于": "关于",
|
"关于": "关于",
|
||||||
"注销成功!": "注销成功!",
|
"注销成功!": "注销成功!",
|
||||||
"个人设置": "个人设置",
|
"个人设置": "个人设置",
|
||||||
"API令牌": "API令牌",
|
"令牌管理": "令牌管理",
|
||||||
"退出": "退出",
|
"退出": "退出",
|
||||||
"关闭侧边栏": "关闭侧边栏",
|
"关闭侧边栏": "关闭侧边栏",
|
||||||
"打开侧边栏": "打开侧边栏",
|
"打开侧边栏": "打开侧边栏",
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ func KlingRequestConvert() func(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Support both model_name and model fields
|
||||||
model, _ := originalReq["model_name"].(string)
|
model, _ := originalReq["model_name"].(string)
|
||||||
|
if model == "" {
|
||||||
|
model, _ = originalReq["model"].(string)
|
||||||
|
}
|
||||||
prompt, _ := originalReq["prompt"].(string)
|
prompt, _ := originalReq["prompt"].(string)
|
||||||
|
|
||||||
unifiedReq := map[string]interface{}{
|
unifiedReq := map[string]interface{}{
|
||||||
|
|||||||
@@ -44,12 +44,14 @@ type requestPayload struct {
|
|||||||
Duration string `json:"duration,omitempty"`
|
Duration string `json:"duration,omitempty"`
|
||||||
AspectRatio string `json:"aspect_ratio,omitempty"`
|
AspectRatio string `json:"aspect_ratio,omitempty"`
|
||||||
ModelName string `json:"model_name,omitempty"`
|
ModelName string `json:"model_name,omitempty"`
|
||||||
|
Model string `json:"model,omitempty"` // Compatible with upstreams that only recognize "model"
|
||||||
CfgScale float64 `json:"cfg_scale,omitempty"`
|
CfgScale float64 `json:"cfg_scale,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type responsePayload struct {
|
type responsePayload struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
TaskId string `json:"task_id"`
|
||||||
RequestId string `json:"request_id"`
|
RequestId string `json:"request_id"`
|
||||||
Data struct {
|
Data struct {
|
||||||
TaskId string `json:"task_id"`
|
TaskId string `json:"task_id"`
|
||||||
@@ -73,21 +75,16 @@ type responsePayload struct {
|
|||||||
|
|
||||||
type TaskAdaptor struct {
|
type TaskAdaptor struct {
|
||||||
ChannelType int
|
ChannelType int
|
||||||
accessKey string
|
apiKey string
|
||||||
secretKey string
|
|
||||||
baseURL string
|
baseURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *TaskAdaptor) Init(info *relaycommon.TaskRelayInfo) {
|
func (a *TaskAdaptor) Init(info *relaycommon.TaskRelayInfo) {
|
||||||
a.ChannelType = info.ChannelType
|
a.ChannelType = info.ChannelType
|
||||||
a.baseURL = info.BaseUrl
|
a.baseURL = info.BaseUrl
|
||||||
|
a.apiKey = info.ApiKey
|
||||||
|
|
||||||
// apiKey format: "access_key|secret_key"
|
// apiKey format: "access_key|secret_key"
|
||||||
keyParts := strings.Split(info.ApiKey, "|")
|
|
||||||
if len(keyParts) == 2 {
|
|
||||||
a.accessKey = strings.TrimSpace(keyParts[0])
|
|
||||||
a.secretKey = strings.TrimSpace(keyParts[1])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateRequestAndSetAction parses body, validates fields and sets default action.
|
// ValidateRequestAndSetAction parses body, validates fields and sets default action.
|
||||||
@@ -166,27 +163,19 @@ func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *rela
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt Kling response parse first.
|
|
||||||
var kResp responsePayload
|
var kResp responsePayload
|
||||||
if err := json.Unmarshal(responseBody, &kResp); err == nil && kResp.Code == 0 {
|
err = json.Unmarshal(responseBody, &kResp)
|
||||||
c.JSON(http.StatusOK, gin.H{"task_id": kResp.Data.TaskId})
|
if err != nil {
|
||||||
|
taskErr = service.TaskErrorWrapper(err, "unmarshal_response_failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if kResp.Code != 0 {
|
||||||
|
taskErr = service.TaskErrorWrapperLocal(fmt.Errorf(kResp.Message), "task_failed", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
kResp.TaskId = kResp.Data.TaskId
|
||||||
|
c.JSON(http.StatusOK, kResp)
|
||||||
return kResp.Data.TaskId, responseBody, nil
|
return kResp.Data.TaskId, responseBody, nil
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback generic task response.
|
|
||||||
var generic dto.TaskResponse[string]
|
|
||||||
if err := json.Unmarshal(responseBody, &generic); err != nil {
|
|
||||||
taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !generic.IsSuccess() {
|
|
||||||
taskErr = service.TaskErrorWrapper(fmt.Errorf(generic.Message), generic.Code, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"task_id": generic.Data})
|
|
||||||
return generic.Data, responseBody, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchTask fetch task status
|
// FetchTask fetch task status
|
||||||
@@ -239,6 +228,7 @@ func (a *TaskAdaptor) convertToRequestPayload(req *SubmitReq) (*requestPayload,
|
|||||||
Duration: fmt.Sprintf("%d", defaultInt(req.Duration, 5)),
|
Duration: fmt.Sprintf("%d", defaultInt(req.Duration, 5)),
|
||||||
AspectRatio: a.getAspectRatio(req.Size),
|
AspectRatio: a.getAspectRatio(req.Size),
|
||||||
ModelName: req.Model,
|
ModelName: req.Model,
|
||||||
|
Model: req.Model, // Keep consistent with model_name, double writing improves compatibility
|
||||||
CfgScale: 0.5,
|
CfgScale: 0.5,
|
||||||
}
|
}
|
||||||
if r.ModelName == "" {
|
if r.ModelName == "" {
|
||||||
@@ -288,21 +278,25 @@ func defaultInt(v int, def int) int {
|
|||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
func (a *TaskAdaptor) createJWTToken() (string, error) {
|
func (a *TaskAdaptor) createJWTToken() (string, error) {
|
||||||
return a.createJWTTokenWithKeys(a.accessKey, a.secretKey)
|
return a.createJWTTokenWithKey(a.apiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) {
|
||||||
|
// parts := strings.Split(apiKey, "|")
|
||||||
|
// if len(parts) != 2 {
|
||||||
|
// return "", fmt.Errorf("invalid API key format, expected 'access_key,secret_key'")
|
||||||
|
// }
|
||||||
|
// return a.createJWTTokenWithKey(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
|
||||||
|
//}
|
||||||
|
|
||||||
func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) {
|
func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) {
|
||||||
parts := strings.Split(apiKey, "|")
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return "", fmt.Errorf("invalid API key format, expected 'access_key,secret_key'")
|
|
||||||
}
|
|
||||||
return a.createJWTTokenWithKeys(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *TaskAdaptor) createJWTTokenWithKeys(accessKey, secretKey string) (string, error) {
|
keyParts := strings.Split(apiKey, "|")
|
||||||
if accessKey == "" || secretKey == "" {
|
accessKey := strings.TrimSpace(keyParts[0])
|
||||||
return "", fmt.Errorf("access key and secret key are required")
|
if len(keyParts) == 1 {
|
||||||
|
return accessKey, nil
|
||||||
}
|
}
|
||||||
|
secretKey := strings.TrimSpace(keyParts[1])
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
claims := jwt.MapClaims{
|
claims := jwt.MapClaims{
|
||||||
"iss": accessKey,
|
"iss": accessKey,
|
||||||
@@ -315,12 +309,12 @@ func (a *TaskAdaptor) createJWTTokenWithKeys(accessKey, secretKey string) (strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) {
|
func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) {
|
||||||
|
taskInfo := &relaycommon.TaskInfo{}
|
||||||
resPayload := responsePayload{}
|
resPayload := responsePayload{}
|
||||||
err := json.Unmarshal(respBody, &resPayload)
|
err := json.Unmarshal(respBody, &resPayload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to unmarshal response body")
|
return nil, errors.Wrap(err, "failed to unmarshal response body")
|
||||||
}
|
}
|
||||||
taskInfo := &relaycommon.TaskInfo{}
|
|
||||||
taskInfo.Code = resPayload.Code
|
taskInfo.Code = resPayload.Code
|
||||||
taskInfo.TaskID = resPayload.Data.TaskId
|
taskInfo.TaskID = resPayload.Data.TaskId
|
||||||
taskInfo.Reason = resPayload.Message
|
taskInfo.Reason = resPayload.Message
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ func Path2RelayKling(method, path string) int {
|
|||||||
relayMode := RelayModeUnknown
|
relayMode := RelayModeUnknown
|
||||||
if method == http.MethodPost && strings.HasSuffix(path, "/video/generations") {
|
if method == http.MethodPost && strings.HasSuffix(path, "/video/generations") {
|
||||||
relayMode = RelayModeKlingSubmit
|
relayMode = RelayModeKlingSubmit
|
||||||
} else if method == http.MethodGet && strings.Contains(path, "/video/generations/") {
|
} else if method == http.MethodGet && (strings.Contains(path, "/video/generations")) {
|
||||||
relayMode = RelayModeKlingFetchByID
|
relayMode = RelayModeKlingFetchByID
|
||||||
}
|
}
|
||||||
return relayMode
|
return relayMode
|
||||||
|
|||||||
@@ -20,5 +20,7 @@ func SetVideoRouter(router *gin.Engine) {
|
|||||||
{
|
{
|
||||||
klingV1Router.POST("/videos/text2video", controller.RelayTask)
|
klingV1Router.POST("/videos/text2video", controller.RelayTask)
|
||||||
klingV1Router.POST("/videos/image2video", controller.RelayTask)
|
klingV1Router.POST("/videos/image2video", controller.RelayTask)
|
||||||
|
klingV1Router.GET("/videos/text2video/:task_id", controller.RelayTask)
|
||||||
|
klingV1Router.GET("/videos/image2video/:task_id", controller.RelayTask)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
web/.eslintrc.cjs
Normal file
34
web/.eslintrc.cjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2021: true, node: true },
|
||||||
|
parserOptions: { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true } },
|
||||||
|
plugins: ['header', 'react-hooks'],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
rules: {
|
||||||
|
'header/header': [2, 'block', [
|
||||||
|
'',
|
||||||
|
'Copyright (C) 2025 QuantumNous',
|
||||||
|
'',
|
||||||
|
'This program is free software: you can redistribute it and/or modify',
|
||||||
|
'it under the terms of the GNU Affero General Public License as',
|
||||||
|
'published by the Free Software Foundation, either version 3 of the',
|
||||||
|
'License, or (at your option) any later version.',
|
||||||
|
'',
|
||||||
|
'This program is distributed in the hope that it will be useful,',
|
||||||
|
'but WITHOUT ANY WARRANTY; without even the implied warranty of',
|
||||||
|
'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the',
|
||||||
|
'GNU Affero General Public License for more details.',
|
||||||
|
'',
|
||||||
|
'You should have received a copy of the GNU Affero General Public License',
|
||||||
|
'along with this program. If not, see <https://www.gnu.org/licenses/>.',
|
||||||
|
'',
|
||||||
|
'For commercial licensing, please contact support@quantumnous.com',
|
||||||
|
''
|
||||||
|
]],
|
||||||
|
'no-multiple-empty-lines': ['error', { max: 1 }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
163
web/bun.lock
163
web/bun.lock
@@ -46,6 +46,9 @@
|
|||||||
"@so1ve/prettier-config": "^3.1.0",
|
"@so1ve/prettier-config": "^3.1.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"eslint": "8.57.0",
|
||||||
|
"eslint-plugin-header": "^3.1.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"tailwindcss": "^3",
|
"tailwindcss": "^3",
|
||||||
@@ -237,6 +240,14 @@
|
|||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
||||||
|
|
||||||
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
|
||||||
|
|
||||||
|
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
|
||||||
|
|
||||||
|
"@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="],
|
||||||
|
|
||||||
|
"@eslint/js": ["@eslint/js@8.57.0", "", {}, "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g=="],
|
||||||
|
|
||||||
"@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="],
|
"@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="],
|
||||||
|
|
||||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.0", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg=="],
|
"@floating-ui/dom": ["@floating-ui/dom@1.7.0", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg=="],
|
||||||
@@ -249,6 +260,12 @@
|
|||||||
|
|
||||||
"@giscus/react": ["@giscus/react@3.1.0", "", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="],
|
"@giscus/react": ["@giscus/react@3.1.0", "", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.11.14", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="],
|
||||||
|
|
||||||
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||||
|
|
||||||
"@iconify/utils": ["@iconify/utils@2.3.0", "", { "dependencies": { "@antfu/install-pkg": "^1.0.0", "@antfu/utils": "^8.1.0", "@iconify/types": "^2.0.0", "debug": "^4.4.0", "globals": "^15.14.0", "kolorist": "^1.8.0", "local-pkg": "^1.0.0", "mlly": "^1.7.4" } }, "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA=="],
|
"@iconify/utils": ["@iconify/utils@2.3.0", "", { "dependencies": { "@antfu/install-pkg": "^1.0.0", "@antfu/utils": "^8.1.0", "@iconify/types": "^2.0.0", "debug": "^4.4.0", "globals": "^15.14.0", "kolorist": "^1.8.0", "local-pkg": "^1.0.0", "mlly": "^1.7.4" } }, "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA=="],
|
||||||
@@ -629,15 +646,17 @@
|
|||||||
|
|
||||||
"abs-svg-path": ["abs-svg-path@0.1.1", "", {}, "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA=="],
|
"abs-svg-path": ["abs-svg-path@0.1.1", "", {}, "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
||||||
"ahooks": ["ahooks@3.8.5", "", { "dependencies": { "@babel/runtime": "^7.21.0", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Y+MLoJpBXVdjsnnBjE5rOSPkQ4DK+8i5aPDzLJdIOsCpo/fiAeXcBY1Y7oWgtOK0TpOz0gFa/XcyO1UGdoqLcw=="],
|
"ahooks": ["ahooks@3.8.5", "", { "dependencies": { "@babel/runtime": "^7.21.0", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Y+MLoJpBXVdjsnnBjE5rOSPkQ4DK+8i5aPDzLJdIOsCpo/fiAeXcBY1Y7oWgtOK0TpOz0gFa/XcyO1UGdoqLcw=="],
|
||||||
|
|
||||||
"ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||||
|
|
||||||
"ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"antd": ["antd@5.25.2", "", { "dependencies": { "@ant-design/colors": "^7.2.0", "@ant-design/cssinjs": "^1.23.0", "@ant-design/cssinjs-utils": "^1.1.3", "@ant-design/fast-color": "^2.0.6", "@ant-design/icons": "^5.6.1", "@ant-design/react-slick": "~1.1.2", "@babel/runtime": "^7.26.0", "@rc-component/color-picker": "~2.0.1", "@rc-component/mutate-observer": "^1.1.0", "@rc-component/qrcode": "~1.0.0", "@rc-component/tour": "~1.15.1", "@rc-component/trigger": "^2.2.6", "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.11", "rc-cascader": "~3.34.0", "rc-checkbox": "~3.5.0", "rc-collapse": "~3.9.0", "rc-dialog": "~9.6.0", "rc-drawer": "~7.2.0", "rc-dropdown": "~4.2.1", "rc-field-form": "~2.7.0", "rc-image": "~7.12.0", "rc-input": "~1.8.0", "rc-input-number": "~9.5.0", "rc-mentions": "~2.20.0", "rc-menu": "~9.16.1", "rc-motion": "^2.9.5", "rc-notification": "~5.6.4", "rc-pagination": "~5.1.0", "rc-picker": "~4.11.3", "rc-progress": "~4.0.0", "rc-rate": "~2.13.1", "rc-resize-observer": "^1.4.3", "rc-segmented": "~2.7.0", "rc-select": "~14.16.8", "rc-slider": "~11.1.8", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", "rc-table": "~7.50.5", "rc-tabs": "~15.6.1", "rc-textarea": "~1.10.0", "rc-tooltip": "~6.4.0", "rc-tree": "~5.13.1", "rc-tree-select": "~5.27.0", "rc-upload": "~4.9.0", "rc-util": "^5.44.4", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-7R2nUvlHhey7Trx64+hCtGXOiy+DTUs1Lv5bwbV1LzEIZIhWb0at1AM6V3K108a5lyoR9n7DX3ptlLF7uYV/DQ=="],
|
"antd": ["antd@5.25.2", "", { "dependencies": { "@ant-design/colors": "^7.2.0", "@ant-design/cssinjs": "^1.23.0", "@ant-design/cssinjs-utils": "^1.1.3", "@ant-design/fast-color": "^2.0.6", "@ant-design/icons": "^5.6.1", "@ant-design/react-slick": "~1.1.2", "@babel/runtime": "^7.26.0", "@rc-component/color-picker": "~2.0.1", "@rc-component/mutate-observer": "^1.1.0", "@rc-component/qrcode": "~1.0.0", "@rc-component/tour": "~1.15.1", "@rc-component/trigger": "^2.2.6", "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.11", "rc-cascader": "~3.34.0", "rc-checkbox": "~3.5.0", "rc-collapse": "~3.9.0", "rc-dialog": "~9.6.0", "rc-drawer": "~7.2.0", "rc-dropdown": "~4.2.1", "rc-field-form": "~2.7.0", "rc-image": "~7.12.0", "rc-input": "~1.8.0", "rc-input-number": "~9.5.0", "rc-mentions": "~2.20.0", "rc-menu": "~9.16.1", "rc-motion": "^2.9.5", "rc-notification": "~5.6.4", "rc-pagination": "~5.1.0", "rc-picker": "~4.11.3", "rc-progress": "~4.0.0", "rc-rate": "~2.13.1", "rc-resize-observer": "^1.4.3", "rc-segmented": "~2.7.0", "rc-select": "~14.16.8", "rc-slider": "~11.1.8", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", "rc-table": "~7.50.5", "rc-tabs": "~15.6.1", "rc-textarea": "~1.10.0", "rc-tooltip": "~6.4.0", "rc-tree": "~5.13.1", "rc-tree-select": "~5.27.0", "rc-upload": "~4.9.0", "rc-util": "^5.44.4", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-7R2nUvlHhey7Trx64+hCtGXOiy+DTUs1Lv5bwbV1LzEIZIhWb0at1AM6V3K108a5lyoR9n7DX3ptlLF7uYV/DQ=="],
|
||||||
|
|
||||||
@@ -649,6 +668,8 @@
|
|||||||
|
|
||||||
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||||
|
|
||||||
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
"array-source": ["array-source@0.0.4", "", {}, "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw=="],
|
"array-source": ["array-source@0.0.4", "", {}, "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw=="],
|
||||||
|
|
||||||
"assign-symbols": ["assign-symbols@1.0.0", "", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="],
|
"assign-symbols": ["assign-symbols@1.0.0", "", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="],
|
||||||
@@ -699,6 +720,8 @@
|
|||||||
|
|
||||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||||
|
|
||||||
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||||
|
|
||||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||||
@@ -851,6 +874,8 @@
|
|||||||
|
|
||||||
"decode-uri-component": ["decode-uri-component@0.4.1", "", {}, "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ=="],
|
"decode-uri-component": ["decode-uri-component@0.4.1", "", {}, "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ=="],
|
||||||
|
|
||||||
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
|
|
||||||
"delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="],
|
"delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="],
|
||||||
|
|
||||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
@@ -865,6 +890,8 @@
|
|||||||
|
|
||||||
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||||
|
|
||||||
|
"doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
|
||||||
|
|
||||||
"dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="],
|
"dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="],
|
||||||
|
|
||||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||||
@@ -887,7 +914,25 @@
|
|||||||
|
|
||||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
|
"eslint": ["eslint@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.0", "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-header": ["eslint-plugin-header@3.1.1", "", { "peerDependencies": { "eslint": ">=7.7.0" } }, "sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
|
||||||
|
|
||||||
|
"eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
|
||||||
|
|
||||||
|
"eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
|
"espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
|
||||||
|
|
||||||
|
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||||
|
|
||||||
|
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||||
|
|
||||||
|
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||||
|
|
||||||
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
|
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
|
||||||
|
|
||||||
@@ -903,6 +948,8 @@
|
|||||||
|
|
||||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||||
|
|
||||||
|
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||||
|
|
||||||
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
||||||
|
|
||||||
"exsolve": ["exsolve@1.0.5", "", {}, "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg=="],
|
"exsolve": ["exsolve@1.0.5", "", {}, "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg=="],
|
||||||
@@ -917,8 +964,14 @@
|
|||||||
|
|
||||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||||
|
|
||||||
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
|
||||||
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||||
|
|
||||||
|
"file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="],
|
||||||
|
|
||||||
"file-selector": ["file-selector@2.1.2", "", { "dependencies": { "tslib": "^2.7.0" } }, "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig=="],
|
"file-selector": ["file-selector@2.1.2", "", { "dependencies": { "tslib": "^2.7.0" } }, "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig=="],
|
||||||
|
|
||||||
"file-source": ["file-source@0.6.1", "", { "dependencies": { "stream-source": "0.3" } }, "sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA=="],
|
"file-source": ["file-source@0.6.1", "", { "dependencies": { "stream-source": "0.3" } }, "sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA=="],
|
||||||
@@ -929,6 +982,12 @@
|
|||||||
|
|
||||||
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
|
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
|
||||||
|
|
||||||
|
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||||
|
|
||||||
|
"flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="],
|
||||||
|
|
||||||
|
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||||
|
|
||||||
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
|
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
|
||||||
|
|
||||||
"for-in": ["for-in@1.0.2", "", {}, "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ=="],
|
"for-in": ["for-in@1.0.2", "", {}, "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ=="],
|
||||||
@@ -969,12 +1028,16 @@
|
|||||||
|
|
||||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
"globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
|
"globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
|
||||||
|
|
||||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||||
|
|
||||||
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
|
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
|
||||||
|
|
||||||
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
"hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="],
|
"hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="],
|
||||||
@@ -1025,12 +1088,16 @@
|
|||||||
|
|
||||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
"immer": ["immer@10.1.1", "", {}, "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw=="],
|
"immer": ["immer@10.1.1", "", {}, "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw=="],
|
||||||
|
|
||||||
"immutable": ["immutable@5.1.2", "", {}, "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ=="],
|
"immutable": ["immutable@5.1.2", "", {}, "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ=="],
|
||||||
|
|
||||||
"import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="],
|
"import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="],
|
||||||
|
|
||||||
|
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||||
|
|
||||||
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||||
|
|
||||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
@@ -1065,6 +1132,8 @@
|
|||||||
|
|
||||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||||
|
|
||||||
|
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
|
||||||
|
|
||||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||||
|
|
||||||
"is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="],
|
"is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="],
|
||||||
@@ -1083,10 +1152,18 @@
|
|||||||
|
|
||||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
|
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||||
|
|
||||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
|
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||||
|
|
||||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||||
|
|
||||||
|
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
|
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||||
|
|
||||||
"json2mq": ["json2mq@0.2.0", "", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="],
|
"json2mq": ["json2mq@0.2.0", "", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="],
|
||||||
|
|
||||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
@@ -1097,6 +1174,8 @@
|
|||||||
|
|
||||||
"katex": ["katex@0.16.22", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg=="],
|
"katex": ["katex@0.16.22", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg=="],
|
||||||
|
|
||||||
|
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
|
|
||||||
"khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
|
"khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
|
||||||
|
|
||||||
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
|
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
|
||||||
@@ -1107,6 +1186,8 @@
|
|||||||
|
|
||||||
"leva": ["leva@0.10.0", "", { "dependencies": { "@radix-ui/react-portal": "1.0.2", "@radix-ui/react-tooltip": "1.0.5", "@stitches/react": "^1.2.8", "@use-gesture/react": "^10.2.5", "colord": "^2.9.2", "dequal": "^2.0.2", "merge-value": "^1.0.0", "react-colorful": "^5.5.1", "react-dropzone": "^12.0.0", "v8n": "^1.3.3", "zustand": "^3.6.9" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-RiNJWmeqQdKIeHuVXgshmxIHu144a2AMYtLxKf8Nm1j93pisDPexuQDHKNdQlbo37wdyDQibLjY9JKGIiD7gaw=="],
|
"leva": ["leva@0.10.0", "", { "dependencies": { "@radix-ui/react-portal": "1.0.2", "@radix-ui/react-tooltip": "1.0.5", "@stitches/react": "^1.2.8", "@use-gesture/react": "^10.2.5", "colord": "^2.9.2", "dequal": "^2.0.2", "merge-value": "^1.0.0", "react-colorful": "^5.5.1", "react-dropzone": "^12.0.0", "v8n": "^1.3.3", "zustand": "^3.6.9" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-RiNJWmeqQdKIeHuVXgshmxIHu144a2AMYtLxKf8Nm1j93pisDPexuQDHKNdQlbo37wdyDQibLjY9JKGIiD7gaw=="],
|
||||||
|
|
||||||
|
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||||
|
|
||||||
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||||
|
|
||||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||||
@@ -1119,12 +1200,16 @@
|
|||||||
|
|
||||||
"local-pkg": ["local-pkg@1.1.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.0.1", "quansync": "^0.2.8" } }, "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg=="],
|
"local-pkg": ["local-pkg@1.1.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.0.1", "quansync": "^0.2.8" } }, "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg=="],
|
||||||
|
|
||||||
|
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||||
|
|
||||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||||
|
|
||||||
"lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
|
"lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
|
||||||
|
|
||||||
"lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
|
"lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
|
||||||
|
|
||||||
|
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||||
|
|
||||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
@@ -1285,6 +1370,8 @@
|
|||||||
|
|
||||||
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||||
|
|
||||||
|
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||||
|
|
||||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||||
|
|
||||||
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
||||||
@@ -1307,6 +1394,12 @@
|
|||||||
|
|
||||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="],
|
"oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="],
|
||||||
|
|
||||||
|
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||||
|
|
||||||
|
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||||
|
|
||||||
|
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||||
|
|
||||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||||
|
|
||||||
"package-manager-detector": ["package-manager-detector@1.3.0", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="],
|
"package-manager-detector": ["package-manager-detector@1.3.0", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="],
|
||||||
@@ -1327,6 +1420,8 @@
|
|||||||
|
|
||||||
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
|
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
|
||||||
|
|
||||||
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
|
|
||||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||||
|
|
||||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
@@ -1375,6 +1470,8 @@
|
|||||||
|
|
||||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||||
|
|
||||||
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
"prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],
|
"prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],
|
||||||
|
|
||||||
"prettier-package-json": ["prettier-package-json@2.8.0", "", { "dependencies": { "@types/parse-author": "^2.0.0", "commander": "^4.0.1", "cosmiconfig": "^7.0.0", "fs-extra": "^10.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4", "parse-author": "^2.0.0", "sort-object-keys": "^1.1.3", "sort-order": "^1.0.1" }, "bin": { "prettier-package-json": "bin/prettier-package-json" } }, "sha512-WxtodH/wWavfw3MR7yK/GrS4pASEQ+iSTkdtSxPJWvqzG55ir5nvbLt9rw5AOiEcqqPCRM92WCtR1rk3TG3JSQ=="],
|
"prettier-package-json": ["prettier-package-json@2.8.0", "", { "dependencies": { "@types/parse-author": "^2.0.0", "commander": "^4.0.1", "cosmiconfig": "^7.0.0", "fs-extra": "^10.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4", "parse-author": "^2.0.0", "sort-object-keys": "^1.1.3", "sort-order": "^1.0.1" }, "bin": { "prettier-package-json": "bin/prettier-package-json" } }, "sha512-WxtodH/wWavfw3MR7yK/GrS4pASEQ+iSTkdtSxPJWvqzG55ir5nvbLt9rw5AOiEcqqPCRM92WCtR1rk3TG3JSQ=="],
|
||||||
@@ -1393,6 +1490,8 @@
|
|||||||
|
|
||||||
"protocol-buffers-schema": ["protocol-buffers-schema@3.6.0", "", {}, "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="],
|
"protocol-buffers-schema": ["protocol-buffers-schema@3.6.0", "", {}, "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="],
|
||||||
|
|
||||||
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
"quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="],
|
"quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="],
|
||||||
|
|
||||||
"query-string": ["query-string@9.2.0", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-YIRhrHujoQxhexwRLxfy3VSjOXmvZRd2nyw1PwL1UUqZ/ys1dEZd1+NSgXkne2l/4X/7OXkigEAuhTX0g/ivJQ=="],
|
"query-string": ["query-string@9.2.0", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-YIRhrHujoQxhexwRLxfy3VSjOXmvZRd2nyw1PwL1UUqZ/ys1dEZd1+NSgXkne2l/4X/7OXkigEAuhTX0g/ivJQ=="],
|
||||||
@@ -1577,6 +1676,8 @@
|
|||||||
|
|
||||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||||
|
|
||||||
|
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
|
||||||
|
|
||||||
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
|
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.30.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.30.0", "@rollup/rollup-android-arm64": "4.30.0", "@rollup/rollup-darwin-arm64": "4.30.0", "@rollup/rollup-darwin-x64": "4.30.0", "@rollup/rollup-freebsd-arm64": "4.30.0", "@rollup/rollup-freebsd-x64": "4.30.0", "@rollup/rollup-linux-arm-gnueabihf": "4.30.0", "@rollup/rollup-linux-arm-musleabihf": "4.30.0", "@rollup/rollup-linux-arm64-gnu": "4.30.0", "@rollup/rollup-linux-arm64-musl": "4.30.0", "@rollup/rollup-linux-loongarch64-gnu": "4.30.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.30.0", "@rollup/rollup-linux-riscv64-gnu": "4.30.0", "@rollup/rollup-linux-s390x-gnu": "4.30.0", "@rollup/rollup-linux-x64-gnu": "4.30.0", "@rollup/rollup-linux-x64-musl": "4.30.0", "@rollup/rollup-win32-arm64-msvc": "4.30.0", "@rollup/rollup-win32-ia32-msvc": "4.30.0", "@rollup/rollup-win32-x64-msvc": "4.30.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-sDnr1pcjTgUT69qBksNF1N1anwfbyYG6TBQ22b03bII8EdiUQ7J0TlozVaTMjT/eEJAO49e1ndV7t+UZfL1+vA=="],
|
"rollup": ["rollup@4.30.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.30.0", "@rollup/rollup-android-arm64": "4.30.0", "@rollup/rollup-darwin-arm64": "4.30.0", "@rollup/rollup-darwin-x64": "4.30.0", "@rollup/rollup-freebsd-arm64": "4.30.0", "@rollup/rollup-freebsd-x64": "4.30.0", "@rollup/rollup-linux-arm-gnueabihf": "4.30.0", "@rollup/rollup-linux-arm-musleabihf": "4.30.0", "@rollup/rollup-linux-arm64-gnu": "4.30.0", "@rollup/rollup-linux-arm64-musl": "4.30.0", "@rollup/rollup-linux-loongarch64-gnu": "4.30.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.30.0", "@rollup/rollup-linux-riscv64-gnu": "4.30.0", "@rollup/rollup-linux-s390x-gnu": "4.30.0", "@rollup/rollup-linux-x64-gnu": "4.30.0", "@rollup/rollup-linux-x64-musl": "4.30.0", "@rollup/rollup-win32-arm64-msvc": "4.30.0", "@rollup/rollup-win32-ia32-msvc": "4.30.0", "@rollup/rollup-win32-x64-msvc": "4.30.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-sDnr1pcjTgUT69qBksNF1N1anwfbyYG6TBQ22b03bII8EdiUQ7J0TlozVaTMjT/eEJAO49e1ndV7t+UZfL1+vA=="],
|
||||||
@@ -1655,10 +1756,12 @@
|
|||||||
|
|
||||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||||
|
|
||||||
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||||
|
|
||||||
"style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
|
"style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
|
||||||
|
|
||||||
"stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
|
"stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
|
||||||
@@ -1667,6 +1770,8 @@
|
|||||||
|
|
||||||
"suf-log": ["suf-log@2.5.3", "", { "dependencies": { "s.color": "0.0.15" } }, "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow=="],
|
"suf-log": ["suf-log@2.5.3", "", { "dependencies": { "s.color": "0.0.15" } }, "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow=="],
|
||||||
|
|
||||||
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
"swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="],
|
"swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="],
|
||||||
@@ -1677,6 +1782,8 @@
|
|||||||
|
|
||||||
"text-encoding": ["text-encoding@0.6.4", "", {}, "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg=="],
|
"text-encoding": ["text-encoding@0.6.4", "", {}, "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg=="],
|
||||||
|
|
||||||
|
"text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
|
||||||
|
|
||||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||||
|
|
||||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||||
@@ -1705,6 +1812,10 @@
|
|||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
|
|
||||||
|
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
|
||||||
|
|
||||||
"typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
|
"typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
|
||||||
|
|
||||||
"typescript": ["typescript@4.4.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ=="],
|
"typescript": ["typescript@4.4.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ=="],
|
||||||
@@ -1733,6 +1844,8 @@
|
|||||||
|
|
||||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||||
|
|
||||||
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
"url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="],
|
"url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="],
|
||||||
|
|
||||||
"use-debounce": ["use-debounce@10.0.4", "", { "peerDependencies": { "react": "*" } }, "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw=="],
|
"use-debounce": ["use-debounce@10.0.4", "", { "peerDependencies": { "react": "*" } }, "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw=="],
|
||||||
@@ -1777,6 +1890,8 @@
|
|||||||
|
|
||||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
@@ -1787,6 +1902,8 @@
|
|||||||
|
|
||||||
"yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
|
"yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
|
||||||
|
|
||||||
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"zustand": ["zustand@3.7.2", "", { "peerDependencies": { "react": ">=16.8" }, "optionalPeers": ["react"] }, "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="],
|
"zustand": ["zustand@3.7.2", "", { "peerDependencies": { "react": ">=16.8" }, "optionalPeers": ["react"] }, "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="],
|
||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
@@ -1807,8 +1924,6 @@
|
|||||||
|
|
||||||
"@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
|
"@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
|
||||||
|
|
||||||
"@emotion/babel-plugin/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
|
||||||
|
|
||||||
"@emotion/babel-plugin/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
|
"@emotion/babel-plugin/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
|
||||||
|
|
||||||
"@emotion/babel-plugin/stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="],
|
"@emotion/babel-plugin/stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="],
|
||||||
@@ -1819,6 +1934,10 @@
|
|||||||
|
|
||||||
"@emotion/serialize/@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="],
|
"@emotion/serialize/@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="],
|
||||||
|
|
||||||
|
"@iconify/utils/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||||
|
|
||||||
"@lobehub/fluent-emoji/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="],
|
"@lobehub/fluent-emoji/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="],
|
||||||
|
|
||||||
"@lobehub/icons/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="],
|
"@lobehub/icons/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="],
|
||||||
@@ -1867,6 +1986,8 @@
|
|||||||
|
|
||||||
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
|
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
|
||||||
|
|
||||||
|
"esast-util-from-js/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||||
|
|
||||||
"extend-shallow/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
"extend-shallow/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
@@ -1887,8 +2008,14 @@
|
|||||||
|
|
||||||
"leva/react-dropzone": ["react-dropzone@12.1.0", "", { "dependencies": { "attr-accept": "^2.2.2", "file-selector": "^0.5.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8" } }, "sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog=="],
|
"leva/react-dropzone": ["react-dropzone@12.1.0", "", { "dependencies": { "attr-accept": "^2.2.2", "file-selector": "^0.5.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8" } }, "sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog=="],
|
||||||
|
|
||||||
|
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||||
|
|
||||||
"mermaid/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
|
"mermaid/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
|
||||||
|
|
||||||
|
"micromark-extension-mdxjs/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||||
|
|
||||||
|
"mlly/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||||
|
|
||||||
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
|
|
||||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
@@ -1909,6 +2036,8 @@
|
|||||||
|
|
||||||
"react-toastify/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="],
|
"react-toastify/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="],
|
||||||
|
|
||||||
|
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
|
|
||||||
"sass/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
"sass/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
"set-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
"set-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
||||||
@@ -1921,12 +2050,10 @@
|
|||||||
|
|
||||||
"string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
"string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|
||||||
|
"string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||||
|
|
||||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
||||||
|
|
||||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
|
||||||
|
|
||||||
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||||
|
|
||||||
"topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
"topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
@@ -1935,12 +2062,12 @@
|
|||||||
|
|
||||||
"vite/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
|
"vite/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||||
|
|
||||||
|
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001690", "", {}, "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w=="],
|
"@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001690", "", {}, "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.76", "", {}, "sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ=="],
|
"@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.76", "", {}, "sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ=="],
|
||||||
@@ -1951,6 +2078,8 @@
|
|||||||
|
|
||||||
"@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
|
"@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||||
|
|
||||||
"@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@0.5.4", "", { "dependencies": { "@floating-ui/core": "^0.7.3" } }, "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg=="],
|
"@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@0.5.4", "", { "dependencies": { "@floating-ui/core": "^0.7.3" } }, "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg=="],
|
||||||
|
|
||||||
"@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="],
|
"@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="],
|
||||||
@@ -1981,11 +2110,11 @@
|
|||||||
|
|
||||||
"simplify-geojson/concat-stream/typedarray": ["typedarray@0.0.7", "", {}, "sha512-ueeb9YybpjhivjbHP2LdFDAjbS948fGEPj+ACAMs4xCMmh72OCOMQWBQKlaN4ZNQ04yfLSDLSx1tGRIoWimObQ=="],
|
"simplify-geojson/concat-stream/typedarray": ["typedarray@0.0.7", "", {}, "sha512-ueeb9YybpjhivjbHP2LdFDAjbS948fGEPj+ACAMs4xCMmh72OCOMQWBQKlaN4ZNQ04yfLSDLSx1tGRIoWimObQ=="],
|
||||||
|
|
||||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||||
|
|
||||||
"@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
"@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,8 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "prettier . --check",
|
"lint": "prettier . --check",
|
||||||
"lint:fix": "prettier . --write",
|
"lint:fix": "prettier . --write",
|
||||||
|
"eslint": "bunx eslint \"**/*.{js,jsx}\" --cache",
|
||||||
|
"eslint:fix": "bunx eslint \"**/*.{js,jsx}\" --fix --cache",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
@@ -71,6 +73,9 @@
|
|||||||
"@so1ve/prettier-config": "^3.1.0",
|
"@so1ve/prettier-config": "^3.1.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"eslint": "8.57.0",
|
||||||
|
"eslint-plugin-header": "^3.1.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"tailwindcss": "^3",
|
"tailwindcss": "^3",
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
|
|||||||
@@ -1,18 +1,36 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { lazy, Suspense } from 'react';
|
import React, { lazy, Suspense } from 'react';
|
||||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import Loading from './components/common/Loading.js';
|
import Loading from './components/common/ui/Loading.js';
|
||||||
import User from './pages/User';
|
import User from './pages/User';
|
||||||
import { AuthRedirect, PrivateRoute } from './helpers';
|
import { AuthRedirect, PrivateRoute } from './helpers';
|
||||||
import RegisterForm from './components/auth/RegisterForm.js';
|
import RegisterForm from './components/auth/RegisterForm.js';
|
||||||
import LoginForm from './components/auth/LoginForm.js';
|
import LoginForm from './components/auth/LoginForm.js';
|
||||||
import NotFound from './pages/NotFound';
|
import NotFound from './pages/NotFound';
|
||||||
import Setting from './pages/Setting';
|
import Setting from './pages/Setting';
|
||||||
import EditUser from './pages/User/EditUser';
|
|
||||||
import PasswordResetForm from './components/auth/PasswordResetForm.js';
|
import PasswordResetForm from './components/auth/PasswordResetForm.js';
|
||||||
import PasswordResetConfirm from './components/auth/PasswordResetConfirm.js';
|
import PasswordResetConfirm from './components/auth/PasswordResetConfirm.js';
|
||||||
import Channel from './pages/Channel';
|
import Channel from './pages/Channel';
|
||||||
import Token from './pages/Token';
|
import Token from './pages/Token';
|
||||||
import EditChannel from './pages/Channel/EditChannel';
|
|
||||||
import Redemption from './pages/Redemption';
|
import Redemption from './pages/Redemption';
|
||||||
import TopUp from './pages/TopUp';
|
import TopUp from './pages/TopUp';
|
||||||
import Log from './pages/Log';
|
import Log from './pages/Log';
|
||||||
@@ -28,7 +46,7 @@ import Setup from './pages/Setup/index.js';
|
|||||||
import SetupCheck from './components/layout/SetupCheck.js';
|
import SetupCheck from './components/layout/SetupCheck.js';
|
||||||
|
|
||||||
const Home = lazy(() => import('./pages/Home'));
|
const Home = lazy(() => import('./pages/Home'));
|
||||||
const Detail = lazy(() => import('./pages/Detail'));
|
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
||||||
const About = lazy(() => import('./pages/About'));
|
const About = lazy(() => import('./pages/About'));
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -61,22 +79,6 @@ function App() {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path='/console/channel/edit/:id'
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
|
||||||
<EditChannel />
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path='/console/channel/add'
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
|
||||||
<EditChannel />
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path='/console/token'
|
path='/console/token'
|
||||||
element={
|
element={
|
||||||
@@ -109,22 +111,6 @@ function App() {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path='/console/user/edit/:id'
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
|
||||||
<EditUser />
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path='/console/user/edit'
|
|
||||||
element={
|
|
||||||
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
|
||||||
<EditUser />
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path='/user/reset'
|
path='/user/reset'
|
||||||
element={
|
element={
|
||||||
@@ -228,7 +214,7 @@ function App() {
|
|||||||
element={
|
element={
|
||||||
<PrivateRoute>
|
<PrivateRoute>
|
||||||
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
||||||
<Detail />
|
<Dashboard />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { UserContext } from '../../context/User/index.js';
|
import { UserContext } from '../../context/User/index.js';
|
||||||
@@ -523,7 +542,7 @@ const LoginForm = () => {
|
|||||||
{/* 背景模糊晕染球 */}
|
{/* 背景模糊晕染球 */}
|
||||||
<div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
|
<div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
|
||||||
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
|
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
|
||||||
<div className="w-full max-w-sm mt-[64px]">
|
<div className="w-full max-w-sm mt-[60px]">
|
||||||
{showEmailLogin || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
|
{showEmailLogin || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
|
||||||
? renderEmailLoginForm()
|
? renderEmailLoginForm()
|
||||||
: renderOAuthOptions()}
|
: renderOAuthOptions()}
|
||||||
|
|||||||
@@ -1,9 +1,28 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { API, showError, showSuccess, updateAPI, setUserData } from '../../helpers';
|
import { API, showError, showSuccess, updateAPI, setUserData } from '../../helpers';
|
||||||
import { UserContext } from '../../context/User';
|
import { UserContext } from '../../context/User';
|
||||||
import Loading from '../common/Loading';
|
import Loading from '../common/ui/Loading';
|
||||||
|
|
||||||
const OAuth2Callback = (props) => {
|
const OAuth2Callback = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { API, copy, showError, showNotice, getLogo, getSystemName } from '../../helpers';
|
import { API, copy, showError, showNotice, getLogo, getSystemName } from '../../helpers';
|
||||||
import { useSearchParams, Link } from 'react-router-dom';
|
import { useSearchParams, Link } from 'react-router-dom';
|
||||||
@@ -82,7 +101,7 @@ const PasswordResetConfirm = () => {
|
|||||||
{/* 背景模糊晕染球 */}
|
{/* 背景模糊晕染球 */}
|
||||||
<div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
|
<div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
|
||||||
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
|
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
|
||||||
<div className="w-full max-w-sm mt-[64px]">
|
<div className="w-full max-w-sm mt-[60px]">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<div className="flex items-center justify-center mb-6 gap-2">
|
<div className="flex items-center justify-center mb-6 gap-2">
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { API, getLogo, showError, showInfo, showSuccess, getSystemName } from '../../helpers';
|
import { API, getLogo, showError, showInfo, showSuccess, getSystemName } from '../../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
@@ -82,7 +101,7 @@ const PasswordResetForm = () => {
|
|||||||
{/* 背景模糊晕染球 */}
|
{/* 背景模糊晕染球 */}
|
||||||
<div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
|
<div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
|
||||||
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
|
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
|
||||||
<div className="w-full max-w-sm mt-[64px]">
|
<div className="w-full max-w-sm mt-[60px]">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<div className="flex items-center justify-center mb-6 gap-2">
|
<div className="flex items-center justify-center mb-6 gap-2">
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useContext, 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 {
|
import {
|
||||||
@@ -540,7 +559,7 @@ const RegisterForm = () => {
|
|||||||
{/* 背景模糊晕染球 */}
|
{/* 背景模糊晕染球 */}
|
||||||
<div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
|
<div className="blur-ball blur-ball-indigo" style={{ top: '-80px', right: '-80px', transform: 'none' }} />
|
||||||
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
|
<div className="blur-ball blur-ball-teal" style={{ top: '50%', left: '-120px' }} />
|
||||||
<div className="w-full max-w-sm mt-[64px]">
|
<div className="w-full max-w-sm mt-[60px]">
|
||||||
{showEmailRegister || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
|
{showEmailRegister || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
|
||||||
? renderEmailRegisterForm()
|
? renderEmailRegisterForm()
|
||||||
: renderOAuthOptions()}
|
: renderOAuthOptions()}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Spin } from '@douyinfe/semi-ui';
|
|
||||||
|
|
||||||
const Loading = ({ size = 'small' }) => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center">
|
|
||||||
<Spin
|
|
||||||
size={size}
|
|
||||||
spinning={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Loading;
|
|
||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Icon } from '@douyinfe/semi-ui';
|
import { Icon } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Icon } from '@douyinfe/semi-ui';
|
import { Icon } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Icon } from '@douyinfe/semi-ui';
|
import { Icon } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
import 'highlight.js/styles/github.css';
|
import 'highlight.js/styles/github.css';
|
||||||
|
|||||||
213
web/src/components/common/ui/CardPro.js
Normal file
213
web/src/components/common/ui/CardPro.js
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Card, Divider, Typography, Button } from '@douyinfe/semi-ui';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||||
|
import { IconEyeOpened, IconEyeClosed } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CardPro 高级卡片组件
|
||||||
|
*
|
||||||
|
* 布局分为6个区域:
|
||||||
|
* 1. 统计信息区域 (statsArea)
|
||||||
|
* 2. 描述信息区域 (descriptionArea)
|
||||||
|
* 3. 类型切换/标签区域 (tabsArea)
|
||||||
|
* 4. 操作按钮区域 (actionsArea)
|
||||||
|
* 5. 搜索表单区域 (searchArea)
|
||||||
|
* 6. 分页区域 (paginationArea) - 固定在卡片底部
|
||||||
|
*
|
||||||
|
* 支持三种布局类型:
|
||||||
|
* - type1: 操作型 (如TokensTable) - 描述信息 + 操作按钮 + 搜索表单
|
||||||
|
* - type2: 查询型 (如LogsTable) - 统计信息 + 搜索表单
|
||||||
|
* - type3: 复杂型 (如ChannelsTable) - 描述信息 + 类型切换 + 操作按钮 + 搜索表单
|
||||||
|
*/
|
||||||
|
const CardPro = ({
|
||||||
|
type = 'type1',
|
||||||
|
className = '',
|
||||||
|
children,
|
||||||
|
// 各个区域的内容
|
||||||
|
statsArea,
|
||||||
|
descriptionArea,
|
||||||
|
tabsArea,
|
||||||
|
actionsArea,
|
||||||
|
searchArea,
|
||||||
|
paginationArea, // 新增分页区域
|
||||||
|
// 卡片属性
|
||||||
|
shadows = 'always',
|
||||||
|
bordered = false,
|
||||||
|
// 自定义样式
|
||||||
|
style,
|
||||||
|
// 国际化函数
|
||||||
|
t = (key) => key,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [showMobileActions, setShowMobileActions] = useState(false);
|
||||||
|
|
||||||
|
const toggleMobileActions = () => {
|
||||||
|
setShowMobileActions(!showMobileActions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasMobileHideableContent = actionsArea || searchArea;
|
||||||
|
|
||||||
|
const renderHeader = () => {
|
||||||
|
const hasContent = statsArea || descriptionArea || tabsArea || actionsArea || searchArea;
|
||||||
|
if (!hasContent) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col w-full">
|
||||||
|
{/* 统计信息区域 - 用于type2 */}
|
||||||
|
{type === 'type2' && statsArea && (
|
||||||
|
<>
|
||||||
|
{statsArea}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 描述信息区域 - 用于type1和type3 */}
|
||||||
|
{(type === 'type1' || type === 'type3') && descriptionArea && (
|
||||||
|
<>
|
||||||
|
{descriptionArea}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 第一个分隔线 - 在描述信息或统计信息后面 */}
|
||||||
|
{((type === 'type1' || type === 'type3') && descriptionArea) ||
|
||||||
|
(type === 'type2' && statsArea) ? (
|
||||||
|
<Divider margin="12px" />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* 类型切换/标签区域 - 主要用于type3 */}
|
||||||
|
{type === 'type3' && tabsArea && (
|
||||||
|
<>
|
||||||
|
{tabsArea}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 移动端操作切换按钮 */}
|
||||||
|
{isMobile && hasMobileHideableContent && (
|
||||||
|
<>
|
||||||
|
<div className="w-full mb-2">
|
||||||
|
<Button
|
||||||
|
onClick={toggleMobileActions}
|
||||||
|
icon={showMobileActions ? <IconEyeClosed /> : <IconEyeOpened />}
|
||||||
|
type="tertiary"
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
{showMobileActions ? t('隐藏操作项') : t('显示操作项')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 操作按钮和搜索表单的容器 */}
|
||||||
|
<div
|
||||||
|
className={`flex flex-col gap-2 ${isMobile && !showMobileActions ? 'hidden' : ''}`}
|
||||||
|
>
|
||||||
|
{/* 操作按钮区域 - 用于type1和type3 */}
|
||||||
|
{(type === 'type1' || type === 'type3') && actionsArea && (
|
||||||
|
Array.isArray(actionsArea) ? (
|
||||||
|
actionsArea.map((area, idx) => (
|
||||||
|
<React.Fragment key={idx}>
|
||||||
|
{idx !== 0 && <Divider />}
|
||||||
|
<div className="w-full">
|
||||||
|
{area}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="w-full">
|
||||||
|
{actionsArea}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 当同时存在操作区和搜索区时,插入分隔线 */}
|
||||||
|
{(actionsArea && searchArea) && <Divider />}
|
||||||
|
|
||||||
|
{/* 搜索表单区域 - 所有类型都可能有 */}
|
||||||
|
{searchArea && (
|
||||||
|
<div className="w-full">
|
||||||
|
{searchArea}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerContent = renderHeader();
|
||||||
|
|
||||||
|
// 渲染分页区域
|
||||||
|
const renderFooter = () => {
|
||||||
|
if (!paginationArea) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center w-full pt-4 border-t" style={{ borderColor: 'var(--semi-color-border)' }}>
|
||||||
|
{paginationArea}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerContent = renderFooter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={`table-scroll-card !rounded-2xl ${className}`}
|
||||||
|
title={headerContent}
|
||||||
|
footer={footerContent}
|
||||||
|
shadows={shadows}
|
||||||
|
bordered={bordered}
|
||||||
|
style={style}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CardPro.propTypes = {
|
||||||
|
// 布局类型
|
||||||
|
type: PropTypes.oneOf(['type1', 'type2', 'type3']),
|
||||||
|
// 样式相关
|
||||||
|
className: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
|
shadows: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||||
|
bordered: PropTypes.bool,
|
||||||
|
// 内容区域
|
||||||
|
statsArea: PropTypes.node,
|
||||||
|
descriptionArea: PropTypes.node,
|
||||||
|
tabsArea: PropTypes.node,
|
||||||
|
actionsArea: PropTypes.oneOfType([
|
||||||
|
PropTypes.node,
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
]),
|
||||||
|
searchArea: PropTypes.node,
|
||||||
|
paginationArea: PropTypes.node,
|
||||||
|
// 表格内容
|
||||||
|
children: PropTypes.node,
|
||||||
|
// 国际化函数
|
||||||
|
t: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CardPro;
|
||||||
237
web/src/components/common/ui/CardTable.js
Normal file
237
web/src/components/common/ui/CardTable.js
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Table, Card, Skeleton, Pagination, Empty, Button, Collapsible } from '@douyinfe/semi-ui';
|
||||||
|
import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CardTable 响应式表格组件
|
||||||
|
*
|
||||||
|
* 在桌面端渲染 Semi-UI 的 Table 组件,在移动端则将每一行数据渲染成 Card 形式。
|
||||||
|
* 该组件与 Table 组件的大部分 API 保持一致,只需将原 Table 换成 CardTable 即可。
|
||||||
|
*/
|
||||||
|
const CardTable = ({
|
||||||
|
columns = [],
|
||||||
|
dataSource = [],
|
||||||
|
loading = false,
|
||||||
|
rowKey = 'key',
|
||||||
|
hidePagination = false,
|
||||||
|
...tableProps
|
||||||
|
}) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [showSkeleton, setShowSkeleton] = useState(loading);
|
||||||
|
const loadingStartRef = useRef(Date.now());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
loadingStartRef.current = Date.now();
|
||||||
|
setShowSkeleton(true);
|
||||||
|
} else {
|
||||||
|
const elapsed = Date.now() - loadingStartRef.current;
|
||||||
|
const remaining = Math.max(0, 500 - elapsed);
|
||||||
|
if (remaining === 0) {
|
||||||
|
setShowSkeleton(false);
|
||||||
|
} else {
|
||||||
|
const timer = setTimeout(() => setShowSkeleton(false), remaining);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [loading]);
|
||||||
|
|
||||||
|
const getRowKey = (record, index) => {
|
||||||
|
if (typeof rowKey === 'function') return rowKey(record);
|
||||||
|
return record[rowKey] !== undefined ? record[rowKey] : index;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isMobile) {
|
||||||
|
const finalTableProps = hidePagination
|
||||||
|
? { ...tableProps, pagination: false }
|
||||||
|
: tableProps;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={dataSource}
|
||||||
|
loading={loading}
|
||||||
|
rowKey={rowKey}
|
||||||
|
{...finalTableProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSkeleton) {
|
||||||
|
const visibleCols = columns.filter((col) => {
|
||||||
|
if (tableProps?.visibleColumns && col.key) {
|
||||||
|
return tableProps.visibleColumns[col.key];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderSkeletonCard = (key) => {
|
||||||
|
const placeholder = (
|
||||||
|
<div className="p-2">
|
||||||
|
{visibleCols.map((col, idx) => {
|
||||||
|
if (!col.title) {
|
||||||
|
return (
|
||||||
|
<div key={idx} className="mt-2 flex justify-end">
|
||||||
|
<Skeleton.Title active style={{ width: 100, height: 24 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={idx} className="flex justify-between items-center py-1 border-b last:border-b-0 border-dashed" style={{ borderColor: 'var(--semi-color-border)' }}>
|
||||||
|
<Skeleton.Title active style={{ width: 80, height: 14 }} />
|
||||||
|
<Skeleton.Title
|
||||||
|
active
|
||||||
|
style={{
|
||||||
|
width: `${50 + (idx % 3) * 10}%`,
|
||||||
|
maxWidth: 180,
|
||||||
|
height: 14,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={key} className="!rounded-2xl shadow-sm">
|
||||||
|
<Skeleton loading={true} active placeholder={placeholder}></Skeleton>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{[1, 2, 3].map((i) => renderSkeletonCard(i))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEmpty = !showSkeleton && (!dataSource || dataSource.length === 0);
|
||||||
|
|
||||||
|
const MobileRowCard = ({ record, index }) => {
|
||||||
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
const rowKeyVal = getRowKey(record, index);
|
||||||
|
|
||||||
|
const hasDetails =
|
||||||
|
tableProps.expandedRowRender &&
|
||||||
|
(!tableProps.rowExpandable || tableProps.rowExpandable(record));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={rowKeyVal} className="!rounded-2xl shadow-sm">
|
||||||
|
{columns.map((col, colIdx) => {
|
||||||
|
if (tableProps?.visibleColumns && !tableProps.visibleColumns[col.key]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = col.title;
|
||||||
|
const cellContent = col.render
|
||||||
|
? col.render(record[col.dataIndex], record, index)
|
||||||
|
: record[col.dataIndex];
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return (
|
||||||
|
<div key={col.key || colIdx} className="mt-2 flex justify-end">
|
||||||
|
{cellContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={col.key || colIdx}
|
||||||
|
className="flex justify-between items-start py-1 border-b last:border-b-0 border-dashed"
|
||||||
|
style={{ borderColor: 'var(--semi-color-border)' }}
|
||||||
|
>
|
||||||
|
<span className="font-medium text-gray-600 mr-2 whitespace-nowrap select-none">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 break-all flex justify-end items-center gap-1">
|
||||||
|
{cellContent !== undefined && cellContent !== null ? cellContent : '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{hasDetails && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
theme='borderless'
|
||||||
|
size='small'
|
||||||
|
className='w-full flex justify-center mt-2'
|
||||||
|
icon={showDetails ? <IconChevronUp /> : <IconChevronDown />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowDetails(!showDetails);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showDetails ? t('收起') : t('详情')}
|
||||||
|
</Button>
|
||||||
|
<Collapsible isOpen={showDetails} keepDOM>
|
||||||
|
<div className="pt-2">
|
||||||
|
{tableProps.expandedRowRender(record, index)}
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
if (tableProps.empty) return tableProps.empty;
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center p-4">
|
||||||
|
<Empty description="No Data" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{dataSource.map((record, index) => (
|
||||||
|
<MobileRowCard key={getRowKey(record, index)} record={record} index={index} />
|
||||||
|
))}
|
||||||
|
{!hidePagination && tableProps.pagination && dataSource.length > 0 && (
|
||||||
|
<div className="mt-2 flex justify-center">
|
||||||
|
<Pagination {...tableProps.pagination} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CardTable.propTypes = {
|
||||||
|
columns: PropTypes.array.isRequired,
|
||||||
|
dataSource: PropTypes.array,
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||||
|
hidePagination: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CardTable;
|
||||||
68
web/src/components/common/ui/CompactModeToggle.js
Normal file
68
web/src/components/common/ui/CompactModeToggle.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 紧凑模式切换按钮组件
|
||||||
|
* 用于在自适应列表和紧凑列表之间切换
|
||||||
|
* 在移动端时自动隐藏,因为移动端使用"显示操作项"按钮来控制内容显示
|
||||||
|
*/
|
||||||
|
const CompactModeToggle = ({
|
||||||
|
compactMode,
|
||||||
|
setCompactMode,
|
||||||
|
t,
|
||||||
|
size = 'small',
|
||||||
|
type = 'tertiary',
|
||||||
|
className = '',
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
// 在移动端隐藏紧凑列表切换按钮
|
||||||
|
if (isMobile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type={type}
|
||||||
|
size={size}
|
||||||
|
className={`w-full md:w-auto ${className}`}
|
||||||
|
onClick={() => setCompactMode(!compactMode)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{compactMode ? t('自适应列表') : t('紧凑列表')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CompactModeToggle.propTypes = {
|
||||||
|
compactMode: PropTypes.bool.isRequired,
|
||||||
|
setCompactMode: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
size: PropTypes.string,
|
||||||
|
type: PropTypes.string,
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CompactModeToggle;
|
||||||
35
web/src/components/common/ui/Loading.js
Normal file
35
web/src/components/common/ui/Loading.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Spin } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
const Loading = ({ size = 'small' }) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center">
|
||||||
|
<Spin
|
||||||
|
size={size}
|
||||||
|
spinning={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
||||||
220
web/src/components/common/ui/ScrollableContainer.js
Normal file
220
web/src/components/common/ui/ScrollableContainer.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useImperativeHandle,
|
||||||
|
forwardRef
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScrollableContainer 可滚动容器组件
|
||||||
|
*
|
||||||
|
* 提供自动检测滚动状态和显示渐变指示器的功能
|
||||||
|
* 当内容超出容器高度且未滚动到底部时,会显示底部渐变指示器
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const ScrollableContainer = forwardRef(({
|
||||||
|
children,
|
||||||
|
maxHeight = '24rem',
|
||||||
|
className = '',
|
||||||
|
contentClassName = 'p-2',
|
||||||
|
fadeIndicatorClassName = '',
|
||||||
|
checkInterval = 100,
|
||||||
|
scrollThreshold = 5,
|
||||||
|
debounceDelay = 16, // ~60fps
|
||||||
|
onScroll,
|
||||||
|
onScrollStateChange,
|
||||||
|
...props
|
||||||
|
}, ref) => {
|
||||||
|
const scrollRef = useRef(null);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const debounceTimerRef = useRef(null);
|
||||||
|
const resizeObserverRef = useRef(null);
|
||||||
|
const onScrollStateChangeRef = useRef(onScrollStateChange);
|
||||||
|
const onScrollRef = useRef(onScroll);
|
||||||
|
|
||||||
|
const [showScrollHint, setShowScrollHint] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onScrollStateChangeRef.current = onScrollStateChange;
|
||||||
|
}, [onScrollStateChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onScrollRef.current = onScroll;
|
||||||
|
}, [onScroll]);
|
||||||
|
|
||||||
|
const debounce = useCallback((func, delay) => {
|
||||||
|
return (...args) => {
|
||||||
|
if (debounceTimerRef.current) {
|
||||||
|
clearTimeout(debounceTimerRef.current);
|
||||||
|
}
|
||||||
|
debounceTimerRef.current = setTimeout(() => func(...args), delay);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkScrollable = useCallback(() => {
|
||||||
|
if (!scrollRef.current) return;
|
||||||
|
|
||||||
|
const element = scrollRef.current;
|
||||||
|
const isScrollable = element.scrollHeight > element.clientHeight;
|
||||||
|
const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - scrollThreshold;
|
||||||
|
const shouldShowHint = isScrollable && !isAtBottom;
|
||||||
|
|
||||||
|
setShowScrollHint(shouldShowHint);
|
||||||
|
|
||||||
|
if (onScrollStateChangeRef.current) {
|
||||||
|
onScrollStateChangeRef.current({
|
||||||
|
isScrollable,
|
||||||
|
isAtBottom,
|
||||||
|
showScrollHint: shouldShowHint,
|
||||||
|
scrollTop: element.scrollTop,
|
||||||
|
scrollHeight: element.scrollHeight,
|
||||||
|
clientHeight: element.clientHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [scrollThreshold]);
|
||||||
|
|
||||||
|
const debouncedCheckScrollable = useMemo(() =>
|
||||||
|
debounce(checkScrollable, debounceDelay),
|
||||||
|
[debounce, checkScrollable, debounceDelay]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleScroll = useCallback((e) => {
|
||||||
|
debouncedCheckScrollable();
|
||||||
|
if (onScrollRef.current) {
|
||||||
|
onScrollRef.current(e);
|
||||||
|
}
|
||||||
|
}, [debouncedCheckScrollable]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
checkScrollable: () => {
|
||||||
|
checkScrollable();
|
||||||
|
},
|
||||||
|
scrollToTop: () => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollToBottom: () => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getScrollInfo: () => {
|
||||||
|
if (!scrollRef.current) return null;
|
||||||
|
const element = scrollRef.current;
|
||||||
|
return {
|
||||||
|
scrollTop: element.scrollTop,
|
||||||
|
scrollHeight: element.scrollHeight,
|
||||||
|
clientHeight: element.clientHeight,
|
||||||
|
isScrollable: element.scrollHeight > element.clientHeight,
|
||||||
|
isAtBottom: element.scrollTop + element.clientHeight >= element.scrollHeight - scrollThreshold
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}), [checkScrollable, scrollThreshold]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
checkScrollable();
|
||||||
|
}, checkInterval);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [checkScrollable, checkInterval]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!scrollRef.current) return;
|
||||||
|
|
||||||
|
if (typeof ResizeObserver === 'undefined') {
|
||||||
|
if (typeof MutationObserver !== 'undefined') {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
debouncedCheckScrollable();
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(scrollRef.current, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true,
|
||||||
|
characterData: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeObserverRef.current = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
debouncedCheckScrollable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserverRef.current.observe(scrollRef.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (resizeObserverRef.current) {
|
||||||
|
resizeObserverRef.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [debouncedCheckScrollable]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (debounceTimerRef.current) {
|
||||||
|
clearTimeout(debounceTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const containerStyle = useMemo(() => ({
|
||||||
|
maxHeight
|
||||||
|
}), [maxHeight]);
|
||||||
|
|
||||||
|
const fadeIndicatorStyle = useMemo(() => ({
|
||||||
|
opacity: showScrollHint ? 1 : 0
|
||||||
|
}), [showScrollHint]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`card-content-container ${className}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className={`overflow-y-auto card-content-scroll ${contentClassName}`}
|
||||||
|
style={containerStyle}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`card-content-fade-indicator ${fadeIndicatorClassName}`}
|
||||||
|
style={fadeIndicatorStyle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ScrollableContainer.displayName = 'ScrollableContainer';
|
||||||
|
|
||||||
|
export default ScrollableContainer;
|
||||||
107
web/src/components/dashboard/AnnouncementsPanel.jsx
Normal file
107
web/src/components/dashboard/AnnouncementsPanel.jsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, Tag, Timeline, Empty } from '@douyinfe/semi-ui';
|
||||||
|
import { Bell } from 'lucide-react';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
|
||||||
|
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
||||||
|
|
||||||
|
const AnnouncementsPanel = ({
|
||||||
|
announcementData,
|
||||||
|
announcementLegendData,
|
||||||
|
CARD_PROPS,
|
||||||
|
ILLUSTRATION_SIZE,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
{...CARD_PROPS}
|
||||||
|
className="shadow-sm !rounded-2xl lg:col-span-2"
|
||||||
|
title={
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Bell size={16} />
|
||||||
|
{t('系统公告')}
|
||||||
|
<Tag color="white" shape="circle">
|
||||||
|
{t('显示最新20条')}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
{/* 图例 */}
|
||||||
|
<div className="flex flex-wrap gap-3 text-xs">
|
||||||
|
{announcementLegendData.map((legend, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-1">
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: legend.color === 'grey' ? '#8b9aa7' :
|
||||||
|
legend.color === 'blue' ? '#3b82f6' :
|
||||||
|
legend.color === 'green' ? '#10b981' :
|
||||||
|
legend.color === 'orange' ? '#f59e0b' :
|
||||||
|
legend.color === 'red' ? '#ef4444' : '#8b9aa7'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-gray-600">{legend.label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
bodyStyle={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
<ScrollableContainer maxHeight="24rem">
|
||||||
|
{announcementData.length > 0 ? (
|
||||||
|
<Timeline mode="alternate">
|
||||||
|
{announcementData.map((item, idx) => (
|
||||||
|
<Timeline.Item
|
||||||
|
key={idx}
|
||||||
|
type={item.type || 'default'}
|
||||||
|
time={item.time}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: marked.parse(item.content || '') }}
|
||||||
|
/>
|
||||||
|
{item.extra && (
|
||||||
|
<div
|
||||||
|
className="text-xs text-gray-500"
|
||||||
|
dangerouslySetInnerHTML={{ __html: marked.parse(item.extra) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Timeline.Item>
|
||||||
|
))}
|
||||||
|
</Timeline>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center items-center py-8">
|
||||||
|
<Empty
|
||||||
|
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||||
|
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
||||||
|
title={t('暂无系统公告')}
|
||||||
|
description={t('请联系管理员在系统设置中配置公告信息')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollableContainer>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnnouncementsPanel;
|
||||||
117
web/src/components/dashboard/ApiInfoPanel.jsx
Normal file
117
web/src/components/dashboard/ApiInfoPanel.jsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, Avatar, Tag, Divider, Empty } from '@douyinfe/semi-ui';
|
||||||
|
import { Server, Gauge, ExternalLink } from 'lucide-react';
|
||||||
|
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
|
||||||
|
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
||||||
|
|
||||||
|
const ApiInfoPanel = ({
|
||||||
|
apiInfoData,
|
||||||
|
handleCopyUrl,
|
||||||
|
handleSpeedTest,
|
||||||
|
CARD_PROPS,
|
||||||
|
FLEX_CENTER_GAP2,
|
||||||
|
ILLUSTRATION_SIZE,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
{...CARD_PROPS}
|
||||||
|
className="bg-gray-50 border-0 !rounded-2xl"
|
||||||
|
title={
|
||||||
|
<div className={FLEX_CENTER_GAP2}>
|
||||||
|
<Server size={16} />
|
||||||
|
{t('API信息')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
bodyStyle={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
<ScrollableContainer maxHeight="24rem">
|
||||||
|
{apiInfoData.length > 0 ? (
|
||||||
|
apiInfoData.map((api) => (
|
||||||
|
<React.Fragment key={api.id}>
|
||||||
|
<div className="flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer">
|
||||||
|
<div className="flex-shrink-0 mr-3">
|
||||||
|
<Avatar
|
||||||
|
size="extra-small"
|
||||||
|
color={api.color}
|
||||||
|
>
|
||||||
|
{api.route.substring(0, 2)}
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex flex-wrap items-center justify-between mb-1 w-full gap-2">
|
||||||
|
<span className="text-sm font-medium text-gray-900 !font-bold break-all">
|
||||||
|
{api.route}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1 mt-1 lg:mt-0">
|
||||||
|
<Tag
|
||||||
|
prefixIcon={<Gauge size={12} />}
|
||||||
|
size="small"
|
||||||
|
color="white"
|
||||||
|
shape='circle'
|
||||||
|
onClick={() => handleSpeedTest(api.url)}
|
||||||
|
className="cursor-pointer hover:opacity-80 text-xs"
|
||||||
|
>
|
||||||
|
{t('测速')}
|
||||||
|
</Tag>
|
||||||
|
<Tag
|
||||||
|
prefixIcon={<ExternalLink size={12} />}
|
||||||
|
size="small"
|
||||||
|
color="white"
|
||||||
|
shape='circle'
|
||||||
|
onClick={() => window.open(api.url, '_blank', 'noopener,noreferrer')}
|
||||||
|
className="cursor-pointer hover:opacity-80 text-xs"
|
||||||
|
>
|
||||||
|
{t('跳转')}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="!text-semi-color-primary break-all cursor-pointer hover:underline mb-1"
|
||||||
|
onClick={() => handleCopyUrl(api.url)}
|
||||||
|
>
|
||||||
|
{api.url}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-500">
|
||||||
|
{api.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center items-center py-8">
|
||||||
|
<Empty
|
||||||
|
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||||
|
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
||||||
|
title={t('暂无API信息')}
|
||||||
|
description={t('请联系管理员在系统设置中配置API信息')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollableContainer>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApiInfoPanel;
|
||||||
117
web/src/components/dashboard/ChartsPanel.jsx
Normal file
117
web/src/components/dashboard/ChartsPanel.jsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, Tabs, TabPane } from '@douyinfe/semi-ui';
|
||||||
|
import { PieChart } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
IconHistogram,
|
||||||
|
IconPulse,
|
||||||
|
IconPieChart2Stroked
|
||||||
|
} from '@douyinfe/semi-icons';
|
||||||
|
import { VChart } from '@visactor/react-vchart';
|
||||||
|
|
||||||
|
const ChartsPanel = ({
|
||||||
|
activeChartTab,
|
||||||
|
setActiveChartTab,
|
||||||
|
spec_line,
|
||||||
|
spec_model_line,
|
||||||
|
spec_pie,
|
||||||
|
spec_rank_bar,
|
||||||
|
CARD_PROPS,
|
||||||
|
CHART_CONFIG,
|
||||||
|
FLEX_CENTER_GAP2,
|
||||||
|
hasApiInfoPanel,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
{...CARD_PROPS}
|
||||||
|
className={`shadow-sm !rounded-2xl ${hasApiInfoPanel ? 'lg:col-span-3' : ''}`}
|
||||||
|
title={
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3">
|
||||||
|
<div className={FLEX_CENTER_GAP2}>
|
||||||
|
<PieChart size={16} />
|
||||||
|
{t('模型数据分析')}
|
||||||
|
</div>
|
||||||
|
<Tabs
|
||||||
|
type="button"
|
||||||
|
activeKey={activeChartTab}
|
||||||
|
onChange={setActiveChartTab}
|
||||||
|
>
|
||||||
|
<TabPane tab={
|
||||||
|
<span>
|
||||||
|
<IconHistogram />
|
||||||
|
{t('消耗分布')}
|
||||||
|
</span>
|
||||||
|
} itemKey="1" />
|
||||||
|
<TabPane tab={
|
||||||
|
<span>
|
||||||
|
<IconPulse />
|
||||||
|
{t('消耗趋势')}
|
||||||
|
</span>
|
||||||
|
} itemKey="2" />
|
||||||
|
<TabPane tab={
|
||||||
|
<span>
|
||||||
|
<IconPieChart2Stroked />
|
||||||
|
{t('调用次数分布')}
|
||||||
|
</span>
|
||||||
|
} itemKey="3" />
|
||||||
|
<TabPane tab={
|
||||||
|
<span>
|
||||||
|
<IconHistogram />
|
||||||
|
{t('调用次数排行')}
|
||||||
|
</span>
|
||||||
|
} itemKey="4" />
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
bodyStyle={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
<div className="h-96 p-2">
|
||||||
|
{activeChartTab === '1' && (
|
||||||
|
<VChart
|
||||||
|
spec={spec_line}
|
||||||
|
option={CHART_CONFIG}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeChartTab === '2' && (
|
||||||
|
<VChart
|
||||||
|
spec={spec_model_line}
|
||||||
|
option={CHART_CONFIG}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeChartTab === '3' && (
|
||||||
|
<VChart
|
||||||
|
spec={spec_pie}
|
||||||
|
option={CHART_CONFIG}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeChartTab === '4' && (
|
||||||
|
<VChart
|
||||||
|
spec={spec_rank_bar}
|
||||||
|
option={CHART_CONFIG}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChartsPanel;
|
||||||
61
web/src/components/dashboard/DashboardHeader.jsx
Normal file
61
web/src/components/dashboard/DashboardHeader.jsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconRefresh, IconSearch } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
const DashboardHeader = ({
|
||||||
|
getGreeting,
|
||||||
|
greetingVisible,
|
||||||
|
showSearchModal,
|
||||||
|
refresh,
|
||||||
|
loading,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
|
const ICON_BUTTON_CLASS = "text-white hover:bg-opacity-80 !rounded-full";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2
|
||||||
|
className="text-2xl font-semibold text-gray-800 transition-opacity duration-1000 ease-in-out"
|
||||||
|
style={{ opacity: greetingVisible ? 1 : 0 }}
|
||||||
|
>
|
||||||
|
{getGreeting}
|
||||||
|
</h2>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
icon={<IconSearch />}
|
||||||
|
onClick={showSearchModal}
|
||||||
|
className={`bg-green-500 hover:bg-green-600 ${ICON_BUTTON_CLASS}`}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
icon={<IconRefresh />}
|
||||||
|
onClick={refresh}
|
||||||
|
loading={loading}
|
||||||
|
className={`bg-blue-500 hover:bg-blue-600 ${ICON_BUTTON_CLASS}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardHeader;
|
||||||
81
web/src/components/dashboard/FaqPanel.jsx
Normal file
81
web/src/components/dashboard/FaqPanel.jsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, Collapse, Empty } from '@douyinfe/semi-ui';
|
||||||
|
import { HelpCircle } from 'lucide-react';
|
||||||
|
import { IconPlus, IconMinus } from '@douyinfe/semi-icons';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
|
||||||
|
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
||||||
|
|
||||||
|
const FaqPanel = ({
|
||||||
|
faqData,
|
||||||
|
CARD_PROPS,
|
||||||
|
FLEX_CENTER_GAP2,
|
||||||
|
ILLUSTRATION_SIZE,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
{...CARD_PROPS}
|
||||||
|
className="shadow-sm !rounded-2xl lg:col-span-1"
|
||||||
|
title={
|
||||||
|
<div className={FLEX_CENTER_GAP2}>
|
||||||
|
<HelpCircle size={16} />
|
||||||
|
{t('常见问答')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
bodyStyle={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
<ScrollableContainer maxHeight="24rem">
|
||||||
|
{faqData.length > 0 ? (
|
||||||
|
<Collapse
|
||||||
|
accordion
|
||||||
|
expandIcon={<IconPlus />}
|
||||||
|
collapseIcon={<IconMinus />}
|
||||||
|
>
|
||||||
|
{faqData.map((item, index) => (
|
||||||
|
<Collapse.Panel
|
||||||
|
key={index}
|
||||||
|
header={item.question}
|
||||||
|
itemKey={index.toString()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: marked.parse(item.answer || '') }}
|
||||||
|
/>
|
||||||
|
</Collapse.Panel>
|
||||||
|
))}
|
||||||
|
</Collapse>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center items-center py-8">
|
||||||
|
<Empty
|
||||||
|
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||||
|
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
||||||
|
title={t('暂无常见问答')}
|
||||||
|
description={t('请联系管理员在系统设置中配置常见问答')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollableContainer>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FaqPanel;
|
||||||
93
web/src/components/dashboard/StatsCards.jsx
Normal file
93
web/src/components/dashboard/StatsCards.jsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, Avatar, Skeleton } from '@douyinfe/semi-ui';
|
||||||
|
import { VChart } from '@visactor/react-vchart';
|
||||||
|
|
||||||
|
const StatsCards = ({
|
||||||
|
groupedStatsData,
|
||||||
|
loading,
|
||||||
|
getTrendSpec,
|
||||||
|
CARD_PROPS,
|
||||||
|
CHART_CONFIG
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{groupedStatsData.map((group, idx) => (
|
||||||
|
<Card
|
||||||
|
key={idx}
|
||||||
|
{...CARD_PROPS}
|
||||||
|
className={`${group.color} border-0 !rounded-2xl w-full`}
|
||||||
|
title={group.title}
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{group.items.map((item, itemIdx) => (
|
||||||
|
<div
|
||||||
|
key={itemIdx}
|
||||||
|
className="flex items-center justify-between cursor-pointer"
|
||||||
|
onClick={item.onClick}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Avatar
|
||||||
|
className="mr-3"
|
||||||
|
size="small"
|
||||||
|
color={item.avatarColor}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-gray-500">{item.title}</div>
|
||||||
|
<div className="text-lg font-semibold">
|
||||||
|
<Skeleton
|
||||||
|
loading={loading}
|
||||||
|
active
|
||||||
|
placeholder={
|
||||||
|
<Skeleton.Paragraph
|
||||||
|
active
|
||||||
|
rows={1}
|
||||||
|
style={{ width: '65px', height: '24px', marginTop: '4px' }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.value}
|
||||||
|
</Skeleton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(loading || (item.trendData && item.trendData.length > 0)) && (
|
||||||
|
<div className="w-24 h-10">
|
||||||
|
<VChart
|
||||||
|
spec={getTrendSpec(item.trendData, item.trendColor)}
|
||||||
|
option={CHART_CONFIG}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatsCards;
|
||||||
136
web/src/components/dashboard/UptimePanel.jsx
Normal file
136
web/src/components/dashboard/UptimePanel.jsx
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, Button, Spin, Tabs, TabPane, Tag, Empty } from '@douyinfe/semi-ui';
|
||||||
|
import { Gauge } from 'lucide-react';
|
||||||
|
import { IconRefresh } from '@douyinfe/semi-icons';
|
||||||
|
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
|
||||||
|
import ScrollableContainer from '../common/ui/ScrollableContainer';
|
||||||
|
|
||||||
|
const UptimePanel = ({
|
||||||
|
uptimeData,
|
||||||
|
uptimeLoading,
|
||||||
|
activeUptimeTab,
|
||||||
|
setActiveUptimeTab,
|
||||||
|
loadUptimeData,
|
||||||
|
uptimeLegendData,
|
||||||
|
renderMonitorList,
|
||||||
|
CARD_PROPS,
|
||||||
|
ILLUSTRATION_SIZE,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
{...CARD_PROPS}
|
||||||
|
className="shadow-sm !rounded-2xl lg:col-span-1"
|
||||||
|
title={
|
||||||
|
<div className="flex items-center justify-between w-full gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Gauge size={16} />
|
||||||
|
{t('服务可用性')}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
icon={<IconRefresh />}
|
||||||
|
onClick={loadUptimeData}
|
||||||
|
loading={uptimeLoading}
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
type='tertiary'
|
||||||
|
className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
bodyStyle={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
{/* 内容区域 */}
|
||||||
|
<div className="relative">
|
||||||
|
<Spin spinning={uptimeLoading}>
|
||||||
|
{uptimeData.length > 0 ? (
|
||||||
|
uptimeData.length === 1 ? (
|
||||||
|
<ScrollableContainer maxHeight="24rem">
|
||||||
|
{renderMonitorList(uptimeData[0].monitors)}
|
||||||
|
</ScrollableContainer>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
type="card"
|
||||||
|
collapsible
|
||||||
|
activeKey={activeUptimeTab}
|
||||||
|
onChange={setActiveUptimeTab}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{uptimeData.map((group, groupIdx) => (
|
||||||
|
<TabPane
|
||||||
|
tab={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Gauge size={14} />
|
||||||
|
{group.categoryName}
|
||||||
|
<Tag
|
||||||
|
color={activeUptimeTab === group.categoryName ? 'red' : 'grey'}
|
||||||
|
size='small'
|
||||||
|
shape='circle'
|
||||||
|
>
|
||||||
|
{group.monitors ? group.monitors.length : 0}
|
||||||
|
</Tag>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
itemKey={group.categoryName}
|
||||||
|
key={groupIdx}
|
||||||
|
>
|
||||||
|
<ScrollableContainer maxHeight="21.5rem">
|
||||||
|
{renderMonitorList(group.monitors)}
|
||||||
|
</ScrollableContainer>
|
||||||
|
</TabPane>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center items-center py-8">
|
||||||
|
<Empty
|
||||||
|
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||||
|
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
||||||
|
title={t('暂无监控数据')}
|
||||||
|
description={t('请联系管理员在系统设置中配置Uptime')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 图例 */}
|
||||||
|
{uptimeData.length > 0 && (
|
||||||
|
<div className="p-3 bg-gray-50 rounded-b-2xl">
|
||||||
|
<div className="flex flex-wrap gap-3 text-xs justify-center">
|
||||||
|
{uptimeLegendData.map((legend, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-1">
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 rounded-full"
|
||||||
|
style={{ backgroundColor: legend.color }}
|
||||||
|
/>
|
||||||
|
<span className="text-gray-600">{legend.label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UptimePanel;
|
||||||
247
web/src/components/dashboard/index.jsx
Normal file
247
web/src/components/dashboard/index.jsx
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useContext, useEffect } from 'react';
|
||||||
|
import { getRelativeTime } from '../../helpers';
|
||||||
|
import { UserContext } from '../../context/User/index.js';
|
||||||
|
import { StatusContext } from '../../context/Status/index.js';
|
||||||
|
|
||||||
|
import DashboardHeader from './DashboardHeader';
|
||||||
|
import StatsCards from './StatsCards';
|
||||||
|
import ChartsPanel from './ChartsPanel';
|
||||||
|
import ApiInfoPanel from './ApiInfoPanel';
|
||||||
|
import AnnouncementsPanel from './AnnouncementsPanel';
|
||||||
|
import FaqPanel from './FaqPanel';
|
||||||
|
import UptimePanel from './UptimePanel';
|
||||||
|
import SearchModal from './modals/SearchModal';
|
||||||
|
|
||||||
|
import { useDashboardData } from '../../hooks/dashboard/useDashboardData';
|
||||||
|
import { useDashboardStats } from '../../hooks/dashboard/useDashboardStats';
|
||||||
|
import { useDashboardCharts } from '../../hooks/dashboard/useDashboardCharts';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CHART_CONFIG,
|
||||||
|
CARD_PROPS,
|
||||||
|
FLEX_CENTER_GAP2,
|
||||||
|
ILLUSTRATION_SIZE,
|
||||||
|
ANNOUNCEMENT_LEGEND_DATA,
|
||||||
|
UPTIME_STATUS_MAP
|
||||||
|
} from '../../constants/dashboard.constants';
|
||||||
|
import {
|
||||||
|
getTrendSpec,
|
||||||
|
handleCopyUrl,
|
||||||
|
handleSpeedTest,
|
||||||
|
getUptimeStatusColor,
|
||||||
|
getUptimeStatusText,
|
||||||
|
renderMonitorList
|
||||||
|
} from '../../helpers/dashboard';
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
// ========== Context ==========
|
||||||
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
|
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||||
|
|
||||||
|
// ========== 主要数据管理 ==========
|
||||||
|
const dashboardData = useDashboardData(userState, userDispatch, statusState);
|
||||||
|
|
||||||
|
// ========== 图表管理 ==========
|
||||||
|
const dashboardCharts = useDashboardCharts(
|
||||||
|
dashboardData.dataExportDefaultTime,
|
||||||
|
dashboardData.setTrendData,
|
||||||
|
dashboardData.setConsumeQuota,
|
||||||
|
dashboardData.setTimes,
|
||||||
|
dashboardData.setConsumeTokens,
|
||||||
|
dashboardData.setPieData,
|
||||||
|
dashboardData.setLineData,
|
||||||
|
dashboardData.setModelColors,
|
||||||
|
dashboardData.t
|
||||||
|
);
|
||||||
|
|
||||||
|
// ========== 统计数据 ==========
|
||||||
|
const { groupedStatsData } = useDashboardStats(
|
||||||
|
userState,
|
||||||
|
dashboardData.consumeQuota,
|
||||||
|
dashboardData.consumeTokens,
|
||||||
|
dashboardData.times,
|
||||||
|
dashboardData.trendData,
|
||||||
|
dashboardData.performanceMetrics,
|
||||||
|
dashboardData.navigate,
|
||||||
|
dashboardData.t
|
||||||
|
);
|
||||||
|
|
||||||
|
// ========== 数据处理 ==========
|
||||||
|
const initChart = async () => {
|
||||||
|
await dashboardData.loadQuotaData().then(data => {
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
dashboardCharts.updateChartData(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await dashboardData.loadUptimeData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
const data = await dashboardData.refresh();
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
dashboardCharts.updateChartData(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchConfirm = async () => {
|
||||||
|
await dashboardData.handleSearchConfirm(dashboardCharts.updateChartData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== 数据准备 ==========
|
||||||
|
const apiInfoData = statusState?.status?.api_info || [];
|
||||||
|
const announcementData = (statusState?.status?.announcements || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
time: getRelativeTime(item.publishDate)
|
||||||
|
}));
|
||||||
|
const faqData = statusState?.status?.faq || [];
|
||||||
|
|
||||||
|
const uptimeLegendData = Object.entries(UPTIME_STATUS_MAP).map(([status, info]) => ({
|
||||||
|
status: Number(status),
|
||||||
|
color: info.color,
|
||||||
|
label: dashboardData.t(info.label)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ========== Effects ==========
|
||||||
|
useEffect(() => {
|
||||||
|
initChart();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full">
|
||||||
|
<DashboardHeader
|
||||||
|
getGreeting={dashboardData.getGreeting}
|
||||||
|
greetingVisible={dashboardData.greetingVisible}
|
||||||
|
showSearchModal={dashboardData.showSearchModal}
|
||||||
|
refresh={handleRefresh}
|
||||||
|
loading={dashboardData.loading}
|
||||||
|
t={dashboardData.t}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SearchModal
|
||||||
|
searchModalVisible={dashboardData.searchModalVisible}
|
||||||
|
handleSearchConfirm={handleSearchConfirm}
|
||||||
|
handleCloseModal={dashboardData.handleCloseModal}
|
||||||
|
isMobile={dashboardData.isMobile}
|
||||||
|
isAdminUser={dashboardData.isAdminUser}
|
||||||
|
inputs={dashboardData.inputs}
|
||||||
|
dataExportDefaultTime={dashboardData.dataExportDefaultTime}
|
||||||
|
timeOptions={dashboardData.timeOptions}
|
||||||
|
handleInputChange={dashboardData.handleInputChange}
|
||||||
|
t={dashboardData.t}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatsCards
|
||||||
|
groupedStatsData={groupedStatsData}
|
||||||
|
loading={dashboardData.loading}
|
||||||
|
getTrendSpec={getTrendSpec}
|
||||||
|
CARD_PROPS={CARD_PROPS}
|
||||||
|
CHART_CONFIG={CHART_CONFIG}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* API信息和图表面板 */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className={`grid grid-cols-1 gap-4 ${dashboardData.hasApiInfoPanel ? 'lg:grid-cols-4' : ''}`}>
|
||||||
|
<ChartsPanel
|
||||||
|
activeChartTab={dashboardData.activeChartTab}
|
||||||
|
setActiveChartTab={dashboardData.setActiveChartTab}
|
||||||
|
spec_line={dashboardCharts.spec_line}
|
||||||
|
spec_model_line={dashboardCharts.spec_model_line}
|
||||||
|
spec_pie={dashboardCharts.spec_pie}
|
||||||
|
spec_rank_bar={dashboardCharts.spec_rank_bar}
|
||||||
|
CARD_PROPS={CARD_PROPS}
|
||||||
|
CHART_CONFIG={CHART_CONFIG}
|
||||||
|
FLEX_CENTER_GAP2={FLEX_CENTER_GAP2}
|
||||||
|
hasApiInfoPanel={dashboardData.hasApiInfoPanel}
|
||||||
|
t={dashboardData.t}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{dashboardData.hasApiInfoPanel && (
|
||||||
|
<ApiInfoPanel
|
||||||
|
apiInfoData={apiInfoData}
|
||||||
|
handleCopyUrl={(url) => handleCopyUrl(url, dashboardData.t)}
|
||||||
|
handleSpeedTest={handleSpeedTest}
|
||||||
|
CARD_PROPS={CARD_PROPS}
|
||||||
|
FLEX_CENTER_GAP2={FLEX_CENTER_GAP2}
|
||||||
|
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
|
||||||
|
t={dashboardData.t}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 系统公告和常见问答卡片 */}
|
||||||
|
{dashboardData.hasInfoPanels && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||||
|
{/* 公告卡片 */}
|
||||||
|
{dashboardData.announcementsEnabled && (
|
||||||
|
<AnnouncementsPanel
|
||||||
|
announcementData={announcementData}
|
||||||
|
announcementLegendData={ANNOUNCEMENT_LEGEND_DATA.map(item => ({
|
||||||
|
...item,
|
||||||
|
label: dashboardData.t(item.label)
|
||||||
|
}))}
|
||||||
|
CARD_PROPS={CARD_PROPS}
|
||||||
|
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
|
||||||
|
t={dashboardData.t}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 常见问答卡片 */}
|
||||||
|
{dashboardData.faqEnabled && (
|
||||||
|
<FaqPanel
|
||||||
|
faqData={faqData}
|
||||||
|
CARD_PROPS={CARD_PROPS}
|
||||||
|
FLEX_CENTER_GAP2={FLEX_CENTER_GAP2}
|
||||||
|
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
|
||||||
|
t={dashboardData.t}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 服务可用性卡片 */}
|
||||||
|
{dashboardData.uptimeEnabled && (
|
||||||
|
<UptimePanel
|
||||||
|
uptimeData={dashboardData.uptimeData}
|
||||||
|
uptimeLoading={dashboardData.uptimeLoading}
|
||||||
|
activeUptimeTab={dashboardData.activeUptimeTab}
|
||||||
|
setActiveUptimeTab={dashboardData.setActiveUptimeTab}
|
||||||
|
loadUptimeData={dashboardData.loadUptimeData}
|
||||||
|
uptimeLegendData={uptimeLegendData}
|
||||||
|
renderMonitorList={(monitors) => renderMonitorList(
|
||||||
|
monitors,
|
||||||
|
(status) => getUptimeStatusColor(status, UPTIME_STATUS_MAP),
|
||||||
|
(status) => getUptimeStatusText(status, UPTIME_STATUS_MAP, dashboardData.t),
|
||||||
|
dashboardData.t
|
||||||
|
)}
|
||||||
|
CARD_PROPS={CARD_PROPS}
|
||||||
|
ILLUSTRATION_SIZE={ILLUSTRATION_SIZE}
|
||||||
|
t={dashboardData.t}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
101
web/src/components/dashboard/modals/SearchModal.jsx
Normal file
101
web/src/components/dashboard/modals/SearchModal.jsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { Modal, Form } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
const SearchModal = ({
|
||||||
|
searchModalVisible,
|
||||||
|
handleSearchConfirm,
|
||||||
|
handleCloseModal,
|
||||||
|
isMobile,
|
||||||
|
isAdminUser,
|
||||||
|
inputs,
|
||||||
|
dataExportDefaultTime,
|
||||||
|
timeOptions,
|
||||||
|
handleInputChange,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
|
const formRef = useRef();
|
||||||
|
|
||||||
|
const FORM_FIELD_PROPS = {
|
||||||
|
className: "w-full mb-2 !rounded-lg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFormField = (Component, props) => (
|
||||||
|
<Component {...FORM_FIELD_PROPS} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const { start_timestamp, end_timestamp, username } = inputs;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('搜索条件')}
|
||||||
|
visible={searchModalVisible}
|
||||||
|
onOk={handleSearchConfirm}
|
||||||
|
onCancel={handleCloseModal}
|
||||||
|
closeOnEsc={true}
|
||||||
|
size={isMobile ? 'full-width' : 'small'}
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Form ref={formRef} layout='vertical' className="w-full">
|
||||||
|
{createFormField(Form.DatePicker, {
|
||||||
|
field: 'start_timestamp',
|
||||||
|
label: t('起始时间'),
|
||||||
|
initValue: start_timestamp,
|
||||||
|
value: start_timestamp,
|
||||||
|
type: 'dateTime',
|
||||||
|
name: 'start_timestamp',
|
||||||
|
onChange: (value) => handleInputChange(value, 'start_timestamp')
|
||||||
|
})}
|
||||||
|
|
||||||
|
{createFormField(Form.DatePicker, {
|
||||||
|
field: 'end_timestamp',
|
||||||
|
label: t('结束时间'),
|
||||||
|
initValue: end_timestamp,
|
||||||
|
value: end_timestamp,
|
||||||
|
type: 'dateTime',
|
||||||
|
name: 'end_timestamp',
|
||||||
|
onChange: (value) => handleInputChange(value, 'end_timestamp')
|
||||||
|
})}
|
||||||
|
|
||||||
|
{createFormField(Form.Select, {
|
||||||
|
field: 'data_export_default_time',
|
||||||
|
label: t('时间粒度'),
|
||||||
|
initValue: dataExportDefaultTime,
|
||||||
|
placeholder: t('时间粒度'),
|
||||||
|
name: 'data_export_default_time',
|
||||||
|
optionList: timeOptions,
|
||||||
|
onChange: (value) => handleInputChange(value, 'data_export_default_time')
|
||||||
|
})}
|
||||||
|
|
||||||
|
{isAdminUser && createFormField(Form.Input, {
|
||||||
|
field: 'username',
|
||||||
|
label: t('用户名称'),
|
||||||
|
value: username,
|
||||||
|
placeholder: t('可选值'),
|
||||||
|
name: 'username',
|
||||||
|
onChange: (value) => handleInputChange(value, 'username')
|
||||||
|
})}
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchModal;
|
||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState, useMemo, useContext } from 'react';
|
import React, { useEffect, useState, useMemo, useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Typography } from '@douyinfe/semi-ui';
|
import { Typography } from '@douyinfe/semi-ui';
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useContext, useEffect, useState, useRef } from 'react';
|
import React, { useContext, useEffect, useState, useRef } from 'react';
|
||||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { UserContext } from '../../context/User/index.js';
|
import { UserContext } from '../../context/User/index.js';
|
||||||
@@ -31,8 +50,8 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { StatusContext } from '../../context/Status/index.js';
|
import { StatusContext } from '../../context/Status/index.js';
|
||||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
import { useIsMobile } from '../../hooks/common/useIsMobile.js';
|
||||||
import { useSidebarCollapsed } from '../../hooks/useSidebarCollapsed.js';
|
import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed.js';
|
||||||
|
|
||||||
const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
@@ -41,6 +60,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
|
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [logoLoaded, setLogoLoaded] = useState(false);
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
const [currentLang, setCurrentLang] = useState(i18n.language);
|
const [currentLang, setCurrentLang] = useState(i18n.language);
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
@@ -207,6 +227,14 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||||||
}
|
}
|
||||||
}, [statusState?.status]);
|
}, [statusState?.status]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLogoLoaded(false);
|
||||||
|
if (!logo) return;
|
||||||
|
const img = new Image();
|
||||||
|
img.src = logo;
|
||||||
|
img.onload = () => setLogoLoaded(true);
|
||||||
|
}, [logo]);
|
||||||
|
|
||||||
const handleLanguageChange = (lang) => {
|
const handleLanguageChange = (lang) => {
|
||||||
i18n.changeLanguage(lang);
|
i18n.changeLanguage(lang);
|
||||||
setMobileMenuOpen(false);
|
setMobileMenuOpen(false);
|
||||||
@@ -336,7 +364,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<IconKey size="small" className="text-gray-500 dark:text-gray-400" />
|
<IconKey size="small" className="text-gray-500 dark:text-gray-400" />
|
||||||
<span>{t('API令牌')}</span>
|
<span>{t('令牌管理')}</span>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
@@ -477,19 +505,20 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/" onClick={() => handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2">
|
<Link to="/" onClick={() => handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2">
|
||||||
<Skeleton
|
<div className="relative w-8 h-8 md:w-8 md:h-8">
|
||||||
loading={isLoading}
|
{(isLoading || !logoLoaded) && (
|
||||||
active
|
|
||||||
placeholder={
|
|
||||||
<Skeleton.Image
|
<Skeleton.Image
|
||||||
active
|
active
|
||||||
className="h-7 md:h-8 !rounded-full"
|
className="absolute inset-0 !rounded-full"
|
||||||
style={{ width: 32, height: 32 }}
|
style={{ width: '100%', height: '100%' }}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
>
|
<img
|
||||||
<img src={logo} alt="logo" className="h-7 md:h-8 transition-transform duration-300 ease-in-out group-hover:scale-105 rounded-full" />
|
src={logo}
|
||||||
</Skeleton>
|
alt="logo"
|
||||||
|
className={`absolute inset-0 w-full h-full transition-opacity duration-200 group-hover:scale-105 rounded-full ${(!isLoading && logoLoaded) ? 'opacity-100' : 'opacity-0'}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="hidden md:flex items-center gap-2">
|
<div className="hidden md:flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState, useContext, useMemo } from 'react';
|
import React, { useEffect, useState, useContext, useMemo } from 'react';
|
||||||
import { Button, Modal, Empty, Tabs, TabPane, Timeline } from '@douyinfe/semi-ui';
|
import { Button, Modal, Empty, Tabs, TabPane, Timeline } from '@douyinfe/semi-ui';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import HeaderBar from './HeaderBar.js';
|
import HeaderBar from './HeaderBar.js';
|
||||||
import { Layout } from '@douyinfe/semi-ui';
|
import { Layout } from '@douyinfe/semi-ui';
|
||||||
import SiderBar from './SiderBar.js';
|
import SiderBar from './SiderBar.js';
|
||||||
@@ -5,8 +24,8 @@ import App from '../../App.js';
|
|||||||
import FooterBar from './Footer.js';
|
import FooterBar from './Footer.js';
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
import { useIsMobile } from '../../hooks/common/useIsMobile.js';
|
||||||
import { useSidebarCollapsed } from '../../hooks/useSidebarCollapsed.js';
|
import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed.js';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { API, getLogo, getSystemName, showError, setStatusData } from '../../helpers/index.js';
|
import { API, getLogo, getSystemName, showError, setStatusData } from '../../helpers/index.js';
|
||||||
import { UserContext } from '../../context/User/index.js';
|
import { UserContext } from '../../context/User/index.js';
|
||||||
@@ -23,7 +42,7 @@ const PageLayout = () => {
|
|||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const shouldHideFooter = location.pathname === '/console/playground' || location.pathname.startsWith('/console/chat');
|
const shouldHideFooter = location.pathname.startsWith('/console');
|
||||||
|
|
||||||
const shouldInnerPadding = location.pathname.includes('/console') &&
|
const shouldInnerPadding = location.pathname.includes('/console') &&
|
||||||
!location.pathname.startsWith('/console/chat') &&
|
!location.pathname.startsWith('/console/chat') &&
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import { Navigate, useLocation } from 'react-router-dom';
|
import { Navigate, useLocation } from 'react-router-dom';
|
||||||
import { StatusContext } from '../../context/Status';
|
import { StatusContext } from '../../context/Status';
|
||||||
|
|||||||
@@ -1,9 +1,28 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { getLucideIcon, sidebarIconColors } from '../../helpers/render.js';
|
import { getLucideIcon, sidebarIconColors } from '../../helpers/render.js';
|
||||||
import { ChevronLeft } from 'lucide-react';
|
import { ChevronLeft } from 'lucide-react';
|
||||||
import { useSidebarCollapsed } from '../../hooks/useSidebarCollapsed.js';
|
import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed.js';
|
||||||
import {
|
import {
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isRoot,
|
isRoot,
|
||||||
@@ -56,7 +75,7 @@ const SiderBar = ({ onNavigate = () => { } }) => {
|
|||||||
: 'tableHiddle',
|
: 'tableHiddle',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: t('API令牌'),
|
text: t('令牌管理'),
|
||||||
itemKey: 'token',
|
itemKey: 'token',
|
||||||
to: '/token',
|
to: '/token',
|
||||||
},
|
},
|
||||||
@@ -109,13 +128,13 @@ const SiderBar = ({ onNavigate = () => { } }) => {
|
|||||||
const adminItems = useMemo(
|
const adminItems = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
text: t('渠道'),
|
text: t('渠道管理'),
|
||||||
itemKey: 'channel',
|
itemKey: 'channel',
|
||||||
to: '/channel',
|
to: '/channel',
|
||||||
className: isAdmin() ? '' : 'tableHiddle',
|
className: isAdmin() ? '' : 'tableHiddle',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: t('兑换码'),
|
text: t('兑换码管理'),
|
||||||
itemKey: 'redemption',
|
itemKey: 'redemption',
|
||||||
to: '/redemption',
|
to: '/redemption',
|
||||||
className: isAdmin() ? '' : 'tableHiddle',
|
className: isAdmin() ? '' : 'tableHiddle',
|
||||||
@@ -421,7 +440,7 @@ const SiderBar = ({ onNavigate = () => { } }) => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onClick={toggleCollapsed}
|
onClick={toggleCollapsed}
|
||||||
iconOnly={collapsed}
|
icononly={collapsed}
|
||||||
style={collapsed ? { padding: '4px', width: '100%' } : { padding: '4px 12px', width: '100%' }}
|
style={collapsed ? { padding: '4px', width: '100%' } : { padding: '4px 12px', width: '100%' }}
|
||||||
>
|
>
|
||||||
{!collapsed ? t('收起侧边栏') : null}
|
{!collapsed ? t('收起侧边栏') : null}
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useState, useMemo, useCallback } from 'react';
|
import React, { useState, useMemo, useCallback } from 'react';
|
||||||
import { Button, Tooltip, Toast } from '@douyinfe/semi-ui';
|
import { Button, Tooltip, Toast } from '@douyinfe/semi-ui';
|
||||||
import { Copy, ChevronDown, ChevronUp } from 'lucide-react';
|
import { Copy, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const CustomInputRender = (props) => {
|
const CustomInputRender = (props) => {
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
TextArea,
|
TextArea,
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
@@ -61,7 +80,7 @@ const FloatingButtons = ({
|
|||||||
? 'linear-gradient(to right, #e11d48, #be123c)'
|
? 'linear-gradient(to right, #e11d48, #be123c)'
|
||||||
: 'linear-gradient(to right, #4f46e5, #6366f1)',
|
: 'linear-gradient(to right, #4f46e5, #6366f1)',
|
||||||
}}
|
}}
|
||||||
className="lg:hidden !rounded-full !p-0"
|
className="lg:hidden"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Typography,
|
Typography,
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MessageContent from './MessageContent';
|
import MessageContent from './MessageContent';
|
||||||
import MessageActions from './MessageActions';
|
import MessageActions from './MessageActions';
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -14,7 +33,7 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { renderGroupOption } from '../../helpers';
|
import { renderGroupOption, modelSelectFilter } from '../../helpers';
|
||||||
import ParameterControl from './ParameterControl';
|
import ParameterControl from './ParameterControl';
|
||||||
import ImageUrlInput from './ImageUrlInput';
|
import ImageUrlInput from './ImageUrlInput';
|
||||||
import ConfigManager from './ConfigManager';
|
import ConfigManager from './ConfigManager';
|
||||||
@@ -154,8 +173,8 @@ const SettingsPanel = ({
|
|||||||
name='model'
|
name='model'
|
||||||
required
|
required
|
||||||
selection
|
selection
|
||||||
searchPosition='dropdown'
|
filter={modelSelectFilter}
|
||||||
filter
|
autoClearSearchValue={false}
|
||||||
onChange={(value) => onInputChange('model', value)}
|
onChange={(value) => onInputChange('model', value)}
|
||||||
value={inputs.model}
|
value={inputs.model}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { Typography } from '@douyinfe/semi-ui';
|
import { Typography } from '@douyinfe/semi-ui';
|
||||||
import MarkdownRenderer from '../common/markdown/MarkdownRenderer';
|
import MarkdownRenderer from '../common/markdown/MarkdownRenderer';
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import { STORAGE_KEYS, DEFAULT_CONFIG } from '../../constants/playground.constants';
|
import { STORAGE_KEYS, DEFAULT_CONFIG } from '../../constants/playground.constants';
|
||||||
|
|
||||||
const MESSAGES_STORAGE_KEY = 'playground_messages';
|
const MESSAGES_STORAGE_KEY = 'playground_messages';
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
export { default as SettingsPanel } from './SettingsPanel';
|
export { default as SettingsPanel } from './SettingsPanel';
|
||||||
export { default as ChatArea } from './ChatArea';
|
export { default as ChatArea } from './ChatArea';
|
||||||
export { default as DebugPanel } from './DebugPanel';
|
export { default as DebugPanel } from './DebugPanel';
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
import { useIsMobile } from '../../hooks/common/useIsMobile.js';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
Table,
|
Table,
|
||||||
@@ -212,11 +231,6 @@ const ChannelSelectorModal = forwardRef(({
|
|||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
showQuickJumper: true,
|
showQuickJumper: true,
|
||||||
pageSizeOptions: ['10', '20', '50', '100'],
|
pageSizeOptions: ['10', '20', '50', '100'],
|
||||||
formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
|
||||||
start: page.currentStart,
|
|
||||||
end: page.currentEnd,
|
|
||||||
total: total,
|
|
||||||
}),
|
|
||||||
onChange: (page, size) => {
|
onChange: (page, size) => {
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
setPageSize(size);
|
setPageSize(size);
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, Spin } from '@douyinfe/semi-ui';
|
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||||
import SettingsChats from '../../pages/Setting/Chat/SettingsChats.js';
|
import SettingsChats from '../../pages/Setting/Chat/SettingsChats.js';
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState, useMemo } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import { Card, Spin, Button, Modal } from '@douyinfe/semi-ui';
|
import { Card, Spin, Button, Modal } from '@douyinfe/semi-ui';
|
||||||
import { API, showError, showSuccess, toBoolean } from '../../helpers';
|
import { API, showError, showSuccess, toBoolean } from '../../helpers';
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, Spin } from '@douyinfe/semi-ui';
|
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||||
import SettingsDrawing from '../../pages/Setting/Drawing/SettingsDrawing.js';
|
import SettingsDrawing from '../../pages/Setting/Drawing/SettingsDrawing.js';
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
|
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, Spin } from '@douyinfe/semi-ui';
|
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||||
import SettingsGeneral from '../../pages/Setting/Operation/SettingsGeneral.js';
|
import SettingsGeneral from '../../pages/Setting/Operation/SettingsGeneral.js';
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Banner,
|
Banner,
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, Spin } from '@douyinfe/semi-ui';
|
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||||
import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralPayment.js';
|
import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralPayment.js';
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
@@ -379,7 +398,7 @@ const PersonalSetting = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-50 mt-[64px]">
|
<div className="bg-gray-50 mt-[60px]">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{/* 主卡片容器 */}
|
{/* 主卡片容器 */}
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, Spin } from '@douyinfe/semi-ui';
|
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
|
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,982 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
|
||||||
Palette,
|
|
||||||
ZoomIn,
|
|
||||||
Shuffle,
|
|
||||||
Move,
|
|
||||||
FileText,
|
|
||||||
Blend,
|
|
||||||
Upload,
|
|
||||||
Minimize2,
|
|
||||||
RotateCcw,
|
|
||||||
PaintBucket,
|
|
||||||
Focus,
|
|
||||||
Move3D,
|
|
||||||
Monitor,
|
|
||||||
UserCheck,
|
|
||||||
HelpCircle,
|
|
||||||
CheckCircle,
|
|
||||||
Clock,
|
|
||||||
Copy,
|
|
||||||
FileX,
|
|
||||||
Pause,
|
|
||||||
XCircle,
|
|
||||||
Loader,
|
|
||||||
AlertCircle,
|
|
||||||
Hash,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import {
|
|
||||||
API,
|
|
||||||
copy,
|
|
||||||
isAdmin,
|
|
||||||
showError,
|
|
||||||
showSuccess,
|
|
||||||
timestamp2string
|
|
||||||
} from '../../helpers';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Checkbox,
|
|
||||||
Divider,
|
|
||||||
Empty,
|
|
||||||
Form,
|
|
||||||
ImagePreview,
|
|
||||||
Layout,
|
|
||||||
Modal,
|
|
||||||
Progress,
|
|
||||||
Skeleton,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
Typography
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import {
|
|
||||||
IllustrationNoResult,
|
|
||||||
IllustrationNoResultDark
|
|
||||||
} from '@douyinfe/semi-illustrations';
|
|
||||||
import { ITEMS_PER_PAGE } from '../../constants';
|
|
||||||
import {
|
|
||||||
IconEyeOpened,
|
|
||||||
IconSearch,
|
|
||||||
} from '@douyinfe/semi-icons';
|
|
||||||
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
const colors = [
|
|
||||||
'amber',
|
|
||||||
'blue',
|
|
||||||
'cyan',
|
|
||||||
'green',
|
|
||||||
'grey',
|
|
||||||
'indigo',
|
|
||||||
'light-blue',
|
|
||||||
'lime',
|
|
||||||
'orange',
|
|
||||||
'pink',
|
|
||||||
'purple',
|
|
||||||
'red',
|
|
||||||
'teal',
|
|
||||||
'violet',
|
|
||||||
'yellow',
|
|
||||||
];
|
|
||||||
|
|
||||||
// 定义列键值常量
|
|
||||||
const COLUMN_KEYS = {
|
|
||||||
SUBMIT_TIME: 'submit_time',
|
|
||||||
DURATION: 'duration',
|
|
||||||
CHANNEL: 'channel',
|
|
||||||
TYPE: 'type',
|
|
||||||
TASK_ID: 'task_id',
|
|
||||||
SUBMIT_RESULT: 'submit_result',
|
|
||||||
TASK_STATUS: 'task_status',
|
|
||||||
PROGRESS: 'progress',
|
|
||||||
IMAGE: 'image',
|
|
||||||
PROMPT: 'prompt',
|
|
||||||
PROMPT_EN: 'prompt_en',
|
|
||||||
FAIL_REASON: 'fail_reason',
|
|
||||||
};
|
|
||||||
|
|
||||||
const LogsTable = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [modalContent, setModalContent] = useState('');
|
|
||||||
|
|
||||||
// 列可见性状态
|
|
||||||
const [visibleColumns, setVisibleColumns] = useState({});
|
|
||||||
const [showColumnSelector, setShowColumnSelector] = useState(false);
|
|
||||||
const isAdminUser = isAdmin();
|
|
||||||
const [compactMode, setCompactMode] = useTableCompactMode('mjLogs');
|
|
||||||
|
|
||||||
// 加载保存的列偏好设置
|
|
||||||
useEffect(() => {
|
|
||||||
const savedColumns = localStorage.getItem('mj-logs-table-columns');
|
|
||||||
if (savedColumns) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(savedColumns);
|
|
||||||
const defaults = getDefaultColumnVisibility();
|
|
||||||
const merged = { ...defaults, ...parsed };
|
|
||||||
setVisibleColumns(merged);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse saved column preferences', e);
|
|
||||||
initDefaultColumns();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
initDefaultColumns();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 获取默认列可见性
|
|
||||||
const getDefaultColumnVisibility = () => {
|
|
||||||
return {
|
|
||||||
[COLUMN_KEYS.SUBMIT_TIME]: true,
|
|
||||||
[COLUMN_KEYS.DURATION]: true,
|
|
||||||
[COLUMN_KEYS.CHANNEL]: isAdminUser,
|
|
||||||
[COLUMN_KEYS.TYPE]: true,
|
|
||||||
[COLUMN_KEYS.TASK_ID]: true,
|
|
||||||
[COLUMN_KEYS.SUBMIT_RESULT]: isAdminUser,
|
|
||||||
[COLUMN_KEYS.TASK_STATUS]: true,
|
|
||||||
[COLUMN_KEYS.PROGRESS]: true,
|
|
||||||
[COLUMN_KEYS.IMAGE]: true,
|
|
||||||
[COLUMN_KEYS.PROMPT]: true,
|
|
||||||
[COLUMN_KEYS.PROMPT_EN]: true,
|
|
||||||
[COLUMN_KEYS.FAIL_REASON]: true,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化默认列可见性
|
|
||||||
const initDefaultColumns = () => {
|
|
||||||
const defaults = getDefaultColumnVisibility();
|
|
||||||
setVisibleColumns(defaults);
|
|
||||||
localStorage.setItem('mj-logs-table-columns', JSON.stringify(defaults));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理列可见性变化
|
|
||||||
const handleColumnVisibilityChange = (columnKey, checked) => {
|
|
||||||
const updatedColumns = { ...visibleColumns, [columnKey]: checked };
|
|
||||||
setVisibleColumns(updatedColumns);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理全选
|
|
||||||
const handleSelectAll = (checked) => {
|
|
||||||
const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
|
|
||||||
const updatedColumns = {};
|
|
||||||
|
|
||||||
allKeys.forEach((key) => {
|
|
||||||
if ((key === COLUMN_KEYS.CHANNEL || key === COLUMN_KEYS.SUBMIT_RESULT) && !isAdminUser) {
|
|
||||||
updatedColumns[key] = false;
|
|
||||||
} else {
|
|
||||||
updatedColumns[key] = checked;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setVisibleColumns(updatedColumns);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新表格时保存列可见性
|
|
||||||
useEffect(() => {
|
|
||||||
if (Object.keys(visibleColumns).length > 0) {
|
|
||||||
localStorage.setItem('mj-logs-table-columns', JSON.stringify(visibleColumns));
|
|
||||||
}
|
|
||||||
}, [visibleColumns]);
|
|
||||||
|
|
||||||
function renderType(type) {
|
|
||||||
switch (type) {
|
|
||||||
case 'IMAGINE':
|
|
||||||
return (
|
|
||||||
<Tag color='blue' shape='circle' prefixIcon={<Palette size={14} />}>
|
|
||||||
{t('绘图')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'UPSCALE':
|
|
||||||
return (
|
|
||||||
<Tag color='orange' shape='circle' prefixIcon={<ZoomIn size={14} />}>
|
|
||||||
{t('放大')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'VIDEO':
|
|
||||||
return (
|
|
||||||
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
|
|
||||||
{t('视频')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'EDITS':
|
|
||||||
return (
|
|
||||||
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
|
|
||||||
{t('编辑')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'VARIATION':
|
|
||||||
return (
|
|
||||||
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
|
|
||||||
{t('变换')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'HIGH_VARIATION':
|
|
||||||
return (
|
|
||||||
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
|
|
||||||
{t('强变换')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'LOW_VARIATION':
|
|
||||||
return (
|
|
||||||
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
|
|
||||||
{t('弱变换')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'PAN':
|
|
||||||
return (
|
|
||||||
<Tag color='cyan' shape='circle' prefixIcon={<Move size={14} />}>
|
|
||||||
{t('平移')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'DESCRIBE':
|
|
||||||
return (
|
|
||||||
<Tag color='yellow' shape='circle' prefixIcon={<FileText size={14} />}>
|
|
||||||
{t('图生文')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'BLEND':
|
|
||||||
return (
|
|
||||||
<Tag color='lime' shape='circle' prefixIcon={<Blend size={14} />}>
|
|
||||||
{t('图混合')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'UPLOAD':
|
|
||||||
return (
|
|
||||||
<Tag color='blue' shape='circle' prefixIcon={<Upload size={14} />}>
|
|
||||||
上传文件
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'SHORTEN':
|
|
||||||
return (
|
|
||||||
<Tag color='pink' shape='circle' prefixIcon={<Minimize2 size={14} />}>
|
|
||||||
{t('缩词')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'REROLL':
|
|
||||||
return (
|
|
||||||
<Tag color='indigo' shape='circle' prefixIcon={<RotateCcw size={14} />}>
|
|
||||||
{t('重绘')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'INPAINT':
|
|
||||||
return (
|
|
||||||
<Tag color='violet' shape='circle' prefixIcon={<PaintBucket size={14} />}>
|
|
||||||
{t('局部重绘-提交')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'ZOOM':
|
|
||||||
return (
|
|
||||||
<Tag color='teal' shape='circle' prefixIcon={<Focus size={14} />}>
|
|
||||||
{t('变焦')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'CUSTOM_ZOOM':
|
|
||||||
return (
|
|
||||||
<Tag color='teal' shape='circle' prefixIcon={<Move3D size={14} />}>
|
|
||||||
{t('自定义变焦-提交')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'MODAL':
|
|
||||||
return (
|
|
||||||
<Tag color='green' shape='circle' prefixIcon={<Monitor size={14} />}>
|
|
||||||
{t('窗口处理')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'SWAP_FACE':
|
|
||||||
return (
|
|
||||||
<Tag color='light-green' shape='circle' prefixIcon={<UserCheck size={14} />}>
|
|
||||||
{t('换脸')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
|
||||||
{t('未知')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCode(code) {
|
|
||||||
switch (code) {
|
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
|
||||||
{t('已提交')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 21:
|
|
||||||
return (
|
|
||||||
<Tag color='lime' shape='circle' prefixIcon={<Clock size={14} />}>
|
|
||||||
{t('等待中')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 22:
|
|
||||||
return (
|
|
||||||
<Tag color='orange' shape='circle' prefixIcon={<Copy size={14} />}>
|
|
||||||
{t('重复提交')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 0:
|
|
||||||
return (
|
|
||||||
<Tag color='yellow' shape='circle' prefixIcon={<FileX size={14} />}>
|
|
||||||
{t('未提交')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
|
||||||
{t('未知')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderStatus(type) {
|
|
||||||
switch (type) {
|
|
||||||
case 'SUCCESS':
|
|
||||||
return (
|
|
||||||
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
|
||||||
{t('成功')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'NOT_START':
|
|
||||||
return (
|
|
||||||
<Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
|
|
||||||
{t('未启动')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'SUBMITTED':
|
|
||||||
return (
|
|
||||||
<Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
|
|
||||||
{t('队列中')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'IN_PROGRESS':
|
|
||||||
return (
|
|
||||||
<Tag color='blue' shape='circle' prefixIcon={<Loader size={14} />}>
|
|
||||||
{t('执行中')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'FAILURE':
|
|
||||||
return (
|
|
||||||
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
|
|
||||||
{t('失败')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'MODAL':
|
|
||||||
return (
|
|
||||||
<Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
|
|
||||||
{t('窗口等待')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
|
||||||
{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} shape='circle' prefixIcon={<Clock size={14} />}>
|
|
||||||
{durationSec} {t('秒')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义所有列
|
|
||||||
const allColumns = [
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.SUBMIT_TIME,
|
|
||||||
title: t('提交时间'),
|
|
||||||
dataIndex: 'submit_time',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{renderTimestamp(text / 1000)}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.DURATION,
|
|
||||||
title: t('花费时间'),
|
|
||||||
dataIndex: 'finish_time',
|
|
||||||
render: (finish, record) => {
|
|
||||||
return renderDuration(record.submit_time, finish);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.CHANNEL,
|
|
||||||
title: t('渠道'),
|
|
||||||
dataIndex: 'channel_id',
|
|
||||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return isAdminUser ? (
|
|
||||||
<div>
|
|
||||||
<Tag
|
|
||||||
color={colors[parseInt(text) % colors.length]}
|
|
||||||
shape='circle'
|
|
||||||
prefixIcon={<Hash size={14} />}
|
|
||||||
onClick={() => {
|
|
||||||
copyText(text);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{' '}
|
|
||||||
{text}{' '}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.TYPE,
|
|
||||||
title: t('类型'),
|
|
||||||
dataIndex: 'action',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{renderType(text)}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.TASK_ID,
|
|
||||||
title: t('任务ID'),
|
|
||||||
dataIndex: 'mj_id',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{text}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.SUBMIT_RESULT,
|
|
||||||
title: t('提交结果'),
|
|
||||||
dataIndex: 'code',
|
|
||||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return isAdminUser ? <div>{renderCode(text)}</div> : <></>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.TASK_STATUS,
|
|
||||||
title: t('任务状态'),
|
|
||||||
dataIndex: 'status',
|
|
||||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{renderStatus(text)}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.PROGRESS,
|
|
||||||
title: t('进度'),
|
|
||||||
dataIndex: 'progress',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
<Progress
|
|
||||||
stroke={
|
|
||||||
record.status === 'FAILURE'
|
|
||||||
? 'var(--semi-color-warning)'
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
percent={text ? parseInt(text.replace('%', '')) : 0}
|
|
||||||
showInfo={true}
|
|
||||||
aria-label='drawing progress'
|
|
||||||
style={{ minWidth: '160px' }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.IMAGE,
|
|
||||||
title: t('结果图片'),
|
|
||||||
dataIndex: 'image_url',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
if (!text) {
|
|
||||||
return t('无');
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
setModalImageUrl(text);
|
|
||||||
setIsModalOpenurl(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('查看图片')}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.PROMPT,
|
|
||||||
title: 'Prompt',
|
|
||||||
dataIndex: 'prompt',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
if (!text) {
|
|
||||||
return t('无');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Typography.Text
|
|
||||||
ellipsis={{ showTooltip: true }}
|
|
||||||
style={{ width: 100 }}
|
|
||||||
onClick={() => {
|
|
||||||
setModalContent(text);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Typography.Text>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.PROMPT_EN,
|
|
||||||
title: 'PromptEn',
|
|
||||||
dataIndex: 'prompt_en',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
if (!text) {
|
|
||||||
return t('无');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Typography.Text
|
|
||||||
ellipsis={{ showTooltip: true }}
|
|
||||||
style={{ width: 100 }}
|
|
||||||
onClick={() => {
|
|
||||||
setModalContent(text);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Typography.Text>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.FAIL_REASON,
|
|
||||||
title: t('失败原因'),
|
|
||||||
dataIndex: 'fail_reason',
|
|
||||||
fixed: 'right',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
if (!text) {
|
|
||||||
return t('无');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Typography.Text
|
|
||||||
ellipsis={{ showTooltip: true }}
|
|
||||||
style={{ width: 100 }}
|
|
||||||
onClick={() => {
|
|
||||||
setModalContent(text);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Typography.Text>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 根据可见性设置过滤列
|
|
||||||
const getVisibleColumns = () => {
|
|
||||||
return allColumns.filter((column) => visibleColumns[column.key]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [logs, setLogs] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [activePage, setActivePage] = useState(1);
|
|
||||||
const [logCount, setLogCount] = useState(0);
|
|
||||||
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
|
||||||
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
|
||||||
const [showBanner, setShowBanner] = useState(false);
|
|
||||||
|
|
||||||
// 定义模态框图片URL的状态和更新函数
|
|
||||||
const [modalImageUrl, setModalImageUrl] = useState('');
|
|
||||||
let now = new Date();
|
|
||||||
|
|
||||||
// Form 初始值
|
|
||||||
const formInitValues = {
|
|
||||||
channel_id: '',
|
|
||||||
mj_id: '',
|
|
||||||
dateRange: [
|
|
||||||
timestamp2string(now.getTime() / 1000 - 2592000),
|
|
||||||
timestamp2string(now.getTime() / 1000 + 3600)
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Form API 引用
|
|
||||||
const [formApi, setFormApi] = useState(null);
|
|
||||||
|
|
||||||
const [stat, setStat] = useState({
|
|
||||||
quota: 0,
|
|
||||||
token: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取表单值的辅助函数
|
|
||||||
const getFormValues = () => {
|
|
||||||
const formValues = formApi ? formApi.getValues() : {};
|
|
||||||
|
|
||||||
// 处理时间范围
|
|
||||||
let start_timestamp = timestamp2string(now.getTime() / 1000 - 2592000);
|
|
||||||
let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
|
|
||||||
|
|
||||||
if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) {
|
|
||||||
start_timestamp = formValues.dateRange[0];
|
|
||||||
end_timestamp = formValues.dateRange[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
channel_id: formValues.channel_id || '',
|
|
||||||
mj_id: formValues.mj_id || '',
|
|
||||||
start_timestamp,
|
|
||||||
end_timestamp,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const enrichLogs = (items) => {
|
|
||||||
return items.map((log) => ({
|
|
||||||
...log,
|
|
||||||
timestamp2string: timestamp2string(log.created_at),
|
|
||||||
key: '' + log.id,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const syncPageData = (payload) => {
|
|
||||||
const items = enrichLogs(payload.items || []);
|
|
||||||
setLogs(items);
|
|
||||||
setLogCount(payload.total || 0);
|
|
||||||
setActivePage(payload.page || 1);
|
|
||||||
setPageSize(payload.page_size || pageSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadLogs = async (page = 1, size = pageSize) => {
|
|
||||||
setLoading(true);
|
|
||||||
const { channel_id, mj_id, start_timestamp, end_timestamp } = getFormValues();
|
|
||||||
let localStartTimestamp = Date.parse(start_timestamp);
|
|
||||||
let localEndTimestamp = Date.parse(end_timestamp);
|
|
||||||
const url = isAdminUser
|
|
||||||
? `/api/mj/?p=${page}&page_size=${size}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
|
|
||||||
: `/api/mj/self/?p=${page}&page_size=${size}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
|
|
||||||
const res = await API.get(url);
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
syncPageData(data);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const pageData = logs;
|
|
||||||
|
|
||||||
const handlePageChange = (page) => {
|
|
||||||
loadLogs(page, pageSize).then();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageSizeChange = async (size) => {
|
|
||||||
localStorage.setItem('mj-page-size', size + '');
|
|
||||||
await loadLogs(1, size);
|
|
||||||
};
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
await loadLogs(1, pageSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyText = async (text) => {
|
|
||||||
if (await copy(text)) {
|
|
||||||
showSuccess(t('已复制:') + text);
|
|
||||||
} else {
|
|
||||||
// setSearchKeyword(text);
|
|
||||||
Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const localPageSize = parseInt(localStorage.getItem('mj-page-size')) || ITEMS_PER_PAGE;
|
|
||||||
setPageSize(localPageSize);
|
|
||||||
loadLogs(1, localPageSize).then();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const mjNotifyEnabled = localStorage.getItem('mj_notify_enabled');
|
|
||||||
if (mjNotifyEnabled !== 'true') {
|
|
||||||
setShowBanner(true);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 列选择器模态框
|
|
||||||
const renderColumnSelector = () => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={t('列设置')}
|
|
||||||
visible={showColumnSelector}
|
|
||||||
onCancel={() => setShowColumnSelector(false)}
|
|
||||||
footer={
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button onClick={() => initDefaultColumns()}>
|
|
||||||
{t('重置')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setShowColumnSelector(false)}>
|
|
||||||
{t('取消')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setShowColumnSelector(false)}>
|
|
||||||
{t('确定')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div style={{ marginBottom: 20 }}>
|
|
||||||
<Checkbox
|
|
||||||
checked={Object.values(visibleColumns).every((v) => v === true)}
|
|
||||||
indeterminate={
|
|
||||||
Object.values(visibleColumns).some((v) => v === true) &&
|
|
||||||
!Object.values(visibleColumns).every((v) => v === true)
|
|
||||||
}
|
|
||||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
|
||||||
>
|
|
||||||
{t('全选')}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4" style={{ border: '1px solid var(--semi-color-border)' }}>
|
|
||||||
{allColumns.map((column) => {
|
|
||||||
// 为非管理员用户跳过管理员专用列
|
|
||||||
if (
|
|
||||||
!isAdminUser &&
|
|
||||||
(column.key === COLUMN_KEYS.CHANNEL ||
|
|
||||||
column.key === COLUMN_KEYS.SUBMIT_RESULT)
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={column.key} className="w-1/2 mb-4 pr-2">
|
|
||||||
<Checkbox
|
|
||||||
checked={!!visibleColumns[column.key]}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleColumnVisibilityChange(column.key, e.target.checked)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{column.title}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{renderColumnSelector()}
|
|
||||||
<Layout>
|
|
||||||
<Card
|
|
||||||
className="!rounded-2xl mb-4"
|
|
||||||
title={
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
|
|
||||||
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
|
|
||||||
<IconEyeOpened className="mr-2" />
|
|
||||||
{loading ? (
|
|
||||||
<Skeleton.Title
|
|
||||||
style={{
|
|
||||||
width: 300,
|
|
||||||
marginBottom: 0,
|
|
||||||
marginTop: 0
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text>
|
|
||||||
{isAdminUser && showBanner
|
|
||||||
? t('当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。')
|
|
||||||
: t('Midjourney 任务记录')}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
className="w-full md:w-auto"
|
|
||||||
onClick={() => setCompactMode(!compactMode)}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{compactMode ? t('自适应列表') : t('紧凑列表')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider margin="12px" />
|
|
||||||
|
|
||||||
{/* 搜索表单区域 */}
|
|
||||||
<Form
|
|
||||||
initValues={formInitValues}
|
|
||||||
getFormApi={(api) => setFormApi(api)}
|
|
||||||
onSubmit={refresh}
|
|
||||||
allowEmpty={true}
|
|
||||||
autoComplete="off"
|
|
||||||
layout="vertical"
|
|
||||||
trigger="change"
|
|
||||||
stopValidateWithError={false}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
||||||
{/* 时间选择器 */}
|
|
||||||
<div className="col-span-1 lg:col-span-2">
|
|
||||||
<Form.DatePicker
|
|
||||||
field='dateRange'
|
|
||||||
className="w-full"
|
|
||||||
type='dateTimeRange'
|
|
||||||
placeholder={[t('开始时间'), t('结束时间')]}
|
|
||||||
showClear
|
|
||||||
pure
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 任务 ID */}
|
|
||||||
<Form.Input
|
|
||||||
field='mj_id'
|
|
||||||
prefix={<IconSearch />}
|
|
||||||
placeholder={t('任务 ID')}
|
|
||||||
showClear
|
|
||||||
pure
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 渠道 ID - 仅管理员可见 */}
|
|
||||||
{isAdminUser && (
|
|
||||||
<Form.Input
|
|
||||||
field='channel_id'
|
|
||||||
prefix={<IconSearch />}
|
|
||||||
placeholder={t('渠道 ID')}
|
|
||||||
showClear
|
|
||||||
pure
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 操作按钮区域 */}
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div></div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
htmlType='submit'
|
|
||||||
loading={loading}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('查询')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
onClick={() => {
|
|
||||||
if (formApi) {
|
|
||||||
formApi.reset();
|
|
||||||
// 重置后立即查询,使用setTimeout确保表单重置完成
|
|
||||||
setTimeout(() => {
|
|
||||||
refresh();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('重置')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
onClick={() => setShowColumnSelector(true)}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('列设置')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
shadows='always'
|
|
||||||
bordered={false}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
columns={compactMode ? getVisibleColumns().map(({ fixed, ...rest }) => rest) : getVisibleColumns()}
|
|
||||||
dataSource={logs}
|
|
||||||
rowKey='key'
|
|
||||||
loading={loading}
|
|
||||||
scroll={compactMode ? undefined : { x: 'max-content' }}
|
|
||||||
className="rounded-xl overflow-hidden"
|
|
||||||
size="middle"
|
|
||||||
empty={
|
|
||||||
<Empty
|
|
||||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
|
||||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
|
||||||
description={t('搜索无结果')}
|
|
||||||
style={{ padding: 30 }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
pagination={{
|
|
||||||
formatPageText: (page) =>
|
|
||||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
|
||||||
start: page.currentStart,
|
|
||||||
end: page.currentEnd,
|
|
||||||
total: logCount,
|
|
||||||
}),
|
|
||||||
currentPage: activePage,
|
|
||||||
pageSize: pageSize,
|
|
||||||
total: logCount,
|
|
||||||
pageSizeOptions: [10, 20, 50, 100],
|
|
||||||
showSizeChanger: true,
|
|
||||||
onPageSizeChange: handlePageSizeChange,
|
|
||||||
onPageChange: handlePageChange,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
visible={isModalOpen}
|
|
||||||
onOk={() => setIsModalOpen(false)}
|
|
||||||
onCancel={() => setIsModalOpen(false)}
|
|
||||||
closable={null}
|
|
||||||
bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式
|
|
||||||
width={800} // 设置模态框宽度
|
|
||||||
>
|
|
||||||
<p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
|
|
||||||
</Modal>
|
|
||||||
<ImagePreview
|
|
||||||
src={modalImageUrl}
|
|
||||||
visible={isModalOpenurl}
|
|
||||||
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LogsTable;
|
|
||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
|
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
|
||||||
import { API, copy, showError, showInfo, showSuccess, getModelCategories, renderModelTag, stringToColor } from '../../helpers';
|
import { API, copy, showError, showInfo, showSuccess, getModelCategories, renderModelTag, stringToColor } from '../../helpers';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -535,12 +554,6 @@ const ModelPricing = () => {
|
|||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
pageSizeOptions: [10, 20, 50, 100],
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
formatPageText: (page) =>
|
|
||||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
|
||||||
start: page.currentStart,
|
|
||||||
end: page.currentEnd,
|
|
||||||
total: filteredModels.length,
|
|
||||||
}),
|
|
||||||
onPageSizeChange: (size) => setPageSize(size),
|
onPageSizeChange: (size) => setPageSize(size),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,629 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import {
|
|
||||||
API,
|
|
||||||
copy,
|
|
||||||
showError,
|
|
||||||
showSuccess,
|
|
||||||
timestamp2string,
|
|
||||||
renderQuota
|
|
||||||
} from '../../helpers';
|
|
||||||
|
|
||||||
import { Ticket } from 'lucide-react';
|
|
||||||
|
|
||||||
import { ITEMS_PER_PAGE } from '../../constants';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Divider,
|
|
||||||
Dropdown,
|
|
||||||
Empty,
|
|
||||||
Form,
|
|
||||||
Modal,
|
|
||||||
Popover,
|
|
||||||
Space,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
Typography
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import {
|
|
||||||
IllustrationNoResult,
|
|
||||||
IllustrationNoResultDark
|
|
||||||
} from '@douyinfe/semi-illustrations';
|
|
||||||
import {
|
|
||||||
IconSearch,
|
|
||||||
IconMore,
|
|
||||||
} from '@douyinfe/semi-icons';
|
|
||||||
import EditRedemption from '../../pages/Redemption/EditRedemption';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
function renderTimestamp(timestamp) {
|
|
||||||
return <>{timestamp2string(timestamp)}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RedemptionsTable = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const isExpired = (rec) => {
|
|
||||||
return rec.status === 1 && rec.expired_time !== 0 && rec.expired_time < Math.floor(Date.now() / 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStatus = (status, record) => {
|
|
||||||
if (isExpired(record)) {
|
|
||||||
return (
|
|
||||||
<Tag color='orange' shape='circle'>{t('已过期')}</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
switch (status) {
|
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<Tag color='green' shape='circle'>
|
|
||||||
{t('未使用')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 2:
|
|
||||||
return (
|
|
||||||
<Tag color='red' shape='circle'>
|
|
||||||
{t('已禁用')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 3:
|
|
||||||
return (
|
|
||||||
<Tag color='grey' shape='circle'>
|
|
||||||
{t('已使用')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Tag color='black' shape='circle'>
|
|
||||||
{t('未知状态')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: t('ID'),
|
|
||||||
dataIndex: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('名称'),
|
|
||||||
dataIndex: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('状态'),
|
|
||||||
dataIndex: 'status',
|
|
||||||
key: 'status',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{renderStatus(text, record)}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('额度'),
|
|
||||||
dataIndex: 'quota',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Tag color='grey' shape='circle'>
|
|
||||||
{renderQuota(parseInt(text))}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('创建时间'),
|
|
||||||
dataIndex: 'created_time',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{renderTimestamp(text)}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('过期时间'),
|
|
||||||
dataIndex: 'expired_time',
|
|
||||||
render: (text) => {
|
|
||||||
return <div>{text === 0 ? t('永不过期') : renderTimestamp(text)}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('兑换人ID'),
|
|
||||||
dataIndex: 'used_user_id',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{text === 0 ? t('无') : text}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
dataIndex: 'operate',
|
|
||||||
fixed: 'right',
|
|
||||||
width: 205,
|
|
||||||
render: (text, record, index) => {
|
|
||||||
// 创建更多操作的下拉菜单项
|
|
||||||
const moreMenuItems = [
|
|
||||||
{
|
|
||||||
node: 'item',
|
|
||||||
name: t('删除'),
|
|
||||||
type: 'danger',
|
|
||||||
onClick: () => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: t('确定是否要删除此兑换码?'),
|
|
||||||
content: t('此修改将不可逆'),
|
|
||||||
onOk: () => {
|
|
||||||
(async () => {
|
|
||||||
await manageRedemption(record.id, 'delete', record);
|
|
||||||
await refresh();
|
|
||||||
setTimeout(() => {
|
|
||||||
if (redemptions.length === 0 && activePage > 1) {
|
|
||||||
refresh(activePage - 1);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
})();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
if (record.status === 1 && !isExpired(record)) {
|
|
||||||
moreMenuItems.push({
|
|
||||||
node: 'item',
|
|
||||||
name: t('禁用'),
|
|
||||||
type: 'warning',
|
|
||||||
onClick: () => {
|
|
||||||
manageRedemption(record.id, 'disable', record);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (!isExpired(record)) {
|
|
||||||
moreMenuItems.push({
|
|
||||||
node: 'item',
|
|
||||||
name: t('启用'),
|
|
||||||
type: 'secondary',
|
|
||||||
onClick: () => {
|
|
||||||
manageRedemption(record.id, 'enable', record);
|
|
||||||
},
|
|
||||||
disabled: record.status === 3,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space>
|
|
||||||
<Popover content={record.key} style={{ padding: 20 }} position='top'>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('查看')}
|
|
||||||
</Button>
|
|
||||||
</Popover>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={async () => {
|
|
||||||
await copyText(record.key);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('复制')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
setEditingRedemption(record);
|
|
||||||
setShowEdit(true);
|
|
||||||
}}
|
|
||||||
disabled={record.status !== 1}
|
|
||||||
>
|
|
||||||
{t('编辑')}
|
|
||||||
</Button>
|
|
||||||
<Dropdown
|
|
||||||
trigger='click'
|
|
||||||
position='bottomRight'
|
|
||||||
menu={moreMenuItems}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
size="small"
|
|
||||||
icon={<IconMore />}
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const [redemptions, setRedemptions] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [activePage, setActivePage] = useState(1);
|
|
||||||
const [searching, setSearching] = useState(false);
|
|
||||||
const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE);
|
|
||||||
const [selectedKeys, setSelectedKeys] = useState([]);
|
|
||||||
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
|
||||||
const [editingRedemption, setEditingRedemption] = useState({
|
|
||||||
id: undefined,
|
|
||||||
});
|
|
||||||
const [showEdit, setShowEdit] = useState(false);
|
|
||||||
const [compactMode, setCompactMode] = useTableCompactMode('redemptions');
|
|
||||||
|
|
||||||
const formInitValues = {
|
|
||||||
searchKeyword: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const [formApi, setFormApi] = useState(null);
|
|
||||||
|
|
||||||
const getFormValues = () => {
|
|
||||||
const formValues = formApi ? formApi.getValues() : {};
|
|
||||||
return {
|
|
||||||
searchKeyword: formValues.searchKeyword || '',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeEdit = () => {
|
|
||||||
setShowEdit(false);
|
|
||||||
setTimeout(() => {
|
|
||||||
setEditingRedemption({
|
|
||||||
id: undefined,
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setRedemptionFormat = (redeptions) => {
|
|
||||||
setRedemptions(redeptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadRedemptions = async (page = 1, pageSize) => {
|
|
||||||
setLoading(true);
|
|
||||||
const res = await API.get(
|
|
||||||
`/api/redemption/?p=${page}&page_size=${pageSize}`,
|
|
||||||
);
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
const newPageData = data.items;
|
|
||||||
setActivePage(data.page <= 0 ? 1 : data.page);
|
|
||||||
setTokenCount(data.total);
|
|
||||||
setRedemptionFormat(newPageData);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeRecord = (key) => {
|
|
||||||
let newDataSource = [...redemptions];
|
|
||||||
if (key != null) {
|
|
||||||
let idx = newDataSource.findIndex((data) => data.key === key);
|
|
||||||
|
|
||||||
if (idx > -1) {
|
|
||||||
newDataSource.splice(idx, 1);
|
|
||||||
setRedemptions(newDataSource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyText = async (text) => {
|
|
||||||
if (await copy(text)) {
|
|
||||||
showSuccess(t('已复制到剪贴板!'));
|
|
||||||
} else {
|
|
||||||
Modal.error({
|
|
||||||
title: t('无法复制到剪贴板,请手动复制'),
|
|
||||||
content: text,
|
|
||||||
size: 'large'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadRedemptions(1, pageSize)
|
|
||||||
.then()
|
|
||||||
.catch((reason) => {
|
|
||||||
showError(reason);
|
|
||||||
});
|
|
||||||
}, [pageSize]);
|
|
||||||
|
|
||||||
const refresh = async (page = activePage) => {
|
|
||||||
const { searchKeyword } = getFormValues();
|
|
||||||
if (searchKeyword === '') {
|
|
||||||
await loadRedemptions(page, pageSize);
|
|
||||||
} else {
|
|
||||||
await searchRedemptions(searchKeyword, page, pageSize);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const manageRedemption = async (id, action, record) => {
|
|
||||||
setLoading(true);
|
|
||||||
let data = { id };
|
|
||||||
let res;
|
|
||||||
switch (action) {
|
|
||||||
case 'delete':
|
|
||||||
res = await API.delete(`/api/redemption/${id}/`);
|
|
||||||
break;
|
|
||||||
case 'enable':
|
|
||||||
data.status = 1;
|
|
||||||
res = await API.put('/api/redemption/?status_only=true', data);
|
|
||||||
break;
|
|
||||||
case 'disable':
|
|
||||||
data.status = 2;
|
|
||||||
res = await API.put('/api/redemption/?status_only=true', data);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const { success, message } = res.data;
|
|
||||||
if (success) {
|
|
||||||
showSuccess(t('操作成功完成!'));
|
|
||||||
let redemption = res.data.data;
|
|
||||||
let newRedemptions = [...redemptions];
|
|
||||||
if (action === 'delete') {
|
|
||||||
} else {
|
|
||||||
record.status = redemption.status;
|
|
||||||
}
|
|
||||||
setRedemptions(newRedemptions);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchRedemptions = async (keyword = null, page, pageSize) => {
|
|
||||||
// 如果没有传递keyword参数,从表单获取值
|
|
||||||
if (keyword === null) {
|
|
||||||
const formValues = getFormValues();
|
|
||||||
keyword = formValues.searchKeyword;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyword === '') {
|
|
||||||
await loadRedemptions(page, pageSize);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSearching(true);
|
|
||||||
const res = await API.get(
|
|
||||||
`/api/redemption/search?keyword=${keyword}&p=${page}&page_size=${pageSize}`,
|
|
||||||
);
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
const newPageData = data.items;
|
|
||||||
setActivePage(data.page);
|
|
||||||
setTokenCount(data.total);
|
|
||||||
setRedemptionFormat(newPageData);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setSearching(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageChange = (page) => {
|
|
||||||
setActivePage(page);
|
|
||||||
const { searchKeyword } = getFormValues();
|
|
||||||
if (searchKeyword === '') {
|
|
||||||
loadRedemptions(page, pageSize).then();
|
|
||||||
} else {
|
|
||||||
searchRedemptions(searchKeyword, page, pageSize).then();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let pageData = redemptions;
|
|
||||||
const rowSelection = {
|
|
||||||
onSelect: (record, selected) => { },
|
|
||||||
onSelectAll: (selected, selectedRows) => { },
|
|
||||||
onChange: (selectedRowKeys, selectedRows) => {
|
|
||||||
setSelectedKeys(selectedRows);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRow = (record, index) => {
|
|
||||||
if (record.status !== 1 || isExpired(record)) {
|
|
||||||
return {
|
|
||||||
style: {
|
|
||||||
background: 'var(--semi-color-disabled-border)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderHeader = () => (
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<div className="mb-2">
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
|
|
||||||
<div className="flex items-center text-orange-500">
|
|
||||||
<Ticket size={16} className="mr-2" />
|
|
||||||
<Text>{t('兑换码可以批量生成和分发,适合用于推广活动或批量充值。')}</Text>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
className="w-full md:w-auto"
|
|
||||||
onClick={() => setCompactMode(!compactMode)}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{compactMode ? t('自适应列表') : t('紧凑列表')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider margin="12px" />
|
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
|
|
||||||
<div className="flex flex-col sm:flex-row gap-2 w-full md:w-auto order-2 md:order-1">
|
|
||||||
<div className="flex gap-2 w-full sm:w-auto">
|
|
||||||
<Button
|
|
||||||
type='primary'
|
|
||||||
className="w-full sm:w-auto"
|
|
||||||
onClick={() => {
|
|
||||||
setEditingRedemption({
|
|
||||||
id: undefined,
|
|
||||||
});
|
|
||||||
setShowEdit(true);
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('添加兑换码')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
className="w-full sm:w-auto"
|
|
||||||
onClick={async () => {
|
|
||||||
if (selectedKeys.length === 0) {
|
|
||||||
showError(t('请至少选择一个兑换码!'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let keys = '';
|
|
||||||
for (let i = 0; i < selectedKeys.length; i++) {
|
|
||||||
keys +=
|
|
||||||
selectedKeys[i].name + ' ' + selectedKeys[i].key + '\n';
|
|
||||||
}
|
|
||||||
await copyText(keys);
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('复制所选兑换码到剪贴板')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type='danger'
|
|
||||||
className="w-full sm:w-auto"
|
|
||||||
onClick={() => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: t('确定清除所有失效兑换码?'),
|
|
||||||
content: t('将删除已使用、已禁用及过期的兑换码,此操作不可撤销。'),
|
|
||||||
onOk: async () => {
|
|
||||||
setLoading(true);
|
|
||||||
const res = await API.delete('/api/redemption/invalid');
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
showSuccess(t('已删除 {{count}} 条失效兑换码', { count: data }));
|
|
||||||
await refresh();
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('清除失效兑换码')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
initValues={formInitValues}
|
|
||||||
getFormApi={(api) => setFormApi(api)}
|
|
||||||
onSubmit={() => {
|
|
||||||
setActivePage(1);
|
|
||||||
searchRedemptions(null, 1, pageSize);
|
|
||||||
}}
|
|
||||||
allowEmpty={true}
|
|
||||||
autoComplete="off"
|
|
||||||
layout="horizontal"
|
|
||||||
trigger="change"
|
|
||||||
stopValidateWithError={false}
|
|
||||||
className="w-full md:w-auto order-1 md:order-2"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
|
|
||||||
<div className="relative w-full md:w-64">
|
|
||||||
<Form.Input
|
|
||||||
field="searchKeyword"
|
|
||||||
prefix={<IconSearch />}
|
|
||||||
placeholder={t('关键字(id或者名称)')}
|
|
||||||
showClear
|
|
||||||
pure
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 w-full md:w-auto">
|
|
||||||
<Button
|
|
||||||
type="tertiary"
|
|
||||||
htmlType="submit"
|
|
||||||
loading={loading || searching}
|
|
||||||
className="flex-1 md:flex-initial md:w-auto"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('查询')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="tertiary"
|
|
||||||
onClick={() => {
|
|
||||||
if (formApi) {
|
|
||||||
formApi.reset();
|
|
||||||
setTimeout(() => {
|
|
||||||
setActivePage(1);
|
|
||||||
loadRedemptions(1, pageSize);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex-1 md:flex-initial md:w-auto"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('重置')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<EditRedemption
|
|
||||||
refresh={refresh}
|
|
||||||
editingRedemption={editingRedemption}
|
|
||||||
visiable={showEdit}
|
|
||||||
handleClose={closeEdit}
|
|
||||||
></EditRedemption>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
className="!rounded-2xl"
|
|
||||||
title={renderHeader()}
|
|
||||||
shadows='always'
|
|
||||||
bordered={false}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
columns={compactMode ? columns.map(({ fixed, ...rest }) => rest) : columns}
|
|
||||||
dataSource={pageData}
|
|
||||||
scroll={compactMode ? undefined : { x: 'max-content' }}
|
|
||||||
pagination={{
|
|
||||||
currentPage: activePage,
|
|
||||||
pageSize: pageSize,
|
|
||||||
total: tokenCount,
|
|
||||||
showSizeChanger: true,
|
|
||||||
pageSizeOptions: [10, 20, 50, 100],
|
|
||||||
formatPageText: (page) =>
|
|
||||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
|
||||||
start: page.currentStart,
|
|
||||||
end: page.currentEnd,
|
|
||||||
total: tokenCount,
|
|
||||||
}),
|
|
||||||
onPageSizeChange: (size) => {
|
|
||||||
setPageSize(size);
|
|
||||||
setActivePage(1);
|
|
||||||
const { searchKeyword } = getFormValues();
|
|
||||||
if (searchKeyword === '') {
|
|
||||||
loadRedemptions(1, size).then();
|
|
||||||
} else {
|
|
||||||
searchRedemptions(searchKeyword, 1, size).then();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPageChange: handlePageChange,
|
|
||||||
}}
|
|
||||||
loading={loading}
|
|
||||||
rowSelection={rowSelection}
|
|
||||||
onRow={handleRow}
|
|
||||||
empty={
|
|
||||||
<Empty
|
|
||||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
|
||||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
|
||||||
description={t('搜索无结果')}
|
|
||||||
style={{ padding: 30 }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
className="rounded-xl overflow-hidden"
|
|
||||||
size="middle"
|
|
||||||
></Table>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RedemptionsTable;
|
|
||||||
@@ -1,813 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
|
||||||
Music,
|
|
||||||
FileText,
|
|
||||||
HelpCircle,
|
|
||||||
CheckCircle,
|
|
||||||
Pause,
|
|
||||||
Clock,
|
|
||||||
Play,
|
|
||||||
XCircle,
|
|
||||||
Loader,
|
|
||||||
List,
|
|
||||||
Hash,
|
|
||||||
Video,
|
|
||||||
Sparkles
|
|
||||||
} from 'lucide-react';
|
|
||||||
import {
|
|
||||||
API,
|
|
||||||
copy,
|
|
||||||
isAdmin,
|
|
||||||
showError,
|
|
||||||
showSuccess,
|
|
||||||
timestamp2string
|
|
||||||
} from '../../helpers';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Checkbox,
|
|
||||||
Divider,
|
|
||||||
Empty,
|
|
||||||
Form,
|
|
||||||
Layout,
|
|
||||||
Modal,
|
|
||||||
Progress,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
Typography
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import {
|
|
||||||
IllustrationNoResult,
|
|
||||||
IllustrationNoResultDark
|
|
||||||
} from '@douyinfe/semi-illustrations';
|
|
||||||
import { ITEMS_PER_PAGE } from '../../constants';
|
|
||||||
import {
|
|
||||||
IconEyeOpened,
|
|
||||||
IconSearch,
|
|
||||||
} from '@douyinfe/semi-icons';
|
|
||||||
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
|
|
||||||
import { TASK_ACTION_GENERATE, TASK_ACTION_TEXT_GENERATE } from '../../constants/common.constant';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
const colors = [
|
|
||||||
'amber',
|
|
||||||
'blue',
|
|
||||||
'cyan',
|
|
||||||
'green',
|
|
||||||
'grey',
|
|
||||||
'indigo',
|
|
||||||
'light-blue',
|
|
||||||
'lime',
|
|
||||||
'orange',
|
|
||||||
'pink',
|
|
||||||
'purple',
|
|
||||||
'red',
|
|
||||||
'teal',
|
|
||||||
'violet',
|
|
||||||
'yellow',
|
|
||||||
];
|
|
||||||
|
|
||||||
// 定义列键值常量
|
|
||||||
const COLUMN_KEYS = {
|
|
||||||
SUBMIT_TIME: 'submit_time',
|
|
||||||
FINISH_TIME: 'finish_time',
|
|
||||||
DURATION: 'duration',
|
|
||||||
CHANNEL: 'channel',
|
|
||||||
PLATFORM: 'platform',
|
|
||||||
TYPE: 'type',
|
|
||||||
TASK_ID: 'task_id',
|
|
||||||
TASK_STATUS: 'task_status',
|
|
||||||
PROGRESS: 'progress',
|
|
||||||
FAIL_REASON: 'fail_reason',
|
|
||||||
RESULT_URL: 'result_url',
|
|
||||||
};
|
|
||||||
|
|
||||||
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}`; // 格式化输出
|
|
||||||
};
|
|
||||||
|
|
||||||
function renderDuration(submit_time, finishTime) {
|
|
||||||
if (!submit_time || !finishTime) return 'N/A';
|
|
||||||
const durationSec = finishTime - submit_time;
|
|
||||||
const color = durationSec > 60 ? 'red' : 'green';
|
|
||||||
|
|
||||||
// 返回带有样式的颜色标签
|
|
||||||
return (
|
|
||||||
<Tag color={color} prefixIcon={<Clock size={14} />}>
|
|
||||||
{durationSec} 秒
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const LogsTable = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [modalContent, setModalContent] = useState('');
|
|
||||||
|
|
||||||
// 列可见性状态
|
|
||||||
const [visibleColumns, setVisibleColumns] = useState({});
|
|
||||||
const [showColumnSelector, setShowColumnSelector] = useState(false);
|
|
||||||
const isAdminUser = isAdmin();
|
|
||||||
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
|
||||||
|
|
||||||
// 加载保存的列偏好设置
|
|
||||||
useEffect(() => {
|
|
||||||
const savedColumns = localStorage.getItem('task-logs-table-columns');
|
|
||||||
if (savedColumns) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(savedColumns);
|
|
||||||
const defaults = getDefaultColumnVisibility();
|
|
||||||
const merged = { ...defaults, ...parsed };
|
|
||||||
setVisibleColumns(merged);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse saved column preferences', e);
|
|
||||||
initDefaultColumns();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
initDefaultColumns();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 获取默认列可见性
|
|
||||||
const getDefaultColumnVisibility = () => {
|
|
||||||
return {
|
|
||||||
[COLUMN_KEYS.SUBMIT_TIME]: true,
|
|
||||||
[COLUMN_KEYS.FINISH_TIME]: true,
|
|
||||||
[COLUMN_KEYS.DURATION]: true,
|
|
||||||
[COLUMN_KEYS.CHANNEL]: isAdminUser,
|
|
||||||
[COLUMN_KEYS.PLATFORM]: true,
|
|
||||||
[COLUMN_KEYS.TYPE]: true,
|
|
||||||
[COLUMN_KEYS.TASK_ID]: true,
|
|
||||||
[COLUMN_KEYS.TASK_STATUS]: true,
|
|
||||||
[COLUMN_KEYS.PROGRESS]: true,
|
|
||||||
[COLUMN_KEYS.FAIL_REASON]: true,
|
|
||||||
[COLUMN_KEYS.RESULT_URL]: true,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化默认列可见性
|
|
||||||
const initDefaultColumns = () => {
|
|
||||||
const defaults = getDefaultColumnVisibility();
|
|
||||||
setVisibleColumns(defaults);
|
|
||||||
localStorage.setItem('task-logs-table-columns', JSON.stringify(defaults));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理列可见性变化
|
|
||||||
const handleColumnVisibilityChange = (columnKey, checked) => {
|
|
||||||
const updatedColumns = { ...visibleColumns, [columnKey]: checked };
|
|
||||||
setVisibleColumns(updatedColumns);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理全选
|
|
||||||
const handleSelectAll = (checked) => {
|
|
||||||
const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
|
|
||||||
const updatedColumns = {};
|
|
||||||
|
|
||||||
allKeys.forEach((key) => {
|
|
||||||
if (key === COLUMN_KEYS.CHANNEL && !isAdminUser) {
|
|
||||||
updatedColumns[key] = false;
|
|
||||||
} else {
|
|
||||||
updatedColumns[key] = checked;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setVisibleColumns(updatedColumns);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新表格时保存列可见性
|
|
||||||
useEffect(() => {
|
|
||||||
if (Object.keys(visibleColumns).length > 0) {
|
|
||||||
localStorage.setItem('task-logs-table-columns', JSON.stringify(visibleColumns));
|
|
||||||
}
|
|
||||||
}, [visibleColumns]);
|
|
||||||
|
|
||||||
const renderType = (type) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'MUSIC':
|
|
||||||
return (
|
|
||||||
<Tag color='grey' shape='circle' prefixIcon={<Music size={14} />}>
|
|
||||||
{t('生成音乐')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'LYRICS':
|
|
||||||
return (
|
|
||||||
<Tag color='pink' shape='circle' prefixIcon={<FileText size={14} />}>
|
|
||||||
{t('生成歌词')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case TASK_ACTION_GENERATE:
|
|
||||||
return (
|
|
||||||
<Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
|
|
||||||
{t('图生视频')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case TASK_ACTION_TEXT_GENERATE:
|
|
||||||
return (
|
|
||||||
<Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
|
|
||||||
{t('文生视频')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
|
||||||
{t('未知')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPlatform = (platform) => {
|
|
||||||
switch (platform) {
|
|
||||||
case 'suno':
|
|
||||||
return (
|
|
||||||
<Tag color='green' shape='circle' prefixIcon={<Music size={14} />}>
|
|
||||||
Suno
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'kling':
|
|
||||||
return (
|
|
||||||
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
|
|
||||||
Kling
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'jimeng':
|
|
||||||
return (
|
|
||||||
<Tag color='purple' shape='circle' prefixIcon={<Video size={14} />}>
|
|
||||||
Jimeng
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
|
||||||
{t('未知')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStatus = (type) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'SUCCESS':
|
|
||||||
return (
|
|
||||||
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
|
||||||
{t('成功')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'NOT_START':
|
|
||||||
return (
|
|
||||||
<Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
|
|
||||||
{t('未启动')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'SUBMITTED':
|
|
||||||
return (
|
|
||||||
<Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
|
|
||||||
{t('队列中')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'IN_PROGRESS':
|
|
||||||
return (
|
|
||||||
<Tag color='blue' shape='circle' prefixIcon={<Play size={14} />}>
|
|
||||||
{t('执行中')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'FAILURE':
|
|
||||||
return (
|
|
||||||
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
|
|
||||||
{t('失败')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'QUEUED':
|
|
||||||
return (
|
|
||||||
<Tag color='orange' shape='circle' prefixIcon={<List size={14} />}>
|
|
||||||
{t('排队中')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 'UNKNOWN':
|
|
||||||
return (
|
|
||||||
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
|
||||||
{t('未知')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case '':
|
|
||||||
return (
|
|
||||||
<Tag color='grey' shape='circle' prefixIcon={<Loader size={14} />}>
|
|
||||||
{t('正在提交')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
|
||||||
{t('未知')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 定义所有列
|
|
||||||
const allColumns = [
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.SUBMIT_TIME,
|
|
||||||
title: t('提交时间'),
|
|
||||||
dataIndex: 'submit_time',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{text ? renderTimestamp(text) : '-'}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.FINISH_TIME,
|
|
||||||
title: t('结束时间'),
|
|
||||||
dataIndex: 'finish_time',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{text ? renderTimestamp(text) : '-'}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.DURATION,
|
|
||||||
title: t('花费时间'),
|
|
||||||
dataIndex: 'finish_time',
|
|
||||||
render: (finish, record) => {
|
|
||||||
return <>{finish ? renderDuration(record.submit_time, finish) : '-'}</>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.CHANNEL,
|
|
||||||
title: t('渠道'),
|
|
||||||
dataIndex: 'channel_id',
|
|
||||||
className: isAdminUser ? 'tableShow' : 'tableHiddle',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return isAdminUser ? (
|
|
||||||
<div>
|
|
||||||
<Tag
|
|
||||||
color={colors[parseInt(text) % colors.length]}
|
|
||||||
size='large'
|
|
||||||
shape='circle'
|
|
||||||
prefixIcon={<Hash size={14} />}
|
|
||||||
onClick={() => {
|
|
||||||
copyText(text);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.PLATFORM,
|
|
||||||
title: t('平台'),
|
|
||||||
dataIndex: 'platform',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{renderPlatform(text)}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.TYPE,
|
|
||||||
title: t('类型'),
|
|
||||||
dataIndex: 'action',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{renderType(text)}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.TASK_ID,
|
|
||||||
title: t('任务ID'),
|
|
||||||
dataIndex: 'task_id',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return (
|
|
||||||
<Typography.Text
|
|
||||||
ellipsis={{ showTooltip: true }}
|
|
||||||
onClick={() => {
|
|
||||||
setModalContent(JSON.stringify(record, null, 2));
|
|
||||||
setIsModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>{text}</div>
|
|
||||||
</Typography.Text>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.TASK_STATUS,
|
|
||||||
title: t('任务状态'),
|
|
||||||
dataIndex: 'status',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{renderStatus(text)}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.PROGRESS,
|
|
||||||
title: t('进度'),
|
|
||||||
dataIndex: 'progress',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
isNaN(text?.replace('%', '')) ? (
|
|
||||||
text || '-'
|
|
||||||
) : (
|
|
||||||
<Progress
|
|
||||||
stroke={
|
|
||||||
record.status === 'FAILURE'
|
|
||||||
? 'var(--semi-color-warning)'
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
percent={text ? parseInt(text.replace('%', '')) : 0}
|
|
||||||
showInfo={true}
|
|
||||||
aria-label='task progress'
|
|
||||||
style={{ minWidth: '160px' }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: COLUMN_KEYS.FAIL_REASON,
|
|
||||||
title: t('详情'),
|
|
||||||
dataIndex: 'fail_reason',
|
|
||||||
fixed: 'right',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
// 仅当为视频生成任务且成功,且 fail_reason 是 URL 时显示可点击链接
|
|
||||||
const isVideoTask = record.action === TASK_ACTION_GENERATE || record.action === TASK_ACTION_TEXT_GENERATE;
|
|
||||||
const isSuccess = record.status === 'SUCCESS';
|
|
||||||
const isUrl = typeof text === 'string' && /^https?:\/\//.test(text);
|
|
||||||
if (isSuccess && isVideoTask && isUrl) {
|
|
||||||
return (
|
|
||||||
<a href={text} target="_blank" rel="noopener noreferrer">
|
|
||||||
{t('点击预览视频')}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!text) {
|
|
||||||
return t('无');
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Typography.Text
|
|
||||||
ellipsis={{ showTooltip: true }}
|
|
||||||
style={{ width: 100 }}
|
|
||||||
onClick={() => {
|
|
||||||
setModalContent(text);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Typography.Text>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 根据可见性设置过滤列
|
|
||||||
const getVisibleColumns = () => {
|
|
||||||
return allColumns.filter((column) => visibleColumns[column.key]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [activePage, setActivePage] = useState(1);
|
|
||||||
const [logCount, setLogCount] = useState(0);
|
|
||||||
const [logs, setLogs] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const [compactMode, setCompactMode] = useTableCompactMode('taskLogs');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
|
|
||||||
setPageSize(localPageSize);
|
|
||||||
loadLogs(1, localPageSize).then();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
let now = new Date();
|
|
||||||
// 初始化start_timestamp为前一天
|
|
||||||
let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
||||||
|
|
||||||
// Form 初始值
|
|
||||||
const formInitValues = {
|
|
||||||
channel_id: '',
|
|
||||||
task_id: '',
|
|
||||||
dateRange: [
|
|
||||||
timestamp2string(zeroNow.getTime() / 1000),
|
|
||||||
timestamp2string(now.getTime() / 1000 + 3600)
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Form API 引用
|
|
||||||
const [formApi, setFormApi] = useState(null);
|
|
||||||
|
|
||||||
// 获取表单值的辅助函数
|
|
||||||
const getFormValues = () => {
|
|
||||||
const formValues = formApi ? formApi.getValues() : {};
|
|
||||||
|
|
||||||
// 处理时间范围
|
|
||||||
let start_timestamp = timestamp2string(zeroNow.getTime() / 1000);
|
|
||||||
let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
|
|
||||||
|
|
||||||
if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) {
|
|
||||||
start_timestamp = formValues.dateRange[0];
|
|
||||||
end_timestamp = formValues.dateRange[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
channel_id: formValues.channel_id || '',
|
|
||||||
task_id: formValues.task_id || '',
|
|
||||||
start_timestamp,
|
|
||||||
end_timestamp,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const enrichLogs = (items) => {
|
|
||||||
return items.map((log) => ({
|
|
||||||
...log,
|
|
||||||
timestamp2string: timestamp2string(log.created_at),
|
|
||||||
key: '' + log.id,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const syncPageData = (payload) => {
|
|
||||||
const items = enrichLogs(payload.items || []);
|
|
||||||
setLogs(items);
|
|
||||||
setLogCount(payload.total || 0);
|
|
||||||
setActivePage(payload.page || 1);
|
|
||||||
setPageSize(payload.page_size || pageSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadLogs = async (page = 1, size = pageSize) => {
|
|
||||||
setLoading(true);
|
|
||||||
const { channel_id, task_id, start_timestamp, end_timestamp } = getFormValues();
|
|
||||||
let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000);
|
|
||||||
let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000);
|
|
||||||
let url = isAdminUser
|
|
||||||
? `/api/task/?p=${page}&page_size=${size}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
|
|
||||||
: `/api/task/self?p=${page}&page_size=${size}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
|
|
||||||
const res = await API.get(url);
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
syncPageData(data);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const pageData = logs;
|
|
||||||
|
|
||||||
const handlePageChange = (page) => {
|
|
||||||
loadLogs(page, pageSize).then();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageSizeChange = async (size) => {
|
|
||||||
localStorage.setItem('task-page-size', size + '');
|
|
||||||
await loadLogs(1, size);
|
|
||||||
};
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
await loadLogs(1, pageSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyText = async (text) => {
|
|
||||||
if (await copy(text)) {
|
|
||||||
showSuccess(t('已复制:') + text);
|
|
||||||
} else {
|
|
||||||
Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 列选择器模态框
|
|
||||||
const renderColumnSelector = () => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={t('列设置')}
|
|
||||||
visible={showColumnSelector}
|
|
||||||
onCancel={() => setShowColumnSelector(false)}
|
|
||||||
footer={
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button onClick={() => initDefaultColumns()}>
|
|
||||||
{t('重置')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setShowColumnSelector(false)}>
|
|
||||||
{t('取消')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setShowColumnSelector(false)}>
|
|
||||||
{t('确定')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div style={{ marginBottom: 20 }}>
|
|
||||||
<Checkbox
|
|
||||||
checked={Object.values(visibleColumns).every((v) => v === true)}
|
|
||||||
indeterminate={
|
|
||||||
Object.values(visibleColumns).some((v) => v === true) &&
|
|
||||||
!Object.values(visibleColumns).every((v) => v === true)
|
|
||||||
}
|
|
||||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
|
||||||
>
|
|
||||||
{t('全选')}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4" style={{ border: '1px solid var(--semi-color-border)' }}>
|
|
||||||
{allColumns.map((column) => {
|
|
||||||
// 为非管理员用户跳过管理员专用列
|
|
||||||
if (!isAdminUser && column.key === COLUMN_KEYS.CHANNEL) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={column.key} className="w-1/2 mb-4 pr-2">
|
|
||||||
<Checkbox
|
|
||||||
checked={!!visibleColumns[column.key]}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleColumnVisibilityChange(column.key, e.target.checked)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{column.title}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{renderColumnSelector()}
|
|
||||||
<Layout>
|
|
||||||
<Card
|
|
||||||
className="!rounded-2xl mb-4"
|
|
||||||
title={
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
|
|
||||||
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
|
|
||||||
<IconEyeOpened className="mr-2" />
|
|
||||||
<Text>{t('任务记录')}</Text>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
className="w-full md:w-auto"
|
|
||||||
onClick={() => setCompactMode(!compactMode)}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{compactMode ? t('自适应列表') : t('紧凑列表')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider margin="12px" />
|
|
||||||
|
|
||||||
{/* 搜索表单区域 */}
|
|
||||||
<Form
|
|
||||||
initValues={formInitValues}
|
|
||||||
getFormApi={(api) => setFormApi(api)}
|
|
||||||
onSubmit={refresh}
|
|
||||||
allowEmpty={true}
|
|
||||||
autoComplete="off"
|
|
||||||
layout="vertical"
|
|
||||||
trigger="change"
|
|
||||||
stopValidateWithError={false}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
||||||
{/* 时间选择器 */}
|
|
||||||
<div className="col-span-1 lg:col-span-2">
|
|
||||||
<Form.DatePicker
|
|
||||||
field='dateRange'
|
|
||||||
className="w-full"
|
|
||||||
type='dateTimeRange'
|
|
||||||
placeholder={[t('开始时间'), t('结束时间')]}
|
|
||||||
showClear
|
|
||||||
pure
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 任务 ID */}
|
|
||||||
<Form.Input
|
|
||||||
field='task_id'
|
|
||||||
prefix={<IconSearch />}
|
|
||||||
placeholder={t('任务 ID')}
|
|
||||||
showClear
|
|
||||||
pure
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 渠道 ID - 仅管理员可见 */}
|
|
||||||
{isAdminUser && (
|
|
||||||
<Form.Input
|
|
||||||
field='channel_id'
|
|
||||||
prefix={<IconSearch />}
|
|
||||||
placeholder={t('渠道 ID')}
|
|
||||||
showClear
|
|
||||||
pure
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 操作按钮区域 */}
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div></div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
htmlType='submit'
|
|
||||||
loading={loading}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('查询')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
onClick={() => {
|
|
||||||
if (formApi) {
|
|
||||||
formApi.reset();
|
|
||||||
// 重置后立即查询,使用setTimeout确保表单重置完成
|
|
||||||
setTimeout(() => {
|
|
||||||
refresh();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('重置')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
onClick={() => setShowColumnSelector(true)}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('列设置')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
shadows='always'
|
|
||||||
bordered={false}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
columns={compactMode ? getVisibleColumns().map(({ fixed, ...rest }) => rest) : getVisibleColumns()}
|
|
||||||
dataSource={logs}
|
|
||||||
rowKey='key'
|
|
||||||
loading={loading}
|
|
||||||
scroll={compactMode ? undefined : { x: 'max-content' }}
|
|
||||||
className="rounded-xl overflow-hidden"
|
|
||||||
size="middle"
|
|
||||||
empty={
|
|
||||||
<Empty
|
|
||||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
|
||||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
|
||||||
description={t('搜索无结果')}
|
|
||||||
style={{ padding: 30 }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
pagination={{
|
|
||||||
formatPageText: (page) =>
|
|
||||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
|
||||||
start: page.currentStart,
|
|
||||||
end: page.currentEnd,
|
|
||||||
total: logCount,
|
|
||||||
}),
|
|
||||||
currentPage: activePage,
|
|
||||||
pageSize: pageSize,
|
|
||||||
total: logCount,
|
|
||||||
pageSizeOptions: [10, 20, 50, 100],
|
|
||||||
showSizeChanger: true,
|
|
||||||
onPageSizeChange: handlePageSizeChange,
|
|
||||||
onPageChange: handlePageChange,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
visible={isModalOpen}
|
|
||||||
onOk={() => setIsModalOpen(false)}
|
|
||||||
onCancel={() => setIsModalOpen(false)}
|
|
||||||
closable={null}
|
|
||||||
bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式
|
|
||||||
width={800} // 设置模态框宽度
|
|
||||||
>
|
|
||||||
<p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
|
|
||||||
</Modal>
|
|
||||||
</Layout>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LogsTable;
|
|
||||||
@@ -1,924 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import {
|
|
||||||
API,
|
|
||||||
copy,
|
|
||||||
showError,
|
|
||||||
showSuccess,
|
|
||||||
timestamp2string,
|
|
||||||
renderGroup,
|
|
||||||
renderQuota,
|
|
||||||
getModelCategories
|
|
||||||
} from '../../helpers';
|
|
||||||
import { ITEMS_PER_PAGE } from '../../constants';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Divider,
|
|
||||||
Dropdown,
|
|
||||||
Empty,
|
|
||||||
Form,
|
|
||||||
Modal,
|
|
||||||
Space,
|
|
||||||
SplitButtonGroup,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
AvatarGroup,
|
|
||||||
Avatar,
|
|
||||||
Tooltip,
|
|
||||||
Progress,
|
|
||||||
Switch,
|
|
||||||
Input,
|
|
||||||
Typography
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import {
|
|
||||||
IllustrationNoResult,
|
|
||||||
IllustrationNoResultDark
|
|
||||||
} from '@douyinfe/semi-illustrations';
|
|
||||||
import {
|
|
||||||
IconSearch,
|
|
||||||
IconTreeTriangleDown,
|
|
||||||
IconCopy,
|
|
||||||
IconEyeOpened,
|
|
||||||
IconEyeClosed,
|
|
||||||
} from '@douyinfe/semi-icons';
|
|
||||||
import { Key } from 'lucide-react';
|
|
||||||
import EditToken from '../../pages/Token/EditToken';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
function renderTimestamp(timestamp) {
|
|
||||||
return <>{timestamp2string(timestamp)}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TokensTable = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: t('名称'),
|
|
||||||
dataIndex: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('状态'),
|
|
||||||
dataIndex: 'status',
|
|
||||||
key: 'status',
|
|
||||||
render: (text, record) => {
|
|
||||||
const enabled = text === 1;
|
|
||||||
const handleToggle = (checked) => {
|
|
||||||
if (checked) {
|
|
||||||
manageToken(record.id, 'enable', record);
|
|
||||||
} else {
|
|
||||||
manageToken(record.id, 'disable', record);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let tagColor = 'black';
|
|
||||||
let tagText = t('未知状态');
|
|
||||||
if (enabled) {
|
|
||||||
tagColor = 'green';
|
|
||||||
tagText = t('已启用');
|
|
||||||
} else if (text === 2) {
|
|
||||||
tagColor = 'red';
|
|
||||||
tagText = t('已禁用');
|
|
||||||
} else if (text === 3) {
|
|
||||||
tagColor = 'yellow';
|
|
||||||
tagText = t('已过期');
|
|
||||||
} else if (text === 4) {
|
|
||||||
tagColor = 'grey';
|
|
||||||
tagText = t('已耗尽');
|
|
||||||
}
|
|
||||||
|
|
||||||
const used = parseInt(record.used_quota) || 0;
|
|
||||||
const remain = parseInt(record.remain_quota) || 0;
|
|
||||||
const total = used + remain;
|
|
||||||
const percent = total > 0 ? (remain / total) * 100 : 0;
|
|
||||||
|
|
||||||
const getProgressColor = (pct) => {
|
|
||||||
if (pct === 100) return 'var(--semi-color-success)';
|
|
||||||
if (pct <= 10) return 'var(--semi-color-danger)';
|
|
||||||
if (pct <= 30) return 'var(--semi-color-warning)';
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const quotaSuffix = record.unlimited_quota ? (
|
|
||||||
<div className='text-xs'>{t('无限额度')}</div>
|
|
||||||
) : (
|
|
||||||
<div className='flex flex-col items-end'>
|
|
||||||
<span className='text-xs leading-none'>{`${renderQuota(remain)} / ${renderQuota(total)}`}</span>
|
|
||||||
<Progress
|
|
||||||
percent={percent}
|
|
||||||
stroke={getProgressColor(percent)}
|
|
||||||
aria-label='quota usage'
|
|
||||||
format={() => `${percent.toFixed(0)}%`}
|
|
||||||
style={{ width: '100%', marginTop: '1px', marginBottom: 0 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const content = (
|
|
||||||
<Tag
|
|
||||||
color={tagColor}
|
|
||||||
shape='circle'
|
|
||||||
size='large'
|
|
||||||
prefixIcon={
|
|
||||||
<Switch
|
|
||||||
size='small'
|
|
||||||
checked={enabled}
|
|
||||||
onChange={handleToggle}
|
|
||||||
aria-label='token status switch'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
suffixIcon={quotaSuffix}
|
|
||||||
>
|
|
||||||
{tagText}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (record.unlimited_quota) {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
<div className='text-xs'>
|
|
||||||
<div>{t('已用额度')}: {renderQuota(used)}</div>
|
|
||||||
<div>{t('剩余额度')}: {renderQuota(remain)} ({percent.toFixed(0)}%)</div>
|
|
||||||
<div>{t('总额度')}: {renderQuota(total)}</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('分组'),
|
|
||||||
dataIndex: 'group',
|
|
||||||
key: 'group',
|
|
||||||
render: (text) => {
|
|
||||||
if (text === 'auto') {
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
content={t('当前分组为 auto,会自动选择最优分组,当一个组不可用时自动降级到下一个组(熔断机制)')}
|
|
||||||
position='top'
|
|
||||||
>
|
|
||||||
<Tag color='white' shape='circle'> {t('智能熔断')} </Tag>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return renderGroup(text);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('密钥'),
|
|
||||||
key: 'token_key',
|
|
||||||
render: (text, record) => {
|
|
||||||
const fullKey = 'sk-' + record.key;
|
|
||||||
const maskedKey = 'sk-' + record.key.slice(0, 4) + '**********' + record.key.slice(-4);
|
|
||||||
const revealed = !!showKeys[record.id];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-[200px]'>
|
|
||||||
<Input
|
|
||||||
readOnly
|
|
||||||
value={revealed ? fullKey : maskedKey}
|
|
||||||
size='small'
|
|
||||||
suffix={
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<Button
|
|
||||||
theme='borderless'
|
|
||||||
size='small'
|
|
||||||
type='tertiary'
|
|
||||||
icon={revealed ? <IconEyeClosed /> : <IconEyeOpened />}
|
|
||||||
aria-label='toggle token visibility'
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowKeys(prev => ({ ...prev, [record.id]: !revealed }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
theme='borderless'
|
|
||||||
size='small'
|
|
||||||
type='tertiary'
|
|
||||||
icon={<IconCopy />}
|
|
||||||
aria-label='copy token key'
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
await copyText(fullKey);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('可用模型'),
|
|
||||||
dataIndex: 'model_limits',
|
|
||||||
render: (text, record) => {
|
|
||||||
if (record.model_limits_enabled && text) {
|
|
||||||
const models = text.split(',').filter(Boolean);
|
|
||||||
const categories = getModelCategories(t);
|
|
||||||
|
|
||||||
const vendorAvatars = [];
|
|
||||||
const matchedModels = new Set();
|
|
||||||
Object.entries(categories).forEach(([key, category]) => {
|
|
||||||
if (key === 'all') return;
|
|
||||||
if (!category.icon || !category.filter) return;
|
|
||||||
const vendorModels = models.filter((m) => category.filter({ model_name: m }));
|
|
||||||
if (vendorModels.length > 0) {
|
|
||||||
vendorAvatars.push(
|
|
||||||
<Tooltip key={key} content={vendorModels.join(', ')} position='top' showArrow>
|
|
||||||
<Avatar size='extra-extra-small' alt={category.label} color='transparent'>
|
|
||||||
{category.icon}
|
|
||||||
</Avatar>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
vendorModels.forEach((m) => matchedModels.add(m));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const unmatchedModels = models.filter((m) => !matchedModels.has(m));
|
|
||||||
if (unmatchedModels.length > 0) {
|
|
||||||
vendorAvatars.push(
|
|
||||||
<Tooltip key='unknown' content={unmatchedModels.join(', ')} position='top' showArrow>
|
|
||||||
<Avatar size='extra-extra-small' alt='unknown'>
|
|
||||||
{t('其他')}
|
|
||||||
</Avatar>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AvatarGroup size='extra-extra-small'>
|
|
||||||
{vendorAvatars}
|
|
||||||
</AvatarGroup>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Tag color='white' shape='circle'>
|
|
||||||
{t('无限制')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('IP限制'),
|
|
||||||
dataIndex: 'allow_ips',
|
|
||||||
render: (text) => {
|
|
||||||
if (!text || text.trim() === '') {
|
|
||||||
return (
|
|
||||||
<Tag color='white' shape='circle'>
|
|
||||||
{t('无限制')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ips = text
|
|
||||||
.split('\n')
|
|
||||||
.map((ip) => ip.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const displayIps = ips.slice(0, 1);
|
|
||||||
const extraCount = ips.length - displayIps.length;
|
|
||||||
|
|
||||||
const ipTags = displayIps.map((ip, idx) => (
|
|
||||||
<Tag key={idx} shape='circle'>
|
|
||||||
{ip}
|
|
||||||
</Tag>
|
|
||||||
));
|
|
||||||
|
|
||||||
if (extraCount > 0) {
|
|
||||||
ipTags.push(
|
|
||||||
<Tooltip
|
|
||||||
key='extra'
|
|
||||||
content={ips.slice(1).join(', ')}
|
|
||||||
position='top'
|
|
||||||
showArrow
|
|
||||||
>
|
|
||||||
<Tag shape='circle'>
|
|
||||||
{'+' + extraCount}
|
|
||||||
</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Space wrap>{ipTags}</Space>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('创建时间'),
|
|
||||||
dataIndex: 'created_time',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{renderTimestamp(text)}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('过期时间'),
|
|
||||||
dataIndex: 'expired_time',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{record.expired_time === -1 ? t('永不过期') : renderTimestamp(text)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
dataIndex: 'operate',
|
|
||||||
fixed: 'right',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
let chats = localStorage.getItem('chats');
|
|
||||||
let chatsArray = [];
|
|
||||||
let shouldUseCustom = true;
|
|
||||||
|
|
||||||
if (shouldUseCustom) {
|
|
||||||
try {
|
|
||||||
chats = JSON.parse(chats);
|
|
||||||
if (Array.isArray(chats)) {
|
|
||||||
for (let i = 0; i < chats.length; i++) {
|
|
||||||
let chat = {};
|
|
||||||
chat.node = 'item';
|
|
||||||
for (let key in chats[i]) {
|
|
||||||
if (chats[i].hasOwnProperty(key)) {
|
|
||||||
chat.key = i;
|
|
||||||
chat.name = key;
|
|
||||||
chat.onClick = () => {
|
|
||||||
onOpenLink(key, chats[i][key], record);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chatsArray.push(chat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
showError(t('聊天链接配置错误,请联系管理员'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space wrap>
|
|
||||||
<SplitButtonGroup
|
|
||||||
className="overflow-hidden"
|
|
||||||
aria-label={t('项目操作按钮组')}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type='tertiary'
|
|
||||||
onClick={() => {
|
|
||||||
if (chatsArray.length === 0) {
|
|
||||||
showError(t('请联系管理员配置聊天链接'));
|
|
||||||
} else {
|
|
||||||
onOpenLink(
|
|
||||||
'default',
|
|
||||||
chats[0][Object.keys(chats[0])[0]],
|
|
||||||
record,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('聊天')}
|
|
||||||
</Button>
|
|
||||||
<Dropdown
|
|
||||||
trigger='click'
|
|
||||||
position='bottomRight'
|
|
||||||
menu={chatsArray}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
icon={<IconTreeTriangleDown />}
|
|
||||||
size="small"
|
|
||||||
></Button>
|
|
||||||
</Dropdown>
|
|
||||||
</SplitButtonGroup>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
setEditingToken(record);
|
|
||||||
setShowEdit(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('编辑')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type='danger'
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: t('确定是否要删除此令牌?'),
|
|
||||||
content: t('此修改将不可逆'),
|
|
||||||
onOk: () => {
|
|
||||||
(async () => {
|
|
||||||
await manageToken(record.id, 'delete', record);
|
|
||||||
await refresh();
|
|
||||||
})();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('删除')}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
|
||||||
const [showEdit, setShowEdit] = useState(false);
|
|
||||||
const [tokens, setTokens] = useState([]);
|
|
||||||
const [selectedKeys, setSelectedKeys] = useState([]);
|
|
||||||
const [tokenCount, setTokenCount] = useState(pageSize);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [activePage, setActivePage] = useState(1);
|
|
||||||
const [searching, setSearching] = useState(false);
|
|
||||||
const [editingToken, setEditingToken] = useState({
|
|
||||||
id: undefined,
|
|
||||||
});
|
|
||||||
const [compactMode, setCompactMode] = useTableCompactMode('tokens');
|
|
||||||
const [showKeys, setShowKeys] = useState({});
|
|
||||||
|
|
||||||
// Form 初始值
|
|
||||||
const formInitValues = {
|
|
||||||
searchKeyword: '',
|
|
||||||
searchToken: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Form API 引用
|
|
||||||
const [formApi, setFormApi] = useState(null);
|
|
||||||
|
|
||||||
// 获取表单值的辅助函数
|
|
||||||
const getFormValues = () => {
|
|
||||||
const formValues = formApi ? formApi.getValues() : {};
|
|
||||||
return {
|
|
||||||
searchKeyword: formValues.searchKeyword || '',
|
|
||||||
searchToken: formValues.searchToken || '',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeEdit = () => {
|
|
||||||
setShowEdit(false);
|
|
||||||
setTimeout(() => {
|
|
||||||
setEditingToken({
|
|
||||||
id: undefined,
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 将后端返回的数据写入状态
|
|
||||||
const syncPageData = (payload) => {
|
|
||||||
setTokens(payload.items || []);
|
|
||||||
setTokenCount(payload.total || 0);
|
|
||||||
setActivePage(payload.page || 1);
|
|
||||||
setPageSize(payload.page_size || pageSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadTokens = async (page = 1, size = pageSize) => {
|
|
||||||
setLoading(true);
|
|
||||||
const res = await API.get(`/api/token/?p=${page}&size=${size}`);
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
syncPageData(data);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const refresh = async (page = activePage) => {
|
|
||||||
await loadTokens(page);
|
|
||||||
setSelectedKeys([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyText = async (text) => {
|
|
||||||
if (await copy(text)) {
|
|
||||||
showSuccess(t('已复制到剪贴板!'));
|
|
||||||
} else {
|
|
||||||
Modal.error({
|
|
||||||
title: t('无法复制到剪贴板,请手动复制'),
|
|
||||||
content: text,
|
|
||||||
size: 'large',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onOpenLink = async (type, url, record) => {
|
|
||||||
let status = localStorage.getItem('status');
|
|
||||||
let serverAddress = '';
|
|
||||||
if (status) {
|
|
||||||
status = JSON.parse(status);
|
|
||||||
serverAddress = status.server_address;
|
|
||||||
}
|
|
||||||
if (serverAddress === '') {
|
|
||||||
serverAddress = window.location.origin;
|
|
||||||
}
|
|
||||||
if (url.includes('{cherryConfig}') === true) {
|
|
||||||
let cherryConfig = {
|
|
||||||
id: 'new-api',
|
|
||||||
baseUrl: serverAddress,
|
|
||||||
apiKey: 'sk-' + record.key,
|
|
||||||
}
|
|
||||||
// 替换 {cherryConfig} 为base64编码的JSON字符串
|
|
||||||
let encodedConfig = encodeURIComponent(
|
|
||||||
btoa(JSON.stringify(cherryConfig))
|
|
||||||
);
|
|
||||||
url = url.replaceAll('{cherryConfig}', encodedConfig);
|
|
||||||
} else {
|
|
||||||
let encodedServerAddress = encodeURIComponent(serverAddress);
|
|
||||||
url = url.replaceAll('{address}', encodedServerAddress);
|
|
||||||
url = url.replaceAll('{key}', 'sk-' + record.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.open(url, '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadTokens(1)
|
|
||||||
.then()
|
|
||||||
.catch((reason) => {
|
|
||||||
showError(reason);
|
|
||||||
});
|
|
||||||
}, [pageSize]);
|
|
||||||
|
|
||||||
const removeRecord = (key) => {
|
|
||||||
let newDataSource = [...tokens];
|
|
||||||
if (key != null) {
|
|
||||||
let idx = newDataSource.findIndex((data) => data.key === key);
|
|
||||||
|
|
||||||
if (idx > -1) {
|
|
||||||
newDataSource.splice(idx, 1);
|
|
||||||
setTokens(newDataSource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const manageToken = async (id, action, record) => {
|
|
||||||
setLoading(true);
|
|
||||||
let data = { id };
|
|
||||||
let res;
|
|
||||||
switch (action) {
|
|
||||||
case 'delete':
|
|
||||||
res = await API.delete(`/api/token/${id}/`);
|
|
||||||
break;
|
|
||||||
case 'enable':
|
|
||||||
data.status = 1;
|
|
||||||
res = await API.put('/api/token/?status_only=true', data);
|
|
||||||
break;
|
|
||||||
case 'disable':
|
|
||||||
data.status = 2;
|
|
||||||
res = await API.put('/api/token/?status_only=true', data);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const { success, message } = res.data;
|
|
||||||
if (success) {
|
|
||||||
showSuccess('操作成功完成!');
|
|
||||||
let token = res.data.data;
|
|
||||||
let newTokens = [...tokens];
|
|
||||||
if (action === 'delete') {
|
|
||||||
} else {
|
|
||||||
record.status = token.status;
|
|
||||||
}
|
|
||||||
setTokens(newTokens);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchTokens = async () => {
|
|
||||||
const { searchKeyword, searchToken } = getFormValues();
|
|
||||||
if (searchKeyword === '' && searchToken === '') {
|
|
||||||
await loadTokens(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSearching(true);
|
|
||||||
const res = await API.get(
|
|
||||||
`/api/token/search?keyword=${searchKeyword}&token=${searchToken}`,
|
|
||||||
);
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
setTokens(data);
|
|
||||||
setTokenCount(data.length);
|
|
||||||
setActivePage(1);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setSearching(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortToken = (key) => {
|
|
||||||
if (tokens.length === 0) return;
|
|
||||||
setLoading(true);
|
|
||||||
let sortedTokens = [...tokens];
|
|
||||||
sortedTokens.sort((a, b) => {
|
|
||||||
return ('' + a[key]).localeCompare(b[key]);
|
|
||||||
});
|
|
||||||
if (sortedTokens[0].id === tokens[0].id) {
|
|
||||||
sortedTokens.reverse();
|
|
||||||
}
|
|
||||||
setTokens(sortedTokens);
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageChange = (page) => {
|
|
||||||
loadTokens(page, pageSize).then();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageSizeChange = async (size) => {
|
|
||||||
setPageSize(size);
|
|
||||||
await loadTokens(1, size);
|
|
||||||
};
|
|
||||||
|
|
||||||
const rowSelection = {
|
|
||||||
onSelect: (record, selected) => { },
|
|
||||||
onSelectAll: (selected, selectedRows) => { },
|
|
||||||
onChange: (selectedRowKeys, selectedRows) => {
|
|
||||||
setSelectedKeys(selectedRows);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRow = (record, index) => {
|
|
||||||
if (record.status !== 1) {
|
|
||||||
return {
|
|
||||||
style: {
|
|
||||||
background: 'var(--semi-color-disabled-border)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const batchDeleteTokens = async () => {
|
|
||||||
if (selectedKeys.length === 0) {
|
|
||||||
showError(t('请先选择要删除的令牌!'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const ids = selectedKeys.map((token) => token.id);
|
|
||||||
const res = await API.post('/api/token/batch', { ids });
|
|
||||||
if (res?.data?.success) {
|
|
||||||
const count = res.data.data || 0;
|
|
||||||
showSuccess(t('已删除 {{count}} 个令牌!', { count }));
|
|
||||||
await refresh();
|
|
||||||
setTimeout(() => {
|
|
||||||
if (tokens.length === 0 && activePage > 1) {
|
|
||||||
refresh(activePage - 1);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
} else {
|
|
||||||
showError(res?.data?.message || t('删除失败'));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showError(error.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderHeader = () => (
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<div className="mb-2">
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
|
|
||||||
<div className="flex items-center text-blue-500">
|
|
||||||
<Key size={16} className="mr-2" />
|
|
||||||
<Text>{t('令牌用于API访问认证,可以设置额度限制和模型权限。')}</Text>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="tertiary"
|
|
||||||
className="w-full md:w-auto"
|
|
||||||
onClick={() => setCompactMode(!compactMode)}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{compactMode ? t('自适应列表') : t('紧凑列表')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider margin="12px" />
|
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
|
|
||||||
<div className="flex flex-wrap gap-2 w-full md:w-auto order-2 md:order-1">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
className="flex-1 md:flex-initial"
|
|
||||||
onClick={() => {
|
|
||||||
setEditingToken({
|
|
||||||
id: undefined,
|
|
||||||
});
|
|
||||||
setShowEdit(true);
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('添加令牌')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
className="flex-1 md:flex-initial"
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedKeys.length === 0) {
|
|
||||||
showError(t('请至少选择一个令牌!'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Modal.info({
|
|
||||||
title: t('复制令牌'),
|
|
||||||
icon: null,
|
|
||||||
content: t('请选择你的复制方式'),
|
|
||||||
footer: (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
onClick={async () => {
|
|
||||||
let content = '';
|
|
||||||
for (let i = 0; i < selectedKeys.length; i++) {
|
|
||||||
content +=
|
|
||||||
selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n';
|
|
||||||
}
|
|
||||||
await copyText(content);
|
|
||||||
Modal.destroyAll();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('名称+密钥')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
let content = '';
|
|
||||||
for (let i = 0; i < selectedKeys.length; i++) {
|
|
||||||
content += 'sk-' + selectedKeys[i].key + '\n';
|
|
||||||
}
|
|
||||||
await copyText(content);
|
|
||||||
Modal.destroyAll();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('仅密钥')}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('复制所选令牌')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='danger'
|
|
||||||
className="w-full md:w-auto"
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedKeys.length === 0) {
|
|
||||||
showError(t('请至少选择一个令牌!'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Modal.confirm({
|
|
||||||
title: t('批量删除令牌'),
|
|
||||||
content: (
|
|
||||||
<div>
|
|
||||||
{t('确定要删除所选的 {{count}} 个令牌吗?', { count: selectedKeys.length })}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
onOk: () => batchDeleteTokens(),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('删除所选令牌')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
initValues={formInitValues}
|
|
||||||
getFormApi={(api) => setFormApi(api)}
|
|
||||||
onSubmit={searchTokens}
|
|
||||||
allowEmpty={true}
|
|
||||||
autoComplete="off"
|
|
||||||
layout="horizontal"
|
|
||||||
trigger="change"
|
|
||||||
stopValidateWithError={false}
|
|
||||||
className="w-full md:w-auto order-1 md:order-2"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
|
|
||||||
<div className="relative w-full md:w-56">
|
|
||||||
<Form.Input
|
|
||||||
field="searchKeyword"
|
|
||||||
prefix={<IconSearch />}
|
|
||||||
placeholder={t('搜索关键字')}
|
|
||||||
showClear
|
|
||||||
pure
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="relative w-full md:w-56">
|
|
||||||
<Form.Input
|
|
||||||
field="searchToken"
|
|
||||||
prefix={<IconSearch />}
|
|
||||||
placeholder={t('密钥')}
|
|
||||||
showClear
|
|
||||||
pure
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 w-full md:w-auto">
|
|
||||||
<Button
|
|
||||||
type="tertiary"
|
|
||||||
htmlType="submit"
|
|
||||||
loading={loading || searching}
|
|
||||||
className="flex-1 md:flex-initial md:w-auto"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('查询')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
onClick={() => {
|
|
||||||
if (formApi) {
|
|
||||||
formApi.reset();
|
|
||||||
// 重置后立即查询,使用setTimeout确保表单重置完成
|
|
||||||
setTimeout(() => {
|
|
||||||
searchTokens();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex-1 md:flex-initial md:w-auto"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('重置')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<EditToken
|
|
||||||
refresh={refresh}
|
|
||||||
editingToken={editingToken}
|
|
||||||
visiable={showEdit}
|
|
||||||
handleClose={closeEdit}
|
|
||||||
></EditToken>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
className="!rounded-2xl"
|
|
||||||
title={renderHeader()}
|
|
||||||
shadows='always'
|
|
||||||
bordered={false}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
columns={compactMode ? columns.map(col => {
|
|
||||||
if (col.dataIndex === 'operate') {
|
|
||||||
const { fixed, ...rest } = col;
|
|
||||||
return rest;
|
|
||||||
}
|
|
||||||
return col;
|
|
||||||
}) : columns}
|
|
||||||
dataSource={tokens}
|
|
||||||
scroll={compactMode ? undefined : { x: 'max-content' }}
|
|
||||||
pagination={{
|
|
||||||
currentPage: activePage,
|
|
||||||
pageSize: pageSize,
|
|
||||||
total: tokenCount,
|
|
||||||
showSizeChanger: true,
|
|
||||||
pageSizeOptions: [10, 20, 50, 100],
|
|
||||||
formatPageText: (page) =>
|
|
||||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
|
||||||
start: page.currentStart,
|
|
||||||
end: page.currentEnd,
|
|
||||||
total: tokenCount,
|
|
||||||
}),
|
|
||||||
onPageSizeChange: handlePageSizeChange,
|
|
||||||
onPageChange: handlePageChange,
|
|
||||||
}}
|
|
||||||
loading={loading}
|
|
||||||
rowSelection={rowSelection}
|
|
||||||
onRow={handleRow}
|
|
||||||
empty={
|
|
||||||
<Empty
|
|
||||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
|
||||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
|
||||||
description={t('搜索无结果')}
|
|
||||||
style={{ padding: 30 }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
className="rounded-xl overflow-hidden"
|
|
||||||
size="middle"
|
|
||||||
></Table>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TokensTable;
|
|
||||||
@@ -1,686 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { API, showError, showSuccess, renderGroup, renderNumber, renderQuota } from '../../helpers';
|
|
||||||
|
|
||||||
import {
|
|
||||||
User,
|
|
||||||
Shield,
|
|
||||||
Crown,
|
|
||||||
HelpCircle,
|
|
||||||
CheckCircle,
|
|
||||||
XCircle,
|
|
||||||
Minus,
|
|
||||||
Coins,
|
|
||||||
Activity,
|
|
||||||
Users,
|
|
||||||
DollarSign,
|
|
||||||
UserPlus,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Divider,
|
|
||||||
Dropdown,
|
|
||||||
Empty,
|
|
||||||
Form,
|
|
||||||
Modal,
|
|
||||||
Space,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
Tooltip,
|
|
||||||
Typography
|
|
||||||
} from '@douyinfe/semi-ui';
|
|
||||||
import {
|
|
||||||
IllustrationNoResult,
|
|
||||||
IllustrationNoResultDark
|
|
||||||
} from '@douyinfe/semi-illustrations';
|
|
||||||
import {
|
|
||||||
IconSearch,
|
|
||||||
IconUserAdd,
|
|
||||||
IconMore,
|
|
||||||
} from '@douyinfe/semi-icons';
|
|
||||||
import { ITEMS_PER_PAGE } from '../../constants';
|
|
||||||
import AddUser from '../../pages/User/AddUser';
|
|
||||||
import EditUser from '../../pages/User/EditUser';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
const UsersTable = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [compactMode, setCompactMode] = useTableCompactMode('users');
|
|
||||||
|
|
||||||
function renderRole(role) {
|
|
||||||
switch (role) {
|
|
||||||
case 1:
|
|
||||||
return (
|
|
||||||
<Tag color='blue' shape='circle' prefixIcon={<User size={14} />}>
|
|
||||||
{t('普通用户')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 10:
|
|
||||||
return (
|
|
||||||
<Tag color='yellow' shape='circle' prefixIcon={<Shield size={14} />}>
|
|
||||||
{t('管理员')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
case 100:
|
|
||||||
return (
|
|
||||||
<Tag color='orange' shape='circle' prefixIcon={<Crown size={14} />}>
|
|
||||||
{t('超级管理员')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Tag color='red' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
|
||||||
{t('未知身份')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderStatus = (status) => {
|
|
||||||
switch (status) {
|
|
||||||
case 1:
|
|
||||||
return <Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>{t('已激活')}</Tag>;
|
|
||||||
case 2:
|
|
||||||
return (
|
|
||||||
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
|
|
||||||
{t('已封禁')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Tag color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
|
||||||
{t('未知状态')}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('用户名'),
|
|
||||||
dataIndex: 'username',
|
|
||||||
render: (text, record) => {
|
|
||||||
const remark = record.remark;
|
|
||||||
if (!remark) {
|
|
||||||
return <span>{text}</span>;
|
|
||||||
}
|
|
||||||
const maxLen = 10;
|
|
||||||
const displayRemark = remark.length > maxLen ? remark.slice(0, maxLen) + '…' : remark;
|
|
||||||
return (
|
|
||||||
<Space spacing={2}>
|
|
||||||
<span>{text}</span>
|
|
||||||
<Tooltip content={remark} position="top" showArrow>
|
|
||||||
<Tag color='white' shape='circle' className="!text-xs">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="w-2 h-2 flex-shrink-0 rounded-full" style={{ backgroundColor: '#10b981' }} />
|
|
||||||
{displayRemark}
|
|
||||||
</div>
|
|
||||||
</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('分组'),
|
|
||||||
dataIndex: 'group',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{renderGroup(text)}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('统计信息'),
|
|
||||||
dataIndex: 'info',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Space spacing={1}>
|
|
||||||
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
|
|
||||||
{t('剩余')}: {renderQuota(record.quota)}
|
|
||||||
</Tag>
|
|
||||||
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
|
|
||||||
{t('已用')}: {renderQuota(record.used_quota)}
|
|
||||||
</Tag>
|
|
||||||
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Activity size={14} />}>
|
|
||||||
{t('调用')}: {renderNumber(record.request_count)}
|
|
||||||
</Tag>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('邀请信息'),
|
|
||||||
dataIndex: 'invite',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Space spacing={1}>
|
|
||||||
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Users size={14} />}>
|
|
||||||
{t('邀请')}: {renderNumber(record.aff_count)}
|
|
||||||
</Tag>
|
|
||||||
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<DollarSign size={14} />}>
|
|
||||||
{t('收益')}: {renderQuota(record.aff_history_quota)}
|
|
||||||
</Tag>
|
|
||||||
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<UserPlus size={14} />}>
|
|
||||||
{record.inviter_id === 0 ? t('无邀请人') : `邀请人: ${record.inviter_id}`}
|
|
||||||
</Tag>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('角色'),
|
|
||||||
dataIndex: 'role',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return <div>{renderRole(text)}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('状态'),
|
|
||||||
dataIndex: 'status',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{record.DeletedAt !== null ? (
|
|
||||||
<Tag color='red' shape='circle' prefixIcon={<Minus size={14} />}>{t('已注销')}</Tag>
|
|
||||||
) : (
|
|
||||||
renderStatus(text)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
dataIndex: 'operate',
|
|
||||||
fixed: 'right',
|
|
||||||
render: (text, record, index) => {
|
|
||||||
if (record.DeletedAt !== null) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建更多操作的下拉菜单项
|
|
||||||
const moreMenuItems = [
|
|
||||||
{
|
|
||||||
node: 'item',
|
|
||||||
name: t('提升'),
|
|
||||||
type: 'warning',
|
|
||||||
onClick: () => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: t('确定要提升此用户吗?'),
|
|
||||||
content: t('此操作将提升用户的权限级别'),
|
|
||||||
onOk: () => {
|
|
||||||
manageUser(record.id, 'promote', record);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
node: 'item',
|
|
||||||
name: t('降级'),
|
|
||||||
type: 'secondary',
|
|
||||||
onClick: () => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: t('确定要降级此用户吗?'),
|
|
||||||
content: t('此操作将降低用户的权限级别'),
|
|
||||||
onOk: () => {
|
|
||||||
manageUser(record.id, 'demote', record);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
node: 'item',
|
|
||||||
name: t('注销'),
|
|
||||||
type: 'danger',
|
|
||||||
onClick: () => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: t('确定是否要注销此用户?'),
|
|
||||||
content: t('相当于删除用户,此修改将不可逆'),
|
|
||||||
onOk: () => {
|
|
||||||
(async () => {
|
|
||||||
await manageUser(record.id, 'delete', record);
|
|
||||||
await refresh();
|
|
||||||
setTimeout(() => {
|
|
||||||
if (users.length === 0 && activePage > 1) {
|
|
||||||
refresh(activePage - 1);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
})();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 动态添加启用/禁用按钮
|
|
||||||
if (record.status === 1) {
|
|
||||||
moreMenuItems.splice(-1, 0, {
|
|
||||||
node: 'item',
|
|
||||||
name: t('禁用'),
|
|
||||||
type: 'warning',
|
|
||||||
onClick: () => {
|
|
||||||
manageUser(record.id, 'disable', record);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
moreMenuItems.splice(-1, 0, {
|
|
||||||
node: 'item',
|
|
||||||
name: t('启用'),
|
|
||||||
type: 'secondary',
|
|
||||||
onClick: () => {
|
|
||||||
manageUser(record.id, 'enable', record);
|
|
||||||
},
|
|
||||||
disabled: record.status === 3,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
setEditingUser(record);
|
|
||||||
setShowEditUser(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('编辑')}
|
|
||||||
</Button>
|
|
||||||
<Dropdown
|
|
||||||
trigger='click'
|
|
||||||
position='bottomRight'
|
|
||||||
menu={moreMenuItems}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
size="small"
|
|
||||||
icon={<IconMore />}
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const [users, setUsers] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [activePage, setActivePage] = useState(1);
|
|
||||||
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
|
||||||
const [searching, setSearching] = useState(false);
|
|
||||||
const [groupOptions, setGroupOptions] = useState([]);
|
|
||||||
const [userCount, setUserCount] = useState(ITEMS_PER_PAGE);
|
|
||||||
const [showAddUser, setShowAddUser] = useState(false);
|
|
||||||
const [showEditUser, setShowEditUser] = useState(false);
|
|
||||||
const [editingUser, setEditingUser] = useState({
|
|
||||||
id: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Form 初始值
|
|
||||||
const formInitValues = {
|
|
||||||
searchKeyword: '',
|
|
||||||
searchGroup: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Form API 引用
|
|
||||||
const [formApi, setFormApi] = useState(null);
|
|
||||||
|
|
||||||
// 获取表单值的辅助函数
|
|
||||||
const getFormValues = () => {
|
|
||||||
const formValues = formApi ? formApi.getValues() : {};
|
|
||||||
return {
|
|
||||||
searchKeyword: formValues.searchKeyword || '',
|
|
||||||
searchGroup: formValues.searchGroup || '',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeRecord = (key) => {
|
|
||||||
let newDataSource = [...users];
|
|
||||||
if (key != null) {
|
|
||||||
let idx = newDataSource.findIndex((data) => data.id === key);
|
|
||||||
|
|
||||||
if (idx > -1) {
|
|
||||||
// update deletedAt
|
|
||||||
newDataSource[idx].DeletedAt = new Date();
|
|
||||||
setUsers(newDataSource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setUserFormat = (users) => {
|
|
||||||
for (let i = 0; i < users.length; i++) {
|
|
||||||
users[i].key = users[i].id;
|
|
||||||
}
|
|
||||||
setUsers(users);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadUsers = async (startIdx, pageSize) => {
|
|
||||||
const res = await API.get(`/api/user/?p=${startIdx}&page_size=${pageSize}`);
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
const newPageData = data.items;
|
|
||||||
setActivePage(data.page);
|
|
||||||
setUserCount(data.total);
|
|
||||||
setUserFormat(newPageData);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadUsers(0, pageSize)
|
|
||||||
.then()
|
|
||||||
.catch((reason) => {
|
|
||||||
showError(reason);
|
|
||||||
});
|
|
||||||
fetchGroups().then();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const manageUser = async (userId, action, record) => {
|
|
||||||
const res = await API.post('/api/user/manage', {
|
|
||||||
id: userId,
|
|
||||||
action,
|
|
||||||
});
|
|
||||||
const { success, message } = res.data;
|
|
||||||
if (success) {
|
|
||||||
showSuccess('操作成功完成!');
|
|
||||||
let user = res.data.data;
|
|
||||||
let newUsers = [...users];
|
|
||||||
if (action === 'delete') {
|
|
||||||
} else {
|
|
||||||
record.status = user.status;
|
|
||||||
record.role = user.role;
|
|
||||||
}
|
|
||||||
setUsers(newUsers);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchUsers = async (
|
|
||||||
startIdx,
|
|
||||||
pageSize,
|
|
||||||
searchKeyword = null,
|
|
||||||
searchGroup = null,
|
|
||||||
) => {
|
|
||||||
// 如果没有传递参数,从表单获取值
|
|
||||||
if (searchKeyword === null || searchGroup === null) {
|
|
||||||
const formValues = getFormValues();
|
|
||||||
searchKeyword = formValues.searchKeyword;
|
|
||||||
searchGroup = formValues.searchGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchKeyword === '' && searchGroup === '') {
|
|
||||||
// if keyword is blank, load files instead.
|
|
||||||
await loadUsers(startIdx, pageSize);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSearching(true);
|
|
||||||
const res = await API.get(
|
|
||||||
`/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`,
|
|
||||||
);
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
const newPageData = data.items;
|
|
||||||
setActivePage(data.page);
|
|
||||||
setUserCount(data.total);
|
|
||||||
setUserFormat(newPageData);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setSearching(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageChange = (page) => {
|
|
||||||
setActivePage(page);
|
|
||||||
const { searchKeyword, searchGroup } = getFormValues();
|
|
||||||
if (searchKeyword === '' && searchGroup === '') {
|
|
||||||
loadUsers(page, pageSize).then();
|
|
||||||
} else {
|
|
||||||
searchUsers(page, pageSize, searchKeyword, searchGroup).then();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeAddUser = () => {
|
|
||||||
setShowAddUser(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeEditUser = () => {
|
|
||||||
setShowEditUser(false);
|
|
||||||
setEditingUser({
|
|
||||||
id: undefined,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const refresh = async (page = activePage) => {
|
|
||||||
const { searchKeyword, searchGroup } = getFormValues();
|
|
||||||
if (searchKeyword === '' && searchGroup === '') {
|
|
||||||
await loadUsers(page, pageSize);
|
|
||||||
} else {
|
|
||||||
await searchUsers(page, pageSize, searchKeyword, searchGroup);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchGroups = async () => {
|
|
||||||
try {
|
|
||||||
let res = await API.get(`/api/group/`);
|
|
||||||
// add 'all' option
|
|
||||||
// res.data.data.unshift('all');
|
|
||||||
if (res === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setGroupOptions(
|
|
||||||
res.data.data.map((group) => ({
|
|
||||||
label: group,
|
|
||||||
value: group,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
showError(error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageSizeChange = async (size) => {
|
|
||||||
localStorage.setItem('page-size', size + '');
|
|
||||||
setPageSize(size);
|
|
||||||
setActivePage(1);
|
|
||||||
loadUsers(activePage, size)
|
|
||||||
.then()
|
|
||||||
.catch((reason) => {
|
|
||||||
showError(reason);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRow = (record, index) => {
|
|
||||||
if (record.DeletedAt !== null || record.status !== 1) {
|
|
||||||
return {
|
|
||||||
style: {
|
|
||||||
background: 'var(--semi-color-disabled-border)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderHeader = () => (
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<div className="mb-2">
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
|
|
||||||
<div className="flex items-center text-blue-500">
|
|
||||||
<IconUserAdd className="mr-2" />
|
|
||||||
<Text>{t('用户管理页面,可以查看和管理所有注册用户的信息、权限和状态。')}</Text>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
className="w-full md:w-auto"
|
|
||||||
onClick={() => setCompactMode(!compactMode)}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{compactMode ? t('自适应列表') : t('紧凑列表')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider margin="12px" />
|
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
|
|
||||||
<div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
|
|
||||||
<Button
|
|
||||||
className="w-full md:w-auto"
|
|
||||||
onClick={() => {
|
|
||||||
setShowAddUser(true);
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('添加用户')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
initValues={formInitValues}
|
|
||||||
getFormApi={(api) => setFormApi(api)}
|
|
||||||
onSubmit={() => {
|
|
||||||
setActivePage(1);
|
|
||||||
searchUsers(1, pageSize);
|
|
||||||
}}
|
|
||||||
allowEmpty={true}
|
|
||||||
autoComplete="off"
|
|
||||||
layout="horizontal"
|
|
||||||
trigger="change"
|
|
||||||
stopValidateWithError={false}
|
|
||||||
className="w-full md:w-auto order-1 md:order-2"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
|
|
||||||
<div className="relative w-full md:w-64">
|
|
||||||
<Form.Input
|
|
||||||
field="searchKeyword"
|
|
||||||
prefix={<IconSearch />}
|
|
||||||
placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
|
|
||||||
showClear
|
|
||||||
pure
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-full md:w-48">
|
|
||||||
<Form.Select
|
|
||||||
field="searchGroup"
|
|
||||||
placeholder={t('选择分组')}
|
|
||||||
optionList={groupOptions}
|
|
||||||
onChange={(value) => {
|
|
||||||
// 分组变化时自动搜索
|
|
||||||
setTimeout(() => {
|
|
||||||
setActivePage(1);
|
|
||||||
searchUsers(1, pageSize);
|
|
||||||
}, 100);
|
|
||||||
}}
|
|
||||||
className="w-full"
|
|
||||||
showClear
|
|
||||||
pure
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 w-full md:w-auto">
|
|
||||||
<Button
|
|
||||||
type="tertiary"
|
|
||||||
htmlType="submit"
|
|
||||||
loading={loading || searching}
|
|
||||||
className="flex-1 md:flex-initial md:w-auto"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('查询')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='tertiary'
|
|
||||||
onClick={() => {
|
|
||||||
if (formApi) {
|
|
||||||
formApi.reset();
|
|
||||||
setTimeout(() => {
|
|
||||||
setActivePage(1);
|
|
||||||
loadUsers(1, pageSize);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex-1 md:flex-initial md:w-auto"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{t('重置')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AddUser
|
|
||||||
refresh={refresh}
|
|
||||||
visible={showAddUser}
|
|
||||||
handleClose={closeAddUser}
|
|
||||||
></AddUser>
|
|
||||||
<EditUser
|
|
||||||
refresh={refresh}
|
|
||||||
visible={showEditUser}
|
|
||||||
handleClose={closeEditUser}
|
|
||||||
editingUser={editingUser}
|
|
||||||
></EditUser>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
className="!rounded-2xl"
|
|
||||||
title={renderHeader()}
|
|
||||||
shadows='always'
|
|
||||||
bordered={false}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
columns={compactMode ? columns.map(({ fixed, ...rest }) => rest) : columns}
|
|
||||||
dataSource={users}
|
|
||||||
scroll={compactMode ? undefined : { x: 'max-content' }}
|
|
||||||
pagination={{
|
|
||||||
formatPageText: (page) =>
|
|
||||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
|
||||||
start: page.currentStart,
|
|
||||||
end: page.currentEnd,
|
|
||||||
total: userCount,
|
|
||||||
}),
|
|
||||||
currentPage: activePage,
|
|
||||||
pageSize: pageSize,
|
|
||||||
total: userCount,
|
|
||||||
pageSizeOpts: [10, 20, 50, 100],
|
|
||||||
showSizeChanger: true,
|
|
||||||
onPageSizeChange: (size) => {
|
|
||||||
handlePageSizeChange(size);
|
|
||||||
},
|
|
||||||
onPageChange: handlePageChange,
|
|
||||||
}}
|
|
||||||
loading={loading}
|
|
||||||
onRow={handleRow}
|
|
||||||
empty={
|
|
||||||
<Empty
|
|
||||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
|
||||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
|
||||||
description={t('搜索无结果')}
|
|
||||||
style={{ padding: 30 }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
className="overflow-hidden"
|
|
||||||
size="middle"
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UsersTable;
|
|
||||||
257
web/src/components/table/channels/ChannelsActions.jsx
Normal file
257
web/src/components/table/channels/ChannelsActions.jsx
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dropdown,
|
||||||
|
Modal,
|
||||||
|
Switch,
|
||||||
|
Typography,
|
||||||
|
Select
|
||||||
|
} from '@douyinfe/semi-ui';
|
||||||
|
import CompactModeToggle from '../../common/ui/CompactModeToggle';
|
||||||
|
|
||||||
|
const ChannelsActions = ({
|
||||||
|
enableBatchDelete,
|
||||||
|
batchDeleteChannels,
|
||||||
|
setShowBatchSetTag,
|
||||||
|
testAllChannels,
|
||||||
|
fixChannelsAbilities,
|
||||||
|
updateAllChannelsBalance,
|
||||||
|
deleteAllDisabledChannels,
|
||||||
|
compactMode,
|
||||||
|
setCompactMode,
|
||||||
|
idSort,
|
||||||
|
setIdSort,
|
||||||
|
setEnableBatchDelete,
|
||||||
|
enableTagMode,
|
||||||
|
setEnableTagMode,
|
||||||
|
statusFilter,
|
||||||
|
setStatusFilter,
|
||||||
|
getFormValues,
|
||||||
|
loadChannels,
|
||||||
|
searchChannels,
|
||||||
|
activeTypeKey,
|
||||||
|
activePage,
|
||||||
|
pageSize,
|
||||||
|
setActivePage,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{/* 第一行:批量操作按钮 + 设置开关 */}
|
||||||
|
<div className="flex flex-col md:flex-row justify-between gap-2">
|
||||||
|
{/* 左侧:批量操作按钮 */}
|
||||||
|
<div className="flex flex-wrap md:flex-nowrap items-center gap-2 w-full md:w-auto order-2 md:order-1">
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
disabled={!enableBatchDelete}
|
||||||
|
type='danger'
|
||||||
|
className="w-full md:w-auto"
|
||||||
|
onClick={() => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('确定是否要删除所选通道?'),
|
||||||
|
content: t('此修改将不可逆'),
|
||||||
|
onOk: () => batchDeleteChannels(),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('删除所选通道')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
disabled={!enableBatchDelete}
|
||||||
|
type='tertiary'
|
||||||
|
onClick={() => setShowBatchSetTag(true)}
|
||||||
|
className="w-full md:w-auto"
|
||||||
|
>
|
||||||
|
{t('批量设置标签')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
size='small'
|
||||||
|
trigger='click'
|
||||||
|
render={
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<Dropdown.Item>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type='tertiary'
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('确定?'),
|
||||||
|
content: t('确定要测试所有通道吗?'),
|
||||||
|
onOk: () => testAllChannels(),
|
||||||
|
size: 'small',
|
||||||
|
centered: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('测试所有通道')}
|
||||||
|
</Button>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('确定是否要修复数据库一致性?'),
|
||||||
|
content: t('进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用'),
|
||||||
|
onOk: () => fixChannelsAbilities(),
|
||||||
|
size: 'sm',
|
||||||
|
centered: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('修复数据库一致性')}
|
||||||
|
</Button>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type='secondary'
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('确定?'),
|
||||||
|
content: t('确定要更新所有已启用通道余额吗?'),
|
||||||
|
onOk: () => updateAllChannelsBalance(),
|
||||||
|
size: 'sm',
|
||||||
|
centered: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('更新所有已启用通道余额')}
|
||||||
|
</Button>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type='danger'
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('确定是否要删除禁用通道?'),
|
||||||
|
content: t('此修改将不可逆'),
|
||||||
|
onOk: () => deleteAllDisabledChannels(),
|
||||||
|
size: 'sm',
|
||||||
|
centered: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('删除禁用通道')}
|
||||||
|
</Button>
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button size='small' theme='light' type='tertiary' className="w-full md:w-auto">
|
||||||
|
{t('批量操作')}
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
<CompactModeToggle
|
||||||
|
compactMode={compactMode}
|
||||||
|
setCompactMode={setCompactMode}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧:设置开关区域 */}
|
||||||
|
<div className="flex flex-col md:flex-row items-start md:items-center gap-2 w-full md:w-auto order-1 md:order-2">
|
||||||
|
<div className="flex items-center justify-between w-full md:w-auto">
|
||||||
|
<Typography.Text strong className="mr-2">
|
||||||
|
{t('使用ID排序')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
size='small'
|
||||||
|
checked={idSort}
|
||||||
|
onChange={(v) => {
|
||||||
|
localStorage.setItem('id-sort', v + '');
|
||||||
|
setIdSort(v);
|
||||||
|
const { searchKeyword, searchGroup, searchModel } = getFormValues();
|
||||||
|
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
|
||||||
|
loadChannels(activePage, pageSize, v, enableTagMode);
|
||||||
|
} else {
|
||||||
|
searchChannels(enableTagMode, activeTypeKey, statusFilter, activePage, pageSize, v);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between w-full md:w-auto">
|
||||||
|
<Typography.Text strong className="mr-2">
|
||||||
|
{t('开启批量操作')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
size='small'
|
||||||
|
checked={enableBatchDelete}
|
||||||
|
onChange={(v) => {
|
||||||
|
localStorage.setItem('enable-batch-delete', v + '');
|
||||||
|
setEnableBatchDelete(v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between w-full md:w-auto">
|
||||||
|
<Typography.Text strong className="mr-2">
|
||||||
|
{t('标签聚合模式')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
size='small'
|
||||||
|
checked={enableTagMode}
|
||||||
|
onChange={(v) => {
|
||||||
|
localStorage.setItem('enable-tag-mode', v + '');
|
||||||
|
setEnableTagMode(v);
|
||||||
|
setActivePage(1);
|
||||||
|
loadChannels(1, pageSize, idSort, v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between w-full md:w-auto">
|
||||||
|
<Typography.Text strong className="mr-2">
|
||||||
|
{t('状态筛选')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Select
|
||||||
|
size='small'
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(v) => {
|
||||||
|
localStorage.setItem('channel-status-filter', v);
|
||||||
|
setStatusFilter(v);
|
||||||
|
setActivePage(1);
|
||||||
|
loadChannels(1, pageSize, idSort, enableTagMode, activeTypeKey, v);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Option value="all">{t('全部')}</Select.Option>
|
||||||
|
<Select.Option value="enabled">{t('已启用')}</Select.Option>
|
||||||
|
<Select.Option value="disabled">{t('已禁用')}</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChannelsActions;
|
||||||
623
web/src/components/table/channels/ChannelsColumnDefs.js
Normal file
623
web/src/components/table/channels/ChannelsColumnDefs.js
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dropdown,
|
||||||
|
InputNumber,
|
||||||
|
Modal,
|
||||||
|
Space,
|
||||||
|
SplitButtonGroup,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Typography
|
||||||
|
} from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
timestamp2string,
|
||||||
|
renderGroup,
|
||||||
|
renderQuota,
|
||||||
|
getChannelIcon,
|
||||||
|
renderQuotaWithAmount
|
||||||
|
} from '../../../helpers/index.js';
|
||||||
|
import { CHANNEL_OPTIONS } from '../../../constants/index.js';
|
||||||
|
import { IconTreeTriangleDown, IconMore } from '@douyinfe/semi-icons';
|
||||||
|
import { FaRandom } from 'react-icons/fa';
|
||||||
|
|
||||||
|
// Render functions
|
||||||
|
const renderType = (type, channelInfo = undefined, t) => {
|
||||||
|
let type2label = new Map();
|
||||||
|
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
|
||||||
|
type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
|
||||||
|
}
|
||||||
|
type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' };
|
||||||
|
|
||||||
|
let icon = getChannelIcon(type);
|
||||||
|
|
||||||
|
if (channelInfo?.is_multi_key) {
|
||||||
|
icon = (
|
||||||
|
channelInfo?.multi_key_mode === 'random' ? (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<FaRandom className="text-blue-500" />
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<IconTreeTriangleDown className="text-blue-500" />
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
color={type2label[type]?.color}
|
||||||
|
shape='circle'
|
||||||
|
prefixIcon={icon}
|
||||||
|
>
|
||||||
|
{type2label[type]?.label}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTagType = (t) => {
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
color='light-blue'
|
||||||
|
shape='circle'
|
||||||
|
type='light'
|
||||||
|
>
|
||||||
|
{t('标签聚合')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStatus = (status, channelInfo = undefined, t) => {
|
||||||
|
if (channelInfo) {
|
||||||
|
if (channelInfo.is_multi_key) {
|
||||||
|
let keySize = channelInfo.multi_key_size;
|
||||||
|
let enabledKeySize = keySize;
|
||||||
|
if (channelInfo.multi_key_status_list) {
|
||||||
|
enabledKeySize = keySize - Object.keys(channelInfo.multi_key_status_list).length;
|
||||||
|
}
|
||||||
|
return renderMultiKeyStatus(status, keySize, enabledKeySize, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (status) {
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<Tag color='green' shape='circle'>
|
||||||
|
{t('已启用')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<Tag color='red' shape='circle'>
|
||||||
|
{t('已禁用')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 3:
|
||||||
|
return (
|
||||||
|
<Tag color='yellow' shape='circle'>
|
||||||
|
{t('自动禁用')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Tag color='grey' shape='circle'>
|
||||||
|
{t('未知状态')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMultiKeyStatus = (status, keySize, enabledKeySize, t) => {
|
||||||
|
switch (status) {
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<Tag color='green' shape='circle'>
|
||||||
|
{t('已启用')} {enabledKeySize}/{keySize}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<Tag color='red' shape='circle'>
|
||||||
|
{t('已禁用')} {enabledKeySize}/{keySize}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 3:
|
||||||
|
return (
|
||||||
|
<Tag color='yellow' shape='circle'>
|
||||||
|
{t('自动禁用')} {enabledKeySize}/{keySize}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Tag color='grey' shape='circle'>
|
||||||
|
{t('未知状态')} {enabledKeySize}/{keySize}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderResponseTime = (responseTime, t) => {
|
||||||
|
let time = responseTime / 1000;
|
||||||
|
time = time.toFixed(2) + t(' 秒');
|
||||||
|
if (responseTime === 0) {
|
||||||
|
return (
|
||||||
|
<Tag color='grey' shape='circle'>
|
||||||
|
{t('未测试')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
} else if (responseTime <= 1000) {
|
||||||
|
return (
|
||||||
|
<Tag color='green' shape='circle'>
|
||||||
|
{time}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
} else if (responseTime <= 3000) {
|
||||||
|
return (
|
||||||
|
<Tag color='lime' shape='circle'>
|
||||||
|
{time}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
} else if (responseTime <= 5000) {
|
||||||
|
return (
|
||||||
|
<Tag color='yellow' shape='circle'>
|
||||||
|
{time}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Tag color='red' shape='circle'>
|
||||||
|
{time}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getChannelsColumns = ({
|
||||||
|
t,
|
||||||
|
COLUMN_KEYS,
|
||||||
|
updateChannelBalance,
|
||||||
|
manageChannel,
|
||||||
|
manageTag,
|
||||||
|
submitTagEdit,
|
||||||
|
testChannel,
|
||||||
|
setCurrentTestChannel,
|
||||||
|
setShowModelTestModal,
|
||||||
|
setEditingChannel,
|
||||||
|
setShowEdit,
|
||||||
|
setShowEditTag,
|
||||||
|
setEditingTag,
|
||||||
|
copySelectedChannel,
|
||||||
|
refresh,
|
||||||
|
activePage,
|
||||||
|
channels
|
||||||
|
}) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.ID,
|
||||||
|
title: t('ID'),
|
||||||
|
dataIndex: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.NAME,
|
||||||
|
title: t('名称'),
|
||||||
|
dataIndex: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.GROUP,
|
||||||
|
title: t('分组'),
|
||||||
|
dataIndex: 'group',
|
||||||
|
render: (text, record, index) => (
|
||||||
|
<div>
|
||||||
|
<Space spacing={2}>
|
||||||
|
{text
|
||||||
|
?.split(',')
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a === 'default') return -1;
|
||||||
|
if (b === 'default') return 1;
|
||||||
|
return a.localeCompare(b);
|
||||||
|
})
|
||||||
|
.map((item, index) => renderGroup(item))}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.TYPE,
|
||||||
|
title: t('类型'),
|
||||||
|
dataIndex: 'type',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
if (record.children === undefined) {
|
||||||
|
if (record.channel_info) {
|
||||||
|
if (record.channel_info.is_multi_key) {
|
||||||
|
return <>{renderType(text, record.channel_info, t)}</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <>{renderType(text, undefined, t)}</>;
|
||||||
|
} else {
|
||||||
|
return <>{renderTagType(t)}</>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.STATUS,
|
||||||
|
title: t('状态'),
|
||||||
|
dataIndex: 'status',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
if (text === 3) {
|
||||||
|
if (record.other_info === '') {
|
||||||
|
record.other_info = '{}';
|
||||||
|
}
|
||||||
|
let otherInfo = JSON.parse(record.other_info);
|
||||||
|
let reason = otherInfo['status_reason'];
|
||||||
|
let time = otherInfo['status_time'];
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
content={t('原因:') + reason + t(',时间:') + timestamp2string(time)}
|
||||||
|
>
|
||||||
|
{renderStatus(text, record.channel_info, t)}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return renderStatus(text, record.channel_info, t);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.RESPONSE_TIME,
|
||||||
|
title: t('响应时间'),
|
||||||
|
dataIndex: 'response_time',
|
||||||
|
render: (text, record, index) => (
|
||||||
|
<div>{renderResponseTime(text, t)}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.BALANCE,
|
||||||
|
title: t('已用/剩余'),
|
||||||
|
dataIndex: 'expired_time',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
if (record.children === undefined) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Space spacing={1}>
|
||||||
|
<Tooltip content={t('已用额度')}>
|
||||||
|
<Tag color='white' type='ghost' shape='circle'>
|
||||||
|
{renderQuota(record.used_quota)}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content={t('剩余额度$') + record.balance + t(',点击更新')}>
|
||||||
|
<Tag
|
||||||
|
color='white'
|
||||||
|
type='ghost'
|
||||||
|
shape='circle'
|
||||||
|
onClick={() => updateChannelBalance(record)}
|
||||||
|
>
|
||||||
|
{renderQuotaWithAmount(record.balance)}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Tooltip content={t('已用额度')}>
|
||||||
|
<Tag color='white' type='ghost' shape='circle'>
|
||||||
|
{renderQuota(record.used_quota)}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.PRIORITY,
|
||||||
|
title: t('优先级'),
|
||||||
|
dataIndex: 'priority',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
if (record.children === undefined) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: 70 }}
|
||||||
|
name='priority'
|
||||||
|
onBlur={(e) => {
|
||||||
|
manageChannel(record.id, 'priority', record, e.target.value);
|
||||||
|
}}
|
||||||
|
keepFocus={true}
|
||||||
|
innerButtons
|
||||||
|
defaultValue={record.priority}
|
||||||
|
min={-999}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: 70 }}
|
||||||
|
name='priority'
|
||||||
|
keepFocus={true}
|
||||||
|
onBlur={(e) => {
|
||||||
|
Modal.warning({
|
||||||
|
title: t('修改子渠道优先级'),
|
||||||
|
content: t('确定要修改所有子渠道优先级为 ') + e.target.value + t(' 吗?'),
|
||||||
|
onOk: () => {
|
||||||
|
if (e.target.value === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitTagEdit('priority', {
|
||||||
|
tag: record.key,
|
||||||
|
priority: e.target.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
innerButtons
|
||||||
|
defaultValue={record.priority}
|
||||||
|
min={-999}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.WEIGHT,
|
||||||
|
title: t('权重'),
|
||||||
|
dataIndex: 'weight',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
if (record.children === undefined) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: 70 }}
|
||||||
|
name='weight'
|
||||||
|
onBlur={(e) => {
|
||||||
|
manageChannel(record.id, 'weight', record, e.target.value);
|
||||||
|
}}
|
||||||
|
keepFocus={true}
|
||||||
|
innerButtons
|
||||||
|
defaultValue={record.weight}
|
||||||
|
min={0}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: 70 }}
|
||||||
|
name='weight'
|
||||||
|
keepFocus={true}
|
||||||
|
onBlur={(e) => {
|
||||||
|
Modal.warning({
|
||||||
|
title: t('修改子渠道权重'),
|
||||||
|
content: t('确定要修改所有子渠道权重为 ') + e.target.value + t(' 吗?'),
|
||||||
|
onOk: () => {
|
||||||
|
if (e.target.value === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitTagEdit('weight', {
|
||||||
|
tag: record.key,
|
||||||
|
weight: e.target.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
innerButtons
|
||||||
|
defaultValue={record.weight}
|
||||||
|
min={-999}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.OPERATE,
|
||||||
|
title: '',
|
||||||
|
dataIndex: 'operate',
|
||||||
|
fixed: 'right',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
if (record.children === undefined) {
|
||||||
|
const moreMenuItems = [
|
||||||
|
{
|
||||||
|
node: 'item',
|
||||||
|
name: t('删除'),
|
||||||
|
type: 'danger',
|
||||||
|
onClick: () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('确定是否要删除此渠道?'),
|
||||||
|
content: t('此修改将不可逆'),
|
||||||
|
onOk: () => {
|
||||||
|
(async () => {
|
||||||
|
await manageChannel(record.id, 'delete', record);
|
||||||
|
await refresh();
|
||||||
|
setTimeout(() => {
|
||||||
|
if (channels.length === 0 && activePage > 1) {
|
||||||
|
refresh(activePage - 1);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: 'item',
|
||||||
|
name: t('复制'),
|
||||||
|
type: 'tertiary',
|
||||||
|
onClick: () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('确定是否要复制此渠道?'),
|
||||||
|
content: t('复制渠道的所有信息'),
|
||||||
|
onOk: () => copySelectedChannel(record),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space wrap>
|
||||||
|
<SplitButtonGroup
|
||||||
|
className="overflow-hidden"
|
||||||
|
aria-label={t('测试单个渠道操作项目组')}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type='tertiary'
|
||||||
|
onClick={() => testChannel(record, '')}
|
||||||
|
>
|
||||||
|
{t('测试')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type='tertiary'
|
||||||
|
icon={<IconTreeTriangleDown />}
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentTestChannel(record);
|
||||||
|
setShowModelTestModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SplitButtonGroup>
|
||||||
|
|
||||||
|
{record.channel_info?.is_multi_key ? (
|
||||||
|
<SplitButtonGroup
|
||||||
|
aria-label={t('多密钥渠道操作项目组')}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
record.status === 1 ? (
|
||||||
|
<Button
|
||||||
|
type='danger'
|
||||||
|
size="small"
|
||||||
|
onClick={() => manageChannel(record.id, 'disable', record)}
|
||||||
|
>
|
||||||
|
{t('禁用')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => manageChannel(record.id, 'enable', record)}
|
||||||
|
>
|
||||||
|
{t('启用')}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<Dropdown
|
||||||
|
trigger='click'
|
||||||
|
position='bottomRight'
|
||||||
|
menu={[
|
||||||
|
{
|
||||||
|
node: 'item',
|
||||||
|
name: t('启用全部密钥'),
|
||||||
|
onClick: () => manageChannel(record.id, 'enable_all', record),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
size="small"
|
||||||
|
icon={<IconTreeTriangleDown />}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</SplitButtonGroup>
|
||||||
|
) : (
|
||||||
|
record.status === 1 ? (
|
||||||
|
<Button
|
||||||
|
type='danger'
|
||||||
|
size="small"
|
||||||
|
onClick={() => manageChannel(record.id, 'disable', record)}
|
||||||
|
>
|
||||||
|
{t('禁用')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => manageChannel(record.id, 'enable', record)}
|
||||||
|
>
|
||||||
|
{t('启用')}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingChannel(record);
|
||||||
|
setShowEdit(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('编辑')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
trigger='click'
|
||||||
|
position='bottomRight'
|
||||||
|
menu={moreMenuItems}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<IconMore />}
|
||||||
|
type='tertiary'
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 标签操作按钮
|
||||||
|
return (
|
||||||
|
<Space wrap>
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
size="small"
|
||||||
|
onClick={() => manageTag(record.key, 'enable')}
|
||||||
|
>
|
||||||
|
{t('启用全部')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
size="small"
|
||||||
|
onClick={() => manageTag(record.key, 'disable')}
|
||||||
|
>
|
||||||
|
{t('禁用全部')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setShowEditTag(true);
|
||||||
|
setEditingTag(record.key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('编辑')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
159
web/src/components/table/channels/ChannelsFilters.jsx
Normal file
159
web/src/components/table/channels/ChannelsFilters.jsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Button, Form } from '@douyinfe/semi-ui';
|
||||||
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
const ChannelsFilters = ({
|
||||||
|
setEditingChannel,
|
||||||
|
setShowEdit,
|
||||||
|
refresh,
|
||||||
|
setShowColumnSelector,
|
||||||
|
formInitValues,
|
||||||
|
setFormApi,
|
||||||
|
searchChannels,
|
||||||
|
enableTagMode,
|
||||||
|
formApi,
|
||||||
|
groupOptions,
|
||||||
|
loading,
|
||||||
|
searching,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-center gap-2 w-full">
|
||||||
|
<div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
theme='light'
|
||||||
|
type='primary'
|
||||||
|
className="w-full md:w-auto"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingChannel({
|
||||||
|
id: undefined,
|
||||||
|
});
|
||||||
|
setShowEdit(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('添加渠道')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type='tertiary'
|
||||||
|
className="w-full md:w-auto"
|
||||||
|
onClick={refresh}
|
||||||
|
>
|
||||||
|
{t('刷新')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type='tertiary'
|
||||||
|
onClick={() => setShowColumnSelector(true)}
|
||||||
|
className="w-full md:w-auto"
|
||||||
|
>
|
||||||
|
{t('列设置')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col md:flex-row items-center gap-2 w-full md:w-auto order-1 md:order-2">
|
||||||
|
<Form
|
||||||
|
initValues={formInitValues}
|
||||||
|
getFormApi={(api) => setFormApi(api)}
|
||||||
|
onSubmit={() => searchChannels(enableTagMode)}
|
||||||
|
allowEmpty={true}
|
||||||
|
autoComplete="off"
|
||||||
|
layout="horizontal"
|
||||||
|
trigger="change"
|
||||||
|
stopValidateWithError={false}
|
||||||
|
className="flex flex-col md:flex-row items-center gap-2 w-full"
|
||||||
|
>
|
||||||
|
<div className="relative w-full md:w-64">
|
||||||
|
<Form.Input
|
||||||
|
size='small'
|
||||||
|
field="searchKeyword"
|
||||||
|
prefix={<IconSearch />}
|
||||||
|
placeholder={t('渠道ID,名称,密钥,API地址')}
|
||||||
|
showClear
|
||||||
|
pure
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full md:w-48">
|
||||||
|
<Form.Input
|
||||||
|
size='small'
|
||||||
|
field="searchModel"
|
||||||
|
prefix={<IconSearch />}
|
||||||
|
placeholder={t('模型关键字')}
|
||||||
|
showClear
|
||||||
|
pure
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full md:w-32">
|
||||||
|
<Form.Select
|
||||||
|
size='small'
|
||||||
|
field="searchGroup"
|
||||||
|
placeholder={t('选择分组')}
|
||||||
|
optionList={[
|
||||||
|
{ label: t('选择分组'), value: null },
|
||||||
|
...groupOptions,
|
||||||
|
]}
|
||||||
|
className="w-full"
|
||||||
|
showClear
|
||||||
|
pure
|
||||||
|
onChange={() => {
|
||||||
|
// 延迟执行搜索,让表单值先更新
|
||||||
|
setTimeout(() => {
|
||||||
|
searchChannels(enableTagMode);
|
||||||
|
}, 0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type="tertiary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={loading || searching}
|
||||||
|
className="w-full md:w-auto"
|
||||||
|
>
|
||||||
|
{t('查询')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
type='tertiary'
|
||||||
|
onClick={() => {
|
||||||
|
if (formApi) {
|
||||||
|
formApi.reset();
|
||||||
|
// 重置后立即查询,使用setTimeout确保表单重置完成
|
||||||
|
setTimeout(() => {
|
||||||
|
refresh();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full md:w-auto"
|
||||||
|
>
|
||||||
|
{t('重置')}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChannelsFilters;
|
||||||
159
web/src/components/table/channels/ChannelsTable.jsx
Normal file
159
web/src/components/table/channels/ChannelsTable.jsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { Empty } from '@douyinfe/semi-ui';
|
||||||
|
import CardTable from '../../common/ui/CardTable.js';
|
||||||
|
import {
|
||||||
|
IllustrationNoResult,
|
||||||
|
IllustrationNoResultDark
|
||||||
|
} from '@douyinfe/semi-illustrations';
|
||||||
|
import { getChannelsColumns } from './ChannelsColumnDefs.js';
|
||||||
|
|
||||||
|
const ChannelsTable = (channelsData) => {
|
||||||
|
const {
|
||||||
|
channels,
|
||||||
|
loading,
|
||||||
|
searching,
|
||||||
|
activePage,
|
||||||
|
pageSize,
|
||||||
|
channelCount,
|
||||||
|
enableBatchDelete,
|
||||||
|
compactMode,
|
||||||
|
visibleColumns,
|
||||||
|
setSelectedChannels,
|
||||||
|
handlePageChange,
|
||||||
|
handlePageSizeChange,
|
||||||
|
handleRow,
|
||||||
|
t,
|
||||||
|
COLUMN_KEYS,
|
||||||
|
// Column functions and data
|
||||||
|
updateChannelBalance,
|
||||||
|
manageChannel,
|
||||||
|
manageTag,
|
||||||
|
submitTagEdit,
|
||||||
|
testChannel,
|
||||||
|
setCurrentTestChannel,
|
||||||
|
setShowModelTestModal,
|
||||||
|
setEditingChannel,
|
||||||
|
setShowEdit,
|
||||||
|
setShowEditTag,
|
||||||
|
setEditingTag,
|
||||||
|
copySelectedChannel,
|
||||||
|
refresh,
|
||||||
|
} = channelsData;
|
||||||
|
|
||||||
|
// Get all columns
|
||||||
|
const allColumns = useMemo(() => {
|
||||||
|
return getChannelsColumns({
|
||||||
|
t,
|
||||||
|
COLUMN_KEYS,
|
||||||
|
updateChannelBalance,
|
||||||
|
manageChannel,
|
||||||
|
manageTag,
|
||||||
|
submitTagEdit,
|
||||||
|
testChannel,
|
||||||
|
setCurrentTestChannel,
|
||||||
|
setShowModelTestModal,
|
||||||
|
setEditingChannel,
|
||||||
|
setShowEdit,
|
||||||
|
setShowEditTag,
|
||||||
|
setEditingTag,
|
||||||
|
copySelectedChannel,
|
||||||
|
refresh,
|
||||||
|
activePage,
|
||||||
|
channels,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
t,
|
||||||
|
COLUMN_KEYS,
|
||||||
|
updateChannelBalance,
|
||||||
|
manageChannel,
|
||||||
|
manageTag,
|
||||||
|
submitTagEdit,
|
||||||
|
testChannel,
|
||||||
|
setCurrentTestChannel,
|
||||||
|
setShowModelTestModal,
|
||||||
|
setEditingChannel,
|
||||||
|
setShowEdit,
|
||||||
|
setShowEditTag,
|
||||||
|
setEditingTag,
|
||||||
|
copySelectedChannel,
|
||||||
|
refresh,
|
||||||
|
activePage,
|
||||||
|
channels,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Filter columns based on visibility settings
|
||||||
|
const getVisibleColumns = () => {
|
||||||
|
return allColumns.filter((column) => visibleColumns[column.key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const visibleColumnsList = useMemo(() => {
|
||||||
|
return getVisibleColumns();
|
||||||
|
}, [visibleColumns, allColumns]);
|
||||||
|
|
||||||
|
const tableColumns = useMemo(() => {
|
||||||
|
return compactMode
|
||||||
|
? visibleColumnsList.map(({ fixed, ...rest }) => rest)
|
||||||
|
: visibleColumnsList;
|
||||||
|
}, [compactMode, visibleColumnsList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardTable
|
||||||
|
columns={tableColumns}
|
||||||
|
dataSource={channels}
|
||||||
|
scroll={compactMode ? undefined : { x: 'max-content' }}
|
||||||
|
pagination={{
|
||||||
|
currentPage: activePage,
|
||||||
|
pageSize: pageSize,
|
||||||
|
total: channelCount,
|
||||||
|
pageSizeOpts: [10, 20, 50, 100],
|
||||||
|
showSizeChanger: true,
|
||||||
|
onPageSizeChange: handlePageSizeChange,
|
||||||
|
onPageChange: handlePageChange,
|
||||||
|
}}
|
||||||
|
hidePagination={true}
|
||||||
|
expandAllRows={false}
|
||||||
|
onRow={handleRow}
|
||||||
|
rowSelection={
|
||||||
|
enableBatchDelete
|
||||||
|
? {
|
||||||
|
onChange: (selectedRowKeys, selectedRows) => {
|
||||||
|
setSelectedChannels(selectedRows);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
empty={
|
||||||
|
<Empty
|
||||||
|
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||||
|
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
||||||
|
description={t('搜索无结果')}
|
||||||
|
style={{ padding: 30 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className="rounded-xl overflow-hidden"
|
||||||
|
size="middle"
|
||||||
|
loading={loading || searching}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChannelsTable;
|
||||||
89
web/src/components/table/channels/ChannelsTabs.jsx
Normal file
89
web/src/components/table/channels/ChannelsTabs.jsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Tabs, TabPane, Tag } from '@douyinfe/semi-ui';
|
||||||
|
import { CHANNEL_OPTIONS } from '../../../constants/index.js';
|
||||||
|
import { getChannelIcon } from '../../../helpers/index.js';
|
||||||
|
|
||||||
|
const ChannelsTabs = ({
|
||||||
|
enableTagMode,
|
||||||
|
activeTypeKey,
|
||||||
|
setActiveTypeKey,
|
||||||
|
channelTypeCounts,
|
||||||
|
availableTypeKeys,
|
||||||
|
loadChannels,
|
||||||
|
activePage,
|
||||||
|
pageSize,
|
||||||
|
idSort,
|
||||||
|
setActivePage,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
|
if (enableTagMode) return null;
|
||||||
|
|
||||||
|
const handleTabChange = (key) => {
|
||||||
|
setActiveTypeKey(key);
|
||||||
|
setActivePage(1);
|
||||||
|
loadChannels(1, pageSize, idSort, enableTagMode, key);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTypeKey}
|
||||||
|
type="card"
|
||||||
|
collapsible
|
||||||
|
onChange={handleTabChange}
|
||||||
|
className="mb-2"
|
||||||
|
>
|
||||||
|
<TabPane
|
||||||
|
itemKey="all"
|
||||||
|
tab={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{t('全部')}
|
||||||
|
<Tag color={activeTypeKey === 'all' ? 'red' : 'grey'} shape='circle'>
|
||||||
|
{channelTypeCounts['all'] || 0}
|
||||||
|
</Tag>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{CHANNEL_OPTIONS.filter((opt) => availableTypeKeys.includes(String(opt.value))).map((option) => {
|
||||||
|
const key = String(option.value);
|
||||||
|
const count = channelTypeCounts[option.value] || 0;
|
||||||
|
return (
|
||||||
|
<TabPane
|
||||||
|
key={key}
|
||||||
|
itemKey={key}
|
||||||
|
tab={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{getChannelIcon(option.value)}
|
||||||
|
{option.label}
|
||||||
|
<Tag color={activeTypeKey === key ? 'red' : 'grey'} shape='circle'>
|
||||||
|
{count}
|
||||||
|
</Tag>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChannelsTabs;
|
||||||
80
web/src/components/table/channels/index.jsx
Normal file
80
web/src/components/table/channels/index.jsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import CardPro from '../../common/ui/CardPro.js';
|
||||||
|
import ChannelsTable from './ChannelsTable.jsx';
|
||||||
|
import ChannelsActions from './ChannelsActions.jsx';
|
||||||
|
import ChannelsFilters from './ChannelsFilters.jsx';
|
||||||
|
import ChannelsTabs from './ChannelsTabs.jsx';
|
||||||
|
import { useChannelsData } from '../../../hooks/channels/useChannelsData.js';
|
||||||
|
import { useIsMobile } from '../../../hooks/common/useIsMobile.js';
|
||||||
|
import BatchTagModal from './modals/BatchTagModal.jsx';
|
||||||
|
import ModelTestModal from './modals/ModelTestModal.jsx';
|
||||||
|
import ColumnSelectorModal from './modals/ColumnSelectorModal.jsx';
|
||||||
|
import EditChannelModal from './modals/EditChannelModal.jsx';
|
||||||
|
import EditTagModal from './modals/EditTagModal.jsx';
|
||||||
|
import { createCardProPagination } from '../../../helpers/utils';
|
||||||
|
|
||||||
|
const ChannelsPage = () => {
|
||||||
|
const channelsData = useChannelsData();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Modals */}
|
||||||
|
<ColumnSelectorModal {...channelsData} />
|
||||||
|
<EditTagModal
|
||||||
|
visible={channelsData.showEditTag}
|
||||||
|
tag={channelsData.editingTag}
|
||||||
|
handleClose={() => channelsData.setShowEditTag(false)}
|
||||||
|
refresh={channelsData.refresh}
|
||||||
|
/>
|
||||||
|
<EditChannelModal
|
||||||
|
refresh={channelsData.refresh}
|
||||||
|
visible={channelsData.showEdit}
|
||||||
|
handleClose={channelsData.closeEdit}
|
||||||
|
editingChannel={channelsData.editingChannel}
|
||||||
|
/>
|
||||||
|
<BatchTagModal {...channelsData} />
|
||||||
|
<ModelTestModal {...channelsData} />
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<CardPro
|
||||||
|
type="type3"
|
||||||
|
tabsArea={<ChannelsTabs {...channelsData} />}
|
||||||
|
actionsArea={<ChannelsActions {...channelsData} />}
|
||||||
|
searchArea={<ChannelsFilters {...channelsData} />}
|
||||||
|
paginationArea={createCardProPagination({
|
||||||
|
currentPage: channelsData.activePage,
|
||||||
|
pageSize: channelsData.pageSize,
|
||||||
|
total: channelsData.channelCount,
|
||||||
|
onPageChange: channelsData.handlePageChange,
|
||||||
|
onPageSizeChange: channelsData.handlePageSizeChange,
|
||||||
|
isMobile: isMobile,
|
||||||
|
})}
|
||||||
|
t={channelsData.t}
|
||||||
|
>
|
||||||
|
<ChannelsTable {...channelsData} />
|
||||||
|
</CardPro>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChannelsPage;
|
||||||
60
web/src/components/table/channels/modals/BatchTagModal.jsx
Normal file
60
web/src/components/table/channels/modals/BatchTagModal.jsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Modal, Input, Typography } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
const BatchTagModal = ({
|
||||||
|
showBatchSetTag,
|
||||||
|
setShowBatchSetTag,
|
||||||
|
batchSetChannelTag,
|
||||||
|
batchSetTagValue,
|
||||||
|
setBatchSetTagValue,
|
||||||
|
selectedChannels,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('批量设置标签')}
|
||||||
|
visible={showBatchSetTag}
|
||||||
|
onOk={batchSetChannelTag}
|
||||||
|
onCancel={() => setShowBatchSetTag(false)}
|
||||||
|
maskClosable={false}
|
||||||
|
centered={true}
|
||||||
|
size="small"
|
||||||
|
className="!rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="mb-5">
|
||||||
|
<Typography.Text>{t('请输入要设置的标签名称')}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder={t('请输入标签名称')}
|
||||||
|
value={batchSetTagValue}
|
||||||
|
onChange={(v) => setBatchSetTagValue(v)}
|
||||||
|
/>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Typography.Text type='secondary'>
|
||||||
|
{t('已选择 ${count} 个渠道').replace('${count}', selectedChannels.length)}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BatchTagModal;
|
||||||
133
web/src/components/table/channels/modals/ColumnSelectorModal.jsx
Normal file
133
web/src/components/table/channels/modals/ColumnSelectorModal.jsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Modal, Button, Checkbox } from '@douyinfe/semi-ui';
|
||||||
|
import { getChannelsColumns } from '../ChannelsColumnDefs.js';
|
||||||
|
|
||||||
|
const ColumnSelectorModal = ({
|
||||||
|
showColumnSelector,
|
||||||
|
setShowColumnSelector,
|
||||||
|
visibleColumns,
|
||||||
|
handleColumnVisibilityChange,
|
||||||
|
handleSelectAll,
|
||||||
|
initDefaultColumns,
|
||||||
|
COLUMN_KEYS,
|
||||||
|
t,
|
||||||
|
// Props needed for getChannelsColumns
|
||||||
|
updateChannelBalance,
|
||||||
|
manageChannel,
|
||||||
|
manageTag,
|
||||||
|
submitTagEdit,
|
||||||
|
testChannel,
|
||||||
|
setCurrentTestChannel,
|
||||||
|
setShowModelTestModal,
|
||||||
|
setEditingChannel,
|
||||||
|
setShowEdit,
|
||||||
|
setShowEditTag,
|
||||||
|
setEditingTag,
|
||||||
|
copySelectedChannel,
|
||||||
|
refresh,
|
||||||
|
activePage,
|
||||||
|
channels,
|
||||||
|
}) => {
|
||||||
|
// Get all columns for display in selector
|
||||||
|
const allColumns = getChannelsColumns({
|
||||||
|
t,
|
||||||
|
COLUMN_KEYS,
|
||||||
|
updateChannelBalance,
|
||||||
|
manageChannel,
|
||||||
|
manageTag,
|
||||||
|
submitTagEdit,
|
||||||
|
testChannel,
|
||||||
|
setCurrentTestChannel,
|
||||||
|
setShowModelTestModal,
|
||||||
|
setEditingChannel,
|
||||||
|
setShowEdit,
|
||||||
|
setShowEditTag,
|
||||||
|
setEditingTag,
|
||||||
|
copySelectedChannel,
|
||||||
|
refresh,
|
||||||
|
activePage,
|
||||||
|
channels,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('列设置')}
|
||||||
|
visible={showColumnSelector}
|
||||||
|
onCancel={() => setShowColumnSelector(false)}
|
||||||
|
footer={
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={() => initDefaultColumns()}>
|
||||||
|
{t('重置')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setShowColumnSelector(false)}>
|
||||||
|
{t('取消')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setShowColumnSelector(false)}>
|
||||||
|
{t('确定')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 20 }}>
|
||||||
|
<Checkbox
|
||||||
|
checked={Object.values(visibleColumns).every((v) => v === true)}
|
||||||
|
indeterminate={
|
||||||
|
Object.values(visibleColumns).some((v) => v === true) &&
|
||||||
|
!Object.values(visibleColumns).every((v) => v === true)
|
||||||
|
}
|
||||||
|
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||||
|
>
|
||||||
|
{t('全选')}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4"
|
||||||
|
style={{ border: '1px solid var(--semi-color-border)' }}
|
||||||
|
>
|
||||||
|
{allColumns.map((column) => {
|
||||||
|
// Skip columns without title
|
||||||
|
if (!column.title) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={column.key}
|
||||||
|
className="w-1/2 mb-4 pr-2"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={!!visibleColumns[column.key]}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleColumnVisibilityChange(column.key, e.target.checked)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{column.title}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColumnSelectorModal;
|
||||||
@@ -1,5 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
API,
|
API,
|
||||||
@@ -7,9 +25,9 @@ import {
|
|||||||
showInfo,
|
showInfo,
|
||||||
showSuccess,
|
showSuccess,
|
||||||
verifyJSON,
|
verifyJSON,
|
||||||
} from '../../helpers';
|
} from '../../../../helpers';
|
||||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
import { useIsMobile } from '../../../../hooks/common/useIsMobile.js';
|
||||||
import { CHANNEL_OPTIONS } from '../../constants';
|
import { CHANNEL_OPTIONS } from '../../../../constants';
|
||||||
import {
|
import {
|
||||||
SideSheet,
|
SideSheet,
|
||||||
Space,
|
Space,
|
||||||
@@ -28,7 +46,7 @@ import {
|
|||||||
Col,
|
Col,
|
||||||
Highlight,
|
Highlight,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { getChannelModels, copy, getChannelIcon, getModelCategories } from '../../helpers';
|
import { getChannelModels, copy, getChannelIcon, getModelCategories, modelSelectFilter } from '../../../../helpers';
|
||||||
import {
|
import {
|
||||||
IconSave,
|
IconSave,
|
||||||
IconClose,
|
IconClose,
|
||||||
@@ -68,7 +86,7 @@ function type2secretPrompt(type) {
|
|||||||
case 33:
|
case 33:
|
||||||
return '按照如下格式输入:Ak|Sk|Region';
|
return '按照如下格式输入:Ak|Sk|Region';
|
||||||
case 50:
|
case 50:
|
||||||
return '按照如下格式输入: AccessKey|SecretKey';
|
return '按照如下格式输入: AccessKey|SecretKey, 如果上游是New API,则直接输ApiKey';
|
||||||
case 51:
|
case 51:
|
||||||
return '按照如下格式输入: Access Key ID|Secret Access Key';
|
return '按照如下格式输入: Access Key ID|Secret Access Key';
|
||||||
default:
|
default:
|
||||||
@@ -76,9 +94,8 @@ function type2secretPrompt(type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditChannel = (props) => {
|
const EditChannelModal = (props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
|
||||||
const channelId = props.editingChannel.id;
|
const channelId = props.editingChannel.id;
|
||||||
const isEdit = channelId !== undefined;
|
const isEdit = channelId !== undefined;
|
||||||
const [loading, setLoading] = useState(isEdit);
|
const [loading, setLoading] = useState(isEdit);
|
||||||
@@ -760,7 +777,7 @@ const EditChannel = (props) => {
|
|||||||
{selected && (
|
{selected && (
|
||||||
<div className="flex-shrink-0 text-blue-600">
|
<div className="flex-shrink-0 text-blue-600">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
|
<path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -836,7 +853,8 @@ const EditChannel = (props) => {
|
|||||||
rules={[{ required: true, message: t('请选择渠道类型') }]}
|
rules={[{ required: true, message: t('请选择渠道类型') }]}
|
||||||
optionList={channelOptionList}
|
optionList={channelOptionList}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
filter
|
filter={modelSelectFilter}
|
||||||
|
autoClearSearchValue={false}
|
||||||
searchPosition='dropdown'
|
searchPosition='dropdown'
|
||||||
onSearch={(value) => setChannelSearchValue(value)}
|
onSearch={(value) => setChannelSearchValue(value)}
|
||||||
renderOptionItem={renderChannelOption}
|
renderOptionItem={renderChannelOption}
|
||||||
@@ -1234,7 +1252,8 @@ const EditChannel = (props) => {
|
|||||||
placeholder={t('请选择该渠道所支持的模型')}
|
placeholder={t('请选择该渠道所支持的模型')}
|
||||||
rules={[{ required: true, message: t('请选择模型') }]}
|
rules={[{ required: true, message: t('请选择模型') }]}
|
||||||
multiple
|
multiple
|
||||||
filter
|
filter={modelSelectFilter}
|
||||||
|
autoClearSearchValue={false}
|
||||||
searchPosition='dropdown'
|
searchPosition='dropdown'
|
||||||
optionList={modelOptions}
|
optionList={modelOptions}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
@@ -1466,4 +1485,4 @@ const EditChannel = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditChannel;
|
export default EditChannelModal;
|
||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
API,
|
API,
|
||||||
@@ -6,7 +25,8 @@ import {
|
|||||||
showSuccess,
|
showSuccess,
|
||||||
showWarning,
|
showWarning,
|
||||||
verifyJSON,
|
verifyJSON,
|
||||||
} from '../../helpers';
|
modelSelectFilter,
|
||||||
|
} from '../../../../helpers';
|
||||||
import {
|
import {
|
||||||
SideSheet,
|
SideSheet,
|
||||||
Space,
|
Space,
|
||||||
@@ -26,7 +46,7 @@ import {
|
|||||||
IconUser,
|
IconUser,
|
||||||
IconCode,
|
IconCode,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { getChannelModels } from '../../helpers';
|
import { getChannelModels } from '../../../../helpers';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
@@ -375,7 +395,8 @@ const EditTagModal = (props) => {
|
|||||||
label={t('模型')}
|
label={t('模型')}
|
||||||
placeholder={t('请选择该渠道所支持的模型,留空则不更改')}
|
placeholder={t('请选择该渠道所支持的模型,留空则不更改')}
|
||||||
multiple
|
multiple
|
||||||
filter
|
filter={modelSelectFilter}
|
||||||
|
autoClearSearchValue={false}
|
||||||
searchPosition='dropdown'
|
searchPosition='dropdown'
|
||||||
optionList={modelOptions}
|
optionList={modelOptions}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
276
web/src/components/table/channels/modals/ModelTestModal.jsx
Normal file
276
web/src/components/table/channels/modals/ModelTestModal.jsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Typography
|
||||||
|
} from '@douyinfe/semi-ui';
|
||||||
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
|
import { copy, showError, showInfo, showSuccess } from '../../../../helpers/index.js';
|
||||||
|
import { MODEL_TABLE_PAGE_SIZE } from '../../../../constants/index.js';
|
||||||
|
|
||||||
|
const ModelTestModal = ({
|
||||||
|
showModelTestModal,
|
||||||
|
currentTestChannel,
|
||||||
|
handleCloseModal,
|
||||||
|
isBatchTesting,
|
||||||
|
batchTestModels,
|
||||||
|
modelSearchKeyword,
|
||||||
|
setModelSearchKeyword,
|
||||||
|
selectedModelKeys,
|
||||||
|
setSelectedModelKeys,
|
||||||
|
modelTestResults,
|
||||||
|
testingModels,
|
||||||
|
testChannel,
|
||||||
|
modelTablePage,
|
||||||
|
setModelTablePage,
|
||||||
|
allSelectingRef,
|
||||||
|
isMobile,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
|
const hasChannel = Boolean(currentTestChannel);
|
||||||
|
|
||||||
|
const filteredModels = hasChannel
|
||||||
|
? currentTestChannel.models
|
||||||
|
.split(',')
|
||||||
|
.filter((model) =>
|
||||||
|
model.toLowerCase().includes(modelSearchKeyword.toLowerCase())
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const handleCopySelected = () => {
|
||||||
|
if (selectedModelKeys.length === 0) {
|
||||||
|
showError(t('请先选择模型!'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
copy(selectedModelKeys.join(',')).then((ok) => {
|
||||||
|
if (ok) {
|
||||||
|
showSuccess(t('已复制 ${count} 个模型').replace('${count}', selectedModelKeys.length));
|
||||||
|
} else {
|
||||||
|
showError(t('复制失败,请手动复制'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectSuccess = () => {
|
||||||
|
if (!currentTestChannel) return;
|
||||||
|
const successKeys = currentTestChannel.models
|
||||||
|
.split(',')
|
||||||
|
.filter((m) => m.toLowerCase().includes(modelSearchKeyword.toLowerCase()))
|
||||||
|
.filter((m) => {
|
||||||
|
const result = modelTestResults[`${currentTestChannel.id}-${m}`];
|
||||||
|
return result && result.success;
|
||||||
|
});
|
||||||
|
if (successKeys.length === 0) {
|
||||||
|
showInfo(t('暂无成功模型'));
|
||||||
|
}
|
||||||
|
setSelectedModelKeys(successKeys);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t('模型名称'),
|
||||||
|
dataIndex: 'model',
|
||||||
|
render: (text) => (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Typography.Text strong>{text}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('状态'),
|
||||||
|
dataIndex: 'status',
|
||||||
|
render: (text, record) => {
|
||||||
|
const testResult = modelTestResults[`${currentTestChannel.id}-${record.model}`];
|
||||||
|
const isTesting = testingModels.has(record.model);
|
||||||
|
|
||||||
|
if (isTesting) {
|
||||||
|
return (
|
||||||
|
<Tag color='blue' shape='circle'>
|
||||||
|
{t('测试中')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!testResult) {
|
||||||
|
return (
|
||||||
|
<Tag color='grey' shape='circle'>
|
||||||
|
{t('未开始')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Tag
|
||||||
|
color={testResult.success ? 'green' : 'red'}
|
||||||
|
shape='circle'
|
||||||
|
>
|
||||||
|
{testResult.success ? t('成功') : t('失败')}
|
||||||
|
</Tag>
|
||||||
|
{testResult.success && (
|
||||||
|
<Typography.Text type="tertiary">
|
||||||
|
{t('请求时长: ${time}s').replace('${time}', testResult.time.toFixed(2))}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
dataIndex: 'operate',
|
||||||
|
render: (text, record) => {
|
||||||
|
const isTesting = testingModels.has(record.model);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
onClick={() => testChannel(currentTestChannel, record.model)}
|
||||||
|
loading={isTesting}
|
||||||
|
size='small'
|
||||||
|
>
|
||||||
|
{t('测试')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const dataSource = (() => {
|
||||||
|
if (!hasChannel) return [];
|
||||||
|
const start = (modelTablePage - 1) * MODEL_TABLE_PAGE_SIZE;
|
||||||
|
const end = start + MODEL_TABLE_PAGE_SIZE;
|
||||||
|
return filteredModels.slice(start, end).map((model) => ({
|
||||||
|
model,
|
||||||
|
key: model,
|
||||||
|
}));
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={hasChannel ? (
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Typography.Text strong className="!text-[var(--semi-color-text-0)] !text-base">
|
||||||
|
{currentTestChannel.name} {t('渠道的模型测试')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="tertiary" className="!text-xs flex items-center">
|
||||||
|
{t('共')} {currentTestChannel.models.split(',').length} {t('个模型')}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
visible={showModelTestModal}
|
||||||
|
onCancel={handleCloseModal}
|
||||||
|
footer={hasChannel ? (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
{isBatchTesting ? (
|
||||||
|
<Button
|
||||||
|
type='danger'
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
>
|
||||||
|
{t('停止测试')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
>
|
||||||
|
{t('取消')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={batchTestModels}
|
||||||
|
loading={isBatchTesting}
|
||||||
|
disabled={isBatchTesting}
|
||||||
|
>
|
||||||
|
{isBatchTesting ? t('测试中...') : t('批量测试${count}个模型').replace(
|
||||||
|
'${count}',
|
||||||
|
filteredModels.length
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
maskClosable={!isBatchTesting}
|
||||||
|
className="!rounded-lg"
|
||||||
|
size={isMobile ? 'full-width' : 'large'}
|
||||||
|
>
|
||||||
|
{hasChannel && (<div className="model-test-scroll">
|
||||||
|
{/* 搜索与操作按钮 */}
|
||||||
|
<div className="flex items-center justify-end gap-2 w-full mb-2">
|
||||||
|
<Input
|
||||||
|
placeholder={t('搜索模型...')}
|
||||||
|
value={modelSearchKeyword}
|
||||||
|
onChange={(v) => {
|
||||||
|
setModelSearchKeyword(v);
|
||||||
|
setModelTablePage(1);
|
||||||
|
}}
|
||||||
|
className="!w-full"
|
||||||
|
prefix={<IconSearch />}
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onClick={handleCopySelected}>
|
||||||
|
{t('复制已选')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
onClick={handleSelectSuccess}
|
||||||
|
>
|
||||||
|
{t('选择成功')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={dataSource}
|
||||||
|
rowSelection={{
|
||||||
|
selectedRowKeys: selectedModelKeys,
|
||||||
|
onChange: (keys) => {
|
||||||
|
if (allSelectingRef.current) {
|
||||||
|
allSelectingRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedModelKeys(keys);
|
||||||
|
},
|
||||||
|
onSelectAll: (checked) => {
|
||||||
|
allSelectingRef.current = true;
|
||||||
|
setSelectedModelKeys(checked ? filteredModels : []);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
pagination={{
|
||||||
|
currentPage: modelTablePage,
|
||||||
|
pageSize: MODEL_TABLE_PAGE_SIZE,
|
||||||
|
total: filteredModels.length,
|
||||||
|
showSizeChanger: false,
|
||||||
|
onPageChange: (page) => setModelTablePage(page),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModelTestModal;
|
||||||
64
web/src/components/table/mj-logs/MjLogsActions.jsx
Normal file
64
web/src/components/table/mj-logs/MjLogsActions.jsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Skeleton, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import { IconEyeOpened } from '@douyinfe/semi-icons';
|
||||||
|
import CompactModeToggle from '../../common/ui/CompactModeToggle';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const MjLogsActions = ({
|
||||||
|
loading,
|
||||||
|
showBanner,
|
||||||
|
isAdminUser,
|
||||||
|
compactMode,
|
||||||
|
setCompactMode,
|
||||||
|
t,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
|
||||||
|
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
|
||||||
|
<IconEyeOpened className="mr-2" />
|
||||||
|
{loading ? (
|
||||||
|
<Skeleton.Title
|
||||||
|
style={{
|
||||||
|
width: 300,
|
||||||
|
marginBottom: 0,
|
||||||
|
marginTop: 0
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text>
|
||||||
|
{isAdminUser && showBanner
|
||||||
|
? t('当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。')
|
||||||
|
: t('Midjourney 任务记录')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<CompactModeToggle
|
||||||
|
compactMode={compactMode}
|
||||||
|
setCompactMode={setCompactMode}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MjLogsActions;
|
||||||
496
web/src/components/table/mj-logs/MjLogsColumnDefs.js
Normal file
496
web/src/components/table/mj-logs/MjLogsColumnDefs.js
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Progress,
|
||||||
|
Tag,
|
||||||
|
Typography
|
||||||
|
} from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
Palette,
|
||||||
|
ZoomIn,
|
||||||
|
Shuffle,
|
||||||
|
Move,
|
||||||
|
FileText,
|
||||||
|
Blend,
|
||||||
|
Upload,
|
||||||
|
Minimize2,
|
||||||
|
RotateCcw,
|
||||||
|
PaintBucket,
|
||||||
|
Focus,
|
||||||
|
Move3D,
|
||||||
|
Monitor,
|
||||||
|
UserCheck,
|
||||||
|
HelpCircle,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
Copy,
|
||||||
|
FileX,
|
||||||
|
Pause,
|
||||||
|
XCircle,
|
||||||
|
Loader,
|
||||||
|
AlertCircle,
|
||||||
|
Hash,
|
||||||
|
Video
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
'amber',
|
||||||
|
'blue',
|
||||||
|
'cyan',
|
||||||
|
'green',
|
||||||
|
'grey',
|
||||||
|
'indigo',
|
||||||
|
'light-blue',
|
||||||
|
'lime',
|
||||||
|
'orange',
|
||||||
|
'pink',
|
||||||
|
'purple',
|
||||||
|
'red',
|
||||||
|
'teal',
|
||||||
|
'violet',
|
||||||
|
'yellow',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Render functions
|
||||||
|
function renderType(type, t) {
|
||||||
|
switch (type) {
|
||||||
|
case 'IMAGINE':
|
||||||
|
return (
|
||||||
|
<Tag color='blue' shape='circle' prefixIcon={<Palette size={14} />}>
|
||||||
|
{t('绘图')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'UPSCALE':
|
||||||
|
return (
|
||||||
|
<Tag color='orange' shape='circle' prefixIcon={<ZoomIn size={14} />}>
|
||||||
|
{t('放大')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'VIDEO':
|
||||||
|
return (
|
||||||
|
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
|
||||||
|
{t('视频')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'EDITS':
|
||||||
|
return (
|
||||||
|
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
|
||||||
|
{t('编辑')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'VARIATION':
|
||||||
|
return (
|
||||||
|
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
|
||||||
|
{t('变换')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'HIGH_VARIATION':
|
||||||
|
return (
|
||||||
|
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
|
||||||
|
{t('强变换')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'LOW_VARIATION':
|
||||||
|
return (
|
||||||
|
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
|
||||||
|
{t('弱变换')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'PAN':
|
||||||
|
return (
|
||||||
|
<Tag color='cyan' shape='circle' prefixIcon={<Move size={14} />}>
|
||||||
|
{t('平移')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'DESCRIBE':
|
||||||
|
return (
|
||||||
|
<Tag color='yellow' shape='circle' prefixIcon={<FileText size={14} />}>
|
||||||
|
{t('图生文')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'BLEND':
|
||||||
|
return (
|
||||||
|
<Tag color='lime' shape='circle' prefixIcon={<Blend size={14} />}>
|
||||||
|
{t('图混合')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'UPLOAD':
|
||||||
|
return (
|
||||||
|
<Tag color='blue' shape='circle' prefixIcon={<Upload size={14} />}>
|
||||||
|
上传文件
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'SHORTEN':
|
||||||
|
return (
|
||||||
|
<Tag color='pink' shape='circle' prefixIcon={<Minimize2 size={14} />}>
|
||||||
|
{t('缩词')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'REROLL':
|
||||||
|
return (
|
||||||
|
<Tag color='indigo' shape='circle' prefixIcon={<RotateCcw size={14} />}>
|
||||||
|
{t('重绘')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'INPAINT':
|
||||||
|
return (
|
||||||
|
<Tag color='violet' shape='circle' prefixIcon={<PaintBucket size={14} />}>
|
||||||
|
{t('局部重绘-提交')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'ZOOM':
|
||||||
|
return (
|
||||||
|
<Tag color='teal' shape='circle' prefixIcon={<Focus size={14} />}>
|
||||||
|
{t('变焦')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'CUSTOM_ZOOM':
|
||||||
|
return (
|
||||||
|
<Tag color='teal' shape='circle' prefixIcon={<Move3D size={14} />}>
|
||||||
|
{t('自定义变焦-提交')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'MODAL':
|
||||||
|
return (
|
||||||
|
<Tag color='green' shape='circle' prefixIcon={<Monitor size={14} />}>
|
||||||
|
{t('窗口处理')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'SWAP_FACE':
|
||||||
|
return (
|
||||||
|
<Tag color='light-green' shape='circle' prefixIcon={<UserCheck size={14} />}>
|
||||||
|
{t('换脸')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||||
|
{t('未知')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCode(code, t) {
|
||||||
|
switch (code) {
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
||||||
|
{t('已提交')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 21:
|
||||||
|
return (
|
||||||
|
<Tag color='lime' shape='circle' prefixIcon={<Clock size={14} />}>
|
||||||
|
{t('等待中')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 22:
|
||||||
|
return (
|
||||||
|
<Tag color='orange' shape='circle' prefixIcon={<Copy size={14} />}>
|
||||||
|
{t('重复提交')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<Tag color='yellow' shape='circle' prefixIcon={<FileX size={14} />}>
|
||||||
|
{t('未提交')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||||
|
{t('未知')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStatus(type, t) {
|
||||||
|
switch (type) {
|
||||||
|
case 'SUCCESS':
|
||||||
|
return (
|
||||||
|
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
||||||
|
{t('成功')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'NOT_START':
|
||||||
|
return (
|
||||||
|
<Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
|
||||||
|
{t('未启动')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'SUBMITTED':
|
||||||
|
return (
|
||||||
|
<Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
|
||||||
|
{t('队列中')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'IN_PROGRESS':
|
||||||
|
return (
|
||||||
|
<Tag color='blue' shape='circle' prefixIcon={<Loader size={14} />}>
|
||||||
|
{t('执行中')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'FAILURE':
|
||||||
|
return (
|
||||||
|
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
|
||||||
|
{t('失败')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case 'MODAL':
|
||||||
|
return (
|
||||||
|
<Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
|
||||||
|
{t('窗口等待')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||||
|
{t('未知')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderTimestamp = (timestampInSeconds) => {
|
||||||
|
const date = new Date(timestampInSeconds * 1000);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = ('0' + (date.getMonth() + 1)).slice(-2);
|
||||||
|
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}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderDuration(submit_time, finishTime, t) {
|
||||||
|
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} shape='circle' prefixIcon={<Clock size={14} />}>
|
||||||
|
{durationSec} {t('秒')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMjLogsColumns = ({
|
||||||
|
t,
|
||||||
|
COLUMN_KEYS,
|
||||||
|
copyText,
|
||||||
|
openContentModal,
|
||||||
|
openImageModal,
|
||||||
|
isAdminUser,
|
||||||
|
}) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.SUBMIT_TIME,
|
||||||
|
title: t('提交时间'),
|
||||||
|
dataIndex: 'submit_time',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return <div>{renderTimestamp(text / 1000)}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.DURATION,
|
||||||
|
title: t('花费时间'),
|
||||||
|
dataIndex: 'finish_time',
|
||||||
|
render: (finish, record) => {
|
||||||
|
return renderDuration(record.submit_time, finish, t);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.CHANNEL,
|
||||||
|
title: t('渠道'),
|
||||||
|
dataIndex: 'channel_id',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return isAdminUser ? (
|
||||||
|
<div>
|
||||||
|
<Tag
|
||||||
|
color={colors[parseInt(text) % colors.length]}
|
||||||
|
shape='circle'
|
||||||
|
prefixIcon={<Hash size={14} />}
|
||||||
|
onClick={() => {
|
||||||
|
copyText(text);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
{text}{' '}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.TYPE,
|
||||||
|
title: t('类型'),
|
||||||
|
dataIndex: 'action',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return <div>{renderType(text, t)}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.TASK_ID,
|
||||||
|
title: t('任务ID'),
|
||||||
|
dataIndex: 'mj_id',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return <div>{text}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.SUBMIT_RESULT,
|
||||||
|
title: t('提交结果'),
|
||||||
|
dataIndex: 'code',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return isAdminUser ? <div>{renderCode(text, t)}</div> : <></>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.TASK_STATUS,
|
||||||
|
title: t('任务状态'),
|
||||||
|
dataIndex: 'status',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return <div>{renderStatus(text, t)}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.PROGRESS,
|
||||||
|
title: t('进度'),
|
||||||
|
dataIndex: 'progress',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
<Progress
|
||||||
|
stroke={
|
||||||
|
record.status === 'FAILURE'
|
||||||
|
? 'var(--semi-color-warning)'
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
percent={text ? parseInt(text.replace('%', '')) : 0}
|
||||||
|
showInfo={true}
|
||||||
|
aria-label='drawing progress'
|
||||||
|
style={{ minWidth: '160px' }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.IMAGE,
|
||||||
|
title: t('结果图片'),
|
||||||
|
dataIndex: 'image_url',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
if (!text) {
|
||||||
|
return t('无');
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
openImageModal(text);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('查看图片')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.PROMPT,
|
||||||
|
title: 'Prompt',
|
||||||
|
dataIndex: 'prompt',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
if (!text) {
|
||||||
|
return t('无');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography.Text
|
||||||
|
ellipsis={{ showTooltip: true }}
|
||||||
|
style={{ width: 100 }}
|
||||||
|
onClick={() => {
|
||||||
|
openContentModal(text);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Typography.Text>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.PROMPT_EN,
|
||||||
|
title: 'PromptEn',
|
||||||
|
dataIndex: 'prompt_en',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
if (!text) {
|
||||||
|
return t('无');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography.Text
|
||||||
|
ellipsis={{ showTooltip: true }}
|
||||||
|
style={{ width: 100 }}
|
||||||
|
onClick={() => {
|
||||||
|
openContentModal(text);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Typography.Text>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: COLUMN_KEYS.FAIL_REASON,
|
||||||
|
title: t('失败原因'),
|
||||||
|
dataIndex: 'fail_reason',
|
||||||
|
fixed: 'right',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
if (!text) {
|
||||||
|
return t('无');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography.Text
|
||||||
|
ellipsis={{ showTooltip: true }}
|
||||||
|
style={{ width: 100 }}
|
||||||
|
onClick={() => {
|
||||||
|
openContentModal(text);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Typography.Text>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
123
web/src/components/table/mj-logs/MjLogsFilters.jsx
Normal file
123
web/src/components/table/mj-logs/MjLogsFilters.jsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Button, Form } from '@douyinfe/semi-ui';
|
||||||
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
const MjLogsFilters = ({
|
||||||
|
formInitValues,
|
||||||
|
setFormApi,
|
||||||
|
refresh,
|
||||||
|
setShowColumnSelector,
|
||||||
|
formApi,
|
||||||
|
loading,
|
||||||
|
isAdminUser,
|
||||||
|
t,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
initValues={formInitValues}
|
||||||
|
getFormApi={(api) => setFormApi(api)}
|
||||||
|
onSubmit={refresh}
|
||||||
|
allowEmpty={true}
|
||||||
|
autoComplete="off"
|
||||||
|
layout="vertical"
|
||||||
|
trigger="change"
|
||||||
|
stopValidateWithError={false}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2">
|
||||||
|
{/* 时间选择器 */}
|
||||||
|
<div className="col-span-1 lg:col-span-2">
|
||||||
|
<Form.DatePicker
|
||||||
|
field='dateRange'
|
||||||
|
className="w-full"
|
||||||
|
type='dateTimeRange'
|
||||||
|
placeholder={[t('开始时间'), t('结束时间')]}
|
||||||
|
showClear
|
||||||
|
pure
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 任务 ID */}
|
||||||
|
<Form.Input
|
||||||
|
field='mj_id'
|
||||||
|
prefix={<IconSearch />}
|
||||||
|
placeholder={t('任务 ID')}
|
||||||
|
showClear
|
||||||
|
pure
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 渠道 ID - 仅管理员可见 */}
|
||||||
|
{isAdminUser && (
|
||||||
|
<Form.Input
|
||||||
|
field='channel_id'
|
||||||
|
prefix={<IconSearch />}
|
||||||
|
placeholder={t('渠道 ID')}
|
||||||
|
showClear
|
||||||
|
pure
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮区域 */}
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div></div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
htmlType='submit'
|
||||||
|
loading={loading}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t('查询')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
onClick={() => {
|
||||||
|
if (formApi) {
|
||||||
|
formApi.reset();
|
||||||
|
setTimeout(() => {
|
||||||
|
refresh();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t('重置')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
onClick={() => setShowColumnSelector(true)}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t('列设置')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MjLogsFilters;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user