Compare commits
10 Commits
44356571a6
...
f55309e953
| Author | SHA1 | Date |
|---|---|---|
|
|
f55309e953 | |
|
|
6836012ab4 | |
|
|
c8d76437cb | |
|
|
04c4e17dcc | |
|
|
42070a5300 | |
|
|
01900f1a3a | |
|
|
006e723ac5 | |
|
|
6a210b9667 | |
|
|
001ed4c17f | |
|
|
ac1120fc33 |
|
|
@ -1348,7 +1348,7 @@ func AddChannel(c *gin.Context) {
|
||||||
if addChannelRequest.Channel != nil {
|
if addChannelRequest.Channel != nil {
|
||||||
channelName = addChannelRequest.Channel.Name
|
channelName = addChannelRequest.Channel.Name
|
||||||
}
|
}
|
||||||
service.RecordCreateOperation(c, "channel", 0, channelName, "创建渠道: "+channelName, "")
|
service.RecordCreateOperation(c, "channel", 0, channelName, "创建渠道:"+channelName, "")
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
|
|
@ -1366,7 +1366,7 @@ func DeleteChannel(c *gin.Context) {
|
||||||
}
|
}
|
||||||
model.InitChannelCache()
|
model.InitChannelCache()
|
||||||
// 记录操作日志
|
// 记录操作日志
|
||||||
service.RecordDeleteOperation(c, "channel", id, "", fmt.Sprintf("删除渠道 (ID: %d)", id), "")
|
service.RecordDeleteOperation(c, "channel", id, "", fmt.Sprintf("删除渠道 (ID:%d)", id), "")
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
|
|
@ -1721,7 +1721,7 @@ func UpdateChannel(c *gin.Context) {
|
||||||
channel.Key = ""
|
channel.Key = ""
|
||||||
clearChannelInfo(&channel.Channel)
|
clearChannelInfo(&channel.Channel)
|
||||||
// 记录操作日志
|
// 记录操作日志
|
||||||
service.RecordUpdateOperation(c, "channel", channel.Id, channel.Name, "更新渠道: "+channel.Name, "")
|
service.RecordUpdateOperation(c, "channel", channel.Id, channel.Name, "更新渠道:"+channel.Name, "")
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@ func CreateModelMeta(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
model.RefreshPricing()
|
model.RefreshPricing()
|
||||||
service.RecordCreateOperation(c, "model", m.Id, m.ModelName, "创建模型: "+m.ModelName, "")
|
service.RecordCreateOperation(c, "model", m.Id, m.ModelName, "创建模型:"+m.ModelName, "")
|
||||||
common.ApiSuccess(c, &m)
|
common.ApiSuccess(c, &m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -319,7 +319,7 @@ func UpdateModelMeta(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
model.RefreshPricing()
|
model.RefreshPricing()
|
||||||
service.RecordUpdateOperation(c, "model", m.Id, m.ModelName, "更新模型: "+m.ModelName, "")
|
service.RecordUpdateOperation(c, "model", m.Id, m.ModelName, "更新模型:"+m.ModelName, "")
|
||||||
common.ApiSuccess(c, &m)
|
common.ApiSuccess(c, &m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -336,7 +336,7 @@ func DeleteModelMeta(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
model.RefreshPricing()
|
model.RefreshPricing()
|
||||||
service.RecordDeleteOperation(c, "model", id, "", fmt.Sprintf("删除模型 (ID: %d)", id), "")
|
service.RecordDeleteOperation(c, "model", id, "", fmt.Sprintf("删除模型 (ID:%d)", id), "")
|
||||||
common.ApiSuccess(c, nil)
|
common.ApiSuccess(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -690,10 +690,10 @@ func UpdateOption(c *gin.Context) {
|
||||||
if modelPricingOptionKeys[option.Key] {
|
if modelPricingOptionKeys[option.Key] {
|
||||||
changes := computeModelPricingDiff(oldVal, valStr)
|
changes := computeModelPricingDiff(oldVal, valStr)
|
||||||
if len(changes) > 0 {
|
if len(changes) > 0 {
|
||||||
service.RecordUpdateWithDiff(c, "setting", 0, option.Key, "更新系统设置: "+option.Key, changes, "")
|
service.RecordUpdateWithDiff(c, "setting", 0, option.Key, "更新系统设置:"+option.Key, changes, "")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
service.RecordUpdateOperation(c, "setting", 0, option.Key, "更新系统设置: "+option.Key, "")
|
service.RecordUpdateOperation(c, "setting", 0, option.Key, "更新系统设置:"+option.Key, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ func AddRedemption(c *gin.Context) {
|
||||||
}
|
}
|
||||||
keys = append(keys, key)
|
keys = append(keys, key)
|
||||||
}
|
}
|
||||||
service.RecordCreateOperation(c, "redemption", 0, redemption.Name, "创建兑换码: "+redemption.Name, "")
|
service.RecordCreateOperation(c, "redemption", 0, redemption.Name, "创建兑换码:"+redemption.Name, "")
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
|
|
@ -122,7 +122,7 @@ func DeleteRedemption(c *gin.Context) {
|
||||||
common.ApiError(c, err)
|
common.ApiError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
service.RecordDeleteOperation(c, "redemption", id, "", fmt.Sprintf("删除兑换码 (ID: %d)", id), "")
|
service.RecordDeleteOperation(c, "redemption", id, "", fmt.Sprintf("删除兑换码 (ID:%d)", id), "")
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
|
|
@ -161,7 +161,7 @@ func UpdateRedemption(c *gin.Context) {
|
||||||
common.ApiError(c, err)
|
common.ApiError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
service.RecordUpdateOperation(c, "redemption", cleanRedemption.Id, cleanRedemption.Name, "更新兑换码: "+cleanRedemption.Name, "")
|
service.RecordUpdateOperation(c, "redemption", cleanRedemption.Id, cleanRedemption.Name, "更新兑换码:"+cleanRedemption.Name, "")
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ func AddToken(c *gin.Context) {
|
||||||
common.ApiError(c, err)
|
common.ApiError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
service.RecordCreateOperation(c, "token", cleanToken.Id, cleanToken.Name, "创建令牌: "+cleanToken.Name, "")
|
service.RecordCreateOperation(c, "token", cleanToken.Id, cleanToken.Name, "创建令牌:"+cleanToken.Name, "")
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
|
|
@ -243,7 +243,7 @@ func DeleteToken(c *gin.Context) {
|
||||||
common.ApiError(c, err)
|
common.ApiError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
service.RecordDeleteOperation(c, "token", id, "", fmt.Sprintf("删除令牌 (ID: %d)", id), "")
|
service.RecordDeleteOperation(c, "token", id, "", fmt.Sprintf("删除令牌 (ID:%d)", id), "")
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
|
|
@ -308,7 +308,7 @@ func UpdateToken(c *gin.Context) {
|
||||||
common.ApiError(c, err)
|
common.ApiError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
service.RecordUpdateOperation(c, "token", cleanToken.Id, cleanToken.Name, "更新令牌: "+cleanToken.Name, "")
|
service.RecordUpdateOperation(c, "token", cleanToken.Id, cleanToken.Name, "更新令牌:"+cleanToken.Name, "")
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
|
|
|
||||||
|
|
@ -1206,7 +1206,7 @@ func UpdateUser(c *gin.Context) {
|
||||||
if originUser.DistributorCommissionBps != updatedUser.DistributorCommissionBps {
|
if originUser.DistributorCommissionBps != updatedUser.DistributorCommissionBps {
|
||||||
changes = append(changes, service.FieldChange{Field: "分销佣金比例", OldValue: fmt.Sprintf("%d‱", originUser.DistributorCommissionBps), NewValue: fmt.Sprintf("%d‱", updatedUser.DistributorCommissionBps)})
|
changes = append(changes, service.FieldChange{Field: "分销佣金比例", OldValue: fmt.Sprintf("%d‱", originUser.DistributorCommissionBps), NewValue: fmt.Sprintf("%d‱", updatedUser.DistributorCommissionBps)})
|
||||||
}
|
}
|
||||||
service.RecordUpdateWithDiff(c, "user", updatedUser.Id, updatedUser.Username, "更新用户: "+updatedUser.Username, changes, "")
|
service.RecordUpdateWithDiff(c, "user", updatedUser.Id, updatedUser.Username, "更新用户:"+updatedUser.Username, changes, "")
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
|
|
@ -1246,7 +1246,7 @@ func AdminClearUserBinding(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
model.RecordLog(user.Id, model.LogTypeManage, fmt.Sprintf("admin cleared %s binding for user %s", bindingType, user.Username))
|
model.RecordLog(user.Id, model.LogTypeManage, fmt.Sprintf("admin cleared %s binding for user %s", bindingType, user.Username))
|
||||||
service.RecordDeleteOperation(c, "user_binding", user.Id, user.Username, "清除用户绑定["+bindingType+"]: "+user.Username, "")
|
service.RecordDeleteOperation(c, "user_binding", user.Id, user.Username, "清除用户绑定["+bindingType+"]:"+user.Username, "")
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
|
|
@ -1421,7 +1421,7 @@ func DeleteUser(c *gin.Context) {
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
service.RecordDeleteOperation(c, "user", id, originUser.Username, "删除用户: "+originUser.Username, "")
|
service.RecordDeleteOperation(c, "user", id, originUser.Username, "删除用户:"+originUser.Username, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteSelf(c *gin.Context) {
|
func DeleteSelf(c *gin.Context) {
|
||||||
|
|
@ -1501,7 +1501,7 @@ func CreateUser(c *gin.Context) {
|
||||||
common.ApiError(c, err)
|
common.ApiError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
service.RecordCreateOperation(c, "user", cleanUser.Id, cleanUser.Username, "创建用户: "+cleanUser.Username, "")
|
service.RecordCreateOperation(c, "user", cleanUser.Id, cleanUser.Username, "创建用户:"+cleanUser.Username, "")
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
|
|
@ -1781,7 +1781,7 @@ func ManageUser(c *gin.Context) {
|
||||||
}
|
}
|
||||||
changes = append(changes, service.FieldChange{Field: "学员", OldValue: oldLabel, NewValue: newLabel})
|
changes = append(changes, service.FieldChange{Field: "学员", OldValue: oldLabel, NewValue: newLabel})
|
||||||
}
|
}
|
||||||
service.RecordUpdateWithDiff(c, "user", user.Id, user.Username, label+": "+user.Username, changes, "")
|
service.RecordUpdateWithDiff(c, "user", user.Id, user.Username, label+":"+user.Username, changes, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch req.Action {
|
switch req.Action {
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,7 @@ func commonActionLabel(action string, targetType string, targetName string) stri
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetName != "" {
|
if targetName != "" {
|
||||||
return actionLabel + typeLabel + ": " + targetName
|
return actionLabel + typeLabel + ":" + targetName
|
||||||
}
|
}
|
||||||
return actionLabel + typeLabel
|
return actionLabel + typeLabel
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import paramiko
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
host = "26.0.12.13"
|
||||||
|
user = "root"
|
||||||
|
password = "root@1234"
|
||||||
|
local_dist = r"D:\tuling_new\web\dist"
|
||||||
|
remote_dist = "/root/tokenFactory/web/dist"
|
||||||
|
|
||||||
|
print("Connecting...")
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect(host, username=user, password=password)
|
||||||
|
|
||||||
|
# Clean old dist files AND rebuild Go
|
||||||
|
print("Cleaning old dist and rebuilding...")
|
||||||
|
stdin, stdout, stderr = ssh.exec_command(
|
||||||
|
"rm -rf /root/tokenFactory/web/dist && mkdir -p /root/tokenFactory/web/dist"
|
||||||
|
)
|
||||||
|
stdout.read()
|
||||||
|
|
||||||
|
sftp = ssh.open_sftp()
|
||||||
|
|
||||||
|
uploaded = 0
|
||||||
|
for root, dirs, files in os.walk(local_dist):
|
||||||
|
rel_path = os.path.relpath(root, local_dist)
|
||||||
|
if rel_path == ".":
|
||||||
|
remote_dir = remote_dist
|
||||||
|
else:
|
||||||
|
remote_dir = remote_dist + "/" + rel_path.replace("\\", "/")
|
||||||
|
stdin, stdout, stderr = ssh.exec_command(f"mkdir -p {remote_dir}")
|
||||||
|
stdout.read()
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
local_file = os.path.join(root, f)
|
||||||
|
remote_file = (remote_dir + "/" + f).replace("\\", "/")
|
||||||
|
try:
|
||||||
|
sftp.put(local_file, remote_file)
|
||||||
|
uploaded += 1
|
||||||
|
if uploaded % 30 == 0:
|
||||||
|
print(f" Uploaded {uploaded} files...")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" FAILED: {f}: {e}")
|
||||||
|
|
||||||
|
sftp.close()
|
||||||
|
print(f"Uploaded {uploaded} files.")
|
||||||
|
|
||||||
|
# Rebuild Go
|
||||||
|
print("Building Go...")
|
||||||
|
stdin, stdout, stderr = ssh.exec_command(
|
||||||
|
"export PATH=$PATH:/usr/local/go/bin && cd /root/tokenFactory && go build -o tf . 2>&1"
|
||||||
|
)
|
||||||
|
out = stdout.read().decode()
|
||||||
|
err = stderr.read().decode()
|
||||||
|
if out.strip():
|
||||||
|
print("stdout:", out[-300:].strip())
|
||||||
|
if err.strip():
|
||||||
|
print("stderr:", err[-300:].strip())
|
||||||
|
|
||||||
|
# Replace binary file (not in a subdirectory — Docker mounts ./token-factory as a file)
|
||||||
|
print("Replacing binary...")
|
||||||
|
stdin, stdout, stderr = ssh.exec_command(
|
||||||
|
"rm -rf /root/tokenFactory/token-factory && cp /root/tokenFactory/tf /root/tokenFactory/token-factory && ls -la /root/tokenFactory/token-factory"
|
||||||
|
)
|
||||||
|
print(stdout.read().decode().strip())
|
||||||
|
|
||||||
|
# Docker restart
|
||||||
|
print("Restarting Docker...")
|
||||||
|
stdin, stdout, stderr = ssh.exec_command(
|
||||||
|
"cd /root/tokenFactory && docker compose down 2>&1 && docker compose up -d 2>&1"
|
||||||
|
)
|
||||||
|
print(stdout.read().decode()[-500:].strip())
|
||||||
|
|
||||||
|
ssh.close()
|
||||||
|
print("Done!")
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
"@visactor/react-vchart": "~1.8.8",
|
"@visactor/react-vchart": "~1.8.8",
|
||||||
"@visactor/vchart": "~1.8.8",
|
"@visactor/vchart": "~1.8.8",
|
||||||
"@visactor/vchart-semi-theme": "~1.8.8",
|
"@visactor/vchart-semi-theme": "~1.8.8",
|
||||||
|
"antd": "^5.29.3",
|
||||||
"axios": "1.13.5",
|
"axios": "1.13.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,38 @@ For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { lazy, Suspense, useContext, useMemo } from 'react';
|
import React, { lazy, Suspense, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
|
class ErrorBoundary extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false, error: null, info: null };
|
||||||
|
}
|
||||||
|
static getDerivedStateFromError(error) {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
componentDidCatch(error, info) {
|
||||||
|
this.setState({ info });
|
||||||
|
console.error('ErrorBoundary caught:', error, info);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 40, color: 'red', fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
|
||||||
|
<h2>页面崩溃: {this.state.error?.message}</h2>
|
||||||
|
<details>
|
||||||
|
<summary>Stack</summary>
|
||||||
|
<pre>{this.state.error?.stack}</pre>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Component Stack</summary>
|
||||||
|
<pre>{this.state.info?.componentStack}</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
import { Route, Routes, useLocation, useParams } from 'react-router-dom';
|
import { Route, Routes, useLocation, useParams } from 'react-router-dom';
|
||||||
import Loading from './components/common/ui/Loading';
|
import Loading from './components/common/ui/Loading';
|
||||||
import User from './pages/User';
|
import User from './pages/User';
|
||||||
|
|
@ -63,6 +95,11 @@ import LuckyBag from './pages/LuckyBag';
|
||||||
import Drawing from './pages/Drawing';
|
import Drawing from './pages/Drawing';
|
||||||
import ProfitDashboard from './pages/ProfitDashboard';
|
import ProfitDashboard from './pages/ProfitDashboard';
|
||||||
import SettingsAntiAbuse from './pages/Setting/Operation/SettingsAntiAbuse';
|
import SettingsAntiAbuse from './pages/Setting/Operation/SettingsAntiAbuse';
|
||||||
|
import Contact from './pages/Contact';
|
||||||
|
import About from './pages/About';
|
||||||
|
import UserAgreement from './pages/UserAgreement';
|
||||||
|
import PrivacyPolicy from './pages/PrivacyPolicy';
|
||||||
|
import Docs from './pages/Docs';
|
||||||
|
|
||||||
const Home = lazy(() => import('./pages/Home'));
|
const Home = lazy(() => import('./pages/Home'));
|
||||||
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
||||||
|
|
@ -70,10 +107,6 @@ const DistributorApply = lazy(() => import('./pages/DistributorApply'));
|
||||||
const DistributorCenter = lazy(() => import('./pages/DistributorCenter'));
|
const DistributorCenter = lazy(() => import('./pages/DistributorCenter'));
|
||||||
const DistributorAdmin = lazy(() => import('./pages/DistributorAdmin'));
|
const DistributorAdmin = lazy(() => import('./pages/DistributorAdmin'));
|
||||||
const InviteRedirect = lazy(() => import('./pages/InviteRedirect'));
|
const InviteRedirect = lazy(() => import('./pages/InviteRedirect'));
|
||||||
const About = lazy(() => import('./pages/About'));
|
|
||||||
const UserAgreement = lazy(() => import('./pages/UserAgreement'));
|
|
||||||
const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy'));
|
|
||||||
const Docs = lazy(() => import('./pages/Docs'));
|
|
||||||
|
|
||||||
function DynamicOAuth2Callback() {
|
function DynamicOAuth2Callback() {
|
||||||
const { provider } = useParams();
|
const { provider } = useParams();
|
||||||
|
|
@ -352,9 +385,11 @@ function App() {
|
||||||
path='/console/personal'
|
path='/console/personal'
|
||||||
element={
|
element={
|
||||||
<PrivateRoute>
|
<PrivateRoute>
|
||||||
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
<ErrorBoundary>
|
||||||
<PersonalSetting />
|
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
||||||
</Suspense>
|
<PersonalSetting />
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -511,6 +546,14 @@ function App() {
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='/contact'
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
||||||
|
<Contact />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/docs'
|
path='/docs'
|
||||||
element={
|
element={
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,16 @@ import React, {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Modal, Form, Typography, Space, Button } from '@douyinfe/semi-ui';
|
|
||||||
import { IconLock } from '@douyinfe/semi-icons';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { UserContext } from '../../context/User';
|
import { Lock as IconLock } from 'lucide-react';
|
||||||
|
import { Modal, Form, Typography, Space, Button } from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
API,
|
API,
|
||||||
showError,
|
showError,
|
||||||
showSuccess,
|
showSuccess,
|
||||||
mergeSelfResponseIntoLocalUser,
|
mergeSelfResponseIntoLocalUser,
|
||||||
} from '../../helpers';
|
} from '../../helpers';
|
||||||
|
import { UserContext } from '../../context/User';
|
||||||
import { buildAdminUserPhoneFieldRules } from '../table/users/modals/userPhoneFormRules';
|
import { buildAdminUserPhoneFieldRules } from '../table/users/modals/userPhoneFormRules';
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -18,8 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
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 { Inbox } from 'lucide-react';
|
||||||
|
|
||||||
|
const ACC = '#635BFF';
|
||||||
|
const ACC_SOFT = '#EEEBFF';
|
||||||
|
const INK = '#0A2540';
|
||||||
|
const SUB = '#425466';
|
||||||
|
const DIM = '#697386';
|
||||||
|
|
||||||
|
const toText = (v) =>
|
||||||
|
typeof v === 'string'
|
||||||
|
? v
|
||||||
|
: v?.props?.children || '';
|
||||||
|
|
||||||
|
const EmptyState = ({ icon, title, description, children, style }) => {
|
||||||
|
const Icon = icon || Inbox;
|
||||||
|
const titleText = toText(title);
|
||||||
|
const descText = toText(description);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='flex flex-col items-center justify-center py-12 text-center'
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='inline-flex h-12 w-12 items-center justify-center rounded-2xl'
|
||||||
|
style={{ background: ACC_SOFT, color: ACC }}
|
||||||
|
>
|
||||||
|
<Icon size={22} />
|
||||||
|
</div>
|
||||||
|
{titleText && (
|
||||||
|
<p className='mt-4 text-sm font-semibold' style={{ color: INK }}>
|
||||||
|
{titleText}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{descText && (
|
||||||
|
<p className='mt-1 text-sm' style={{ color: DIM }}>
|
||||||
|
{descText}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!titleText && !descText && (
|
||||||
|
<p className='mt-4 text-sm' style={{ color: DIM }}>
|
||||||
|
暂无数据
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{children && <div className='mt-4'>{children}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmptyState;
|
||||||
|
|
@ -50,8 +50,7 @@ const Dashboard = () => {
|
||||||
const { unreadCount } = useUserMessageUnreadCount(user);
|
const { unreadCount } = useUserMessageUnreadCount(user);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='min-h-full bg-slate-50 pb-16 text-slate-900 dark:bg-neutral-950 dark:text-white'>
|
<div className='flex flex-col gap-6 pb-8'>
|
||||||
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
|
||||||
<TopBar
|
<TopBar
|
||||||
onSearch={() => {}}
|
onSearch={() => {}}
|
||||||
onRefresh={dashboard.refresh}
|
onRefresh={dashboard.refresh}
|
||||||
|
|
@ -60,7 +59,7 @@ const Dashboard = () => {
|
||||||
unreadCount={unreadCount}
|
unreadCount={unreadCount}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='space-y-6 pt-8 pb-8'>
|
<div className='space-y-6 pb-8'>
|
||||||
<Hero user={user} />
|
<Hero user={user} />
|
||||||
|
|
||||||
{/* 第一行:5 个核心指标卡 */}
|
{/* 第一行:5 个核心指标卡 */}
|
||||||
|
|
@ -97,7 +96,6 @@ const Dashboard = () => {
|
||||||
<div className='flex items-center justify-center border-t border-slate-200 py-6 text-xs text-slate-400 dark:border-white/10 dark:text-white/30'>
|
<div className='flex items-center justify-center border-t border-slate-200 py-6 text-xs text-slate-400 dark:border-white/10 dark:text-white/30'>
|
||||||
<span>{getSystemName()} · 2026</span>
|
<span>{getSystemName()} · 2026</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { IconClose } from '@douyinfe/semi-icons';
|
||||||
import SiderBar from './SiderBar';
|
import SiderBar from './SiderBar';
|
||||||
import App from '../../App';
|
import App from '../../App';
|
||||||
import FooterBar from './Footer';
|
import FooterBar from './Footer';
|
||||||
|
import OnboardingWizard from '../onboarding/OnboardingWizard';
|
||||||
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/common/useIsMobile';
|
import { useIsMobile } from '../../hooks/common/useIsMobile';
|
||||||
|
|
@ -402,6 +403,7 @@ const PageLayout = () => {
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<OnboardingWizard />
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ const routerMap = {
|
||||||
'model-heat': '/console/model-heat',
|
'model-heat': '/console/model-heat',
|
||||||
playground: '/console/playground',
|
playground: '/console/playground',
|
||||||
benchmarks: '/benchmarks',
|
benchmarks: '/benchmarks',
|
||||||
|
'api-tester': '/console/api-tester',
|
||||||
personal: '/console/personal',
|
personal: '/console/personal',
|
||||||
supplier: null,
|
supplier: null,
|
||||||
distributor: '/console/distributor/admin',
|
distributor: '/console/distributor/admin',
|
||||||
|
|
@ -90,6 +91,11 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
||||||
itemKey: 'benchmarks',
|
itemKey: 'benchmarks',
|
||||||
to: '/benchmarks',
|
to: '/benchmarks',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: t('API 测速'),
|
||||||
|
itemKey: 'api-tester',
|
||||||
|
to: '/api-tester',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: t('数据看板'),
|
text: t('数据看板'),
|
||||||
itemKey: 'detail',
|
itemKey: 'detail',
|
||||||
|
|
@ -201,6 +207,12 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
||||||
|
|
||||||
const adminItems = useMemo(() => {
|
const adminItems = useMemo(() => {
|
||||||
const items = [
|
const items = [
|
||||||
|
{
|
||||||
|
text: t('操作记录'),
|
||||||
|
itemKey: 'operation-log',
|
||||||
|
to: '/console/operation-log',
|
||||||
|
className: isAdmin() ? '' : 'tableHiddle',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: t('渠道管理'),
|
text: t('渠道管理'),
|
||||||
itemKey: 'channel',
|
itemKey: 'channel',
|
||||||
|
|
@ -243,12 +255,6 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
||||||
to: '/user',
|
to: '/user',
|
||||||
className: isAdmin() ? '' : 'tableHiddle',
|
className: isAdmin() ? '' : 'tableHiddle',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: t('操作记录'),
|
|
||||||
itemKey: 'operation-log',
|
|
||||||
to: '/console/operation-log',
|
|
||||||
className: isAdmin() ? '' : 'tableHiddle',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: t('代理管理'),
|
text: t('代理管理'),
|
||||||
itemKey: 'distributor',
|
itemKey: 'distributor',
|
||||||
|
|
|
||||||
|
|
@ -17,86 +17,88 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
import { Input, Dropdown, Typography } from '@douyinfe/semi-ui';
|
import { Input, Typography } from '@douyinfe/semi-ui';
|
||||||
import { IconSearch } from '@douyinfe/semi-icons';
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
|
import { API, isAdmin, showSuccess } from '../../../helpers';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
const mockSearchData = [
|
const { Text } = Typography;
|
||||||
{
|
|
||||||
month: '四月 2026',
|
const SEARCH_DEBOUNCE_MS = 300;
|
||||||
items: [
|
|
||||||
{ id: 1, name: 'Google: Gemma 4 31B', icon: '⬥', color: 'text-blue-500' },
|
function debounce(fn, ms) {
|
||||||
{
|
let timer;
|
||||||
id: 2,
|
return (...args) => {
|
||||||
name: 'Qwen: Qwen3.6 Plus (free)',
|
clearTimeout(timer);
|
||||||
icon: '⬡',
|
timer = setTimeout(() => fn(...args), ms);
|
||||||
color: 'text-purple-500',
|
};
|
||||||
},
|
}
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Z.ai: GLM 5V Turbo',
|
|
||||||
icon: '⬢',
|
|
||||||
color: 'text-gray-800 dark:text-white',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Arcee AI: Trinity Large Thinking',
|
|
||||||
icon: '⬢',
|
|
||||||
color: 'text-teal-500',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: 'xAI: Grok 4.20 Multi-Agent',
|
|
||||||
icon: '⚡',
|
|
||||||
color: 'text-gray-800 dark:text-white',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: 'xAI: Grok 4.20',
|
|
||||||
icon: '⚡',
|
|
||||||
color: 'text-gray-800 dark:text-white',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
month: '三月 2026',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
name: 'Google: Lyria 3 Pro Preview',
|
|
||||||
icon: '⬥',
|
|
||||||
color: 'text-blue-500',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const SearchDropdown = ({ isMobile }) => {
|
const SearchDropdown = ({ isMobile }) => {
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [filteredData, setFilteredData] = useState(mockSearchData);
|
const [models, setModels] = useState([]);
|
||||||
|
const [results, setResults] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const dropdownRef = useRef(null);
|
const dropdownRef = useRef(null);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 预加载模型数据
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await API.get('/api/pricing', { disableDuplicate: true });
|
||||||
|
if (res?.data?.success && Array.isArray(res.data.data)) {
|
||||||
|
setModels(res.data.data);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const doSearch = useCallback(
|
||||||
|
(q) => {
|
||||||
|
const trimmed = q.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
setResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
const lower = trimmed.toLowerCase();
|
||||||
|
const matched = [];
|
||||||
|
for (const m of models) {
|
||||||
|
const name = (m.model_name || '').toLowerCase();
|
||||||
|
const desc = (m.description || '').toLowerCase();
|
||||||
|
const tags = (m.tags || '').toLowerCase();
|
||||||
|
if (name.includes(lower) || desc.includes(lower) || tags.includes(lower)) {
|
||||||
|
matched.push({
|
||||||
|
type: 'model',
|
||||||
|
label: m.model_name,
|
||||||
|
desc: m.description || '',
|
||||||
|
navigate: `/pricing?search=${encodeURIComponent(trimmed)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (matched.length >= 8) break;
|
||||||
|
}
|
||||||
|
setResults(matched);
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
[models],
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedSearch = useCallback(
|
||||||
|
debounce(doSearch, SEARCH_DEBOUNCE_MS),
|
||||||
|
[doSearch],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchValue.trim() === '') {
|
debouncedSearch(searchValue);
|
||||||
setFilteredData(mockSearchData);
|
}, [searchValue, debouncedSearch]);
|
||||||
} else {
|
|
||||||
const filtered = mockSearchData
|
|
||||||
.map((group) => ({
|
|
||||||
...group,
|
|
||||||
items: group.items.filter((item) =>
|
|
||||||
item.name.toLowerCase().includes(searchValue.toLowerCase()),
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
.filter((group) => group.items.length > 0);
|
|
||||||
setFilteredData(filtered);
|
|
||||||
}
|
|
||||||
}, [searchValue]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event) => {
|
const handleKeyDown = (event) => {
|
||||||
if (event.key === '/') {
|
if (event.key === '/' && document.activeElement === document.body) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
|
|
@ -106,89 +108,110 @@ const SearchDropdown = ({ isMobile }) => {
|
||||||
inputRef.current?.blur();
|
inputRef.current?.blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
return () => {
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleItemClick = (item) => {
|
const handleItemClick = (item) => {
|
||||||
console.log('Selected:', item);
|
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
setSearchValue('');
|
setSearchValue('');
|
||||||
|
setResults([]);
|
||||||
|
if (item.navigate) {
|
||||||
|
if (item.navigate.startsWith('/')) {
|
||||||
|
navigate(item.navigate);
|
||||||
|
} else {
|
||||||
|
window.open(item.navigate, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeLabel = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'model': return '模型';
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderDropdownContent = () => {
|
const renderDropdownContent = () => {
|
||||||
|
if (!searchValue.trim()) {
|
||||||
|
return (
|
||||||
|
<div className='px-4 py-8 text-center'>
|
||||||
|
<Text className='!text-sm !text-semi-color-text-2 dark:!text-gray-400'>
|
||||||
|
输入关键词搜索模型
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className='px-4 py-8 text-center'>
|
||||||
|
<Text className='!text-sm !text-semi-color-text-2 dark:!text-gray-400'>
|
||||||
|
搜索中...
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (results.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className='px-4 py-8 text-center'>
|
||||||
|
<Text className='!text-sm !text-semi-color-text-2 dark:!text-gray-400'>
|
||||||
|
未找到匹配结果
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className='w-80 md:w-96 max-h-96 overflow-y-auto'>
|
<div className='max-h-96 overflow-y-auto py-2'>
|
||||||
{filteredData.length > 0 ? (
|
{results.map((item, i) => (
|
||||||
filteredData.map((group) => (
|
<div
|
||||||
<div key={group.month} className='py-2'>
|
key={`${item.type}-${i}`}
|
||||||
<Typography.Text className='!px-4 !py-2 !text-xs !font-semibold !text-semi-color-text-2 dark:!text-gray-400 uppercase tracking-wider block'>
|
onClick={() => handleItemClick(item)}
|
||||||
{group.month}
|
className='px-4 py-2.5 flex items-center gap-3 cursor-pointer hover:bg-semi-color-fill-1 dark:hover:bg-gray-700 transition-colors'
|
||||||
</Typography.Text>
|
>
|
||||||
<div>
|
<span className='shrink-0 rounded bg-blue-100 dark:bg-blue-900/40 px-1.5 py-0.5 text-xs text-blue-600 dark:text-blue-300'>
|
||||||
{group.items.map((item) => (
|
{typeLabel(item.type)}
|
||||||
<div
|
</span>
|
||||||
key={item.id}
|
<div className='min-w-0 flex-1'>
|
||||||
onClick={() => handleItemClick(item)}
|
<Text className='!text-sm !font-medium !text-semi-color-text-0 dark:!text-gray-200 block truncate'>
|
||||||
className='px-4 py-2.5 flex items-center gap-3 cursor-pointer hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 transition-colors'
|
{item.label}
|
||||||
>
|
</Text>
|
||||||
<span className={`text-lg ${item.color}`}>{item.icon}</span>
|
{item.desc && (
|
||||||
<Typography.Text className='!text-sm !font-medium !text-semi-color-text-0 dark:!text-gray-200'>
|
<Text className='!text-xs !text-semi-color-text-2 dark:!text-gray-400 block truncate'>
|
||||||
{item.name}
|
{item.desc}
|
||||||
</Typography.Text>
|
</Text>
|
||||||
</div>
|
)}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className='px-4 py-8 text-center'>
|
|
||||||
<Typography.Text className='!text-sm !text-semi-color-text-2 dark:!text-gray-400'>
|
|
||||||
No results found
|
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative' ref={dropdownRef}>
|
<div className='relative' ref={dropdownRef}>
|
||||||
<Dropdown
|
<div className='relative'>
|
||||||
visible={visible}
|
<Input
|
||||||
onVisibleChange={setVisible}
|
ref={inputRef}
|
||||||
position='bottomLeft'
|
placeholder='搜索模型...'
|
||||||
trigger='custom'
|
prefix={<IconSearch className='text-semi-color-text-2 dark:text-gray-400' />}
|
||||||
getPopupContainer={() => dropdownRef.current}
|
suffix={
|
||||||
render={
|
<kbd className='hidden sm:inline-block px-1.5 py-0.5 text-xs font-semibold text-semi-color-text-2 dark:text-gray-400 bg-semi-color-fill-0 dark:bg-gray-700 border border-semi-color-border dark:border-gray-600 rounded'>
|
||||||
<div className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-800 dark:!border-gray-600'>
|
/
|
||||||
{renderDropdownContent()}
|
</kbd>
|
||||||
</div>
|
}
|
||||||
}
|
value={searchValue}
|
||||||
>
|
onChange={setSearchValue}
|
||||||
<div className='relative'>
|
onFocus={() => { if (searchValue.trim()) setVisible(true); }}
|
||||||
<Input
|
onBlur={() => setTimeout(() => setVisible(false), 150)}
|
||||||
ref={inputRef}
|
className='!w-40 lg:!w-56 !h-9 !text-sm !bg-semi-color-fill-0 dark:!bg-gray-800/50 !border-semi-color-border dark:!border-gray-700 hover:!border-semi-color-primary dark:hover:!border-blue-400 focus:!border-semi-color-primary dark:focus:!border-blue-400'
|
||||||
placeholder='Search'
|
style={{ borderRadius: '6px' }}
|
||||||
prefix={
|
/>
|
||||||
<IconSearch className='text-semi-color-text-2 dark:text-gray-400' />
|
</div>
|
||||||
}
|
{visible && (results.length > 0 || searchValue.trim()) && (
|
||||||
suffix={
|
<div className='absolute left-0 top-full mt-1 w-80 md:w-96 bg-semi-color-bg-overlay border border-semi-color-border shadow-lg rounded-lg dark:bg-gray-800 dark:border-gray-600 z-50'>
|
||||||
<kbd className='px-1.5 py-0.5 text-xs font-semibold !text-semi-color-text-2 dark:!text-gray-400 !bg-semi-color-fill-0 dark:!bg-gray-700 border !border-semi-color-border dark:!border-gray-600 rounded'>
|
{renderDropdownContent()}
|
||||||
/
|
|
||||||
</kbd>
|
|
||||||
}
|
|
||||||
value={searchValue}
|
|
||||||
onChange={setSearchValue}
|
|
||||||
onFocus={() => setVisible(true)}
|
|
||||||
className='!w-48 lg:!w-64 !h-9 !text-sm !bg-semi-color-fill-0 dark:!bg-gray-800/50 !border-semi-color-border dark:!border-gray-700 hover:!border-semi-color-primary dark:hover:!border-blue-400 focus:!border-semi-color-primary dark:focus:!border-blue-400'
|
|
||||||
style={{ borderRadius: '6px', paddingRight: '10px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -43,27 +43,30 @@ const DeploymentAccessGuard = ({
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
<Card loading={true} style={{ minHeight: '400px' }}>
|
<Card loading={true} style={{ minHeight: '400px' }}>
|
||||||
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
||||||
<Text type='secondary'>{t('加载设置中...')}</Text>
|
<Text type='secondary'>{t('加载设置中...')}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
className='mt-[60px] px-4'
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
style={{
|
<div
|
||||||
minHeight: 'calc(100vh - 60px)',
|
style={{
|
||||||
display: 'flex',
|
minHeight: 'calc(100vh - 160px)',
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
}}
|
justifyContent: 'center',
|
||||||
>
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '600px',
|
maxWidth: '600px',
|
||||||
|
|
@ -276,18 +279,22 @@ const DeploymentAccessGuard = ({
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionLoading || (connectionOk === null && !connectionError)) {
|
if (connectionLoading || (connectionOk === null && !connectionError)) {
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
<Card loading={true} style={{ minHeight: '400px' }}>
|
<Card loading={true} style={{ minHeight: '400px' }}>
|
||||||
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
||||||
<Text type='secondary'>{t('正在检查 io.net 连接...')}</Text>
|
<Text type='secondary'>{t('正在检查 io.net 连接...')}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -301,15 +308,16 @@ const DeploymentAccessGuard = ({
|
||||||
const detail = connectionError?.message || '';
|
const detail = connectionError?.message || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
className='mt-[60px] px-4'
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
style={{
|
<div
|
||||||
minHeight: 'calc(100vh - 60px)',
|
style={{
|
||||||
display: 'flex',
|
minHeight: 'calc(100vh - 160px)',
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
}}
|
justifyContent: 'center',
|
||||||
>
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '600px',
|
maxWidth: '600px',
|
||||||
|
|
@ -402,6 +410,8 @@ const DeploymentAccessGuard = ({
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
/*
|
||||||
|
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, useContext } from 'react';
|
||||||
|
import { Modal, Button, Steps, Typography, Space } from '@douyinfe/semi-ui';
|
||||||
|
import { IconCopy, IconLink } from '@douyinfe/semi-icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { UserContext } from '../../context/User';
|
||||||
|
import { copy, showSuccess } from '../../helpers';
|
||||||
|
|
||||||
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'onboarding_completed';
|
||||||
|
|
||||||
|
const OnboardingWizard = () => {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [userState] = useContext(UserContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const completed = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!completed && userState?.user) {
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
}, [userState?.user]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, '1');
|
||||||
|
setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTokenStepExample = () => {
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
const demoKey = 'sk-your-api-key-here';
|
||||||
|
return `curl ${baseUrl}/v1/chat/completions \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-H "Authorization: Bearer ${demoKey}" \\
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-3.5-turbo",
|
||||||
|
"messages": [{"role": "user", "content": "Hello!"}]
|
||||||
|
}'`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
title: t('获取 API Key'),
|
||||||
|
content: (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<Text>{t('前往令牌管理页面创建您的第一个 API Key。')}</Text>
|
||||||
|
<div className='rounded-lg bg-semi-color-fill-0 dark:bg-gray-800 p-4'>
|
||||||
|
<Text className='!text-sm !text-semi-color-text-2'>
|
||||||
|
1. 点击下方按钮进入令牌页面<br />
|
||||||
|
2. 点击「添加令牌」创建新 Key<br />
|
||||||
|
3. 复制生成的 sk- 开头的 Key 妥善保存
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
icon={<IconLink />}
|
||||||
|
onClick={() => {
|
||||||
|
handleClose();
|
||||||
|
navigate('/console/token');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('前往令牌管理')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('选择模型'),
|
||||||
|
content: (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<Text>{t('浏览模型市场,选择适合您需求的模型。')}</Text>
|
||||||
|
<div className='rounded-lg bg-semi-color-fill-0 dark:bg-gray-800 p-4'>
|
||||||
|
<Text className='!text-sm !text-semi-color-text-2'>
|
||||||
|
支持文本对话、图片生成、视频生成、语音等多种模型。<br />
|
||||||
|
可按价格、供应商、功能标签筛选。
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
icon={<IconLink />}
|
||||||
|
onClick={() => {
|
||||||
|
handleClose();
|
||||||
|
navigate('/pricing');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('浏览模型市场')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('首次调用'),
|
||||||
|
content: (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<Text>{t('使用以下 cURL 命令发送您的第一个 API 请求:')}</Text>
|
||||||
|
<div className='relative rounded-lg bg-gray-900 dark:bg-gray-950 p-4 group'>
|
||||||
|
<Button
|
||||||
|
theme='borderless'
|
||||||
|
type='tertiary'
|
||||||
|
size='small'
|
||||||
|
icon={<IconCopy />}
|
||||||
|
className='!absolute top-2 right-2 !text-gray-400 hover:!text-white'
|
||||||
|
onClick={() => {
|
||||||
|
copy(getTokenStepExample());
|
||||||
|
showSuccess(t('已复制'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<pre className='text-xs text-green-400 overflow-x-auto whitespace-pre-wrap'>
|
||||||
|
{getTokenStepExample()}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<Text className='!text-xs !text-semi-color-text-2'>
|
||||||
|
{t('将 {key} 替换为您上一步创建的 API Key,将 model 替换为您选择的模型名称。', { key: 'sk-your-api-key-here' })}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
type='tertiary'
|
||||||
|
icon={<IconLink />}
|
||||||
|
onClick={() => {
|
||||||
|
handleClose();
|
||||||
|
navigate('/docs');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('查看完整 API 文档')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<span className='text-lg'>🚀</span>
|
||||||
|
<Title heading={5} className='!mb-0'>
|
||||||
|
{t('欢迎使用')}
|
||||||
|
</Title>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
visible={visible}
|
||||||
|
onCancel={handleClose}
|
||||||
|
footer={
|
||||||
|
<Space>
|
||||||
|
{step > 0 && (
|
||||||
|
<Button onClick={() => setStep((s) => s - 1)}>
|
||||||
|
{t('上一步')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{step < steps.length - 1 ? (
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
onClick={() => setStep((s) => s + 1)}
|
||||||
|
>
|
||||||
|
{t('下一步')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button type='primary' onClick={handleClose}>
|
||||||
|
{t('开始使用')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
width={560}
|
||||||
|
maskClosable={false}
|
||||||
|
>
|
||||||
|
<Steps current={step} className='mb-6'>
|
||||||
|
{steps.map((s) => (
|
||||||
|
<Steps.Step key={s.title} title={s.title} />
|
||||||
|
))}
|
||||||
|
</Steps>
|
||||||
|
<div className='min-h-[160px]'>{steps[step].content}</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OnboardingWizard;
|
||||||
|
|
@ -555,7 +555,8 @@ const PersonalSetting = () => {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mx-auto mt-[60px] max-w-[1400px] px-4 pb-16 sm:px-6 lg:px-10'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
{/* 顶部用户信息区域 */}
|
{/* 顶部用户信息区域 */}
|
||||||
<UserInfoHeader t={t} userState={userState} />
|
<UserInfoHeader t={t} userState={userState} />
|
||||||
|
|
||||||
|
|
@ -676,6 +677,7 @@ const PersonalSetting = () => {
|
||||||
turnstileSiteKey={turnstileSiteKey}
|
turnstileSiteKey={turnstileSiteKey}
|
||||||
setTurnstileToken={setTurnstileToken}
|
setTurnstileToken={setTurnstileToken}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import {
|
||||||
IconAlertTriangle,
|
IconAlertTriangle,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
|
IconShield,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { ScanFace } from 'lucide-react';
|
import { ScanFace } from 'lucide-react';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,11 @@ For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Tag, Space, Skeleton } from '@douyinfe/semi-ui';
|
import { Tag, Space, Skeleton, Button } from '@douyinfe/semi-ui';
|
||||||
import { renderQuota } from '../../../helpers';
|
import { renderQuota } from '../../../helpers';
|
||||||
import CompactModeToggle from '../../common/ui/CompactModeToggle';
|
import CompactModeToggle from '../../common/ui/CompactModeToggle';
|
||||||
import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime';
|
import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime';
|
||||||
|
import { Download } from 'lucide-react';
|
||||||
|
|
||||||
const LogsActions = ({
|
const LogsActions = ({
|
||||||
stat,
|
stat,
|
||||||
|
|
@ -29,6 +30,8 @@ const LogsActions = ({
|
||||||
showStat,
|
showStat,
|
||||||
compactMode,
|
compactMode,
|
||||||
setCompactMode,
|
setCompactMode,
|
||||||
|
exporting,
|
||||||
|
handleExportLogs,
|
||||||
t,
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
const showSkeleton = useMinimumLoadingTime(loadingStat);
|
const showSkeleton = useMinimumLoadingTime(loadingStat);
|
||||||
|
|
@ -83,11 +86,23 @@ const LogsActions = ({
|
||||||
</Space>
|
</Space>
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
|
|
||||||
<CompactModeToggle
|
<Space>
|
||||||
compactMode={compactMode}
|
<Button
|
||||||
setCompactMode={setCompactMode}
|
icon={<Download size={14} />}
|
||||||
t={t}
|
size='small'
|
||||||
/>
|
theme='outline'
|
||||||
|
type='tertiary'
|
||||||
|
loading={exporting}
|
||||||
|
onClick={handleExportLogs}
|
||||||
|
>
|
||||||
|
{t('导出 CSV')}
|
||||||
|
</Button>
|
||||||
|
<CompactModeToggle
|
||||||
|
compactMode={compactMode}
|
||||||
|
setCompactMode={setCompactMode}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -864,7 +864,7 @@ const TopUp = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mx-auto w-full max-w-[1400px] px-4 pb-16 sm:px-6 lg:px-10'>
|
<div className='mx-auto w-full max-w-[1400px] px-4 pb-16 pt-20 sm:px-6 lg:px-10'>
|
||||||
{/* 划转模态框 */}
|
{/* 划转模态框 */}
|
||||||
<TransferModal
|
<TransferModal
|
||||||
t={t}
|
t={t}
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,17 @@ import {
|
||||||
Input,
|
Input,
|
||||||
Tag,
|
Tag,
|
||||||
Select,
|
Select,
|
||||||
|
Space,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IllustrationNoResult,
|
IllustrationNoResult,
|
||||||
IllustrationNoResultDark,
|
IllustrationNoResultDark,
|
||||||
} from '@douyinfe/semi-illustrations';
|
} from '@douyinfe/semi-illustrations';
|
||||||
import { Coins } from 'lucide-react';
|
import { Coins, Download } from 'lucide-react';
|
||||||
import { IconSearch } from '@douyinfe/semi-icons';
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
import { API, timestamp2string } from '../../../helpers';
|
import { API, timestamp2string } from '../../../helpers';
|
||||||
import { isAdmin } from '../../../helpers/utils';
|
import { isAdmin } from '../../../helpers/utils';
|
||||||
|
import { exportCSV } from '../../../helpers/export';
|
||||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
|
@ -202,6 +204,45 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [exporting, setExporting] = useState(false);
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
setExporting(true);
|
||||||
|
try {
|
||||||
|
const adminNow = isAdmin();
|
||||||
|
const base = adminNow ? '/api/user/topup' : '/api/user/topup/self';
|
||||||
|
const tn = tradeNoFilter.trim();
|
||||||
|
const un = usernameFilter.trim();
|
||||||
|
const params = { p: 1, page_size: 10000 };
|
||||||
|
if (tn) params.trade_no = tn;
|
||||||
|
if (adminNow && un) params.username = un;
|
||||||
|
if (statusFilter && statusFilter !== TOPUP_STATUS_ALL) {
|
||||||
|
params.status = statusFilter;
|
||||||
|
}
|
||||||
|
const res = await API.get(base, { params, disableDuplicate: true });
|
||||||
|
const { success, data } = res.data;
|
||||||
|
if (success && data?.items?.length) {
|
||||||
|
const columns = [
|
||||||
|
{ title: t('订单号'), dataIndex: 'trade_no' },
|
||||||
|
{ title: t('用户名'), dataIndex: 'username' },
|
||||||
|
{ title: t('支付方式'), dataIndex: 'payment_method' },
|
||||||
|
{ title: t('充值额度'), dataIndex: 'amount' },
|
||||||
|
{ title: t('支付金额'), dataIndex: 'money' },
|
||||||
|
{ title: t('状态'), dataIndex: 'status' },
|
||||||
|
{ title: t('创建时间'), dataIndex: 'create_time', render: (v) => v ? timestamp2string(v) : '' },
|
||||||
|
];
|
||||||
|
exportCSV(data.items, columns, `topup-records-${new Date().toISOString().slice(0, 10)}`);
|
||||||
|
Toast.success({ content: t('导出成功') });
|
||||||
|
} else {
|
||||||
|
Toast.info({ content: t('无数据可导出') });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Toast.error({ content: t('导出失败') });
|
||||||
|
} finally {
|
||||||
|
setExporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const confirmAdminComplete = (tradeNo) => {
|
const confirmAdminComplete = (tradeNo) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: t('确认补单'),
|
title: t('确认补单'),
|
||||||
|
|
@ -332,6 +373,18 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
||||||
>
|
>
|
||||||
{/* 筛选条件单行排列;宽度不足时横向滚动,避免折成两行 */}
|
{/* 筛选条件单行排列;宽度不足时横向滚动,避免折成两行 */}
|
||||||
<div className='mb-3 flex w-full flex-row flex-nowrap items-center gap-2 overflow-x-auto'>
|
<div className='mb-3 flex w-full flex-row flex-nowrap items-center gap-2 overflow-x-auto'>
|
||||||
|
<div className='shrink-0'>
|
||||||
|
<Button
|
||||||
|
icon={<Download size={14} />}
|
||||||
|
size='small'
|
||||||
|
theme='outline'
|
||||||
|
type='tertiary'
|
||||||
|
loading={exporting}
|
||||||
|
onClick={handleExport}
|
||||||
|
>
|
||||||
|
{t('导出 CSV')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
{userIsAdmin ? (
|
{userIsAdmin ? (
|
||||||
<Input
|
<Input
|
||||||
className='min-w-[104px] flex-1'
|
className='min-w-[104px] flex-1'
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,10 @@ For commercial licensing, please contact support@quantumnous.com
|
||||||
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { reducer, initialState } from './reducer';
|
import { API } from '../../helpers/api';
|
||||||
|
import { initialState, reducer } from './reducer';
|
||||||
|
import { mergeSelfResponseIntoLocalUser } from '../../helpers/data';
|
||||||
import { normalizeLanguage } from '../../i18n/language';
|
import { normalizeLanguage } from '../../i18n/language';
|
||||||
import { API, mergeSelfResponseIntoLocalUser } from '../../helpers';
|
|
||||||
import AdminInitialSetupModal from '../../components/auth/AdminInitialSetupModal';
|
import AdminInitialSetupModal from '../../components/auth/AdminInitialSetupModal';
|
||||||
|
|
||||||
export const UserContext = React.createContext({
|
export const UserContext = React.createContext({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/**
|
||||||
|
* 导出数据为 CSV 文件并触发浏览器下载。
|
||||||
|
* @param {Array<object>} rows 数据行
|
||||||
|
* @param {Array<{title: string, dataIndex: string, render?: function}>} columns 列定义
|
||||||
|
* @param {string} filename 文件名(不含扩展名)
|
||||||
|
*/
|
||||||
|
export function exportCSV(rows, columns, filename = 'export') {
|
||||||
|
if (!rows || !rows.length) return;
|
||||||
|
|
||||||
|
const BOM = '';
|
||||||
|
const header = columns.map((col) => `"${(col.title || col.dataIndex).replace(/"/g, '""')}"`).join(',');
|
||||||
|
const body = rows
|
||||||
|
.map((row) =>
|
||||||
|
columns
|
||||||
|
.map((col) => {
|
||||||
|
let val = row[col.dataIndex];
|
||||||
|
if (typeof col.render === 'function') {
|
||||||
|
// render 函数返回 React 元素时提取文本
|
||||||
|
const rendered = col.render(val, row);
|
||||||
|
if (rendered == null || rendered === '') return '""';
|
||||||
|
if (typeof rendered === 'string' || typeof rendered === 'number') {
|
||||||
|
val = String(rendered).replace(/"/g, '""');
|
||||||
|
} else if (rendered && typeof rendered === 'object' && rendered.props) {
|
||||||
|
// 简单提取 React 元素文本
|
||||||
|
val = extractText(rendered).replace(/"/g, '""');
|
||||||
|
} else {
|
||||||
|
val = String(rendered).replace(/"/g, '""');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val = val != null ? String(val).replace(/"/g, '""') : '';
|
||||||
|
}
|
||||||
|
return `"${val}"`;
|
||||||
|
})
|
||||||
|
.join(','),
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const csv = BOM + header + '\n' + body;
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${filename}.csv`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractText(element) {
|
||||||
|
if (element == null) return '';
|
||||||
|
if (typeof element === 'string' || typeof element === 'number') return String(element);
|
||||||
|
if (Array.isArray(element)) return element.map(extractText).join('');
|
||||||
|
if (element.props && element.props.children) return extractText(element.props.children);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
@ -111,13 +111,18 @@ import {
|
||||||
SiOkta,
|
SiOkta,
|
||||||
SiOpenid,
|
SiOpenid,
|
||||||
SiReddit,
|
SiReddit,
|
||||||
SiSlack,
|
|
||||||
SiTelegram,
|
SiTelegram,
|
||||||
SiTwitch,
|
SiTwitch,
|
||||||
SiWechat,
|
SiWechat,
|
||||||
SiX,
|
SiX,
|
||||||
} from 'react-icons/si';
|
} from 'react-icons/si';
|
||||||
|
|
||||||
|
const SiSlack = (props) => (
|
||||||
|
<svg stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" height="1em" width="1em" {...props}>
|
||||||
|
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.528 2.528 0 0 1-2.521-2.521V2.522A2.528 2.528 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.528 2.528 0 0 1-2.521-2.522v-2.522h2.521zM15.165 17.688a2.527 2.527 0 0 1-2.521-2.523 2.527 2.527 0 0 1 2.521-2.521h6.313A2.528 2.528 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
// 获取侧边栏Lucide图标组件
|
// 获取侧边栏Lucide图标组件
|
||||||
export function getLucideIcon(key, selected = false) {
|
export function getLucideIcon(key, selected = false) {
|
||||||
const size = 16;
|
const size = 16;
|
||||||
|
|
|
||||||
|
|
@ -18,15 +18,12 @@ For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Toast, Pagination } from '@douyinfe/semi-ui';
|
import { Toast, Pagination } from '@douyinfe/semi-ui';
|
||||||
import { toastConstants } from '../constants';
|
|
||||||
import React from 'react';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import {
|
import {
|
||||||
THINK_TAG_REGEX,
|
THINK_TAG_REGEX,
|
||||||
MESSAGE_ROLES,
|
MESSAGE_ROLES,
|
||||||
} from '../constants/playground.constants';
|
} from '../constants/playground.constants';
|
||||||
import { TABLE_COMPACT_MODES_KEY } from '../constants';
|
import { TABLE_COMPACT_MODES_KEY } from '../constants';
|
||||||
import { USER_ROLES } from '../constants/user.constants';
|
import { toastConstants } from '../constants/toast.constants';
|
||||||
import { MOBILE_BREAKPOINT } from '../hooks/common/useIsMobile';
|
import { MOBILE_BREAKPOINT } from '../hooks/common/useIsMobile';
|
||||||
|
|
||||||
const HTMLToastContent = ({ htmlContent }) => {
|
const HTMLToastContent = ({ htmlContent }) => {
|
||||||
|
|
@ -144,6 +141,15 @@ export function parseCommissionPercentStringToBps(s) {
|
||||||
export function getSystemName() {
|
export function getSystemName() {
|
||||||
let system_name = localStorage.getItem('system_name');
|
let system_name = localStorage.getItem('system_name');
|
||||||
if (!system_name) return 'TuringToken';
|
if (!system_name) return 'TuringToken';
|
||||||
|
// 旧品牌名缓存迁移:清掉 TokenFactory / TokenFactoryOpen / 开放词元工厂 等历史默认,避免登录页/控制台标题闪烁。
|
||||||
|
if (
|
||||||
|
system_name === 'TokenFactory' ||
|
||||||
|
system_name === 'TokenFactoryOpen' ||
|
||||||
|
system_name === '开放词元工厂'
|
||||||
|
) {
|
||||||
|
localStorage.setItem('system_name', 'TuringToken');
|
||||||
|
return 'TuringToken';
|
||||||
|
}
|
||||||
return system_name;
|
return system_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal, Tag } from '@douyinfe/semi-ui';
|
import { Modal, Tag, Toast } from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
API,
|
API,
|
||||||
getTodayStartTimestamp,
|
getTodayStartTimestamp,
|
||||||
|
|
@ -38,6 +38,7 @@ import {
|
||||||
} from '../../helpers';
|
} from '../../helpers';
|
||||||
import { ITEMS_PER_PAGE } from '../../constants';
|
import { ITEMS_PER_PAGE } from '../../constants';
|
||||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||||
|
import { exportCSV } from '../../helpers/export';
|
||||||
import ParamOverrideEntry from '../../components/table/usage-logs/components/ParamOverrideEntry';
|
import ParamOverrideEntry from '../../components/table/usage-logs/components/ParamOverrideEntry';
|
||||||
|
|
||||||
export const useLogsData = () => {
|
export const useLogsData = () => {
|
||||||
|
|
@ -550,6 +551,7 @@ export const useLogsData = () => {
|
||||||
visible: false,
|
visible: false,
|
||||||
record: null,
|
record: null,
|
||||||
});
|
});
|
||||||
|
const [exporting, setExporting] = useState(false);
|
||||||
|
|
||||||
// Initialize default column visibility
|
// Initialize default column visibility
|
||||||
const initDefaultColumns = () => {
|
const initDefaultColumns = () => {
|
||||||
|
|
@ -1307,6 +1309,60 @@ export const useLogsData = () => {
|
||||||
await loadLogs(1, pageSize);
|
await loadLogs(1, pageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExportLogs = useCallback(async () => {
|
||||||
|
setExporting(true);
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
username,
|
||||||
|
token_name,
|
||||||
|
model_name,
|
||||||
|
start_timestamp,
|
||||||
|
end_timestamp,
|
||||||
|
channel,
|
||||||
|
group,
|
||||||
|
request_id,
|
||||||
|
logType: formLogType,
|
||||||
|
} = getFormValues();
|
||||||
|
const currentLogType =
|
||||||
|
formLogType !== undefined ? formLogType : logType;
|
||||||
|
const localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||||
|
const localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||||
|
let url;
|
||||||
|
if (isAdminUser) {
|
||||||
|
url = `/api/log/?p=1&page_size=10000&type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}&request_id=${request_id}`;
|
||||||
|
} else {
|
||||||
|
url = `/api/log/self/?p=1&page_size=10000&type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}&request_id=${request_id}`;
|
||||||
|
}
|
||||||
|
url = encodeURI(url);
|
||||||
|
const res = await API.get(url);
|
||||||
|
const { success, data } = res.data;
|
||||||
|
if (success && data?.items?.length) {
|
||||||
|
const typeMap = { 0: t('未知'), 1: t('充值'), 2: t('消费'), 3: t('管理'), 4: t('系统'), 5: t('错误'), 6: t('退款') };
|
||||||
|
const columns = [
|
||||||
|
{ title: t('时间'), dataIndex: 'created_at', render: (v) => v ? timestamp2string(v) : '' },
|
||||||
|
{ title: t('渠道'), dataIndex: 'channel' },
|
||||||
|
{ title: t('用户名'), dataIndex: 'username' },
|
||||||
|
{ title: t('令牌'), dataIndex: 'token_name' },
|
||||||
|
{ title: t('分组'), dataIndex: 'group' },
|
||||||
|
{ title: t('类型'), dataIndex: 'type', render: (v) => typeMap[v] || t('未知') },
|
||||||
|
{ title: t('模型'), dataIndex: 'model_name' },
|
||||||
|
{ title: t('输入Tokens'), dataIndex: 'prompt_tokens' },
|
||||||
|
{ title: t('输出Tokens'), dataIndex: 'completion_tokens' },
|
||||||
|
{ title: t('花费'), dataIndex: 'quota', render: (v) => renderQuota(v) },
|
||||||
|
{ title: t('IP'), dataIndex: 'ip' },
|
||||||
|
];
|
||||||
|
exportCSV(data.items, columns, `usage-logs-${new Date().toISOString().slice(0, 10)}`);
|
||||||
|
Toast.success({ content: t('导出成功') });
|
||||||
|
} else {
|
||||||
|
Toast.info({ content: t('无数据可导出') });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Toast.error({ content: t('导出失败') });
|
||||||
|
} finally {
|
||||||
|
setExporting(false);
|
||||||
|
}
|
||||||
|
}, [getFormValues, logType, isAdminUser, t]);
|
||||||
|
|
||||||
// Copy text function
|
// Copy text function
|
||||||
const copyText = async (e, text) => {
|
const copyText = async (e, text) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -1408,6 +1464,10 @@ export const useLogsData = () => {
|
||||||
setLogType,
|
setLogType,
|
||||||
openParamOverrideModal,
|
openParamOverrideModal,
|
||||||
|
|
||||||
|
// Export
|
||||||
|
exporting,
|
||||||
|
handleExportLogs,
|
||||||
|
|
||||||
// Translation
|
// Translation
|
||||||
t,
|
t,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -39,17 +39,17 @@ i18n
|
||||||
load: 'currentOnly',
|
load: 'currentOnly',
|
||||||
supportedLngs: supportedLanguages,
|
supportedLngs: supportedLanguages,
|
||||||
resources: {
|
resources: {
|
||||||
en: enTranslation,
|
en: { translation: enTranslation },
|
||||||
'zh-CN': zhCNTranslation,
|
'zh-CN': { translation: zhCNTranslation },
|
||||||
'zh-TW': zhTWTranslation,
|
'zh-TW': { translation: zhTWTranslation },
|
||||||
fr: frTranslation,
|
fr: { translation: frTranslation },
|
||||||
ru: ruTranslation,
|
ru: { translation: ruTranslation },
|
||||||
ja: jaTranslation,
|
ja: { translation: jaTranslation },
|
||||||
vi: viTranslation,
|
vi: { translation: viTranslation },
|
||||||
id: idTranslation,
|
id: { translation: idTranslation },
|
||||||
ms: msTranslation,
|
ms: { translation: msTranslation },
|
||||||
th: thTranslation,
|
th: { translation: thTranslation },
|
||||||
sw: swTranslation,
|
sw: { translation: swTranslation },
|
||||||
},
|
},
|
||||||
fallbackLng: 'zh-CN',
|
fallbackLng: 'zh-CN',
|
||||||
lng: 'zh-CN',
|
lng: 'zh-CN',
|
||||||
|
|
|
||||||
|
|
@ -5695,6 +5695,21 @@
|
||||||
"主题切换": "Theme Toggle",
|
"主题切换": "Theme Toggle",
|
||||||
"顶栏显示主题切换按钮,允许用户切换亮色/暗色模式": "Show theme toggle in header bar, allowing users to switch between light/dark mode",
|
"顶栏显示主题切换按钮,允许用户切换亮色/暗色模式": "Show theme toggle in header bar, allowing users to switch between light/dark mode",
|
||||||
"语言选择器": "Language Selector",
|
"语言选择器": "Language Selector",
|
||||||
"顶栏显示语言选择器,允许用户切换界面语言": "Show language selector in header bar, allowing users to switch interface language"
|
"顶栏显示语言选择器,允许用户切换界面语言": "Show language selector in header bar, allowing users to switch interface language",
|
||||||
|
"All systems normal": "All systems normal",
|
||||||
|
"函数": "function",
|
||||||
|
"一样轻。": "— that light.",
|
||||||
|
"把 OpenAI、Claude、Gemini、DeepSeek 等几十个模型收进同一个 base_url,切换模型、改价格、改延迟,不必动客户端。": "Bring OpenAI, Claude, Gemini, DeepSeek and dozens of other models behind a single base_url. Switch models, pricing, latency — no client changes.",
|
||||||
|
"免费注册": "Sign up free",
|
||||||
|
"联系销售": "Contact sales",
|
||||||
|
"注册即送 ¥10": "¥10 free credits on signup",
|
||||||
|
"无月费": "No monthly fee",
|
||||||
|
"比开放透明": "Transparent pricing",
|
||||||
|
"你的客户端只需一句": "Your client only needs one",
|
||||||
|
",其余的事——路由、价格、容灾——都替你做了。": ", and we handle routing, pricing, and failover for you.",
|
||||||
|
"立即接入。": "Integrate now.",
|
||||||
|
"注册送 ¥10 试用金,分键跑完一组 benchmark,一拍压测。": "Get ¥10 trial credit on signup. Run a benchmark, then a load test.",
|
||||||
|
"定价": "Pricing",
|
||||||
|
"调用 AI 像调用": "Calling AI is as light as calling"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5554,6 +5554,21 @@
|
||||||
"主题切换": "主题切换",
|
"主题切换": "主题切换",
|
||||||
"顶栏显示主题切换按钮,允许用户切换亮色/暗色模式": "顶栏显示主题切换按钮,允许用户切换亮色/暗色模式",
|
"顶栏显示主题切换按钮,允许用户切换亮色/暗色模式": "顶栏显示主题切换按钮,允许用户切换亮色/暗色模式",
|
||||||
"语言选择器": "语言选择器",
|
"语言选择器": "语言选择器",
|
||||||
"顶栏显示语言选择器,允许用户切换界面语言": "顶栏显示语言选择器,允许用户切换界面语言"
|
"顶栏显示语言选择器,允许用户切换界面语言": "顶栏显示语言选择器,允许用户切换界面语言",
|
||||||
|
"All systems normal": "All systems normal",
|
||||||
|
"函数": "函数",
|
||||||
|
"一样轻。": "一样轻。",
|
||||||
|
"把 OpenAI、Claude、Gemini、DeepSeek 等几十个模型收进同一个 base_url,切换模型、改价格、改延迟,不必动客户端。": "把 OpenAI、Claude、Gemini、DeepSeek 等几十个模型收进同一个 base_url,切换模型、改价格、改延迟,不必动客户端。",
|
||||||
|
"免费注册": "免费注册",
|
||||||
|
"联系销售": "联系销售",
|
||||||
|
"注册即送 ¥10": "注册即送 ¥10",
|
||||||
|
"无月费": "无月费",
|
||||||
|
"比开放透明": "比开放透明",
|
||||||
|
"你的客户端只需一句": "你的客户端只需一句",
|
||||||
|
",其余的事——路由、价格、容灾——都替你做了。": ",其余的事——路由、价格、容灾——都替你做了。",
|
||||||
|
"立即接入。": "立即接入。",
|
||||||
|
"注册送 ¥10 试用金,分键跑完一组 benchmark,一拍压测。": "注册送 ¥10 试用金,分键跑完一组 benchmark,一拍压测。",
|
||||||
|
"定价": "定价",
|
||||||
|
"调用 AI 像调用": "调用 AI 像调用"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1187,7 +1187,7 @@ html.dark .home-distributor-cta-btn {
|
||||||
.distributor-apply-page-root {
|
.distributor-apply-page-root {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
top: 64px;
|
top: 80px;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,8 @@ const About = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
{aboutLoaded && about === '' ? (
|
{aboutLoaded && about === '' ? (
|
||||||
<div className='flex justify-center items-center h-screen p-8'>
|
<div className='flex justify-center items-center h-screen p-8'>
|
||||||
<Empty
|
<Empty
|
||||||
|
|
@ -166,6 +167,7 @@ const About = () => {
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -308,16 +308,16 @@ const ApiTester = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='min-h-full bg-slate-50 pb-16 text-slate-900 dark:bg-neutral-950 dark:text-white'>
|
<div className='min-h-full pb-16 text-slate-900 dark:text-white'>
|
||||||
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
<div className='mx-auto w-full max-w-[1400px] px-4 pt-20 sm:px-6 lg:px-10'>
|
||||||
<div className='space-y-6 pt-8 pb-8'>
|
<div className='space-y-8 pb-8'>
|
||||||
{/* 页面标题区 */}
|
{/* 页面标题区 */}
|
||||||
<div className='flex flex-wrap items-end justify-between gap-4'>
|
<div className='flex flex-wrap items-end justify-between gap-4'>
|
||||||
<div>
|
<div>
|
||||||
<h1 className='text-3xl font-bold tracking-tight text-slate-900 md:text-4xl dark:text-white'>
|
<h1 className='text-xl font-bold tracking-tight text-slate-900 md:text-2xl dark:text-white'>
|
||||||
API 快速测试
|
API 快速测试
|
||||||
</h1>
|
</h1>
|
||||||
<p className='mt-2 text-sm text-slate-500 dark:text-white/50'>
|
<p className='mt-2 text-xs text-slate-500 dark:text-white/50'>
|
||||||
当前协议:{format === 'anthropic' ? 'Anthropic Messages' : 'OpenAI Chat Completions'} · 请求地址 {finalEndpoint}
|
当前协议:{format === 'anthropic' ? 'Anthropic Messages' : 'OpenAI Chat Completions'} · 请求地址 {finalEndpoint}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -330,14 +330,14 @@ const ApiTester = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 第一行:API Key / 测试目标 / 模型 */}
|
{/* 第一行:API Key / 测试目标 / 模型 */}
|
||||||
<div className='grid grid-cols-1 gap-5 lg:grid-cols-3'>
|
<div className='grid grid-cols-1 gap-6 lg:grid-cols-3'>
|
||||||
{/* API Key 输入卡片 */}
|
{/* API Key 输入卡片 */}
|
||||||
<div className='rounded-2xl border border-slate-200 bg-white p-5 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
|
<div className='rounded-2xl border border-slate-200 bg-white p-6 transition-shadow hover:shadow-md dark:border-white/10 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
|
||||||
<div className='mb-3 flex items-center gap-2'>
|
<div className='mb-3 flex items-center gap-2'>
|
||||||
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
|
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
|
||||||
<KeyRound size={15} />
|
<KeyRound size={15} />
|
||||||
</div>
|
</div>
|
||||||
<div className='text-sm font-semibold text-slate-900 dark:text-white'>API Key</div>
|
<div className='text-xs font-semibold text-slate-900 dark:text-white'>API Key</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<input
|
<input
|
||||||
|
|
@ -360,19 +360,19 @@ const ApiTester = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 测试目标选择卡片 */}
|
{/* 测试目标选择卡片 */}
|
||||||
<div className='rounded-2xl border border-slate-200 bg-white p-5 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
|
<div className='rounded-2xl border border-slate-200 bg-white p-6 transition-shadow hover:shadow-md dark:border-white/10 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
|
||||||
<div className='mb-3 flex items-center justify-between'>
|
<div className='mb-3 flex items-center justify-between'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
|
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
|
||||||
<Globe size={15} />
|
<Globe size={15} />
|
||||||
</div>
|
</div>
|
||||||
<div className='text-sm font-semibold text-slate-900 dark:text-white'>
|
<div className='text-xs font-semibold text-slate-900 dark:text-white'>
|
||||||
测试目标
|
测试目标
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCustomBase((v) => !v)}
|
onClick={() => setShowCustomBase((v) => !v)}
|
||||||
className='inline-flex items-center gap-1 text-[11px] text-slate-400 transition-colors hover:text-slate-700 dark:text-white/40 dark:hover:text-white'
|
className='inline-flex items-center gap-1 text-xs text-slate-400 transition-colors hover:text-slate-700 dark:text-white/40 dark:hover:text-white'
|
||||||
title='切换自定义域名'
|
title='切换自定义域名'
|
||||||
>
|
>
|
||||||
{showCustomBase ? '收起域名' : '换个平台'}
|
{showCustomBase ? '收起域名' : '换个平台'}
|
||||||
|
|
@ -386,10 +386,10 @@ const ApiTester = () => {
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
return (
|
return (
|
||||||
<div className='space-y-2.5'>
|
<div className='space-y-3'>
|
||||||
{Object.entries(grouped).map(([group, items]) => (
|
{Object.entries(grouped).map(([group, items]) => (
|
||||||
<div key={group}>
|
<div key={group}>
|
||||||
<div className='mb-1.5 text-[10px] font-medium uppercase tracking-wider text-slate-400 dark:text-white/40'>
|
<div className='mb-1.5 text-[11px] font-medium uppercase tracking-wider text-slate-400 dark:text-white/40'>
|
||||||
{group}
|
{group}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-wrap gap-1.5'>
|
<div className='flex flex-wrap gap-1.5'>
|
||||||
|
|
@ -397,7 +397,7 @@ const ApiTester = () => {
|
||||||
<button
|
<button
|
||||||
key={s.id}
|
key={s.id}
|
||||||
onClick={() => applyScene(s)}
|
onClick={() => applyScene(s)}
|
||||||
className='inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-[11px] font-medium text-slate-600 transition hover:border-blue-400 hover:bg-blue-50 hover:text-blue-600 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70 dark:hover:border-blue-400 dark:hover:bg-blue-500/10 dark:hover:text-blue-400'
|
className='inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-600 transition hover:border-blue-400 hover:bg-blue-50 hover:text-blue-600 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70 dark:hover:border-blue-400 dark:hover:bg-blue-500/10 dark:hover:text-blue-400'
|
||||||
>
|
>
|
||||||
<s.Icon size={11} /> {s.label}
|
<s.Icon size={11} /> {s.label}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -408,12 +408,12 @@ const ApiTester = () => {
|
||||||
|
|
||||||
<div className='border-t border-slate-100 pt-2.5 dark:border-white/5'>
|
<div className='border-t border-slate-100 pt-2.5 dark:border-white/5'>
|
||||||
<div className='mb-1.5 flex items-center justify-between'>
|
<div className='mb-1.5 flex items-center justify-between'>
|
||||||
<div className='text-[10px] font-medium uppercase tracking-wider text-slate-400 dark:text-white/40'>
|
<div className='text-[11px] font-medium uppercase tracking-wider text-slate-400 dark:text-white/40'>
|
||||||
我的目标
|
我的目标
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={saveCurrentAsCustom}
|
onClick={saveCurrentAsCustom}
|
||||||
className='inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white px-2.5 py-1 text-[10px] font-medium text-slate-600 transition hover:border-blue-400 hover:text-blue-600 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70 dark:hover:text-blue-400'
|
className='inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white px-2.5 py-1 text-xs font-medium text-slate-600 transition hover:border-blue-400 hover:text-blue-600 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/70 dark:hover:text-blue-400'
|
||||||
>
|
>
|
||||||
<Plus size={10} /> 保存当前
|
<Plus size={10} /> 保存当前
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -431,7 +431,7 @@ const ApiTester = () => {
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => applyScene(s)}
|
onClick={() => applyScene(s)}
|
||||||
className='inline-flex items-center gap-1.5 px-3 py-1.5 text-[11px] font-medium text-slate-600 transition hover:bg-blue-50 hover:text-blue-600 dark:text-white/70 dark:hover:bg-blue-500/10 dark:hover:text-blue-400'
|
className='inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-slate-600 transition hover:bg-blue-50 hover:text-blue-600 dark:text-white/70 dark:hover:bg-blue-500/10 dark:hover:text-blue-400'
|
||||||
>
|
>
|
||||||
<Bookmark size={11} /> {s.label}
|
<Bookmark size={11} /> {s.label}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -452,14 +452,14 @@ const ApiTester = () => {
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
<div className='mt-3'>
|
<div className='mt-3'>
|
||||||
<label className='mb-1.5 block text-[11px] font-medium text-slate-500 dark:text-white/50'>
|
<label className='mb-1.5 block text-xs font-medium text-slate-500 dark:text-white/50'>
|
||||||
完整请求地址
|
完整请求地址
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
value={isAbsoluteEndpoint ? endpointPath : `${endpointBase}${endpointPath}`}
|
value={isAbsoluteEndpoint ? endpointPath : `${endpointBase}${endpointPath}`}
|
||||||
onChange={(e) => applyFullUrl(e.target.value)}
|
onChange={(e) => applyFullUrl(e.target.value)}
|
||||||
placeholder='https://你的平台域名/v1/chat/completions'
|
placeholder='https://你的平台域名/v1/chat/completions'
|
||||||
className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 font-mono text-[11px] text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
|
className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 font-mono text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -487,22 +487,22 @@ const ApiTester = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 模型选择卡片 */}
|
{/* 模型选择卡片 */}
|
||||||
<div className='rounded-2xl border border-slate-200 bg-white p-5 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
|
<div className='rounded-2xl border border-slate-200 bg-white p-6 transition-shadow hover:shadow-md dark:border-white/10 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
|
||||||
<div className='mb-3 flex items-center gap-2'>
|
<div className='mb-3 flex items-center gap-2'>
|
||||||
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
|
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
|
||||||
<Cpu size={15} />
|
<Cpu size={15} />
|
||||||
</div>
|
</div>
|
||||||
<div className='text-sm font-semibold text-slate-900 dark:text-white'>模型</div>
|
<div className='text-xs font-semibold text-slate-900 dark:text-white'>模型</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
value={model}
|
value={model}
|
||||||
onChange={(e) => setModel(e.target.value)}
|
onChange={(e) => setModel(e.target.value)}
|
||||||
placeholder='例如 gpt-4o-mini / claude-3-5-sonnet ...'
|
placeholder='例如 gpt-4o-mini / claude-3-5-sonnet ...'
|
||||||
className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 font-mono text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
|
className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 font-mono text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
|
||||||
/>
|
/>
|
||||||
{format === 'anthropic' && (
|
{format === 'anthropic' && (
|
||||||
<div className='mt-3'>
|
<div className='mt-3'>
|
||||||
<label className='mb-1.5 block text-[11px] font-medium text-slate-500 dark:text-white/50'>
|
<label className='mb-1.5 block text-xs font-medium text-slate-500 dark:text-white/50'>
|
||||||
max_tokens(Anthropic 必填)
|
max_tokens(Anthropic 必填)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -511,7 +511,7 @@ const ApiTester = () => {
|
||||||
max={8192}
|
max={8192}
|
||||||
value={maxTokens}
|
value={maxTokens}
|
||||||
onChange={(e) => setMaxTokens(Number(e.target.value) || 1)}
|
onChange={(e) => setMaxTokens(Number(e.target.value) || 1)}
|
||||||
className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 font-mono text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
|
className='w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 font-mono text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -519,7 +519,7 @@ const ApiTester = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 第二行:请求参数 + 响应结果 */}
|
{/* 第二行:请求参数 + 响应结果 */}
|
||||||
<div className='grid grid-cols-1 gap-5 lg:grid-cols-2'>
|
<div className='grid grid-cols-1 gap-6 lg:grid-cols-2'>
|
||||||
{/* 请求参数卡片 */}
|
{/* 请求参数卡片 */}
|
||||||
<div className='rounded-2xl border border-slate-200 bg-white p-6 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
|
<div className='rounded-2xl border border-slate-200 bg-white p-6 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20'>
|
||||||
<div className='mb-4 flex items-center justify-between'>
|
<div className='mb-4 flex items-center justify-between'>
|
||||||
|
|
@ -527,7 +527,7 @@ const ApiTester = () => {
|
||||||
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
|
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
|
||||||
<MessageSquare size={15} />
|
<MessageSquare size={15} />
|
||||||
</div>
|
</div>
|
||||||
<div className='text-sm font-semibold text-slate-900 dark:text-white'>
|
<div className='text-xs font-semibold text-slate-900 dark:text-white'>
|
||||||
请求参数
|
请求参数
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -547,7 +547,7 @@ const ApiTester = () => {
|
||||||
|
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
<div>
|
<div>
|
||||||
<label className='mb-1.5 block text-[11px] font-medium uppercase tracking-wider text-slate-500 dark:text-white/50'>
|
<label className='mb-1.5 block text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-white/50'>
|
||||||
{format === 'anthropic' ? 'system(顶层字段)' : '系统提示'}
|
{format === 'anthropic' ? 'system(顶层字段)' : '系统提示'}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|
@ -555,12 +555,12 @@ const ApiTester = () => {
|
||||||
onChange={(e) => setSystemPrompt(e.target.value)}
|
onChange={(e) => setSystemPrompt(e.target.value)}
|
||||||
rows={2}
|
rows={2}
|
||||||
placeholder={format === 'anthropic' ? '(可选)Anthropic 顶层 system 字段' : '(可选)设置模型角色'}
|
placeholder={format === 'anthropic' ? '(可选)Anthropic 顶层 system 字段' : '(可选)设置模型角色'}
|
||||||
className='w-full resize-none rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
|
className='w-full resize-none rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className='mb-1.5 block text-[11px] font-medium uppercase tracking-wider text-slate-500 dark:text-white/50'>
|
<label className='mb-1.5 block text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-white/50'>
|
||||||
用户提示 <span className='text-red-500'>*</span>
|
用户提示 <span className='text-red-500'>*</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|
@ -568,7 +568,7 @@ const ApiTester = () => {
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
rows={5}
|
rows={5}
|
||||||
placeholder='输入你想让模型回答的内容'
|
placeholder='输入你想让模型回答的内容'
|
||||||
className='w-full resize-none rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
|
className='w-full resize-none rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-xs text-slate-900 outline-none transition focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-500/20 dark:border-white/10 dark:bg-white/[0.04] dark:text-white dark:focus:border-blue-400 dark:focus:bg-white/[0.06]'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -581,12 +581,12 @@ const ApiTester = () => {
|
||||||
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
|
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'>
|
||||||
<Zap size={15} />
|
<Zap size={15} />
|
||||||
</div>
|
</div>
|
||||||
<div className='text-sm font-semibold text-slate-900 dark:text-white'>
|
<div className='text-xs font-semibold text-slate-900 dark:text-white'>
|
||||||
响应结果
|
响应结果
|
||||||
</div>
|
</div>
|
||||||
{status && (
|
{status && (
|
||||||
<span
|
<span
|
||||||
className={`ml-2 inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[11px] font-medium ${
|
className={`ml-2 inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${
|
||||||
status.ok
|
status.ok
|
||||||
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400'
|
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400'
|
||||||
: 'bg-red-50 text-red-600 dark:bg-red-500/10 dark:text-red-400'
|
: 'bg-red-50 text-red-600 dark:bg-red-500/10 dark:text-red-400'
|
||||||
|
|
@ -607,7 +607,7 @@ const ApiTester = () => {
|
||||||
navigator.clipboard?.writeText(response);
|
navigator.clipboard?.writeText(response);
|
||||||
Toast.success('已复制响应');
|
Toast.success('已复制响应');
|
||||||
}}
|
}}
|
||||||
className='inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-[11px] font-medium text-slate-600 transition-colors hover:border-slate-300 hover:text-slate-900 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/60 dark:hover:border-white/20 dark:hover:text-white'
|
className='inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-600 transition-colors hover:border-slate-300 hover:text-slate-900 dark:border-white/10 dark:bg-white/[0.02] dark:text-white/60 dark:hover:border-white/20 dark:hover:text-white'
|
||||||
>
|
>
|
||||||
<Copy size={11} /> 复制
|
<Copy size={11} /> 复制
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -621,7 +621,7 @@ const ApiTester = () => {
|
||||||
请求中...
|
请求中...
|
||||||
</div>
|
</div>
|
||||||
) : response ? (
|
) : response ? (
|
||||||
<pre className='max-h-[480px] overflow-auto p-4 font-mono text-[11px] leading-relaxed text-slate-800 dark:text-white/80'>
|
<pre className='max-h-[480px] overflow-auto p-4 font-mono text-xs leading-relaxed text-slate-800 dark:text-white/80'>
|
||||||
{response}
|
{response}
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,10 @@ const File = () => {
|
||||||
}, [editId]);
|
}, [editId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<ChannelsTable />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<ChannelsTable />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,364 @@
|
||||||
|
/*
|
||||||
|
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 { ArrowRight, Mail, MessageSquare, Building2, ShieldCheck, Check } from 'lucide-react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const ACC = '#635BFF';
|
||||||
|
const ACC_SOFT = '#EEEBFF';
|
||||||
|
const BG = '#FFFFFF';
|
||||||
|
const SOFT = '#FBFAF7';
|
||||||
|
const TINT = '#F6F4FF';
|
||||||
|
const INK = '#0A2540';
|
||||||
|
const SUB = '#425466';
|
||||||
|
const DIM = '#697386';
|
||||||
|
const LINE = '#E3E8EE';
|
||||||
|
|
||||||
|
function Field({ label, value, onChange, type = 'text', required = false, placeholder = '' }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 text-xs" style={{ color: DIM }}>
|
||||||
|
{label}{required && <span style={{ color: ACC }}> *</span>}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
required={required}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="w-full rounded-lg border bg-white px-3.5 py-2.5 text-sm outline-none transition-colors focus:border-[#635BFF]"
|
||||||
|
style={{ borderColor: LINE, color: INK }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoCard({ icon, title, primary, sub }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-xl border bg-white p-5 transition-all hover:-translate-y-0.5 hover:shadow-sm"
|
||||||
|
style={{ borderColor: LINE }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<div
|
||||||
|
className="inline-flex h-8 w-8 items-center justify-center rounded-lg shrink-0"
|
||||||
|
style={{ background: ACC_SOFT, color: ACC }}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-semibold tracking-tight" style={{ color: INK }}>{title}</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3.5 pl-[42px]">
|
||||||
|
<div className="text-[15px] font-medium" style={{ color: ACC }}>{primary}</div>
|
||||||
|
<div className="mt-1 text-xs leading-[1.6]" style={{ color: DIM }}>{sub}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MiniTopBar() {
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-50 border-b border-[#E3E8EE] bg-white/85 backdrop-blur-md">
|
||||||
|
<div className="flex h-16 items-center px-6 lg:px-10 max-w-[1200px] mx-auto">
|
||||||
|
<Link to="/" className="flex items-center gap-2.5">
|
||||||
|
<img src="/logo.jpg" alt="logo" className="h-7 w-7 rounded-full" />
|
||||||
|
<span className="font-heading text-[15px] font-semibold tracking-tight" style={{ color: INK }}>TuringToken</span>
|
||||||
|
</Link>
|
||||||
|
<div className="hidden md:flex items-center gap-2 ml-6 rounded-full px-2.5 py-1 text-[11px]" style={{ background: '#F6F9FC', color: DIM }}>
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
|
||||||
|
<span>服务正常 · 99.95%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1" />
|
||||||
|
<Link
|
||||||
|
to="/console"
|
||||||
|
className="inline-flex h-9 items-center rounded-full px-3.5 text-sm hover:bg-[#0A254006]"
|
||||||
|
style={{ color: SUB }}
|
||||||
|
>
|
||||||
|
控制台
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/register"
|
||||||
|
className="ml-2 inline-flex h-9 items-center gap-1.5 rounded-full px-4 text-sm font-medium text-white hover:opacity-90"
|
||||||
|
style={{ background: INK }}
|
||||||
|
>
|
||||||
|
免费注册 <ArrowRight size={12} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Contact() {
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
company: '',
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
volume: '',
|
||||||
|
message: '',
|
||||||
|
});
|
||||||
|
const update = (k) => (e) => setForm({ ...form, [k]: e.target.value });
|
||||||
|
const submit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('已收到,我们会在 24 小时内联系你。');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen" style={{ background: BG, color: INK }}>
|
||||||
|
<MiniTopBar />
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<section className="relative overflow-hidden" style={{ background: BG }}>
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute inset-x-0 top-0 h-[600px]"
|
||||||
|
style={{ background: 'radial-gradient(80% 60% at 50% 0%, #EEEBFF 0%, transparent 70%)' }}
|
||||||
|
/>
|
||||||
|
<div className="relative max-w-[1200px] mx-auto px-6 lg:px-10 pt-20 pb-14 lg:pt-28 lg:pb-20 text-center">
|
||||||
|
<div className="inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs" style={{ borderColor: LINE, color: DIM }}>
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full" style={{ background: ACC }} />
|
||||||
|
<span>contact sales</span>
|
||||||
|
</div>
|
||||||
|
<h1
|
||||||
|
className="mt-6 font-heading font-semibold leading-[1.04] tracking-[-0.025em]"
|
||||||
|
style={{ fontSize: 'clamp(40px, 5.4vw, 72px)', color: INK }}
|
||||||
|
>
|
||||||
|
告诉我们,<br />
|
||||||
|
你的 <span style={{ color: ACC }}>AI 场景</span>。
|
||||||
|
</h1>
|
||||||
|
<p className="mt-6 mx-auto max-w-2xl text-base leading-[1.65]" style={{ color: SUB }}>
|
||||||
|
大客户、定制路由、长期合约、技术对接 — 都从这里开始。
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 flex flex-wrap items-center justify-center gap-6 text-xs" style={{ color: DIM }}>
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<Check size={12} style={{ color: ACC }} /> 24 小时内回复
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<Check size={12} style={{ color: ACC }} /> 可签 NDA
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<Check size={12} style={{ color: ACC }} /> 无电销骚扰
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Form + Info */}
|
||||||
|
<section style={{ background: BG }}>
|
||||||
|
<div className="max-w-[1200px] mx-auto px-6 lg:px-10 pb-24">
|
||||||
|
<div className="grid gap-8 lg:grid-cols-[1.4fr_1fr]">
|
||||||
|
{/* Form */}
|
||||||
|
<form
|
||||||
|
onSubmit={submit}
|
||||||
|
className="rounded-2xl border bg-white p-8 lg:p-10 shadow-sm"
|
||||||
|
style={{ borderColor: LINE }}
|
||||||
|
>
|
||||||
|
<div className="font-mono text-[11px] uppercase tracking-[0.25em]" style={{ color: DIM }}>get in touch</div>
|
||||||
|
<h2
|
||||||
|
className="mt-4 font-heading text-2xl font-semibold leading-[1.2] tracking-tight lg:text-[32px]"
|
||||||
|
style={{ color: INK }}
|
||||||
|
>
|
||||||
|
填一下,<br />我们 24h 内联系你。
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="mt-8 grid gap-5 md:grid-cols-2">
|
||||||
|
<Field label="公司名" value={form.company} onChange={update('company')} required />
|
||||||
|
<Field label="你的姓名" value={form.name} onChange={update('name')} required />
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">
|
||||||
|
<Field
|
||||||
|
label="工作邮箱"
|
||||||
|
type="email"
|
||||||
|
value={form.email}
|
||||||
|
onChange={update('email')}
|
||||||
|
required
|
||||||
|
placeholder="you@company.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="mb-2 text-xs" style={{ color: DIM }}>月调用量</div>
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
value={form.volume}
|
||||||
|
onChange={update('volume')}
|
||||||
|
className="w-full appearance-none rounded-lg border bg-white pl-3.5 pr-9 py-2.5 text-sm outline-none transition-colors focus:border-[#635BFF]"
|
||||||
|
style={{ borderColor: LINE, color: form.volume ? INK : DIM }}
|
||||||
|
>
|
||||||
|
<option value="">选择大致量级</option>
|
||||||
|
<option>10 万以下</option>
|
||||||
|
<option>10 万 – 100 万</option>
|
||||||
|
<option>100 万 – 1000 万</option>
|
||||||
|
<option>1000 万以上</option>
|
||||||
|
<option>暂未确定</option>
|
||||||
|
</select>
|
||||||
|
<span className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-xs" style={{ color: DIM }}>▾</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="mb-2 text-xs" style={{ color: DIM }}>你的场景</div>
|
||||||
|
<textarea
|
||||||
|
value={form.message}
|
||||||
|
onChange={update('message')}
|
||||||
|
rows={4}
|
||||||
|
placeholder="例如:做企业知识库客服,需要稳定的 Claude + 国产模型双备援,月调用量约 300 万..."
|
||||||
|
className="w-full rounded-lg border bg-white px-3.5 py-2.5 text-sm outline-none transition-colors focus:border-[#635BFF] resize-y"
|
||||||
|
style={{ borderColor: LINE, color: INK }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-7 flex flex-wrap items-center gap-4">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="inline-flex h-11 items-center gap-2 rounded-full px-6 text-sm font-semibold text-white hover:opacity-90"
|
||||||
|
style={{ background: INK }}
|
||||||
|
>
|
||||||
|
发送 <ArrowRight size={13} />
|
||||||
|
</button>
|
||||||
|
<span className="text-xs" style={{ color: DIM }}>
|
||||||
|
提交即同意我们通过邮箱回访,仅一次
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Info side */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<InfoCard
|
||||||
|
icon={<Mail size={16} />}
|
||||||
|
title="销售咨询"
|
||||||
|
primary="sales@turingtoken.ai"
|
||||||
|
sub="工作日 24h 内回复;加急请电话"
|
||||||
|
/>
|
||||||
|
<InfoCard
|
||||||
|
icon={<MessageSquare size={16} />}
|
||||||
|
title="技术支持"
|
||||||
|
primary="support@turingtoken.ai"
|
||||||
|
sub="已签合约客户优先;P0 故障 1h 响应"
|
||||||
|
/>
|
||||||
|
<InfoCard
|
||||||
|
icon={<ShieldCheck size={16} />}
|
||||||
|
title="法务 / NDA"
|
||||||
|
primary="legal@turingtoken.ai"
|
||||||
|
sub="签前可先发 NDA 模板,含双向保密"
|
||||||
|
/>
|
||||||
|
<InfoCard
|
||||||
|
icon={<Building2 size={16} />}
|
||||||
|
title="办公室"
|
||||||
|
primary="上海 · 北京 · 深圳"
|
||||||
|
sub="仅预约接待,不接受 walk-in"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* FAQ */}
|
||||||
|
<section style={{ background: SOFT }}>
|
||||||
|
<div className="max-w-[1100px] mx-auto px-6 lg:px-10 py-24 lg:py-32">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="font-mono text-[11px] uppercase tracking-[0.25em]" style={{ color: DIM }}>frequently asked</div>
|
||||||
|
<h2
|
||||||
|
className="mt-4 font-heading text-3xl font-semibold tracking-tight lg:text-4xl"
|
||||||
|
style={{ color: INK }}
|
||||||
|
>
|
||||||
|
常见问题。
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="mt-14 grid gap-5 md:grid-cols-2">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
q: '支持私有化部署吗?',
|
||||||
|
a: '支持。签约后 2–4 周内完成,含多模型路由、监控面板、计费、权限管理、SSO。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: '大客户有专属价格吗?',
|
||||||
|
a: '月调用量 100 万以上可谈。多数客户能拿到比 OpenAI 直连便宜 30%+ 的合约价。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: '能签长期合约吗?',
|
||||||
|
a: '可以。年付 / 季付都支持,附 SLA 99.95% 兜底,故障按比例退款。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: '数据合规怎么做?',
|
||||||
|
a: '全链路 TLS 1.3;境内流量走国内供应商;日志 30 天滚动删除;支持 GDPR 与个保法。',
|
||||||
|
},
|
||||||
|
].map((f, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="rounded-xl border bg-white p-6"
|
||||||
|
style={{ borderColor: LINE }}
|
||||||
|
>
|
||||||
|
<h3 className="font-heading text-lg font-semibold tracking-tight" style={{ color: INK }}>{f.q}</h3>
|
||||||
|
<p className="mt-3 text-sm leading-[1.7]" style={{ color: SUB }}>{f.a}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Closing */}
|
||||||
|
<section className="relative overflow-hidden" style={{ background: BG }}>
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute inset-x-0 bottom-0 h-[400px]"
|
||||||
|
style={{ background: 'radial-gradient(60% 60% at 50% 100%, #EEEBFF 0%, transparent 70%)' }}
|
||||||
|
/>
|
||||||
|
<div className="relative mx-auto max-w-3xl px-6 lg:px-10 py-24 lg:py-32 text-center">
|
||||||
|
<h2
|
||||||
|
className="font-heading text-3xl font-semibold leading-tight tracking-[-0.02em] lg:text-[44px]"
|
||||||
|
style={{ color: INK }}
|
||||||
|
>
|
||||||
|
先用起来,再聊。
|
||||||
|
</h2>
|
||||||
|
<p className="mt-5 text-base leading-[1.7]" style={{ color: SUB }}>
|
||||||
|
注册送 ¥10 试跑金,足够跑完一组 benchmark、一轮压测。聊完再决定也不迟。
|
||||||
|
</p>
|
||||||
|
<div className="mt-9 flex flex-wrap items-center justify-center gap-3">
|
||||||
|
<Link
|
||||||
|
to="/register"
|
||||||
|
className="inline-flex h-11 items-center gap-2 rounded-full px-6 text-sm font-semibold text-white hover:opacity-90"
|
||||||
|
style={{ background: INK }}
|
||||||
|
>
|
||||||
|
免费注册 <ArrowRight size={13} />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="inline-flex h-11 items-center gap-1.5 rounded-full border px-6 text-sm hover:bg-[#0A254006]"
|
||||||
|
style={{ borderColor: INK, color: INK }}
|
||||||
|
>
|
||||||
|
先看首页
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="border-t" style={{ borderColor: LINE, background: BG }}>
|
||||||
|
<div className="max-w-[1200px] mx-auto px-6 lg:px-10 py-12">
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<Link to="/" className="inline-flex items-center gap-2.5">
|
||||||
|
<img src="/logo.jpg" alt="logo" className="h-7 w-7 rounded-full" />
|
||||||
|
<span className="font-heading text-base font-semibold tracking-tight" style={{ color: INK }}>TuringToken</span>
|
||||||
|
</Link>
|
||||||
|
<span className="text-xs" style={{ color: DIM }}>© 2026 TuringToken · Made for developers.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -21,8 +21,10 @@ import React from 'react';
|
||||||
import Dashboard from '../../components/dashboard';
|
import Dashboard from '../../components/dashboard';
|
||||||
|
|
||||||
const Detail = () => (
|
const Detail = () => (
|
||||||
<div className='mt-[60px] w-full max-w-[1600px] mx-auto px-3 sm:px-4 lg:px-6 pb-6'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<Dashboard />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<Dashboard />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1128,7 +1128,8 @@ export default function DistributorAdmin() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2 pb-16'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
<Typography.Title heading={3}>{t('代理管理中心')}</Typography.Title>
|
<Typography.Title heading={3}>{t('代理管理中心')}</Typography.Title>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|
@ -1975,6 +1976,7 @@ export default function DistributorAdmin() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ export default function DistributorApply() {
|
||||||
|
|
||||||
if (isDist) {
|
if (isDist) {
|
||||||
return (
|
return (
|
||||||
<div className='distributor-apply-page-root'>
|
<div className='distributor-apply-page-root text-slate-900 dark:text-white'>
|
||||||
<div className='w-full max-w-3xl px-1'>
|
<div className='w-full max-w-3xl px-1'>
|
||||||
<ApplyStatusNotice variant='success' className='w-full'>
|
<ApplyStatusNotice variant='success' className='w-full'>
|
||||||
{t('您已是代理,无需再次申请')}
|
{t('您已是代理,无需再次申请')}
|
||||||
|
|
|
||||||
|
|
@ -668,7 +668,8 @@ export default function DistributorCenter() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-14 px-4 pb-16 max-w-7xl mx-auto'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
<Title heading={3} className='mb-8'>
|
<Title heading={3} className='mb-8'>
|
||||||
{t('代理分销')}
|
{t('代理分销')}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
@ -1196,6 +1197,7 @@ export default function DistributorCenter() {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -21,8 +21,10 @@ import React from 'react';
|
||||||
import UsageLogsTable from '../../components/table/usage-logs';
|
import UsageLogsTable from '../../components/table/usage-logs';
|
||||||
|
|
||||||
const Token = () => (
|
const Token = () => (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<UsageLogsTable />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<UsageLogsTable />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ import React from 'react';
|
||||||
import MjLogsTable from '../../components/table/mj-logs';
|
import MjLogsTable from '../../components/table/mj-logs';
|
||||||
|
|
||||||
const Midjourney = () => (
|
const Midjourney = () => (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<MjLogsTable />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<MjLogsTable />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,10 @@ const ModelPage = () => {
|
||||||
}, [editId, modelName]);
|
}, [editId, modelName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<ModelsTable />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<ModelsTable />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
/*
|
||||||
|
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 { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Select,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Empty,
|
||||||
|
Toast,
|
||||||
|
} from '@douyinfe/semi-ui';
|
||||||
|
import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
|
||||||
|
import { API, renderQuota, isAdmin } from '../../helpers';
|
||||||
|
|
||||||
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
|
const MAX_COMPARE = 4;
|
||||||
|
|
||||||
|
const endpointLabels = {
|
||||||
|
chat: '对话',
|
||||||
|
image: '图片',
|
||||||
|
audio: '音频',
|
||||||
|
video: '视频',
|
||||||
|
embeddings: '嵌入',
|
||||||
|
rerank: '重排',
|
||||||
|
moderate: '审核',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModelComparison = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [models, setModels] = useState([]);
|
||||||
|
const [selected, setSelected] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await API.get('/api/pricing', { disableDuplicate: true });
|
||||||
|
if (res?.data?.success && Array.isArray(res.data.data)) {
|
||||||
|
setModels(res.data.data);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Toast.error({ content: t('加载模型列表失败') });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const modelOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
models.map((m) => ({
|
||||||
|
value: m.model_name,
|
||||||
|
label: m.model_name,
|
||||||
|
})),
|
||||||
|
[models],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedModels = useMemo(
|
||||||
|
() => models.filter((m) => selected.includes(m.model_name)),
|
||||||
|
[models, selected],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelect = (vals) => {
|
||||||
|
if (vals.length > MAX_COMPARE) {
|
||||||
|
Toast.warning({ content: t('最多同时对比 {{max}} 个模型', { max: MAX_COMPARE }) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelected(vals);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (name) => {
|
||||||
|
setSelected((prev) => prev.filter((n) => n !== name));
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatEndpointTypes = (types) => {
|
||||||
|
if (!Array.isArray(types) || types.length === 0) return '-';
|
||||||
|
return types.map((ep) => endpointLabels[ep] || ep).join('、');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTags = (tags) => {
|
||||||
|
if (!tags) return '-';
|
||||||
|
const arr = typeof tags === 'string' ? tags.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
||||||
|
return arr.length > 0 ? arr.join('、') : '-';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPrice = (model) => {
|
||||||
|
const ratio = Number(model.model_ratio || 0);
|
||||||
|
const completionRatio = model.completion_ratio != null
|
||||||
|
? Number(model.completion_ratio)
|
||||||
|
: ratio;
|
||||||
|
if (Number(model.model_price || 0) > 0) {
|
||||||
|
return `$${Number(model.model_price).toFixed(4)} (固定价)`;
|
||||||
|
}
|
||||||
|
return `输入 ${ratio.toFixed(1)}x / 输出 ${completionRatio.toFixed(1)}x`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Comparison rows
|
||||||
|
const compareRows = [
|
||||||
|
{ key: 'vendor', label: t('供应商'), render: (m) => m.owner_by || '-' },
|
||||||
|
{ key: 'description', label: t('简介'), render: (m) => m.description || '-' },
|
||||||
|
{ key: 'pricing', label: t('价格'), render: (m) => formatPrice(m) },
|
||||||
|
{ key: 'endpoints', label: t('支持功能'), render: (m) => formatEndpointTypes(m.supported_endpoint_types) },
|
||||||
|
{ key: 'tags', label: t('标签'), render: (m) => formatTags(m.tags) },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<div className='mb-6 flex items-center justify-between'>
|
||||||
|
<Title heading={3} className='!mb-0'>
|
||||||
|
{t('模型对比')}
|
||||||
|
</Title>
|
||||||
|
<Text className='!text-semi-color-text-2'>
|
||||||
|
{t('已选 {{n}} / {{max}}', { n: selected.length, max: MAX_COMPARE })}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className='mb-6'>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<IconPlus className='text-semi-color-text-2' />
|
||||||
|
<Select
|
||||||
|
placeholder={t('选择模型进行对比(最多 {{max}} 个)', { max: MAX_COMPARE })}
|
||||||
|
multiple
|
||||||
|
value={selected}
|
||||||
|
onChange={handleSelect}
|
||||||
|
optionList={modelOptions}
|
||||||
|
filter
|
||||||
|
style={{ flex: 1, minWidth: 300 }}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{selected.length > 0 && (
|
||||||
|
<div className='mt-3 flex flex-wrap gap-2'>
|
||||||
|
{selected.map((name) => (
|
||||||
|
<Tag
|
||||||
|
key={name}
|
||||||
|
color='blue'
|
||||||
|
closable
|
||||||
|
onClose={() => handleRemove(name)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{selectedModels.length > 0 ? (
|
||||||
|
<Card>
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className='w-full border-collapse text-sm'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className='sticky left-0 z-10 bg-semi-color-bg-2 dark:bg-gray-900 px-4 py-3 text-left font-medium text-semi-color-text-2 border-b border-semi-color-border w-[120px]'>
|
||||||
|
{t('属性')}
|
||||||
|
</th>
|
||||||
|
{selectedModels.map((m) => (
|
||||||
|
<th
|
||||||
|
key={m.model_name}
|
||||||
|
className='px-4 py-3 text-left font-semibold text-semi-color-text-0 border-b border-semi-color-border min-w-[180px]'
|
||||||
|
>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Text strong>{m.model_name}</Text>
|
||||||
|
<Button
|
||||||
|
theme='borderless'
|
||||||
|
type='tertiary'
|
||||||
|
size='small'
|
||||||
|
icon={<IconDelete />}
|
||||||
|
onClick={() => handleRemove(m.model_name)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{compareRows.map((row) => (
|
||||||
|
<tr key={row.key}>
|
||||||
|
<td className='sticky left-0 z-10 bg-semi-color-bg-2 dark:bg-gray-900 px-4 py-3 font-medium text-semi-color-text-2 border-b border-semi-color-border'>
|
||||||
|
{row.label}
|
||||||
|
</td>
|
||||||
|
{selectedModels.map((m) => (
|
||||||
|
<td
|
||||||
|
key={m.model_name}
|
||||||
|
className='px-4 py-3 text-semi-color-text-0 border-b border-semi-color-border'
|
||||||
|
>
|
||||||
|
{row.render(m)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card>
|
||||||
|
<Empty
|
||||||
|
title={t('请选择模型')}
|
||||||
|
description={t('在上方选择 2-4 个模型进行并排对比')}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModelComparison;
|
||||||
|
|
@ -41,8 +41,10 @@ const ModelDeploymentPage = () => {
|
||||||
connectionError={connectionError}
|
connectionError={connectionError}
|
||||||
onRetry={() => testConnection()}
|
onRetry={() => testConnection()}
|
||||||
>
|
>
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<DeploymentsTable />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<DeploymentsTable />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DeploymentAccessGuard>
|
</DeploymentAccessGuard>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ import CombinedHeatConfig from './CombinedHeatConfig';
|
||||||
|
|
||||||
const ModelHeat = () => {
|
const ModelHeat = () => {
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<CombinedHeatConfig />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<CombinedHeatConfig />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,10 @@ const OperationLog = () => {
|
||||||
const [detailVisible, setDetailVisible] = useState(false);
|
const [detailVisible, setDetailVisible] = useState(false);
|
||||||
const [selectedLog, setSelectedLog] = useState(null);
|
const [selectedLog, setSelectedLog] = useState(null);
|
||||||
|
|
||||||
|
// Cleanup modal
|
||||||
|
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||||
|
const [deleteDate, setDeleteDate] = useState(null);
|
||||||
|
|
||||||
const fetchLogs = async () => {
|
const fetchLogs = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -132,14 +136,17 @@ const OperationLog = () => {
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteOldLogs = async () => {
|
const handleDeleteOldLogs = () => {
|
||||||
if (!dateRange || !dateRange[1]) {
|
setDeleteDate(null);
|
||||||
|
setDeleteModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
if (!deleteDate) {
|
||||||
showError('请选择截止日期');
|
showError('请选择截止日期');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const targetTimestamp = Math.floor(
|
const targetTimestamp = Math.floor(new Date(deleteDate).getTime() / 1000);
|
||||||
new Date(dateRange[1]).getTime() / 1000
|
|
||||||
);
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认清理',
|
title: '确认清理',
|
||||||
content: `确定要清理 ${timestamp2string(targetTimestamp)} 之前的操作日志吗?此操作不可逆。`,
|
content: `确定要清理 ${timestamp2string(targetTimestamp)} 之前的操作日志吗?此操作不可逆。`,
|
||||||
|
|
@ -151,6 +158,7 @@ const OperationLog = () => {
|
||||||
const { success, data, message } = res.data;
|
const { success, data, message } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
showSuccess(`成功清理 ${data} 条操作日志`);
|
showSuccess(`成功清理 ${data} 条操作日志`);
|
||||||
|
setDeleteModalVisible(false);
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
|
|
@ -206,7 +214,7 @@ const OperationLog = () => {
|
||||||
dataIndex: 'target_name',
|
dataIndex: 'target_name',
|
||||||
key: 'target_name',
|
key: 'target_name',
|
||||||
width: 160,
|
width: 160,
|
||||||
render: (text, record) => text || `ID: ${record.target_id}`,
|
render: (text, record) => text || `ID:${record.target_id}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('操作描述'),
|
title: t('操作描述'),
|
||||||
|
|
@ -239,7 +247,8 @@ const OperationLog = () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '20px' }}>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
<Card>
|
<Card>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -338,6 +347,27 @@ const OperationLog = () => {
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={t('清理日志')}
|
||||||
|
visible={deleteModalVisible}
|
||||||
|
onCancel={() => setDeleteModalVisible(false)}
|
||||||
|
onOk={handleConfirmDelete}
|
||||||
|
okText={t('确认清理')}
|
||||||
|
cancelText={t('取消')}
|
||||||
|
width={420}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
<Text>{t('选择截止日期,将清理该日期之前的所有操作日志。此操作不可逆。')}</Text>
|
||||||
|
<DatePicker
|
||||||
|
type='dateTime'
|
||||||
|
placeholder={t('选择截止日期')}
|
||||||
|
value={deleteDate}
|
||||||
|
onChange={setDeleteDate}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={t('操作详情')}
|
title={t('操作详情')}
|
||||||
visible={detailVisible}
|
visible={detailVisible}
|
||||||
|
|
@ -401,6 +431,7 @@ const OperationLog = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ import RedemptionsTable from '../../components/table/redemptions';
|
||||||
|
|
||||||
const Redemption = () => {
|
const Redemption = () => {
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<RedemptionsTable />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<RedemptionsTable />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,8 @@ const Setting = () => {
|
||||||
}
|
}
|
||||||
}, [location.search]);
|
}, [location.search]);
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Layout.Content>
|
<Layout.Content>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|
@ -234,6 +235,7 @@ const Setting = () => {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ import SubscriptionsPage from '../../components/table/subscriptions';
|
||||||
|
|
||||||
const Subscription = () => {
|
const Subscription = () => {
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<SubscriptionsPage />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<SubscriptionsPage />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,8 @@ const SupplierApplyPage = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='mt-[60px] px-2 flex items-center justify-center'
|
className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white flex items-center justify-center'
|
||||||
style={{ minHeight: 'calc(100vh - 360px)' }}
|
style={{ minHeight: 'calc(100vh - 200px)' }}
|
||||||
>
|
>
|
||||||
<Spin size='large' />
|
<Spin size='large' />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -121,7 +121,8 @@ const SupplierApplyPage = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
<SupplierApplicationModal
|
<SupplierApplicationModal
|
||||||
visible={showApplicationModal}
|
visible={showApplicationModal}
|
||||||
handleClose={handleModalClose}
|
handleClose={handleModalClose}
|
||||||
|
|
@ -220,6 +221,7 @@ const SupplierApplyPage = () => {
|
||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ const SupplierChannelContent = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
<ColumnSelectorModal {...channelsData} />
|
<ColumnSelectorModal {...channelsData} />
|
||||||
<EditTagModal
|
<EditTagModal
|
||||||
|
|
@ -116,6 +117,7 @@ const SupplierChannelContent = () => {
|
||||||
>
|
>
|
||||||
<ChannelsTable {...channelsData} />
|
<ChannelsTable {...channelsData} />
|
||||||
</CardPro>
|
</CardPro>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -126,7 +128,8 @@ const SupplierChannelPage = () => {
|
||||||
|
|
||||||
if (!isSupplier()) {
|
if (!isSupplier()) {
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
<div
|
<div
|
||||||
className='flex items-center justify-center'
|
className='flex items-center justify-center'
|
||||||
style={{ minHeight: 'calc(100vh - 360px)' }}
|
style={{ minHeight: 'calc(100vh - 360px)' }}
|
||||||
|
|
@ -153,6 +156,7 @@ const SupplierChannelPage = () => {
|
||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -282,7 +282,8 @@ export default function SupplierDashboardPage() {
|
||||||
|
|
||||||
if (!canAccess) {
|
if (!canAccess) {
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
<div
|
<div
|
||||||
className='flex items-center justify-center'
|
className='flex items-center justify-center'
|
||||||
style={{ minHeight: 'calc(100vh - 360px)' }}
|
style={{ minHeight: 'calc(100vh - 360px)' }}
|
||||||
|
|
@ -307,12 +308,14 @@ export default function SupplierDashboardPage() {
|
||||||
</Button>
|
</Button>
|
||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] w-full max-w-[1600px] mx-auto px-3 sm:px-4 lg:px-6 pb-6'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
<Spin spinning={loading} size='large'>
|
<Spin spinning={loading} size='large'>
|
||||||
<div className='flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between mb-3'>
|
<div className='flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between mb-3'>
|
||||||
<div className='flex items-center gap-2 min-w-0'>
|
<div className='flex items-center gap-2 min-w-0'>
|
||||||
|
|
@ -450,6 +453,7 @@ export default function SupplierDashboardPage() {
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Spin>
|
</Spin>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,8 @@ const SupplierPricingSettingsContent = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
<Spin spinning={loading} size='large'>
|
<Spin spinning={loading} size='large'>
|
||||||
<Card style={{ marginTop: '10px' }}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<Tabs type='card' defaultActiveKey='visual'>
|
<Tabs type='card' defaultActiveKey='visual'>
|
||||||
|
|
@ -177,6 +178,7 @@ const SupplierPricingSettingsContent = () => {
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Card>
|
</Card>
|
||||||
</Spin>
|
</Spin>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -190,7 +192,8 @@ const PricingSettingsPage = () => {
|
||||||
|
|
||||||
if (!isSupplier()) {
|
if (!isSupplier()) {
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
<div
|
<div
|
||||||
className='flex items-center justify-center'
|
className='flex items-center justify-center'
|
||||||
style={{ minHeight: 'calc(100vh - 360px)' }}
|
style={{ minHeight: 'calc(100vh - 360px)' }}
|
||||||
|
|
@ -217,6 +220,7 @@ const PricingSettingsPage = () => {
|
||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ import SupplierApplicationsPage from '../../../components/table/supplier-applica
|
||||||
|
|
||||||
const SupplierApplication = () => {
|
const SupplierApplication = () => {
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<SupplierApplicationsPage />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<SupplierApplicationsPage />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ import SuppliersPage from '../../../components/table/suppliers';
|
||||||
|
|
||||||
const Suppliers = () => {
|
const Suppliers = () => {
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<SuppliersPage />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<SuppliersPage />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ import React from 'react';
|
||||||
import TaskLogsTable from '../../components/table/task-logs';
|
import TaskLogsTable from '../../components/table/task-logs';
|
||||||
|
|
||||||
const Task = () => (
|
const Task = () => (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<TaskLogsTable />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<TaskLogsTable />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ import TokensTable from '../../components/table/tokens';
|
||||||
|
|
||||||
const Token = () => {
|
const Token = () => {
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<TokensTable />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<TokensTable />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ import UsersTable from '../../components/table/users';
|
||||||
|
|
||||||
const User = () => {
|
const User = () => {
|
||||||
return (
|
return (
|
||||||
<div className='mt-[60px] px-2'>
|
<div className='min-h-full pb-16 pt-20 text-slate-900 dark:text-white'>
|
||||||
<UsersTable />
|
<div className='mx-auto w-full max-w-[1400px] px-4 sm:px-6 lg:px-10'>
|
||||||
|
<UsersTable />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export default defineConfig({
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
force: true,
|
include: ['date-fns/locale'],
|
||||||
esbuildOptions: {
|
esbuildOptions: {
|
||||||
loader: {
|
loader: {
|
||||||
'.js': 'jsx',
|
'.js': 'jsx',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue