Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
c821b0ed
authored
Jun 09, 2025
by
Calcium-Ion
Committed by
GitHub
Jun 09, 2025
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'alpha' into fix-balance-unit-sync
parents
d30ae820
be71bbb6
Hide whitespace changes
Inline
Side-by-side
Showing
53 changed files
with
3371 additions
and
1780 deletions
+3371
-1780
common/redis.go
+3
-3
controller/channel-test.go
+2
-2
controller/channel.go
+41
-0
controller/misc.go
+1
-0
controller/option.go
+9
-1
dto/claude.go
+67
-36
dto/openai_request.go
+260
-51
makefile
+1
-1
model/option.go
+1
-0
model/token_cache.go
+1
-1
model/user_cache.go
+3
-2
relay/channel/ali/text.go
+1
-2
relay/channel/baidu/relay-baidu.go
+1
-2
relay/channel/claude/relay-claude.go
+5
-8
relay/channel/cohere/relay-cohere.go
+1
-2
relay/channel/coze/dto.go
+1
-1
relay/channel/dify/relay-dify.go
+1
-2
relay/channel/gemini/relay-gemini.go
+1
-8
relay/channel/mistral/text.go
+1
-1
relay/channel/palm/relay-palm.go
+1
-2
relay/channel/tencent/relay-tencent.go
+1
-2
relay/channel/xunfei/relay-xunfei.go
+1
-2
relay/channel/zhipu/relay-zhipu.go
+1
-2
router/api-router.go
+1
-0
service/token_counter.go
+10
-6
setting/api_info.go
+125
-0
web/public/example.png
+0
-0
web/src/components/auth/LoginForm.js
+3
-15
web/src/components/auth/PasswordResetConfirm.js
+3
-15
web/src/components/auth/PasswordResetForm.js
+3
-15
web/src/components/auth/RegisterForm.js
+4
-14
web/src/components/settings/DashboardSetting.js
+58
-0
web/src/components/settings/PersonalSetting.js
+95
-94
web/src/components/table/ChannelsTable.js
+170
-294
web/src/components/table/LogsTable.js
+239
-141
web/src/components/table/MjLogsTable.js
+188
-103
web/src/components/table/ModelPricing.js
+14
-1
web/src/components/table/RedemptionsTable.js
+113
-36
web/src/components/table/TaskLogsTable.js
+159
-87
web/src/components/table/TokensTable.js
+139
-55
web/src/components/table/UsersTable.js
+146
-63
web/src/helpers/render.js
+155
-65
web/src/i18n/locales/en.json
+21
-1
web/src/index.css
+24
-0
web/src/index.js
+1
-0
web/src/pages/Channel/EditTagModal.js
+28
-4
web/src/pages/Detail/index.js
+582
-380
web/src/pages/Home/index.js
+53
-61
web/src/pages/Setting/Dashboard/SettingsAPIInfo.js
+400
-0
web/src/pages/Setting/index.js
+6
-1
web/src/pages/Setup/index.js
+1
-1
web/src/pages/Token/EditToken.js
+9
-3
web/src/pages/TopUp/index.js
+216
-194
No files found.
common/redis.go
View file @
c821b0ed
...
@@ -92,12 +92,12 @@ func RedisDel(key string) error {
...
@@ -92,12 +92,12 @@ func RedisDel(key string) error {
return
RDB
.
Del
(
ctx
,
key
)
.
Err
()
return
RDB
.
Del
(
ctx
,
key
)
.
Err
()
}
}
func
Redis
HDelObj
(
key
string
)
error
{
func
Redis
DelKey
(
key
string
)
error
{
if
DebugEnabled
{
if
DebugEnabled
{
SysLog
(
fmt
.
Sprintf
(
"Redis
HDEL
: key=%s"
,
key
))
SysLog
(
fmt
.
Sprintf
(
"Redis
DEL Key
: key=%s"
,
key
))
}
}
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
return
RDB
.
H
Del
(
ctx
,
key
)
.
Err
()
return
RDB
.
Del
(
ctx
,
key
)
.
Err
()
}
}
func
RedisHSetObj
(
key
string
,
obj
interface
{},
expiration
time
.
Duration
)
error
{
func
RedisHSetObj
(
key
string
,
obj
interface
{},
expiration
time
.
Duration
)
error
{
...
...
controller/channel-test.go
View file @
c821b0ed
...
@@ -200,10 +200,10 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
...
@@ -200,10 +200,10 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
}
else
{
}
else
{
testRequest
.
MaxTokens
=
10
testRequest
.
MaxTokens
=
10
}
}
content
,
_
:=
json
.
Marshal
(
"hi"
)
testMessage
:=
dto
.
Message
{
testMessage
:=
dto
.
Message
{
Role
:
"user"
,
Role
:
"user"
,
Content
:
content
,
Content
:
"hi"
,
}
}
testRequest
.
Model
=
model
testRequest
.
Model
=
model
testRequest
.
Messages
=
append
(
testRequest
.
Messages
,
testMessage
)
testRequest
.
Messages
=
append
(
testRequest
.
Messages
,
testMessage
)
...
...
controller/channel.go
View file @
c821b0ed
...
@@ -623,3 +623,44 @@ func BatchSetChannelTag(c *gin.Context) {
...
@@ -623,3 +623,44 @@ func BatchSetChannelTag(c *gin.Context) {
})
})
return
return
}
}
func
GetTagModels
(
c
*
gin
.
Context
)
{
tag
:=
c
.
Query
(
"tag"
)
if
tag
==
""
{
c
.
JSON
(
http
.
StatusBadRequest
,
gin
.
H
{
"success"
:
false
,
"message"
:
"tag不能为空"
,
})
return
}
channels
,
err
:=
model
.
GetChannelsByTag
(
tag
,
false
)
// Assuming false for idSort is fine here
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusInternalServerError
,
gin
.
H
{
"success"
:
false
,
"message"
:
err
.
Error
(),
})
return
}
var
longestModels
string
maxLength
:=
0
// Find the longest models string among all channels with the given tag
for
_
,
channel
:=
range
channels
{
if
channel
.
Models
!=
""
{
currentModels
:=
strings
.
Split
(
channel
.
Models
,
","
)
if
len
(
currentModels
)
>
maxLength
{
maxLength
=
len
(
currentModels
)
longestModels
=
channel
.
Models
}
}
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
longestModels
,
})
return
}
controller/misc.go
View file @
c821b0ed
...
@@ -74,6 +74,7 @@ func GetStatus(c *gin.Context) {
...
@@ -74,6 +74,7 @@ func GetStatus(c *gin.Context) {
"oidc_client_id"
:
system_setting
.
GetOIDCSettings
()
.
ClientId
,
"oidc_client_id"
:
system_setting
.
GetOIDCSettings
()
.
ClientId
,
"oidc_authorization_endpoint"
:
system_setting
.
GetOIDCSettings
()
.
AuthorizationEndpoint
,
"oidc_authorization_endpoint"
:
system_setting
.
GetOIDCSettings
()
.
AuthorizationEndpoint
,
"setup"
:
constant
.
Setup
,
"setup"
:
constant
.
Setup
,
"api_info"
:
setting
.
GetApiInfo
(),
},
},
})
})
return
return
...
...
controller/option.go
View file @
c821b0ed
...
@@ -119,7 +119,15 @@ func UpdateOption(c *gin.Context) {
...
@@ -119,7 +119,15 @@ func UpdateOption(c *gin.Context) {
})
})
return
return
}
}
case
"ApiInfo"
:
err
=
setting
.
ValidateApiInfo
(
option
.
Value
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
err
.
Error
(),
})
return
}
}
}
err
=
model
.
UpdateOption
(
option
.
Key
,
option
.
Value
)
err
=
model
.
UpdateOption
(
option
.
Key
,
option
.
Value
)
if
err
!=
nil
{
if
err
!=
nil
{
...
...
dto/claude.go
View file @
c821b0ed
package
dto
package
dto
import
"encoding/json"
import
(
"encoding/json"
"one-api/common"
)
type
ClaudeMetadata
struct
{
type
ClaudeMetadata
struct
{
UserId
string
`json:"user_id"`
UserId
string
`json:"user_id"`
...
@@ -20,11 +23,11 @@ type ClaudeMediaMessage struct {
...
@@ -20,11 +23,11 @@ type ClaudeMediaMessage struct {
Delta
string
`json:"delta,omitempty"`
Delta
string
`json:"delta,omitempty"`
CacheControl
json
.
RawMessage
`json:"cache_control,omitempty"`
CacheControl
json
.
RawMessage
`json:"cache_control,omitempty"`
// tool_calls
// tool_calls
Id
string
`json:"id,omitempty"`
Id
string
`json:"id,omitempty"`
Name
string
`json:"name,omitempty"`
Name
string
`json:"name,omitempty"`
Input
any
`json:"input,omitempty"`
Input
any
`json:"input,omitempty"`
Content
json
.
RawMessage
`json:"content,omitempty"`
Content
any
`json:"content,omitempty"`
ToolUseId
string
`json:"tool_use_id,omitempty"`
ToolUseId
string
`json:"tool_use_id,omitempty"`
}
}
func
(
c
*
ClaudeMediaMessage
)
SetText
(
s
string
)
{
func
(
c
*
ClaudeMediaMessage
)
SetText
(
s
string
)
{
...
@@ -39,15 +42,39 @@ func (c *ClaudeMediaMessage) GetText() string {
...
@@ -39,15 +42,39 @@ func (c *ClaudeMediaMessage) GetText() string {
}
}
func
(
c
*
ClaudeMediaMessage
)
IsStringContent
()
bool
{
func
(
c
*
ClaudeMediaMessage
)
IsStringContent
()
bool
{
var
content
string
if
c
.
Content
==
nil
{
return
json
.
Unmarshal
(
c
.
Content
,
&
content
)
==
nil
return
false
}
_
,
ok
:=
c
.
Content
.
(
string
)
if
ok
{
return
true
}
return
false
}
}
func
(
c
*
ClaudeMediaMessage
)
GetStringContent
()
string
{
func
(
c
*
ClaudeMediaMessage
)
GetStringContent
()
string
{
var
content
string
if
c
.
Content
==
nil
{
if
err
:=
json
.
Unmarshal
(
c
.
Content
,
&
content
);
err
==
nil
{
return
""
return
content
}
}
switch
c
.
Content
.
(
type
)
{
case
string
:
return
c
.
Content
.
(
string
)
case
[]
any
:
var
contentStr
string
for
_
,
contentItem
:=
range
c
.
Content
.
([]
any
)
{
contentMap
,
ok
:=
contentItem
.
(
map
[
string
]
any
)
if
!
ok
{
continue
}
if
contentMap
[
"type"
]
==
ContentTypeText
{
if
subStr
,
ok
:=
contentMap
[
"text"
]
.
(
string
);
ok
{
contentStr
+=
subStr
}
}
}
return
contentStr
}
return
""
return
""
}
}
...
@@ -57,16 +84,12 @@ func (c *ClaudeMediaMessage) GetJsonRowString() string {
...
@@ -57,16 +84,12 @@ func (c *ClaudeMediaMessage) GetJsonRowString() string {
}
}
func
(
c
*
ClaudeMediaMessage
)
SetContent
(
content
any
)
{
func
(
c
*
ClaudeMediaMessage
)
SetContent
(
content
any
)
{
jsonContent
,
_
:=
json
.
Marshal
(
content
)
c
.
Content
=
content
c
.
Content
=
jsonContent
}
}
func
(
c
*
ClaudeMediaMessage
)
ParseMediaContent
()
[]
ClaudeMediaMessage
{
func
(
c
*
ClaudeMediaMessage
)
ParseMediaContent
()
[]
ClaudeMediaMessage
{
var
mediaContent
[]
ClaudeMediaMessage
mediaContent
,
_
:=
common
.
Any2Type
[[]
ClaudeMediaMessage
](
c
.
Content
)
if
err
:=
json
.
Unmarshal
(
c
.
Content
,
&
mediaContent
);
err
==
nil
{
return
mediaContent
return
mediaContent
}
return
make
([]
ClaudeMediaMessage
,
0
)
}
}
type
ClaudeMessageSource
struct
{
type
ClaudeMessageSource
struct
{
...
@@ -82,14 +105,36 @@ type ClaudeMessage struct {
...
@@ -82,14 +105,36 @@ type ClaudeMessage struct {
}
}
func
(
c
*
ClaudeMessage
)
IsStringContent
()
bool
{
func
(
c
*
ClaudeMessage
)
IsStringContent
()
bool
{
if
c
.
Content
==
nil
{
return
false
}
_
,
ok
:=
c
.
Content
.
(
string
)
_
,
ok
:=
c
.
Content
.
(
string
)
return
ok
return
ok
}
}
func
(
c
*
ClaudeMessage
)
GetStringContent
()
string
{
func
(
c
*
ClaudeMessage
)
GetStringContent
()
string
{
if
c
.
IsStringContent
()
{
if
c
.
Content
==
nil
{
return
""
}
switch
c
.
Content
.
(
type
)
{
case
string
:
return
c
.
Content
.
(
string
)
return
c
.
Content
.
(
string
)
case
[]
any
:
var
contentStr
string
for
_
,
contentItem
:=
range
c
.
Content
.
([]
any
)
{
contentMap
,
ok
:=
contentItem
.
(
map
[
string
]
any
)
if
!
ok
{
continue
}
if
contentMap
[
"type"
]
==
ContentTypeText
{
if
subStr
,
ok
:=
contentMap
[
"text"
]
.
(
string
);
ok
{
contentStr
+=
subStr
}
}
}
return
contentStr
}
}
return
""
return
""
}
}
...
@@ -98,15 +143,7 @@ func (c *ClaudeMessage) SetStringContent(content string) {
...
@@ -98,15 +143,7 @@ func (c *ClaudeMessage) SetStringContent(content string) {
}
}
func
(
c
*
ClaudeMessage
)
ParseContent
()
([]
ClaudeMediaMessage
,
error
)
{
func
(
c
*
ClaudeMessage
)
ParseContent
()
([]
ClaudeMediaMessage
,
error
)
{
// map content to []ClaudeMediaMessage
return
common
.
Any2Type
[[]
ClaudeMediaMessage
](
c
.
Content
)
// parse to json
jsonContent
,
_
:=
json
.
Marshal
(
c
.
Content
)
var
contentList
[]
ClaudeMediaMessage
err
:=
json
.
Unmarshal
(
jsonContent
,
&
contentList
)
if
err
!=
nil
{
return
make
([]
ClaudeMediaMessage
,
0
),
err
}
return
contentList
,
nil
}
}
type
Tool
struct
{
type
Tool
struct
{
...
@@ -161,14 +198,8 @@ func (c *ClaudeRequest) SetStringSystem(system string) {
...
@@ -161,14 +198,8 @@ func (c *ClaudeRequest) SetStringSystem(system string) {
}
}
func
(
c
*
ClaudeRequest
)
ParseSystem
()
[]
ClaudeMediaMessage
{
func
(
c
*
ClaudeRequest
)
ParseSystem
()
[]
ClaudeMediaMessage
{
// map content to []ClaudeMediaMessage
mediaContent
,
_
:=
common
.
Any2Type
[[]
ClaudeMediaMessage
](
c
.
System
)
// parse to json
return
mediaContent
jsonContent
,
_
:=
json
.
Marshal
(
c
.
System
)
var
contentList
[]
ClaudeMediaMessage
if
err
:=
json
.
Unmarshal
(
jsonContent
,
&
contentList
);
err
==
nil
{
return
contentList
}
return
make
([]
ClaudeMediaMessage
,
0
)
}
}
type
ClaudeError
struct
{
type
ClaudeError
struct
{
...
...
dto/openai_request.go
View file @
c821b0ed
...
@@ -19,43 +19,43 @@ type FormatJsonSchema struct {
...
@@ -19,43 +19,43 @@ type FormatJsonSchema struct {
}
}
type
GeneralOpenAIRequest
struct
{
type
GeneralOpenAIRequest
struct
{
Model
string
`json:"model,omitempty"`
Model
string
`json:"model,omitempty"`
Messages
[]
Message
`json:"messages,omitempty"`
Messages
[]
Message
`json:"messages,omitempty"`
Prompt
any
`json:"prompt,omitempty"`
Prompt
any
`json:"prompt,omitempty"`
Prefix
any
`json:"prefix,omitempty"`
Prefix
any
`json:"prefix,omitempty"`
Suffix
any
`json:"suffix,omitempty"`
Suffix
any
`json:"suffix,omitempty"`
Stream
bool
`json:"stream,omitempty"`
Stream
bool
`json:"stream,omitempty"`
StreamOptions
*
StreamOptions
`json:"stream_options,omitempty"`
StreamOptions
*
StreamOptions
`json:"stream_options,omitempty"`
MaxTokens
uint
`json:"max_tokens,omitempty"`
MaxTokens
uint
`json:"max_tokens,omitempty"`
MaxCompletionTokens
uint
`json:"max_completion_tokens,omitempty"`
MaxCompletionTokens
uint
`json:"max_completion_tokens,omitempty"`
ReasoningEffort
string
`json:"reasoning_effort,omitempty"`
ReasoningEffort
string
`json:"reasoning_effort,omitempty"`
Temperature
*
float64
`json:"temperature,omitempty"`
Temperature
*
float64
`json:"temperature,omitempty"`
TopP
float64
`json:"top_p,omitempty"`
TopP
float64
`json:"top_p,omitempty"`
TopK
int
`json:"top_k,omitempty"`
TopK
int
`json:"top_k,omitempty"`
Stop
any
`json:"stop,omitempty"`
Stop
any
`json:"stop,omitempty"`
N
int
`json:"n,omitempty"`
N
int
`json:"n,omitempty"`
Input
any
`json:"input,omitempty"`
Input
any
`json:"input,omitempty"`
Instruction
string
`json:"instruction,omitempty"`
Instruction
string
`json:"instruction,omitempty"`
Size
string
`json:"size,omitempty"`
Size
string
`json:"size,omitempty"`
Functions
any
`json:"functions,omitempty"`
Functions
json
.
RawMessage
`json:"functions,omitempty"`
FrequencyPenalty
float64
`json:"frequency_penalty,omitempty"`
FrequencyPenalty
float64
`json:"frequency_penalty,omitempty"`
PresencePenalty
float64
`json:"presence_penalty,omitempty"`
PresencePenalty
float64
`json:"presence_penalty,omitempty"`
ResponseFormat
*
ResponseFormat
`json:"response_format,omitempty"`
ResponseFormat
*
ResponseFormat
`json:"response_format,omitempty"`
EncodingFormat
any
`json:"encoding_format,omitempty"`
EncodingFormat
json
.
RawMessage
`json:"encoding_format,omitempty"`
Seed
float64
`json:"seed,omitempty"`
Seed
float64
`json:"seed,omitempty"`
ParallelTooCalls
*
bool
`json:"parallel_tool_calls,omitempty"`
ParallelTooCalls
*
bool
`json:"parallel_tool_calls,omitempty"`
Tools
[]
ToolCallRequest
`json:"tools,omitempty"`
Tools
[]
ToolCallRequest
`json:"tools,omitempty"`
ToolChoice
any
`json:"tool_choice,omitempty"`
ToolChoice
any
`json:"tool_choice,omitempty"`
User
string
`json:"user,omitempty"`
User
string
`json:"user,omitempty"`
LogProbs
bool
`json:"logprobs,omitempty"`
LogProbs
bool
`json:"logprobs,omitempty"`
TopLogProbs
int
`json:"top_logprobs,omitempty"`
TopLogProbs
int
`json:"top_logprobs,omitempty"`
Dimensions
int
`json:"dimensions,omitempty"`
Dimensions
int
`json:"dimensions,omitempty"`
Modalities
any
`json:"modalities,omitempty"`
Modalities
json
.
RawMessage
`json:"modalities,omitempty"`
Audio
any
`json:"audio,omitempty"`
Audio
json
.
RawMessage
`json:"audio,omitempty"`
EnableThinking
any
`json:"enable_thinking,omitempty"`
// ali
EnableThinking
any
`json:"enable_thinking,omitempty"`
// ali
ExtraBody
any
`json:"extra_body,omitempty"`
ExtraBody
json
.
RawMessage
`json:"extra_body,omitempty"`
WebSearchOptions
*
WebSearchOptions
`json:"web_search_options,omitempty"`
WebSearchOptions
*
WebSearchOptions
`json:"web_search_options,omitempty"`
// OpenRouter Params
// OpenRouter Params
Reasoning
json
.
RawMessage
`json:"reasoning,omitempty"`
Reasoning
json
.
RawMessage
`json:"reasoning,omitempty"`
}
}
...
@@ -107,16 +107,16 @@ func (r *GeneralOpenAIRequest) ParseInput() []string {
...
@@ -107,16 +107,16 @@ func (r *GeneralOpenAIRequest) ParseInput() []string {
}
}
type
Message
struct
{
type
Message
struct
{
Role
string
`json:"role"`
Role
string
`json:"role"`
Content
json
.
RawMessage
`json:"content"`
Content
any
`json:"content"`
Name
*
string
`json:"name,omitempty"`
Name
*
string
`json:"name,omitempty"`
Prefix
*
bool
`json:"prefix,omitempty"`
Prefix
*
bool
`json:"prefix,omitempty"`
ReasoningContent
string
`json:"reasoning_content,omitempty"`
ReasoningContent
string
`json:"reasoning_content,omitempty"`
Reasoning
string
`json:"reasoning,omitempty"`
Reasoning
string
`json:"reasoning,omitempty"`
ToolCalls
json
.
RawMessage
`json:"tool_calls,omitempty"`
ToolCalls
json
.
RawMessage
`json:"tool_calls,omitempty"`
ToolCallId
string
`json:"tool_call_id,omitempty"`
ToolCallId
string
`json:"tool_call_id,omitempty"`
parsedContent
[]
MediaContent
parsedContent
[]
MediaContent
parsedStringContent
*
string
//
parsedStringContent *string
}
}
type
MediaContent
struct
{
type
MediaContent
struct
{
...
@@ -132,21 +132,50 @@ type MediaContent struct {
...
@@ -132,21 +132,50 @@ type MediaContent struct {
func
(
m
*
MediaContent
)
GetImageMedia
()
*
MessageImageUrl
{
func
(
m
*
MediaContent
)
GetImageMedia
()
*
MessageImageUrl
{
if
m
.
ImageUrl
!=
nil
{
if
m
.
ImageUrl
!=
nil
{
return
m
.
ImageUrl
.
(
*
MessageImageUrl
)
if
_
,
ok
:=
m
.
ImageUrl
.
(
*
MessageImageUrl
);
ok
{
return
m
.
ImageUrl
.
(
*
MessageImageUrl
)
}
if
itemMap
,
ok
:=
m
.
ImageUrl
.
(
map
[
string
]
any
);
ok
{
out
:=
&
MessageImageUrl
{
Url
:
common
.
Interface2String
(
itemMap
[
"url"
]),
Detail
:
common
.
Interface2String
(
itemMap
[
"detail"
]),
MimeType
:
common
.
Interface2String
(
itemMap
[
"mime_type"
]),
}
return
out
}
}
}
return
nil
return
nil
}
}
func
(
m
*
MediaContent
)
GetInputAudio
()
*
MessageInputAudio
{
func
(
m
*
MediaContent
)
GetInputAudio
()
*
MessageInputAudio
{
if
m
.
InputAudio
!=
nil
{
if
m
.
InputAudio
!=
nil
{
return
m
.
InputAudio
.
(
*
MessageInputAudio
)
if
_
,
ok
:=
m
.
InputAudio
.
(
*
MessageInputAudio
);
ok
{
return
m
.
InputAudio
.
(
*
MessageInputAudio
)
}
if
itemMap
,
ok
:=
m
.
InputAudio
.
(
map
[
string
]
any
);
ok
{
out
:=
&
MessageInputAudio
{
Data
:
common
.
Interface2String
(
itemMap
[
"data"
]),
Format
:
common
.
Interface2String
(
itemMap
[
"format"
]),
}
return
out
}
}
}
return
nil
return
nil
}
}
func
(
m
*
MediaContent
)
GetFile
()
*
MessageFile
{
func
(
m
*
MediaContent
)
GetFile
()
*
MessageFile
{
if
m
.
File
!=
nil
{
if
m
.
File
!=
nil
{
return
m
.
File
.
(
*
MessageFile
)
if
_
,
ok
:=
m
.
File
.
(
*
MessageFile
);
ok
{
return
m
.
File
.
(
*
MessageFile
)
}
if
itemMap
,
ok
:=
m
.
File
.
(
map
[
string
]
any
);
ok
{
out
:=
&
MessageFile
{
FileName
:
common
.
Interface2String
(
itemMap
[
"file_name"
]),
FileData
:
common
.
Interface2String
(
itemMap
[
"file_data"
]),
FileId
:
common
.
Interface2String
(
itemMap
[
"file_id"
]),
}
return
out
}
}
}
return
nil
return
nil
}
}
...
@@ -212,6 +241,186 @@ func (m *Message) SetToolCalls(toolCalls any) {
...
@@ -212,6 +241,186 @@ func (m *Message) SetToolCalls(toolCalls any) {
}
}
func
(
m
*
Message
)
StringContent
()
string
{
func
(
m
*
Message
)
StringContent
()
string
{
switch
m
.
Content
.
(
type
)
{
case
string
:
return
m
.
Content
.
(
string
)
case
[]
any
:
var
contentStr
string
for
_
,
contentItem
:=
range
m
.
Content
.
([]
any
)
{
contentMap
,
ok
:=
contentItem
.
(
map
[
string
]
any
)
if
!
ok
{
continue
}
if
contentMap
[
"type"
]
==
ContentTypeText
{
if
subStr
,
ok
:=
contentMap
[
"text"
]
.
(
string
);
ok
{
contentStr
+=
subStr
}
}
}
return
contentStr
}
return
""
}
func
(
m
*
Message
)
SetNullContent
()
{
m
.
Content
=
nil
m
.
parsedContent
=
nil
}
func
(
m
*
Message
)
SetStringContent
(
content
string
)
{
m
.
Content
=
content
m
.
parsedContent
=
nil
}
func
(
m
*
Message
)
SetMediaContent
(
content
[]
MediaContent
)
{
m
.
Content
=
content
m
.
parsedContent
=
content
}
func
(
m
*
Message
)
IsStringContent
()
bool
{
_
,
ok
:=
m
.
Content
.
(
string
)
if
ok
{
return
true
}
return
false
}
func
(
m
*
Message
)
ParseContent
()
[]
MediaContent
{
if
m
.
Content
==
nil
{
return
nil
}
if
len
(
m
.
parsedContent
)
>
0
{
return
m
.
parsedContent
}
var
contentList
[]
MediaContent
// 先尝试解析为字符串
content
,
ok
:=
m
.
Content
.
(
string
)
if
ok
{
contentList
=
[]
MediaContent
{{
Type
:
ContentTypeText
,
Text
:
content
,
}}
m
.
parsedContent
=
contentList
return
contentList
}
// 尝试解析为数组
//var arrayContent []map[string]interface{}
arrayContent
,
ok
:=
m
.
Content
.
([]
any
)
if
!
ok
{
return
contentList
}
for
_
,
contentItemAny
:=
range
arrayContent
{
mediaItem
,
ok
:=
contentItemAny
.
(
MediaContent
)
if
ok
{
contentList
=
append
(
contentList
,
mediaItem
)
continue
}
contentItem
,
ok
:=
contentItemAny
.
(
map
[
string
]
any
)
if
!
ok
{
continue
}
contentType
,
ok
:=
contentItem
[
"type"
]
.
(
string
)
if
!
ok
{
continue
}
switch
contentType
{
case
ContentTypeText
:
if
text
,
ok
:=
contentItem
[
"text"
]
.
(
string
);
ok
{
contentList
=
append
(
contentList
,
MediaContent
{
Type
:
ContentTypeText
,
Text
:
text
,
})
}
case
ContentTypeImageURL
:
imageUrl
:=
contentItem
[
"image_url"
]
temp
:=
&
MessageImageUrl
{
Detail
:
"high"
,
}
switch
v
:=
imageUrl
.
(
type
)
{
case
string
:
temp
.
Url
=
v
case
map
[
string
]
interface
{}
:
url
,
ok1
:=
v
[
"url"
]
.
(
string
)
detail
,
ok2
:=
v
[
"detail"
]
.
(
string
)
if
ok2
{
temp
.
Detail
=
detail
}
if
ok1
{
temp
.
Url
=
url
}
}
contentList
=
append
(
contentList
,
MediaContent
{
Type
:
ContentTypeImageURL
,
ImageUrl
:
temp
,
})
case
ContentTypeInputAudio
:
if
audioData
,
ok
:=
contentItem
[
"input_audio"
]
.
(
map
[
string
]
interface
{});
ok
{
data
,
ok1
:=
audioData
[
"data"
]
.
(
string
)
format
,
ok2
:=
audioData
[
"format"
]
.
(
string
)
if
ok1
&&
ok2
{
temp
:=
&
MessageInputAudio
{
Data
:
data
,
Format
:
format
,
}
contentList
=
append
(
contentList
,
MediaContent
{
Type
:
ContentTypeInputAudio
,
InputAudio
:
temp
,
})
}
}
case
ContentTypeFile
:
if
fileData
,
ok
:=
contentItem
[
"file"
]
.
(
map
[
string
]
interface
{});
ok
{
fileId
,
ok3
:=
fileData
[
"file_id"
]
.
(
string
)
if
ok3
{
contentList
=
append
(
contentList
,
MediaContent
{
Type
:
ContentTypeFile
,
File
:
&
MessageFile
{
FileId
:
fileId
,
},
})
}
else
{
fileName
,
ok1
:=
fileData
[
"filename"
]
.
(
string
)
fileDataStr
,
ok2
:=
fileData
[
"file_data"
]
.
(
string
)
if
ok1
&&
ok2
{
contentList
=
append
(
contentList
,
MediaContent
{
Type
:
ContentTypeFile
,
File
:
&
MessageFile
{
FileName
:
fileName
,
FileData
:
fileDataStr
,
},
})
}
}
}
case
ContentTypeVideoUrl
:
if
videoUrl
,
ok
:=
contentItem
[
"video_url"
]
.
(
string
);
ok
{
contentList
=
append
(
contentList
,
MediaContent
{
Type
:
ContentTypeVideoUrl
,
VideoUrl
:
&
MessageVideoUrl
{
Url
:
videoUrl
,
},
})
}
}
}
if
len
(
contentList
)
>
0
{
m
.
parsedContent
=
contentList
}
return
contentList
}
// old code
/*func (m *Message) StringContent() string {
if m.parsedStringContent != nil {
if m.parsedStringContent != nil {
return *m.parsedStringContent
return *m.parsedStringContent
}
}
...
@@ -382,7 +591,7 @@ func (m *Message) ParseContent() []MediaContent {
...
@@ -382,7 +591,7 @@ func (m *Message) ParseContent() []MediaContent {
m.parsedContent = contentList
m.parsedContent = contentList
}
}
return contentList
return contentList
}
}
*/
type
WebSearchOptions
struct
{
type
WebSearchOptions
struct
{
SearchContextSize
string
`json:"search_context_size,omitempty"`
SearchContextSize
string
`json:"search_context_size,omitempty"`
...
...
makefile
View file @
c821b0ed
...
@@ -7,7 +7,7 @@ all: build-frontend start-backend
...
@@ -7,7 +7,7 @@ all: build-frontend start-backend
build-frontend
:
build-frontend
:
@
echo
"Building frontend..."
@
echo
"Building frontend..."
@
cd
$(FRONTEND_DIR)
&&
npm install
&&
DISABLE_ESLINT_PLUGIN
=
'true'
VITE_REACT_APP_VERSION
=
$
(
cat
VERSION
)
npm
run build
@
cd
$(FRONTEND_DIR)
&&
bun install
&&
DISABLE_ESLINT_PLUGIN
=
'true'
VITE_REACT_APP_VERSION
=
$
(
cat
VERSION
)
bun
run build
start-backend
:
start-backend
:
@
echo
"Starting backend dev server..."
@
echo
"Starting backend dev server..."
...
...
model/option.go
View file @
c821b0ed
...
@@ -122,6 +122,7 @@ func InitOptionMap() {
...
@@ -122,6 +122,7 @@ func InitOptionMap() {
common
.
OptionMap
[
"SensitiveWords"
]
=
setting
.
SensitiveWordsToString
()
common
.
OptionMap
[
"SensitiveWords"
]
=
setting
.
SensitiveWordsToString
()
common
.
OptionMap
[
"StreamCacheQueueLength"
]
=
strconv
.
Itoa
(
setting
.
StreamCacheQueueLength
)
common
.
OptionMap
[
"StreamCacheQueueLength"
]
=
strconv
.
Itoa
(
setting
.
StreamCacheQueueLength
)
common
.
OptionMap
[
"AutomaticDisableKeywords"
]
=
operation_setting
.
AutomaticDisableKeywordsToString
()
common
.
OptionMap
[
"AutomaticDisableKeywords"
]
=
operation_setting
.
AutomaticDisableKeywordsToString
()
common
.
OptionMap
[
"ApiInfo"
]
=
""
// 自动添加所有注册的模型配置
// 自动添加所有注册的模型配置
modelConfigs
:=
config
.
GlobalConfig
.
ExportAllConfigs
()
modelConfigs
:=
config
.
GlobalConfig
.
ExportAllConfigs
()
...
...
model/token_cache.go
View file @
c821b0ed
...
@@ -19,7 +19,7 @@ func cacheSetToken(token Token) error {
...
@@ -19,7 +19,7 @@ func cacheSetToken(token Token) error {
func
cacheDeleteToken
(
key
string
)
error
{
func
cacheDeleteToken
(
key
string
)
error
{
key
=
common
.
GenerateHMAC
(
key
)
key
=
common
.
GenerateHMAC
(
key
)
err
:=
common
.
Redis
HDelObj
(
fmt
.
Sprintf
(
"token:%s"
,
key
))
err
:=
common
.
Redis
DelKey
(
fmt
.
Sprintf
(
"token:%s"
,
key
))
if
err
!=
nil
{
if
err
!=
nil
{
return
err
return
err
}
}
...
...
model/user_cache.go
View file @
c821b0ed
...
@@ -3,11 +3,12 @@ package model
...
@@ -3,11 +3,12 @@ package model
import
(
import
(
"encoding/json"
"encoding/json"
"fmt"
"fmt"
"github.com/gin-gonic/gin"
"one-api/common"
"one-api/common"
"one-api/constant"
"one-api/constant"
"time"
"time"
"github.com/gin-gonic/gin"
"github.com/bytedance/gopkg/util/gopool"
"github.com/bytedance/gopkg/util/gopool"
)
)
...
@@ -57,7 +58,7 @@ func invalidateUserCache(userId int) error {
...
@@ -57,7 +58,7 @@ func invalidateUserCache(userId int) error {
if
!
common
.
RedisEnabled
{
if
!
common
.
RedisEnabled
{
return
nil
return
nil
}
}
return
common
.
Redis
HDelObj
(
getUserCacheKey
(
userId
))
return
common
.
Redis
DelKey
(
getUserCacheKey
(
userId
))
}
}
// updateUserCache updates all user cache fields using hash
// updateUserCache updates all user cache fields using hash
...
...
relay/channel/ali/text.go
View file @
c821b0ed
...
@@ -96,12 +96,11 @@ func embeddingResponseAli2OpenAI(response *AliEmbeddingResponse, model string) *
...
@@ -96,12 +96,11 @@ func embeddingResponseAli2OpenAI(response *AliEmbeddingResponse, model string) *
}
}
func
responseAli2OpenAI
(
response
*
AliResponse
)
*
dto
.
OpenAITextResponse
{
func
responseAli2OpenAI
(
response
*
AliResponse
)
*
dto
.
OpenAITextResponse
{
content
,
_
:=
json
.
Marshal
(
response
.
Output
.
Text
)
choice
:=
dto
.
OpenAITextResponseChoice
{
choice
:=
dto
.
OpenAITextResponseChoice
{
Index
:
0
,
Index
:
0
,
Message
:
dto
.
Message
{
Message
:
dto
.
Message
{
Role
:
"assistant"
,
Role
:
"assistant"
,
Content
:
conten
t
,
Content
:
response
.
Output
.
Tex
t
,
},
},
FinishReason
:
response
.
Output
.
FinishReason
,
FinishReason
:
response
.
Output
.
FinishReason
,
}
}
...
...
relay/channel/baidu/relay-baidu.go
View file @
c821b0ed
...
@@ -53,12 +53,11 @@ func requestOpenAI2Baidu(request dto.GeneralOpenAIRequest) *BaiduChatRequest {
...
@@ -53,12 +53,11 @@ func requestOpenAI2Baidu(request dto.GeneralOpenAIRequest) *BaiduChatRequest {
}
}
func
responseBaidu2OpenAI
(
response
*
BaiduChatResponse
)
*
dto
.
OpenAITextResponse
{
func
responseBaidu2OpenAI
(
response
*
BaiduChatResponse
)
*
dto
.
OpenAITextResponse
{
content
,
_
:=
json
.
Marshal
(
response
.
Result
)
choice
:=
dto
.
OpenAITextResponseChoice
{
choice
:=
dto
.
OpenAITextResponseChoice
{
Index
:
0
,
Index
:
0
,
Message
:
dto
.
Message
{
Message
:
dto
.
Message
{
Role
:
"assistant"
,
Role
:
"assistant"
,
Content
:
conten
t
,
Content
:
response
.
Resul
t
,
},
},
FinishReason
:
"stop"
,
FinishReason
:
"stop"
,
}
}
...
...
relay/channel/claude/relay-claude.go
View file @
c821b0ed
...
@@ -48,9 +48,9 @@ func RequestOpenAI2ClaudeComplete(textRequest dto.GeneralOpenAIRequest) *dto.Cla
...
@@ -48,9 +48,9 @@ func RequestOpenAI2ClaudeComplete(textRequest dto.GeneralOpenAIRequest) *dto.Cla
prompt
:=
""
prompt
:=
""
for
_
,
message
:=
range
textRequest
.
Messages
{
for
_
,
message
:=
range
textRequest
.
Messages
{
if
message
.
Role
==
"user"
{
if
message
.
Role
==
"user"
{
prompt
+=
fmt
.
Sprintf
(
"
\n\n
Human: %s"
,
message
.
Content
)
prompt
+=
fmt
.
Sprintf
(
"
\n\n
Human: %s"
,
message
.
StringContent
()
)
}
else
if
message
.
Role
==
"assistant"
{
}
else
if
message
.
Role
==
"assistant"
{
prompt
+=
fmt
.
Sprintf
(
"
\n\n
Assistant: %s"
,
message
.
Content
)
prompt
+=
fmt
.
Sprintf
(
"
\n\n
Assistant: %s"
,
message
.
StringContent
()
)
}
else
if
message
.
Role
==
"system"
{
}
else
if
message
.
Role
==
"system"
{
if
prompt
==
""
{
if
prompt
==
""
{
prompt
=
message
.
StringContent
()
prompt
=
message
.
StringContent
()
...
@@ -155,15 +155,13 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.Cla
...
@@ -155,15 +155,13 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.Cla
}
}
if
lastMessage
.
Role
==
message
.
Role
&&
lastMessage
.
Role
!=
"tool"
{
if
lastMessage
.
Role
==
message
.
Role
&&
lastMessage
.
Role
!=
"tool"
{
if
lastMessage
.
IsStringContent
()
&&
message
.
IsStringContent
()
{
if
lastMessage
.
IsStringContent
()
&&
message
.
IsStringContent
()
{
content
,
_
:=
json
.
Marshal
(
strings
.
Trim
(
fmt
.
Sprintf
(
"%s %s"
,
lastMessage
.
StringContent
(),
message
.
StringContent
()),
"
\"
"
))
fmtMessage
.
SetStringContent
(
strings
.
Trim
(
fmt
.
Sprintf
(
"%s %s"
,
lastMessage
.
StringContent
(),
message
.
StringContent
()),
"
\"
"
))
fmtMessage
.
Content
=
content
// delete last message
// delete last message
formatMessages
=
formatMessages
[
:
len
(
formatMessages
)
-
1
]
formatMessages
=
formatMessages
[
:
len
(
formatMessages
)
-
1
]
}
}
}
}
if
fmtMessage
.
Content
==
nil
{
if
fmtMessage
.
Content
==
nil
{
content
,
_
:=
json
.
Marshal
(
"..."
)
fmtMessage
.
SetStringContent
(
"..."
)
fmtMessage
.
Content
=
content
}
}
formatMessages
=
append
(
formatMessages
,
fmtMessage
)
formatMessages
=
append
(
formatMessages
,
fmtMessage
)
lastMessage
=
fmtMessage
lastMessage
=
fmtMessage
...
@@ -397,12 +395,11 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse) *dto
...
@@ -397,12 +395,11 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse) *dto
thinkingContent
:=
""
thinkingContent
:=
""
if
reqMode
==
RequestModeCompletion
{
if
reqMode
==
RequestModeCompletion
{
content
,
_
:=
json
.
Marshal
(
strings
.
TrimPrefix
(
claudeResponse
.
Completion
,
" "
))
choice
:=
dto
.
OpenAITextResponseChoice
{
choice
:=
dto
.
OpenAITextResponseChoice
{
Index
:
0
,
Index
:
0
,
Message
:
dto
.
Message
{
Message
:
dto
.
Message
{
Role
:
"assistant"
,
Role
:
"assistant"
,
Content
:
content
,
Content
:
strings
.
TrimPrefix
(
claudeResponse
.
Completion
,
" "
)
,
Name
:
nil
,
Name
:
nil
,
},
},
FinishReason
:
stopReasonClaude2OpenAI
(
claudeResponse
.
StopReason
),
FinishReason
:
stopReasonClaude2OpenAI
(
claudeResponse
.
StopReason
),
...
...
relay/channel/cohere/relay-cohere.go
View file @
c821b0ed
...
@@ -195,11 +195,10 @@ func cohereHandler(c *gin.Context, resp *http.Response, modelName string, prompt
...
@@ -195,11 +195,10 @@ func cohereHandler(c *gin.Context, resp *http.Response, modelName string, prompt
openaiResp
.
Model
=
modelName
openaiResp
.
Model
=
modelName
openaiResp
.
Usage
=
usage
openaiResp
.
Usage
=
usage
content
,
_
:=
json
.
Marshal
(
cohereResp
.
Text
)
openaiResp
.
Choices
=
[]
dto
.
OpenAITextResponseChoice
{
openaiResp
.
Choices
=
[]
dto
.
OpenAITextResponseChoice
{
{
{
Index
:
0
,
Index
:
0
,
Message
:
dto
.
Message
{
Content
:
co
nten
t
,
Role
:
"assistant"
},
Message
:
dto
.
Message
{
Content
:
co
hereResp
.
Tex
t
,
Role
:
"assistant"
},
FinishReason
:
stopReasonCohere2OpenAI
(
cohereResp
.
FinishReason
),
FinishReason
:
stopReasonCohere2OpenAI
(
cohereResp
.
FinishReason
),
},
},
}
}
...
...
relay/channel/coze/dto.go
View file @
c821b0ed
...
@@ -10,7 +10,7 @@ type CozeError struct {
...
@@ -10,7 +10,7 @@ type CozeError struct {
type
CozeEnterMessage
struct
{
type
CozeEnterMessage
struct
{
Role
string
`json:"role"`
Role
string
`json:"role"`
Type
string
`json:"type,omitempty"`
Type
string
`json:"type,omitempty"`
Content
json
.
RawMessage
`json:"content,omitempty"`
Content
any
`json:"content,omitempty"`
MetaData
json
.
RawMessage
`json:"meta_data,omitempty"`
MetaData
json
.
RawMessage
`json:"meta_data,omitempty"`
ContentType
string
`json:"content_type,omitempty"`
ContentType
string
`json:"content_type,omitempty"`
}
}
...
...
relay/channel/dify/relay-dify.go
View file @
c821b0ed
...
@@ -278,12 +278,11 @@ func difyHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInf
...
@@ -278,12 +278,11 @@ func difyHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInf
Created
:
common
.
GetTimestamp
(),
Created
:
common
.
GetTimestamp
(),
Usage
:
difyResponse
.
MetaData
.
Usage
,
Usage
:
difyResponse
.
MetaData
.
Usage
,
}
}
content
,
_
:=
json
.
Marshal
(
difyResponse
.
Answer
)
choice
:=
dto
.
OpenAITextResponseChoice
{
choice
:=
dto
.
OpenAITextResponseChoice
{
Index
:
0
,
Index
:
0
,
Message
:
dto
.
Message
{
Message
:
dto
.
Message
{
Role
:
"assistant"
,
Role
:
"assistant"
,
Content
:
content
,
Content
:
difyResponse
.
Answer
,
},
},
FinishReason
:
"stop"
,
FinishReason
:
"stop"
,
}
}
...
...
relay/channel/gemini/relay-gemini.go
View file @
c821b0ed
...
@@ -175,12 +175,6 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
...
@@ -175,12 +175,6 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
// common.SysLog("tools: " + fmt.Sprintf("%+v", geminiRequest.Tools))
// common.SysLog("tools: " + fmt.Sprintf("%+v", geminiRequest.Tools))
// json_data, _ := json.Marshal(geminiRequest.Tools)
// json_data, _ := json.Marshal(geminiRequest.Tools)
// common.SysLog("tools_json: " + string(json_data))
// common.SysLog("tools_json: " + string(json_data))
}
else
if
textRequest
.
Functions
!=
nil
{
//geminiRequest.Tools = []GeminiChatTool{
// {
// FunctionDeclarations: textRequest.Functions,
// },
//}
}
}
if
textRequest
.
ResponseFormat
!=
nil
&&
(
textRequest
.
ResponseFormat
.
Type
==
"json_schema"
||
textRequest
.
ResponseFormat
.
Type
==
"json_object"
)
{
if
textRequest
.
ResponseFormat
!=
nil
&&
(
textRequest
.
ResponseFormat
.
Type
==
"json_schema"
||
textRequest
.
ResponseFormat
.
Type
==
"json_object"
)
{
...
@@ -609,14 +603,13 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
...
@@ -609,14 +603,13 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
Created
:
common
.
GetTimestamp
(),
Created
:
common
.
GetTimestamp
(),
Choices
:
make
([]
dto
.
OpenAITextResponseChoice
,
0
,
len
(
response
.
Candidates
)),
Choices
:
make
([]
dto
.
OpenAITextResponseChoice
,
0
,
len
(
response
.
Candidates
)),
}
}
content
,
_
:=
json
.
Marshal
(
""
)
isToolCall
:=
false
isToolCall
:=
false
for
_
,
candidate
:=
range
response
.
Candidates
{
for
_
,
candidate
:=
range
response
.
Candidates
{
choice
:=
dto
.
OpenAITextResponseChoice
{
choice
:=
dto
.
OpenAITextResponseChoice
{
Index
:
int
(
candidate
.
Index
),
Index
:
int
(
candidate
.
Index
),
Message
:
dto
.
Message
{
Message
:
dto
.
Message
{
Role
:
"assistant"
,
Role
:
"assistant"
,
Content
:
content
,
Content
:
""
,
},
},
FinishReason
:
constant
.
FinishReasonStop
,
FinishReason
:
constant
.
FinishReasonStop
,
}
}
...
...
relay/channel/mistral/text.go
View file @
c821b0ed
...
@@ -47,7 +47,7 @@ func requestOpenAI2Mistral(request *dto.GeneralOpenAIRequest) *dto.GeneralOpenAI
...
@@ -47,7 +47,7 @@ func requestOpenAI2Mistral(request *dto.GeneralOpenAIRequest) *dto.GeneralOpenAI
}
}
mediaMessages
:=
message
.
ParseContent
()
mediaMessages
:=
message
.
ParseContent
()
if
message
.
Role
==
"assistant"
&&
message
.
ToolCalls
!=
nil
&&
string
(
message
.
Content
)
==
"null
"
{
if
message
.
Role
==
"assistant"
&&
message
.
ToolCalls
!=
nil
&&
message
.
Content
==
"
"
{
mediaMessages
=
[]
dto
.
MediaContent
{}
mediaMessages
=
[]
dto
.
MediaContent
{}
}
}
for
j
,
mediaMessage
:=
range
mediaMessages
{
for
j
,
mediaMessage
:=
range
mediaMessages
{
...
...
relay/channel/palm/relay-palm.go
View file @
c821b0ed
...
@@ -45,12 +45,11 @@ func responsePaLM2OpenAI(response *PaLMChatResponse) *dto.OpenAITextResponse {
...
@@ -45,12 +45,11 @@ func responsePaLM2OpenAI(response *PaLMChatResponse) *dto.OpenAITextResponse {
Choices
:
make
([]
dto
.
OpenAITextResponseChoice
,
0
,
len
(
response
.
Candidates
)),
Choices
:
make
([]
dto
.
OpenAITextResponseChoice
,
0
,
len
(
response
.
Candidates
)),
}
}
for
i
,
candidate
:=
range
response
.
Candidates
{
for
i
,
candidate
:=
range
response
.
Candidates
{
content
,
_
:=
json
.
Marshal
(
candidate
.
Content
)
choice
:=
dto
.
OpenAITextResponseChoice
{
choice
:=
dto
.
OpenAITextResponseChoice
{
Index
:
i
,
Index
:
i
,
Message
:
dto
.
Message
{
Message
:
dto
.
Message
{
Role
:
"assistant"
,
Role
:
"assistant"
,
Content
:
content
,
Content
:
c
andidate
.
C
ontent
,
},
},
FinishReason
:
"stop"
,
FinishReason
:
"stop"
,
}
}
...
...
relay/channel/tencent/relay-tencent.go
View file @
c821b0ed
...
@@ -56,12 +56,11 @@ func responseTencent2OpenAI(response *TencentChatResponse) *dto.OpenAITextRespon
...
@@ -56,12 +56,11 @@ func responseTencent2OpenAI(response *TencentChatResponse) *dto.OpenAITextRespon
},
},
}
}
if
len
(
response
.
Choices
)
>
0
{
if
len
(
response
.
Choices
)
>
0
{
content
,
_
:=
json
.
Marshal
(
response
.
Choices
[
0
]
.
Messages
.
Content
)
choice
:=
dto
.
OpenAITextResponseChoice
{
choice
:=
dto
.
OpenAITextResponseChoice
{
Index
:
0
,
Index
:
0
,
Message
:
dto
.
Message
{
Message
:
dto
.
Message
{
Role
:
"assistant"
,
Role
:
"assistant"
,
Content
:
c
ontent
,
Content
:
response
.
Choices
[
0
]
.
Messages
.
C
ontent
,
},
},
FinishReason
:
response
.
Choices
[
0
]
.
FinishReason
,
FinishReason
:
response
.
Choices
[
0
]
.
FinishReason
,
}
}
...
...
relay/channel/xunfei/relay-xunfei.go
View file @
c821b0ed
...
@@ -61,12 +61,11 @@ func responseXunfei2OpenAI(response *XunfeiChatResponse) *dto.OpenAITextResponse
...
@@ -61,12 +61,11 @@ func responseXunfei2OpenAI(response *XunfeiChatResponse) *dto.OpenAITextResponse
},
},
}
}
}
}
content
,
_
:=
json
.
Marshal
(
response
.
Payload
.
Choices
.
Text
[
0
]
.
Content
)
choice
:=
dto
.
OpenAITextResponseChoice
{
choice
:=
dto
.
OpenAITextResponseChoice
{
Index
:
0
,
Index
:
0
,
Message
:
dto
.
Message
{
Message
:
dto
.
Message
{
Role
:
"assistant"
,
Role
:
"assistant"
,
Content
:
c
ontent
,
Content
:
response
.
Payload
.
Choices
.
Text
[
0
]
.
C
ontent
,
},
},
FinishReason
:
constant
.
FinishReasonStop
,
FinishReason
:
constant
.
FinishReasonStop
,
}
}
...
...
relay/channel/zhipu/relay-zhipu.go
View file @
c821b0ed
...
@@ -108,12 +108,11 @@ func responseZhipu2OpenAI(response *ZhipuResponse) *dto.OpenAITextResponse {
...
@@ -108,12 +108,11 @@ func responseZhipu2OpenAI(response *ZhipuResponse) *dto.OpenAITextResponse {
Usage
:
response
.
Data
.
Usage
,
Usage
:
response
.
Data
.
Usage
,
}
}
for
i
,
choice
:=
range
response
.
Data
.
Choices
{
for
i
,
choice
:=
range
response
.
Data
.
Choices
{
content
,
_
:=
json
.
Marshal
(
strings
.
Trim
(
choice
.
Content
,
"
\"
"
))
openaiChoice
:=
dto
.
OpenAITextResponseChoice
{
openaiChoice
:=
dto
.
OpenAITextResponseChoice
{
Index
:
i
,
Index
:
i
,
Message
:
dto
.
Message
{
Message
:
dto
.
Message
{
Role
:
choice
.
Role
,
Role
:
choice
.
Role
,
Content
:
content
,
Content
:
strings
.
Trim
(
choice
.
Content
,
"
\"
"
)
,
},
},
FinishReason
:
""
,
FinishReason
:
""
,
}
}
...
...
router/api-router.go
View file @
c821b0ed
...
@@ -105,6 +105,7 @@ func SetApiRouter(router *gin.Engine) {
...
@@ -105,6 +105,7 @@ func SetApiRouter(router *gin.Engine) {
channelRoute
.
GET
(
"/fetch_models/:id"
,
controller
.
FetchUpstreamModels
)
channelRoute
.
GET
(
"/fetch_models/:id"
,
controller
.
FetchUpstreamModels
)
channelRoute
.
POST
(
"/fetch_models"
,
controller
.
FetchModels
)
channelRoute
.
POST
(
"/fetch_models"
,
controller
.
FetchModels
)
channelRoute
.
POST
(
"/batch/tag"
,
controller
.
BatchSetChannelTag
)
channelRoute
.
POST
(
"/batch/tag"
,
controller
.
BatchSetChannelTag
)
channelRoute
.
GET
(
"/tag/models"
,
controller
.
GetTagModels
)
}
}
tokenRoute
:=
apiRouter
.
Group
(
"/token"
)
tokenRoute
:=
apiRouter
.
Group
(
"/token"
)
tokenRoute
.
Use
(
middleware
.
UserAuth
())
tokenRoute
.
Use
(
middleware
.
UserAuth
())
...
...
service/token_counter.go
View file @
c821b0ed
...
@@ -261,12 +261,16 @@ func CountTokenClaudeMessages(messages []dto.ClaudeMessage, model string, stream
...
@@ -261,12 +261,16 @@ func CountTokenClaudeMessages(messages []dto.ClaudeMessage, model string, stream
//}
//}
tokenNum
+=
1000
tokenNum
+=
1000
case
"tool_use"
:
case
"tool_use"
:
tokenNum
+=
getTokenNum
(
tokenEncoder
,
mediaMessage
.
Name
)
if
mediaMessage
.
Input
!=
nil
{
inputJSON
,
_
:=
json
.
Marshal
(
mediaMessage
.
Input
)
tokenNum
+=
getTokenNum
(
tokenEncoder
,
mediaMessage
.
Name
)
tokenNum
+=
getTokenNum
(
tokenEncoder
,
string
(
inputJSON
))
inputJSON
,
_
:=
json
.
Marshal
(
mediaMessage
.
Input
)
tokenNum
+=
getTokenNum
(
tokenEncoder
,
string
(
inputJSON
))
}
case
"tool_result"
:
case
"tool_result"
:
contentJSON
,
_
:=
json
.
Marshal
(
mediaMessage
.
Content
)
if
mediaMessage
.
Content
!=
nil
{
tokenNum
+=
getTokenNum
(
tokenEncoder
,
string
(
contentJSON
))
contentJSON
,
_
:=
json
.
Marshal
(
mediaMessage
.
Content
)
tokenNum
+=
getTokenNum
(
tokenEncoder
,
string
(
contentJSON
))
}
}
}
}
}
}
}
...
@@ -386,7 +390,7 @@ func CountTokenMessages(info *relaycommon.RelayInfo, messages []dto.Message, mod
...
@@ -386,7 +390,7 @@ func CountTokenMessages(info *relaycommon.RelayInfo, messages []dto.Message, mod
for
_
,
message
:=
range
messages
{
for
_
,
message
:=
range
messages
{
tokenNum
+=
tokensPerMessage
tokenNum
+=
tokensPerMessage
tokenNum
+=
getTokenNum
(
tokenEncoder
,
message
.
Role
)
tokenNum
+=
getTokenNum
(
tokenEncoder
,
message
.
Role
)
if
len
(
message
.
Content
)
>
0
{
if
message
.
Content
!=
nil
{
if
message
.
Name
!=
nil
{
if
message
.
Name
!=
nil
{
tokenNum
+=
tokensPerName
tokenNum
+=
tokensPerName
tokenNum
+=
getTokenNum
(
tokenEncoder
,
*
message
.
Name
)
tokenNum
+=
getTokenNum
(
tokenEncoder
,
*
message
.
Name
)
...
...
setting/api_info.go
0 → 100644
View file @
c821b0ed
package
setting
import
(
"encoding/json"
"fmt"
"net/url"
"one-api/common"
"regexp"
"strings"
)
// ValidateApiInfo 验证API信息格式
func
ValidateApiInfo
(
apiInfoStr
string
)
error
{
if
apiInfoStr
==
""
{
return
nil
// 空字符串是合法的
}
var
apiInfoList
[]
map
[
string
]
interface
{}
if
err
:=
json
.
Unmarshal
([]
byte
(
apiInfoStr
),
&
apiInfoList
);
err
!=
nil
{
return
fmt
.
Errorf
(
"API信息格式错误:%s"
,
err
.
Error
())
}
// 验证数组长度
if
len
(
apiInfoList
)
>
50
{
return
fmt
.
Errorf
(
"API信息数量不能超过50个"
)
}
// 允许的颜色值
validColors
:=
map
[
string
]
bool
{
"blue"
:
true
,
"green"
:
true
,
"cyan"
:
true
,
"purple"
:
true
,
"pink"
:
true
,
"red"
:
true
,
"orange"
:
true
,
"amber"
:
true
,
"yellow"
:
true
,
"lime"
:
true
,
"light-green"
:
true
,
"teal"
:
true
,
"light-blue"
:
true
,
"indigo"
:
true
,
"violet"
:
true
,
"grey"
:
true
,
}
// URL正则表达式
urlRegex
:=
regexp
.
MustCompile
(
`^https?://[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(/.*)?$`
)
for
i
,
apiInfo
:=
range
apiInfoList
{
// 检查必填字段
urlStr
,
ok
:=
apiInfo
[
"url"
]
.
(
string
)
if
!
ok
||
urlStr
==
""
{
return
fmt
.
Errorf
(
"第%d个API信息缺少URL字段"
,
i
+
1
)
}
route
,
ok
:=
apiInfo
[
"route"
]
.
(
string
)
if
!
ok
||
route
==
""
{
return
fmt
.
Errorf
(
"第%d个API信息缺少线路描述字段"
,
i
+
1
)
}
description
,
ok
:=
apiInfo
[
"description"
]
.
(
string
)
if
!
ok
||
description
==
""
{
return
fmt
.
Errorf
(
"第%d个API信息缺少说明字段"
,
i
+
1
)
}
color
,
ok
:=
apiInfo
[
"color"
]
.
(
string
)
if
!
ok
||
color
==
""
{
return
fmt
.
Errorf
(
"第%d个API信息缺少颜色字段"
,
i
+
1
)
}
// 验证URL格式
if
!
urlRegex
.
MatchString
(
urlStr
)
{
return
fmt
.
Errorf
(
"第%d个API信息的URL格式不正确"
,
i
+
1
)
}
// 验证URL可解析性
if
_
,
err
:=
url
.
Parse
(
urlStr
);
err
!=
nil
{
return
fmt
.
Errorf
(
"第%d个API信息的URL无法解析:%s"
,
i
+
1
,
err
.
Error
())
}
// 验证字段长度
if
len
(
urlStr
)
>
500
{
return
fmt
.
Errorf
(
"第%d个API信息的URL长度不能超过500字符"
,
i
+
1
)
}
if
len
(
route
)
>
100
{
return
fmt
.
Errorf
(
"第%d个API信息的线路描述长度不能超过100字符"
,
i
+
1
)
}
if
len
(
description
)
>
200
{
return
fmt
.
Errorf
(
"第%d个API信息的说明长度不能超过200字符"
,
i
+
1
)
}
// 验证颜色值
if
!
validColors
[
color
]
{
return
fmt
.
Errorf
(
"第%d个API信息的颜色值不合法"
,
i
+
1
)
}
// 检查并过滤危险字符(防止XSS)
dangerousChars
:=
[]
string
{
"<script"
,
"<iframe"
,
"javascript:"
,
"onload="
,
"onerror="
,
"onclick="
}
for
_
,
dangerous
:=
range
dangerousChars
{
if
strings
.
Contains
(
strings
.
ToLower
(
description
),
dangerous
)
{
return
fmt
.
Errorf
(
"第%d个API信息的说明包含不允许的内容"
,
i
+
1
)
}
if
strings
.
Contains
(
strings
.
ToLower
(
route
),
dangerous
)
{
return
fmt
.
Errorf
(
"第%d个API信息的线路描述包含不允许的内容"
,
i
+
1
)
}
}
}
return
nil
}
// GetApiInfo 获取API信息列表
func
GetApiInfo
()
[]
map
[
string
]
interface
{}
{
// 从OptionMap中获取API信息,如果不存在则返回空数组
common
.
OptionMapRWMutex
.
RLock
()
apiInfoStr
,
exists
:=
common
.
OptionMap
[
"ApiInfo"
]
common
.
OptionMapRWMutex
.
RUnlock
()
if
!
exists
||
apiInfoStr
==
""
{
// 如果没有配置,返回空数组
return
[]
map
[
string
]
interface
{}{}
}
// 解析存储的API信息
var
apiInfo
[]
map
[
string
]
interface
{}
if
err
:=
json
.
Unmarshal
([]
byte
(
apiInfoStr
),
&
apiInfo
);
err
!=
nil
{
// 如果解析失败,返回空数组
return
[]
map
[
string
]
interface
{}{}
}
return
apiInfo
}
\ No newline at end of file
web/public/example.png
deleted
100644 → 0
View file @
d30ae820
551 KB
web/src/components/auth/LoginForm.js
View file @
c821b0ed
...
@@ -32,7 +32,6 @@ import OIDCIcon from '../common/logo/OIDCIcon.js';
...
@@ -32,7 +32,6 @@ import OIDCIcon from '../common/logo/OIDCIcon.js';
import
WeChatIcon
from
'../common/logo/WeChatIcon.js'
;
import
WeChatIcon
from
'../common/logo/WeChatIcon.js'
;
import
LinuxDoIcon
from
'../common/logo/LinuxDoIcon.js'
;
import
LinuxDoIcon
from
'../common/logo/LinuxDoIcon.js'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
Background
from
'/example.png'
;
const
LoginForm
=
()
=>
{
const
LoginForm
=
()
=>
{
const
[
inputs
,
setInputs
]
=
useState
({
const
[
inputs
,
setInputs
]
=
useState
({
...
@@ -266,7 +265,7 @@ const LoginForm = () => {
...
@@ -266,7 +265,7 @@ const LoginForm = () => {
<
div
className
=
"w-full max-w-md"
>
<
div
className
=
"w-full max-w-md"
>
<
div
className
=
"flex items-center justify-center mb-6 gap-2"
>
<
div
className
=
"flex items-center justify-center mb-6 gap-2"
>
<
img
src
=
{
logo
}
alt
=
"Logo"
className
=
"h-10 rounded-full"
/>
<
img
src
=
{
logo
}
alt
=
"Logo"
className
=
"h-10 rounded-full"
/>
<
Title
heading
=
{
3
}
className
=
'!text-
white
'
>
{
systemName
}
<
/Title
>
<
Title
heading
=
{
3
}
className
=
'!text-
gray-800
'
>
{
systemName
}
<
/Title
>
<
/div
>
<
/div
>
<
Card
className
=
"shadow-xl border-0 !rounded-2xl overflow-hidden"
>
<
Card
className
=
"shadow-xl border-0 !rounded-2xl overflow-hidden"
>
...
@@ -500,19 +499,8 @@ const LoginForm = () => {
...
@@ -500,19 +499,8 @@ const LoginForm = () => {
};
};
return
(
return
(
<
div
className
=
"min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden"
>
<
div
className
=
"bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"
>
{
/* 背景图片容器 - 放大并保持居中 */
}
<
div
className
=
"w-full max-w-sm"
>
<
div
className
=
"absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
style
=
{{
backgroundImage
:
`url(
${
Background
}
)`
}}
><
/div
>
{
/* 半透明遮罩层 */
}
<
div
className
=
"absolute inset-0 bg-gradient-to-br from-teal-500/30 via-blue-500/30 to-purple-500/30 backdrop-blur-sm z-0"
><
/div
>
<
div
className
=
"w-full max-w-sm relative z-10"
>
{
showEmailLogin
||
!
(
status
.
github_oauth
||
status
.
oidc_enabled
||
status
.
wechat_login
||
status
.
linuxdo_oauth
||
status
.
telegram_oauth
)
{
showEmailLogin
||
!
(
status
.
github_oauth
||
status
.
oidc_enabled
||
status
.
wechat_login
||
status
.
linuxdo_oauth
||
status
.
telegram_oauth
)
?
renderEmailLoginForm
()
?
renderEmailLoginForm
()
:
renderOAuthOptions
()}
:
renderOAuthOptions
()}
...
...
web/src/components/auth/PasswordResetConfirm.js
View file @
c821b0ed
...
@@ -4,7 +4,6 @@ import { useSearchParams, Link } from 'react-router-dom';
...
@@ -4,7 +4,6 @@ import { useSearchParams, Link } from 'react-router-dom';
import
{
Button
,
Card
,
Form
,
Typography
,
Banner
}
from
'@douyinfe/semi-ui'
;
import
{
Button
,
Card
,
Form
,
Typography
,
Banner
}
from
'@douyinfe/semi-ui'
;
import
{
IconMail
,
IconLock
,
IconCopy
}
from
'@douyinfe/semi-icons'
;
import
{
IconMail
,
IconLock
,
IconCopy
}
from
'@douyinfe/semi-icons'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
Background
from
'/example.png'
;
const
{
Text
,
Title
}
=
Typography
;
const
{
Text
,
Title
}
=
Typography
;
...
@@ -79,24 +78,13 @@ const PasswordResetConfirm = () => {
...
@@ -79,24 +78,13 @@ const PasswordResetConfirm = () => {
}
}
return
(
return
(
<
div
className
=
"min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden"
>
<
div
className
=
"bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"
>
{
/* 背景图片容器 - 放大并保持居中 */
}
<
div
className
=
"w-full max-w-sm"
>
<
div
className
=
"absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
style
=
{{
backgroundImage
:
`url(
${
Background
}
)`
}}
><
/div
>
{
/* 半透明遮罩层 */
}
<
div
className
=
"absolute inset-0 bg-gradient-to-br from-teal-500/30 via-blue-500/30 to-purple-500/30 backdrop-blur-sm z-0"
><
/div
>
<
div
className
=
"w-full max-w-sm relative z-10"
>
<
div
className
=
"flex flex-col items-center"
>
<
div
className
=
"flex flex-col items-center"
>
<
div
className
=
"w-full max-w-md"
>
<
div
className
=
"w-full max-w-md"
>
<
div
className
=
"flex items-center justify-center mb-6 gap-2"
>
<
div
className
=
"flex items-center justify-center mb-6 gap-2"
>
<
img
src
=
{
logo
}
alt
=
"Logo"
className
=
"h-10 rounded-full"
/>
<
img
src
=
{
logo
}
alt
=
"Logo"
className
=
"h-10 rounded-full"
/>
<
Title
heading
=
{
3
}
className
=
'!text-
white
'
>
{
systemName
}
<
/Title
>
<
Title
heading
=
{
3
}
className
=
'!text-
gray-800
'
>
{
systemName
}
<
/Title
>
<
/div
>
<
/div
>
<
Card
className
=
"shadow-xl border-0 !rounded-2xl overflow-hidden"
>
<
Card
className
=
"shadow-xl border-0 !rounded-2xl overflow-hidden"
>
...
...
web/src/components/auth/PasswordResetForm.js
View file @
c821b0ed
...
@@ -5,7 +5,6 @@ import { Button, Card, Form, Typography } from '@douyinfe/semi-ui';
...
@@ -5,7 +5,6 @@ import { Button, Card, Form, Typography } from '@douyinfe/semi-ui';
import
{
IconMail
}
from
'@douyinfe/semi-icons'
;
import
{
IconMail
}
from
'@douyinfe/semi-icons'
;
import
{
Link
}
from
'react-router-dom'
;
import
{
Link
}
from
'react-router-dom'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
Background
from
'/example.png'
;
const
{
Text
,
Title
}
=
Typography
;
const
{
Text
,
Title
}
=
Typography
;
...
@@ -79,24 +78,13 @@ const PasswordResetForm = () => {
...
@@ -79,24 +78,13 @@ const PasswordResetForm = () => {
}
}
return
(
return
(
<
div
className
=
"min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden"
>
<
div
className
=
"bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"
>
{
/* 背景图片容器 - 放大并保持居中 */
}
<
div
className
=
"w-full max-w-sm"
>
<
div
className
=
"absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
style
=
{{
backgroundImage
:
`url(
${
Background
}
)`
}}
><
/div
>
{
/* 半透明遮罩层 */
}
<
div
className
=
"absolute inset-0 bg-gradient-to-br from-teal-500/30 via-blue-500/30 to-purple-500/30 backdrop-blur-sm z-0"
><
/div
>
<
div
className
=
"w-full max-w-sm relative z-10"
>
<
div
className
=
"flex flex-col items-center"
>
<
div
className
=
"flex flex-col items-center"
>
<
div
className
=
"w-full max-w-md"
>
<
div
className
=
"w-full max-w-md"
>
<
div
className
=
"flex items-center justify-center mb-6 gap-2"
>
<
div
className
=
"flex items-center justify-center mb-6 gap-2"
>
<
img
src
=
{
logo
}
alt
=
"Logo"
className
=
"h-10 rounded-full"
/>
<
img
src
=
{
logo
}
alt
=
"Logo"
className
=
"h-10 rounded-full"
/>
<
Title
heading
=
{
3
}
className
=
'!text-
white
'
>
{
systemName
}
<
/Title
>
<
Title
heading
=
{
3
}
className
=
'!text-
gray-800
'
>
{
systemName
}
<
/Title
>
<
/div
>
<
/div
>
<
Card
className
=
"shadow-xl border-0 !rounded-2xl overflow-hidden"
>
<
Card
className
=
"shadow-xl border-0 !rounded-2xl overflow-hidden"
>
...
...
web/src/components/auth/RegisterForm.js
View file @
c821b0ed
...
@@ -33,7 +33,6 @@ import WeChatIcon from '../common/logo/WeChatIcon.js';
...
@@ -33,7 +33,6 @@ import WeChatIcon from '../common/logo/WeChatIcon.js';
import
TelegramLoginButton
from
'react-telegram-login/src'
;
import
TelegramLoginButton
from
'react-telegram-login/src'
;
import
{
UserContext
}
from
'../../context/User/index.js'
;
import
{
UserContext
}
from
'../../context/User/index.js'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
Background
from
'/example.png'
;
const
RegisterForm
=
()
=>
{
const
RegisterForm
=
()
=>
{
const
{
t
}
=
useTranslation
();
const
{
t
}
=
useTranslation
();
...
@@ -272,7 +271,7 @@ const RegisterForm = () => {
...
@@ -272,7 +271,7 @@ const RegisterForm = () => {
<
div
className
=
"w-full max-w-md"
>
<
div
className
=
"w-full max-w-md"
>
<
div
className
=
"flex items-center justify-center mb-6 gap-2"
>
<
div
className
=
"flex items-center justify-center mb-6 gap-2"
>
<
img
src
=
{
logo
}
alt
=
"Logo"
className
=
"h-10 rounded-full"
/>
<
img
src
=
{
logo
}
alt
=
"Logo"
className
=
"h-10 rounded-full"
/>
<
Title
heading
=
{
3
}
className
=
'!text-
white
'
>
{
systemName
}
<
/Title
>
<
Title
heading
=
{
3
}
className
=
'!text-
gray-800
'
>
{
systemName
}
<
/Title
>
<
/div
>
<
/div
>
<
Card
className
=
"shadow-xl border-0 !rounded-2xl overflow-hidden"
>
<
Card
className
=
"shadow-xl border-0 !rounded-2xl overflow-hidden"
>
...
@@ -379,7 +378,7 @@ const RegisterForm = () => {
...
@@ -379,7 +378,7 @@ const RegisterForm = () => {
<
div
className
=
"w-full max-w-md"
>
<
div
className
=
"w-full max-w-md"
>
<
div
className
=
"flex items-center justify-center mb-6 gap-2"
>
<
div
className
=
"flex items-center justify-center mb-6 gap-2"
>
<
img
src
=
{
logo
}
alt
=
"Logo"
className
=
"h-10 rounded-full"
/>
<
img
src
=
{
logo
}
alt
=
"Logo"
className
=
"h-10 rounded-full"
/>
<
Title
heading
=
{
3
}
className
=
'!text-
white
'
>
{
systemName
}
<
/Title
>
<
Title
heading
=
{
3
}
className
=
'!text-
gray-800
'
>
{
systemName
}
<
/Title
>
<
/div
>
<
/div
>
<
Card
className
=
"shadow-xl border-0 !rounded-2xl overflow-hidden"
>
<
Card
className
=
"shadow-xl border-0 !rounded-2xl overflow-hidden"
>
...
@@ -542,17 +541,8 @@ const RegisterForm = () => {
...
@@ -542,17 +541,8 @@ const RegisterForm = () => {
};
};
return
(
return
(
<
div
className
=
"min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden"
>
<
div
className
=
"bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"
>
<
div
<
div
className
=
"w-full max-w-sm"
>
className
=
"absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
style
=
{{
backgroundImage
:
`url(
${
Background
}
)`
}}
><
/div
>
<
div
className
=
"absolute inset-0 bg-gradient-to-br from-teal-500/30 via-blue-500/30 to-purple-500/30 backdrop-blur-sm z-0"
><
/div
>
<
div
className
=
"w-full max-w-sm relative z-10"
>
{
showEmailRegister
||
!
(
status
.
github_oauth
||
status
.
oidc_enabled
||
status
.
wechat_login
||
status
.
linuxdo_oauth
||
status
.
telegram_oauth
)
{
showEmailRegister
||
!
(
status
.
github_oauth
||
status
.
oidc_enabled
||
status
.
wechat_login
||
status
.
linuxdo_oauth
||
status
.
telegram_oauth
)
?
renderEmailRegisterForm
()
?
renderEmailRegisterForm
()
:
renderOAuthOptions
()}
:
renderOAuthOptions
()}
...
...
web/src/components/settings/DashboardSetting.js
0 → 100644
View file @
c821b0ed
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
Card
,
Spin
}
from
'@douyinfe/semi-ui'
;
import
{
API
,
showError
}
from
'../../helpers'
;
import
SettingsAPIInfo
from
'../../pages/Setting/Dashboard/SettingsAPIInfo.js'
;
const
DashboardSetting
=
()
=>
{
let
[
inputs
,
setInputs
]
=
useState
({
ApiInfo
:
''
,
});
let
[
loading
,
setLoading
]
=
useState
(
false
);
const
getOptions
=
async
()
=>
{
const
res
=
await
API
.
get
(
'/api/option/'
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
let
newInputs
=
{};
data
.
forEach
((
item
)
=>
{
if
(
item
.
key
in
inputs
)
{
newInputs
[
item
.
key
]
=
item
.
value
;
}
});
setInputs
(
newInputs
);
}
else
{
showError
(
message
);
}
};
async
function
onRefresh
()
{
try
{
setLoading
(
true
);
await
getOptions
();
}
catch
(
error
)
{
showError
(
'刷新失败'
);
console
.
error
(
error
);
}
finally
{
setLoading
(
false
);
}
}
useEffect
(()
=>
{
onRefresh
();
},
[]);
return
(
<>
<
Spin
spinning
=
{
loading
}
size
=
'large'
>
{
/* API信息管理 */
}
<
Card
style
=
{{
marginTop
:
'10px'
}}
>
<
SettingsAPIInfo
options
=
{
inputs
}
refresh
=
{
onRefresh
}
/
>
<
/Card
>
<
/Spin
>
<
/
>
);
};
export
default
DashboardSetting
;
\ No newline at end of file
web/src/components/settings/PersonalSetting.js
View file @
c821b0ed
...
@@ -104,6 +104,33 @@ const PersonalSetting = () => {
...
@@ -104,6 +104,33 @@ const PersonalSetting = () => {
});
});
const
[
modelsLoading
,
setModelsLoading
]
=
useState
(
true
);
const
[
modelsLoading
,
setModelsLoading
]
=
useState
(
true
);
const
[
showWebhookDocs
,
setShowWebhookDocs
]
=
useState
(
true
);
const
[
showWebhookDocs
,
setShowWebhookDocs
]
=
useState
(
true
);
const
[
isDarkMode
,
setIsDarkMode
]
=
useState
(
false
);
// 检测暗色模式
useEffect
(()
=>
{
const
checkDarkMode
=
()
=>
{
const
isDark
=
document
.
documentElement
.
classList
.
contains
(
'dark'
)
||
window
.
matchMedia
(
'(prefers-color-scheme: dark)'
).
matches
;
setIsDarkMode
(
isDark
);
};
checkDarkMode
();
// 监听主题变化
const
observer
=
new
MutationObserver
(
checkDarkMode
);
observer
.
observe
(
document
.
documentElement
,
{
attributes
:
true
,
attributeFilter
:
[
'class'
]
});
const
mediaQuery
=
window
.
matchMedia
(
'(prefers-color-scheme: dark)'
);
mediaQuery
.
addListener
(
checkDarkMode
);
return
()
=>
{
observer
.
disconnect
();
mediaQuery
.
removeListener
(
checkDarkMode
);
};
},
[]);
useEffect
(()
=>
{
useEffect
(()
=>
{
let
status
=
localStorage
.
getItem
(
'status'
);
let
status
=
localStorage
.
getItem
(
'status'
);
...
@@ -384,107 +411,81 @@ const PersonalSetting = () => {
...
@@ -384,107 +411,81 @@ const PersonalSetting = () => {
<
Card
className
=
"!rounded-2xl shadow-lg border-0"
>
<
Card
className
=
"!rounded-2xl shadow-lg border-0"
>
{
/* 顶部用户信息区域 */
}
{
/* 顶部用户信息区域 */
}
<
Card
<
Card
className
=
"!rounded-2xl !border-0 !shadow-
2xl
overflow-hidden"
className
=
"!rounded-2xl !border-0 !shadow-
lg
overflow-hidden"
style
=
{{
style
=
{{
background
:
'linear-gradient(135deg, #6366f1 0%, #8b5cf6 25%, #a855f7 50%, #c084fc 75%, #d8b4fe 100%)'
,
background
:
isDarkMode
?
'linear-gradient(135deg, #1e293b 0%, #334155 50%, #475569 100%)'
:
'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%)'
,
position
:
'relative'
position
:
'relative'
}}
}}
bodyStyle
=
{{
padding
:
0
}}
bodyStyle
=
{{
padding
:
0
}}
>
>
{
/* 装饰性背景元素 */
}
{
/* 装饰性背景元素 */
}
<
div
className
=
"absolute inset-0 overflow-hidden"
>
<
div
className
=
"absolute inset-0 overflow-hidden"
>
<
div
className
=
"absolute -top-10 -right-10 w-40 h-40 bg-
white
opacity-5 rounded-full"
><
/div
>
<
div
className
=
"absolute -top-10 -right-10 w-40 h-40 bg-
slate-400 dark:bg-slate-500
opacity-5 rounded-full"
><
/div
>
<
div
className
=
"absolute -bottom-16 -left-16 w-48 h-48 bg-
white opacity-3
rounded-full"
><
/div
>
<
div
className
=
"absolute -bottom-16 -left-16 w-48 h-48 bg-
slate-300 dark:bg-slate-400 opacity-8
rounded-full"
><
/div
>
<
div
className
=
"absolute top-1/2 right-1/4 w-24 h-24 bg-
yellow-400 opacity-10
rounded-full"
><
/div
>
<
div
className
=
"absolute top-1/2 right-1/4 w-24 h-24 bg-
slate-400 dark:bg-slate-500 opacity-6
rounded-full"
><
/div
>
<
/div
>
<
/div
>
<
div
className
=
"relative p-4 sm:p-6 md:p-8
"
style
=
{{
color
:
'white'
}}
>
<
div
className
=
"relative p-4 sm:p-6 md:p-8
text-gray-600 dark:text-gray-300"
>
<
div
className
=
"flex justify-between items-start mb-4 sm:mb-6"
>
<
div
className
=
"flex justify-between items-start mb-4 sm:mb-6"
>
<
div
className
=
"flex items-center flex-1 min-w-0"
>
<
div
className
=
"flex items-center flex-1 min-w-0"
>
<
Avatar
<
Avatar
size
=
'large'
size
=
'large'
color
=
{
stringToColor
(
getUsername
())}
className
=
"mr-3 sm:mr-4 shadow-md flex-shrink-0 bg-slate-500 dark:bg-slate-400"
border
=
{{
motion
:
true
}}
contentMotion
=
{
true
}
className
=
"mr-3 sm:mr-4 shadow-lg flex-shrink-0"
>
>
{
getAvatarText
()}
{
getAvatarText
()}
<
/Avatar
>
<
/Avatar
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"text-base sm:text-lg font-semibold truncate
"
style
=
{{
color
:
'white'
}}
>
<
div
className
=
"text-base sm:text-lg font-semibold truncate
text-gray-800 dark:text-gray-100"
>
{
getUsername
()}
{
getUsername
()}
<
/div
>
<
/div
>
<
div
className
=
"mt-1 flex flex-wrap gap-1 sm:gap-2"
>
<
div
className
=
"mt-1 flex flex-wrap gap-1 sm:gap-2"
>
{
isRoot
()
?
(
{
isRoot
()
?
(
<
Tag
<
Tag
color
=
'red'
size
=
'small'
size
=
'small'
style
=
{{
className
=
"!rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border border-gray-200 dark:border-gray-600"
backgroundColor
:
'rgba(255, 255, 255, 0.95)'
,
style
=
{{
fontWeight
:
'500'
}}
color
:
'#dc2626'
,
fontWeight
:
'600'
}}
className
=
"!rounded-full"
>
>
{
t
(
'超级管理员'
)}
{
t
(
'超级管理员'
)}
<
/Tag
>
<
/Tag
>
)
:
isAdmin
()
?
(
)
:
isAdmin
()
?
(
<
Tag
<
Tag
color
=
'orange'
size
=
'small'
size
=
'small'
style
=
{{
className
=
"!rounded-full bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border border-gray-200 dark:border-gray-600"
backgroundColor
:
'rgba(255, 255, 255, 0.95)'
,
style
=
{{
fontWeight
:
'500'
}}
color
:
'#ea580c'
,
fontWeight
:
'600'
}}
className
=
"!rounded-full"
>
>
{
t
(
'管理员'
)}
{
t
(
'管理员'
)}
<
/Tag
>
<
/Tag
>
)
:
(
)
:
(
<
Tag
<
Tag
color
=
'blue'
size
=
'small'
size
=
'small'
style
=
{{
className
=
"!rounded-full bg-slate-50 dark:bg-slate-700 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-600"
backgroundColor
:
'rgba(255, 255, 255, 0.95)'
,
style
=
{{
fontWeight
:
'500'
}}
color
:
'#2563eb'
,
fontWeight
:
'600'
}}
className
=
"!rounded-full"
>
>
{
t
(
'普通用户'
)}
{
t
(
'普通用户'
)}
<
/Tag
>
<
/Tag
>
)}
)}
<
Tag
<
Tag
color
=
'green'
size
=
'small'
size
=
'small'
className
=
"!rounded-full"
className
=
"!rounded-full bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-600"
style
=
{{
style
=
{{
fontWeight
:
'500'
}}
backgroundColor
:
'rgba(255, 255, 255, 0.95)'
,
color
:
'#16a34a'
,
fontWeight
:
'600'
}}
>
>
ID
:
{
userState
?.
user
?.
id
}
ID
:
{
userState
?.
user
?.
id
}
<
/Tag
>
<
/Tag
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
<
div
className
=
"w-10 h-10 sm:w-12 sm:h-12 rounded-lg flex items-center justify-center shadow-md flex-shrink-0 ml-2 bg-slate-400 dark:bg-slate-500"
>
className
=
"w-10 h-10 sm:w-12 sm:h-12 rounded-lg flex items-center justify-center shadow-lg flex-shrink-0 ml-2"
<
IconUser
size
=
"default"
className
=
"text-white"
/>
style
=
{{
background
:
`linear-gradient(135deg,
${
stringToColor
(
getUsername
())}
0%, #f59e0b 100%)`
}}
>
<
IconUser
size
=
"default"
style
=
{{
color
:
'white'
}}
/
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
className
=
"mb-4 sm:mb-6"
>
<
div
className
=
"mb-4 sm:mb-6"
>
<
div
className
=
"text-xs sm:text-sm mb-1 sm:mb-2
"
style
=
{{
color
:
'rgba(255, 255, 255, 0.7)'
}}
>
<
div
className
=
"text-xs sm:text-sm mb-1 sm:mb-2
text-gray-500 dark:text-gray-400"
>
{
t
(
'当前余额'
)}
{
t
(
'当前余额'
)}
<
/div
>
<
/div
>
<
div
className
=
"text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide
"
style
=
{{
color
:
'white'
}}
>
<
div
className
=
"text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide
text-gray-900 dark:text-gray-100"
>
{
renderQuota
(
userState
?.
user
?.
quota
)}
{
renderQuota
(
userState
?.
user
?.
quota
)}
<
/div
>
<
/div
>
<
/div
>
<
/div
>
...
@@ -492,33 +493,33 @@ const PersonalSetting = () => {
...
@@ -492,33 +493,33 @@ const PersonalSetting = () => {
<
div
className
=
"flex flex-col sm:flex-row sm:justify-between sm:items-end"
>
<
div
className
=
"flex flex-col sm:flex-row sm:justify-between sm:items-end"
>
<
div
className
=
"grid grid-cols-3 gap-2 sm:flex sm:space-x-6 lg:space-x-8 mb-3 sm:mb-0"
>
<
div
className
=
"grid grid-cols-3 gap-2 sm:flex sm:space-x-6 lg:space-x-8 mb-3 sm:mb-0"
>
<
div
className
=
"text-center sm:text-left"
>
<
div
className
=
"text-center sm:text-left"
>
<
div
className
=
"text-xs
"
style
=
{{
color
:
'rgba(255, 255, 255, 0.6)'
}}
>
<
div
className
=
"text-xs
text-gray-400 dark:text-gray-500"
>
{
t
(
'历史消耗'
)}
{
t
(
'历史消耗'
)}
<
/div
>
<
/div
>
<
div
className
=
"text-xs sm:text-sm font-medium truncate
"
style
=
{{
color
:
'white'
}}
>
<
div
className
=
"text-xs sm:text-sm font-medium truncate
text-gray-600 dark:text-gray-300"
>
{
renderQuota
(
userState
?.
user
?.
used_quota
)}
{
renderQuota
(
userState
?.
user
?.
used_quota
)}
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
className
=
"text-center sm:text-left"
>
<
div
className
=
"text-center sm:text-left"
>
<
div
className
=
"text-xs
"
style
=
{{
color
:
'rgba(255, 255, 255, 0.6)'
}}
>
<
div
className
=
"text-xs
text-gray-400 dark:text-gray-500"
>
{
t
(
'请求次数'
)}
{
t
(
'请求次数'
)}
<
/div
>
<
/div
>
<
div
className
=
"text-xs sm:text-sm font-medium truncate
"
style
=
{{
color
:
'white'
}}
>
<
div
className
=
"text-xs sm:text-sm font-medium truncate
text-gray-600 dark:text-gray-300"
>
{
userState
.
user
?.
request_count
||
0
}
{
userState
.
user
?.
request_count
||
0
}
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
className
=
"text-center sm:text-left"
>
<
div
className
=
"text-center sm:text-left"
>
<
div
className
=
"text-xs
"
style
=
{{
color
:
'rgba(255, 255, 255, 0.6)'
}}
>
<
div
className
=
"text-xs
text-gray-400 dark:text-gray-500"
>
{
t
(
'用户分组'
)}
{
t
(
'用户分组'
)}
<
/div
>
<
/div
>
<
div
className
=
"text-xs sm:text-sm font-medium truncate
"
style
=
{{
color
:
'white'
}}
>
<
div
className
=
"text-xs sm:text-sm font-medium truncate
text-gray-600 dark:text-gray-300"
>
{
userState
?.
user
?.
group
||
t
(
'默认'
)}
{
userState
?.
user
?.
group
||
t
(
'默认'
)}
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
className
=
"absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-
yellow-400 via-orange-400 to-red-400"
style
=
{{
opacity
:
0.6
}}
><
/div
>
<
div
className
=
"absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-
slate-300 via-slate-400 to-slate-500 dark:from-slate-600 dark:via-slate-500 dark:to-slate-400 opacity-40"
><
/div
>
<
/div
>
<
/div
>
<
/Card
>
<
/Card
>
...
@@ -537,10 +538,10 @@ const PersonalSetting = () => {
...
@@ -537,10 +538,10 @@ const PersonalSetting = () => {
>
>
<
div
className
=
"gap-6 py-4"
>
<
div
className
=
"gap-6 py-4"
>
{
/* 可用模型部分 */
}
{
/* 可用模型部分 */
}
<
div
className
=
"bg-gray-50 rounded-xl"
>
<
div
className
=
"bg-gray-50
dark:bg-gray-800
rounded-xl"
>
<
div
className
=
"flex items-center mb-4"
>
<
div
className
=
"flex items-center mb-4"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
purple-5
0 flex items-center justify-center mr-3"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
slate-100 dark:bg-slate-70
0 flex items-center justify-center mr-3"
>
<
Settings
size
=
{
20
}
className
=
"text-
purple-5
00"
/>
<
Settings
size
=
{
20
}
className
=
"text-
slate-600 dark:text-slate-3
00"
/>
<
/div
>
<
/div
>
<
div
>
<
div
>
<
Typography
.
Title
heading
=
{
6
}
className
=
"mb-0"
>
{
t
(
'模型列表'
)}
<
/Typography.Title
>
<
Typography
.
Title
heading
=
{
6
}
className
=
"mb-0"
>
{
t
(
'模型列表'
)}
<
/Typography.Title
>
...
@@ -629,7 +630,7 @@ const PersonalSetting = () => {
...
@@ -629,7 +630,7 @@ const PersonalSetting = () => {
<
/Tabs
>
<
/Tabs
>
<
/div
>
<
/div
>
<
div
className
=
"bg-white rounded-lg p-3"
>
<
div
className
=
"bg-white
dark:bg-gray-700
rounded-lg p-3"
>
{(()
=>
{
{(()
=>
{
// 根据当前选中的分类过滤模型
// 根据当前选中的分类过滤模型
const
categories
=
getModelCategories
(
t
);
const
categories
=
getModelCategories
(
t
);
...
@@ -736,9 +737,9 @@ const PersonalSetting = () => {
...
@@ -736,9 +737,9 @@ const PersonalSetting = () => {
shadows
=
'hover'
shadows
=
'hover'
>
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center flex-1"
>
<
div
className
=
"flex items-center flex-1"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
red-5
0 flex items-center justify-center mr-3"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
slate-100 dark:bg-slate-70
0 flex items-center justify-center mr-3"
>
<
IconMail
size
=
"default"
className
=
"text-
red-5
00"
/>
<
IconMail
size
=
"default"
className
=
"text-
slate-600 dark:text-slate-3
00"
/>
<
/div
>
<
/div
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"font-medium text-gray-900"
>
{
t
(
'邮箱'
)}
<
/div
>
<
div
className
=
"font-medium text-gray-900"
>
{
t
(
'邮箱'
)}
<
/div
>
...
@@ -771,8 +772,8 @@ const PersonalSetting = () => {
...
@@ -771,8 +772,8 @@ const PersonalSetting = () => {
>
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center flex-1"
>
<
div
className
=
"flex items-center flex-1"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
green-5
0 flex items-center justify-center mr-3"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
slate-100 dark:bg-slate-70
0 flex items-center justify-center mr-3"
>
<
SiWechat
size
=
{
20
}
className
=
"text-
green-5
00"
/>
<
SiWechat
size
=
{
20
}
className
=
"text-
slate-600 dark:text-slate-3
00"
/>
<
/div
>
<
/div
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"font-medium text-gray-900"
>
{
t
(
'微信'
)}
<
/div
>
<
div
className
=
"font-medium text-gray-900"
>
{
t
(
'微信'
)}
<
/div
>
...
@@ -808,8 +809,8 @@ const PersonalSetting = () => {
...
@@ -808,8 +809,8 @@ const PersonalSetting = () => {
>
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center flex-1"
>
<
div
className
=
"flex items-center flex-1"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
gray-1
00 flex items-center justify-center mr-3"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
slate-100 dark:bg-slate-7
00 flex items-center justify-center mr-3"
>
<
IconGithubLogo
size
=
"default"
className
=
"text-
gray-7
00"
/>
<
IconGithubLogo
size
=
"default"
className
=
"text-
slate-600 dark:text-slate-3
00"
/>
<
/div
>
<
/div
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"font-medium text-gray-900"
>
{
t
(
'GitHub'
)}
<
/div
>
<
div
className
=
"font-medium text-gray-900"
>
{
t
(
'GitHub'
)}
<
/div
>
...
@@ -844,8 +845,8 @@ const PersonalSetting = () => {
...
@@ -844,8 +845,8 @@ const PersonalSetting = () => {
>
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center flex-1"
>
<
div
className
=
"flex items-center flex-1"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
indigo-5
0 flex items-center justify-center mr-3"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
slate-100 dark:bg-slate-70
0 flex items-center justify-center mr-3"
>
<
IconShield
size
=
"default"
className
=
"text-
indigo-5
00"
/>
<
IconShield
size
=
"default"
className
=
"text-
slate-600 dark:text-slate-3
00"
/>
<
/div
>
<
/div
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"font-medium text-gray-900"
>
{
t
(
'OIDC'
)}
<
/div
>
<
div
className
=
"font-medium text-gray-900"
>
{
t
(
'OIDC'
)}
<
/div
>
...
@@ -883,8 +884,8 @@ const PersonalSetting = () => {
...
@@ -883,8 +884,8 @@ const PersonalSetting = () => {
>
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center flex-1"
>
<
div
className
=
"flex items-center flex-1"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
blue-5
0 flex items-center justify-center mr-3"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
slate-100 dark:bg-slate-70
0 flex items-center justify-center mr-3"
>
<
SiTelegram
size
=
{
20
}
className
=
"text-
blue-5
00"
/>
<
SiTelegram
size
=
{
20
}
className
=
"text-
slate-600 dark:text-slate-3
00"
/>
<
/div
>
<
/div
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"font-medium text-gray-900"
>
{
t
(
'Telegram'
)}
<
/div
>
<
div
className
=
"font-medium text-gray-900"
>
{
t
(
'Telegram'
)}
<
/div
>
...
@@ -926,8 +927,8 @@ const PersonalSetting = () => {
...
@@ -926,8 +927,8 @@ const PersonalSetting = () => {
>
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center flex-1"
>
<
div
className
=
"flex items-center flex-1"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
orange-5
0 flex items-center justify-center mr-3"
>
<
div
className
=
"w-10 h-10 rounded-full bg-
slate-100 dark:bg-slate-70
0 flex items-center justify-center mr-3"
>
<
SiLinux
size
=
{
20
}
className
=
"text-
orange-5
00"
/>
<
SiLinux
size
=
{
20
}
className
=
"text-
slate-600 dark:text-slate-3
00"
/>
<
/div
>
<
/div
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"font-medium text-gray-900"
>
{
t
(
'LinuxDO'
)}
<
/div
>
<
div
className
=
"font-medium text-gray-900"
>
{
t
(
'LinuxDO'
)}
<
/div
>
...
@@ -978,8 +979,8 @@ const PersonalSetting = () => {
...
@@ -978,8 +979,8 @@ const PersonalSetting = () => {
>
>
<
div
className
=
"flex flex-col sm:flex-row items-start sm:justify-between gap-4"
>
<
div
className
=
"flex flex-col sm:flex-row items-start sm:justify-between gap-4"
>
<
div
className
=
"flex items-start w-full sm:w-auto"
>
<
div
className
=
"flex items-start w-full sm:w-auto"
>
<
div
className
=
"w-12 h-12 rounded-full bg-
blue-5
0 flex items-center justify-center mr-4 flex-shrink-0"
>
<
div
className
=
"w-12 h-12 rounded-full bg-
slate-10
0 flex items-center justify-center mr-4 flex-shrink-0"
>
<
IconKey
size
=
"large"
className
=
"text-
blue-5
00"
/>
<
IconKey
size
=
"large"
className
=
"text-
slate-6
00"
/>
<
/div
>
<
/div
>
<
div
className
=
"flex-1"
>
<
div
className
=
"flex-1"
>
<
Typography
.
Title
heading
=
{
6
}
className
=
"mb-1"
>
<
Typography
.
Title
heading
=
{
6
}
className
=
"mb-1"
>
...
@@ -1006,7 +1007,7 @@ const PersonalSetting = () => {
...
@@ -1006,7 +1007,7 @@ const PersonalSetting = () => {
type
=
"primary"
type
=
"primary"
theme
=
"solid"
theme
=
"solid"
onClick
=
{
generateAccessToken
}
onClick
=
{
generateAccessToken
}
className
=
"!rounded-lg !bg-
blue-500 hover:!bg-blue-6
00 w-full sm:w-auto"
className
=
"!rounded-lg !bg-
slate-600 hover:!bg-slate-7
00 w-full sm:w-auto"
icon
=
{
<
IconKey
/>
}
icon
=
{
<
IconKey
/>
}
>
>
{
systemToken
?
t
(
'重新生成'
)
:
t
(
'生成令牌'
)}
{
systemToken
?
t
(
'重新生成'
)
:
t
(
'生成令牌'
)}
...
@@ -1022,8 +1023,8 @@ const PersonalSetting = () => {
...
@@ -1022,8 +1023,8 @@ const PersonalSetting = () => {
>
>
<
div
className
=
"flex flex-col sm:flex-row items-start sm:justify-between gap-4"
>
<
div
className
=
"flex flex-col sm:flex-row items-start sm:justify-between gap-4"
>
<
div
className
=
"flex items-start w-full sm:w-auto"
>
<
div
className
=
"flex items-start w-full sm:w-auto"
>
<
div
className
=
"w-12 h-12 rounded-full bg-
orange-5
0 flex items-center justify-center mr-4 flex-shrink-0"
>
<
div
className
=
"w-12 h-12 rounded-full bg-
slate-10
0 flex items-center justify-center mr-4 flex-shrink-0"
>
<
IconLock
size
=
"large"
className
=
"text-
orange-5
00"
/>
<
IconLock
size
=
"large"
className
=
"text-
slate-6
00"
/>
<
/div
>
<
/div
>
<
div
>
<
div
>
<
Typography
.
Title
heading
=
{
6
}
className
=
"mb-1"
>
<
Typography
.
Title
heading
=
{
6
}
className
=
"mb-1"
>
...
@@ -1038,7 +1039,7 @@ const PersonalSetting = () => {
...
@@ -1038,7 +1039,7 @@ const PersonalSetting = () => {
type
=
"primary"
type
=
"primary"
theme
=
"solid"
theme
=
"solid"
onClick
=
{()
=>
setShowChangePasswordModal
(
true
)}
onClick
=
{()
=>
setShowChangePasswordModal
(
true
)}
className
=
"!rounded-lg !bg-
orange-500 hover:!bg-orange-6
00 w-full sm:w-auto"
className
=
"!rounded-lg !bg-
slate-600 hover:!bg-slate-7
00 w-full sm:w-auto"
icon
=
{
<
IconLock
/>
}
icon
=
{
<
IconLock
/>
}
>
>
{
t
(
'修改密码'
)}
{
t
(
'修改密码'
)}
...
@@ -1054,11 +1055,11 @@ const PersonalSetting = () => {
...
@@ -1054,11 +1055,11 @@ const PersonalSetting = () => {
>
>
<
div
className
=
"flex flex-col sm:flex-row items-start sm:justify-between gap-4"
>
<
div
className
=
"flex flex-col sm:flex-row items-start sm:justify-between gap-4"
>
<
div
className
=
"flex items-start w-full sm:w-auto"
>
<
div
className
=
"flex items-start w-full sm:w-auto"
>
<
div
className
=
"w-12 h-12 rounded-full bg-
red-5
0 flex items-center justify-center mr-4 flex-shrink-0"
>
<
div
className
=
"w-12 h-12 rounded-full bg-
slate-10
0 flex items-center justify-center mr-4 flex-shrink-0"
>
<
IconDelete
size
=
"large"
className
=
"text-
red-5
00"
/>
<
IconDelete
size
=
"large"
className
=
"text-
slate-6
00"
/>
<
/div
>
<
/div
>
<
div
>
<
div
>
<
Typography
.
Title
heading
=
{
6
}
className
=
"mb-1 text-
red-6
00"
>
<
Typography
.
Title
heading
=
{
6
}
className
=
"mb-1 text-
slate-7
00"
>
{
t
(
'删除账户'
)}
{
t
(
'删除账户'
)}
<
/Typography.Title
>
<
/Typography.Title
>
<
Typography
.
Text
type
=
"tertiary"
className
=
"text-sm"
>
<
Typography
.
Text
type
=
"tertiary"
className
=
"text-sm"
>
...
@@ -1070,7 +1071,7 @@ const PersonalSetting = () => {
...
@@ -1070,7 +1071,7 @@ const PersonalSetting = () => {
type
=
"danger"
type
=
"danger"
theme
=
"solid"
theme
=
"solid"
onClick
=
{()
=>
setShowAccountDeleteModal
(
true
)}
onClick
=
{()
=>
setShowAccountDeleteModal
(
true
)}
className
=
"!rounded-lg w-full sm:w-auto"
className
=
"!rounded-lg w-full sm:w-auto
!bg-slate-500 hover:!bg-slate-600
"
icon
=
{
<
IconDelete
/>
}
icon
=
{
<
IconDelete
/>
}
>
>
{
t
(
'删除账户'
)}
{
t
(
'删除账户'
)}
...
@@ -1111,7 +1112,7 @@ const PersonalSetting = () => {
...
@@ -1111,7 +1112,7 @@ const PersonalSetting = () => {
>
>
<
Radio
value
=
'email'
className
=
"!p-4 !rounded-lg"
>
<
Radio
value
=
'email'
className
=
"!p-4 !rounded-lg"
>
<
div
className
=
"flex items-center"
>
<
div
className
=
"flex items-center"
>
<
IconMail
className
=
"mr-2 text-
blue-5
00"
/>
<
IconMail
className
=
"mr-2 text-
slate-6
00"
/>
<
div
>
<
div
>
<
div
className
=
"font-medium"
>
{
t
(
'邮件通知'
)}
<
/div
>
<
div
className
=
"font-medium"
>
{
t
(
'邮件通知'
)}
<
/div
>
<
div
className
=
"text-sm text-gray-500"
>
{
t
(
'通过邮件接收通知'
)}
<
/div
>
<
div
className
=
"text-sm text-gray-500"
>
{
t
(
'通过邮件接收通知'
)}
<
/div
>
...
@@ -1120,7 +1121,7 @@ const PersonalSetting = () => {
...
@@ -1120,7 +1121,7 @@ const PersonalSetting = () => {
<
/Radio
>
<
/Radio
>
<
Radio
value
=
'webhook'
className
=
"!p-4 !rounded-lg"
>
<
Radio
value
=
'webhook'
className
=
"!p-4 !rounded-lg"
>
<
div
className
=
"flex items-center"
>
<
div
className
=
"flex items-center"
>
<
Webhook
size
=
{
16
}
className
=
"mr-2 text-
green-5
00"
/>
<
Webhook
size
=
{
16
}
className
=
"mr-2 text-
slate-6
00"
/>
<
div
>
<
div
>
<
div
className
=
"font-medium"
>
{
t
(
'Webhook通知'
)}
<
/div
>
<
div
className
=
"font-medium"
>
{
t
(
'Webhook通知'
)}
<
/div
>
<
div
className
=
"text-sm text-gray-500"
>
{
t
(
'通过HTTP请求接收通知'
)}
<
/div
>
<
div
className
=
"text-sm text-gray-500"
>
{
t
(
'通过HTTP请求接收通知'
)}
<
/div
>
...
@@ -1167,11 +1168,11 @@ const PersonalSetting = () => {
...
@@ -1167,11 +1168,11 @@ const PersonalSetting = () => {
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
className
=
"bg-
yellow
-50 rounded-xl"
>
<
div
className
=
"bg-
slate
-50 rounded-xl"
>
<
div
className
=
"flex items-center justify-between cursor-pointer"
onClick
=
{()
=>
setShowWebhookDocs
(
!
showWebhookDocs
)}
>
<
div
className
=
"flex items-center justify-between cursor-pointer"
onClick
=
{()
=>
setShowWebhookDocs
(
!
showWebhookDocs
)}
>
<
div
className
=
"flex items-center"
>
<
div
className
=
"flex items-center"
>
<
Globe
size
=
{
16
}
className
=
"mr-2 text-
yellow
-600"
/>
<
Globe
size
=
{
16
}
className
=
"mr-2 text-
slate
-600"
/>
<
Typography
.
Text
strong
className
=
"text-
yellow-8
00"
>
<
Typography
.
Text
strong
className
=
"text-
slate-7
00"
>
{
t
(
'Webhook请求结构'
)}
{
t
(
'Webhook请求结构'
)}
<
/Typography.Text
>
<
/Typography.Text
>
<
/div
>
<
/div
>
...
@@ -1254,11 +1255,11 @@ const PersonalSetting = () => {
...
@@ -1254,11 +1255,11 @@ const PersonalSetting = () => {
itemKey
=
'price'
itemKey
=
'price'
>
>
<
div
className
=
"py-4"
>
<
div
className
=
"py-4"
>
<
div
className
=
"bg-white rounded-xl"
>
<
div
className
=
"bg-white rounded-xl"
>
<
div
className
=
"flex items-start"
>
<
div
className
=
"flex items-start"
>
<
div
className
=
"w-10 h-10 rounded-full bg-orange-5
0 flex items-center justify-center mt-1"
>
<
div
className
=
"w-10 h-10 rounded-full bg-slate-10
0 flex items-center justify-center mt-1"
>
<
Shield
size
=
{
20
}
className
=
"text-orange-5
00"
/>
<
Shield
size
=
{
20
}
className
=
"text-slate-6
00"
/>
<
/div
>
<
/div
>
<
div
className
=
"flex-1"
>
<
div
className
=
"flex-1"
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center justify-between"
>
<
div
>
<
div
>
...
@@ -1292,7 +1293,7 @@ const PersonalSetting = () => {
...
@@ -1292,7 +1293,7 @@ const PersonalSetting = () => {
type
=
'primary'
type
=
'primary'
onClick
=
{
saveNotificationSettings
}
onClick
=
{
saveNotificationSettings
}
size
=
"large"
size
=
"large"
className
=
"!rounded-lg !bg-
purple-500 hover:!bg-purple-6
00"
className
=
"!rounded-lg !bg-
slate-600 hover:!bg-slate-7
00"
icon
=
{
<
IconSetting
/>
}
icon
=
{
<
IconSetting
/>
}
>
>
{
t
(
'保存设置'
)}
{
t
(
'保存设置'
)}
...
@@ -1408,7 +1409,7 @@ const PersonalSetting = () => {
...
@@ -1408,7 +1409,7 @@ const PersonalSetting = () => {
theme
=
"solid"
theme
=
"solid"
size
=
'large'
size
=
'large'
onClick
=
{
bindWeChat
}
onClick
=
{
bindWeChat
}
className
=
"!rounded-lg w-full !bg-
green-500 hover:!bg-green-6
00"
className
=
"!rounded-lg w-full !bg-
slate-600 hover:!bg-slate-7
00"
icon
=
{
<
SiWechat
size
=
{
16
}
/>
}
icon
=
{
<
SiWechat
size
=
{
16
}
/>
}
>
>
{
t
(
'绑定'
)}
{
t
(
'绑定'
)}
...
...
web/src/components/table/ChannelsTable.js
View file @
c821b0ed
...
@@ -6,15 +6,31 @@ import {
...
@@ -6,15 +6,31 @@ import {
showSuccess
,
showSuccess
,
timestamp2string
,
timestamp2string
,
renderGroup
,
renderGroup
,
renderQuotaWithAmount
,
renderNumberWithPoint
,
renderQuota
renderQuota
,
getChannelIcon
}
from
'../../helpers/index.js'
;
}
from
'../../helpers/index.js'
;
import
{
CheckCircle
,
XCircle
,
AlertCircle
,
HelpCircle
,
TestTube
,
Zap
,
Timer
,
Clock
,
AlertTriangle
,
Coins
,
Tags
}
from
'lucide-react'
;
import
{
CHANNEL_OPTIONS
,
ITEMS_PER_PAGE
}
from
'../../constants/index.js'
;
import
{
CHANNEL_OPTIONS
,
ITEMS_PER_PAGE
}
from
'../../constants/index.js'
;
import
{
import
{
Button
,
Button
,
Divider
,
Divider
,
Dropdown
,
Dropdown
,
Empty
,
Input
,
Input
,
InputNumber
,
InputNumber
,
Modal
,
Modal
,
...
@@ -25,13 +41,15 @@ import {
...
@@ -25,13 +41,15 @@ import {
Tag
,
Tag
,
Tooltip
,
Tooltip
,
Typography
,
Typography
,
Checkbox
,
Card
,
Card
,
Select
Form
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
IllustrationNoResult
,
IllustrationNoResultDark
}
from
'@douyinfe/semi-illustrations'
;
import
EditChannel
from
'../../pages/Channel/EditChannel.js'
;
import
EditChannel
from
'../../pages/Channel/EditChannel.js'
;
import
{
import
{
IconList
,
IconTreeTriangleDown
,
IconTreeTriangleDown
,
IconFilter
,
IconFilter
,
IconPlus
,
IconPlus
,
...
@@ -64,7 +82,12 @@ const ChannelsTable = () => {
...
@@ -64,7 +82,12 @@ const ChannelsTable = () => {
type2label
[
0
]
=
{
value
:
0
,
label
:
t
(
'未知类型'
),
color
:
'grey'
};
type2label
[
0
]
=
{
value
:
0
,
label
:
t
(
'未知类型'
),
color
:
'grey'
};
}
}
return
(
return
(
<
Tag
size
=
'large'
color
=
{
type2label
[
type
]?.
color
}
shape
=
'circle'
>
<
Tag
size
=
'large'
color
=
{
type2label
[
type
]?.
color
}
shape
=
'circle'
prefixIcon
=
{
getChannelIcon
(
type
)}
>
{
type2label
[
type
]?.
label
}
{
type2label
[
type
]?.
label
}
<
/Tag
>
<
/Tag
>
);
);
...
@@ -74,7 +97,7 @@ const ChannelsTable = () => {
...
@@ -74,7 +97,7 @@ const ChannelsTable = () => {
return
(
return
(
<
Tag
<
Tag
color
=
'light-blue'
color
=
'light-blue'
prefixIcon
=
{
<
IconList
/>
}
prefixIcon
=
{
<
Tags
size
=
{
14
}
/>
}
size
=
'large'
size
=
'large'
shape
=
'circle'
shape
=
'circle'
type
=
'light'
type
=
'light'
...
@@ -88,25 +111,25 @@ const ChannelsTable = () => {
...
@@ -88,25 +111,25 @@ const ChannelsTable = () => {
switch
(
status
)
{
switch
(
status
)
{
case
1
:
case
1
:
return
(
return
(
<
Tag
size
=
'large'
color
=
'green'
shape
=
'circle'
>
<
Tag
size
=
'large'
color
=
'green'
shape
=
'circle'
prefixIcon
=
{
<
CheckCircle
size
=
{
14
}
/>}
>
{
t
(
'已启用'
)}
{
t
(
'已启用'
)}
<
/Tag
>
<
/Tag
>
);
);
case
2
:
case
2
:
return
(
return
(
<
Tag
size
=
'large'
color
=
'yellow'
shape
=
'circle'
>
<
Tag
size
=
'large'
color
=
'yellow'
shape
=
'circle'
prefixIcon
=
{
<
XCircle
size
=
{
14
}
/>}
>
{
t
(
'已禁用'
)}
{
t
(
'已禁用'
)}
<
/Tag
>
<
/Tag
>
);
);
case
3
:
case
3
:
return
(
return
(
<
Tag
size
=
'large'
color
=
'yellow'
shape
=
'circle'
>
<
Tag
size
=
'large'
color
=
'yellow'
shape
=
'circle'
prefixIcon
=
{
<
AlertCircle
size
=
{
14
}
/>}
>
{
t
(
'自动禁用'
)}
{
t
(
'自动禁用'
)}
<
/Tag
>
<
/Tag
>
);
);
default
:
default
:
return
(
return
(
<
Tag
size
=
'large'
color
=
'grey'
shape
=
'circle'
>
<
Tag
size
=
'large'
color
=
'grey'
shape
=
'circle'
prefixIcon
=
{
<
HelpCircle
size
=
{
14
}
/>}
>
{
t
(
'未知状态'
)}
{
t
(
'未知状态'
)}
<
/Tag
>
<
/Tag
>
);
);
...
@@ -118,139 +141,48 @@ const ChannelsTable = () => {
...
@@ -118,139 +141,48 @@ const ChannelsTable = () => {
time
=
time
.
toFixed
(
2
)
+
t
(
' 秒'
);
time
=
time
.
toFixed
(
2
)
+
t
(
' 秒'
);
if
(
responseTime
===
0
)
{
if
(
responseTime
===
0
)
{
return
(
return
(
<
Tag
size
=
'large'
color
=
'grey'
shape
=
'circle'
>
<
Tag
size
=
'large'
color
=
'grey'
shape
=
'circle'
prefixIcon
=
{
<
TestTube
size
=
{
14
}
/>}
>
{
t
(
'未测试'
)}
{
t
(
'未测试'
)}
<
/Tag
>
<
/Tag
>
);
);
}
else
if
(
responseTime
<=
1000
)
{
}
else
if
(
responseTime
<=
1000
)
{
return
(
return
(
<
Tag
size
=
'large'
color
=
'green'
shape
=
'circle'
>
<
Tag
size
=
'large'
color
=
'green'
shape
=
'circle'
prefixIcon
=
{
<
Zap
size
=
{
14
}
/>}
>
{
time
}
{
time
}
<
/Tag
>
<
/Tag
>
);
);
}
else
if
(
responseTime
<=
3000
)
{
}
else
if
(
responseTime
<=
3000
)
{
return
(
return
(
<
Tag
size
=
'large'
color
=
'lime'
shape
=
'circle'
>
<
Tag
size
=
'large'
color
=
'lime'
shape
=
'circle'
prefixIcon
=
{
<
Timer
size
=
{
14
}
/>}
>
{
time
}
{
time
}
<
/Tag
>
<
/Tag
>
);
);
}
else
if
(
responseTime
<=
5000
)
{
}
else
if
(
responseTime
<=
5000
)
{
return
(
return
(
<
Tag
size
=
'large'
color
=
'yellow'
shape
=
'circle'
>
<
Tag
size
=
'large'
color
=
'yellow'
shape
=
'circle'
prefixIcon
=
{
<
Clock
size
=
{
14
}
/>}
>
{
time
}
{
time
}
<
/Tag
>
<
/Tag
>
);
);
}
else
{
}
else
{
return
(
return
(
<
Tag
size
=
'large'
color
=
'red'
shape
=
'circle'
>
<
Tag
size
=
'large'
color
=
'red'
shape
=
'circle'
prefixIcon
=
{
<
AlertTriangle
size
=
{
14
}
/>}
>
{
time
}
{
time
}
<
/Tag
>
<
/Tag
>
);
);
}
}
};
};
// Define column keys for selection
// Define all columns
const
COLUMN_KEYS
=
{
const
columns
=
[
ID
:
'id'
,
NAME
:
'name'
,
GROUP
:
'group'
,
TYPE
:
'type'
,
STATUS
:
'status'
,
RESPONSE_TIME
:
'response_time'
,
BALANCE
:
'balance'
,
PRIORITY
:
'priority'
,
WEIGHT
:
'weight'
,
OPERATE
:
'operate'
,
};
// State for column visibility
const
[
visibleColumns
,
setVisibleColumns
]
=
useState
({});
const
[
showColumnSelector
,
setShowColumnSelector
]
=
useState
(
false
);
// Load saved column preferences from localStorage
useEffect
(()
=>
{
const
savedColumns
=
localStorage
.
getItem
(
'channels-table-columns'
);
if
(
savedColumns
)
{
try
{
const
parsed
=
JSON
.
parse
(
savedColumns
);
// Make sure all columns are accounted for
const
defaults
=
getDefaultColumnVisibility
();
const
merged
=
{
...
defaults
,
...
parsed
};
setVisibleColumns
(
merged
);
}
catch
(
e
)
{
console
.
error
(
'Failed to parse saved column preferences'
,
e
);
initDefaultColumns
();
}
}
else
{
initDefaultColumns
();
}
},
[]);
// Update table when column visibility changes
useEffect
(()
=>
{
if
(
Object
.
keys
(
visibleColumns
).
length
>
0
)
{
// Save to localStorage
localStorage
.
setItem
(
'channels-table-columns'
,
JSON
.
stringify
(
visibleColumns
),
);
}
},
[
visibleColumns
]);
// Get default column visibility
const
getDefaultColumnVisibility
=
()
=>
{
return
{
[
COLUMN_KEYS
.
ID
]:
true
,
[
COLUMN_KEYS
.
NAME
]:
true
,
[
COLUMN_KEYS
.
GROUP
]:
true
,
[
COLUMN_KEYS
.
TYPE
]:
true
,
[
COLUMN_KEYS
.
STATUS
]:
true
,
[
COLUMN_KEYS
.
RESPONSE_TIME
]:
true
,
[
COLUMN_KEYS
.
BALANCE
]:
true
,
[
COLUMN_KEYS
.
PRIORITY
]:
true
,
[
COLUMN_KEYS
.
WEIGHT
]:
true
,
[
COLUMN_KEYS
.
OPERATE
]:
true
,
};
};
// Initialize default column visibility
const
initDefaultColumns
=
()
=>
{
const
defaults
=
getDefaultColumnVisibility
();
setVisibleColumns
(
defaults
);
};
// Handle column visibility change
const
handleColumnVisibilityChange
=
(
columnKey
,
checked
)
=>
{
const
updatedColumns
=
{
...
visibleColumns
,
[
columnKey
]:
checked
};
setVisibleColumns
(
updatedColumns
);
};
// Handle "Select All" checkbox
const
handleSelectAll
=
(
checked
)
=>
{
const
allKeys
=
Object
.
keys
(
COLUMN_KEYS
).
map
((
key
)
=>
COLUMN_KEYS
[
key
]);
const
updatedColumns
=
{};
allKeys
.
forEach
((
key
)
=>
{
updatedColumns
[
key
]
=
checked
;
});
setVisibleColumns
(
updatedColumns
);
};
// Define all columns with keys
const
allColumns
=
[
{
{
key
:
COLUMN_KEYS
.
ID
,
title
:
t
(
'ID'
),
title
:
t
(
'ID'
),
dataIndex
:
'id'
,
dataIndex
:
'id'
,
},
},
{
{
key
:
COLUMN_KEYS
.
NAME
,
title
:
t
(
'名称'
),
title
:
t
(
'名称'
),
dataIndex
:
'name'
,
dataIndex
:
'name'
,
},
},
{
{
key
:
COLUMN_KEYS
.
GROUP
,
title
:
t
(
'分组'
),
title
:
t
(
'分组'
),
dataIndex
:
'group'
,
dataIndex
:
'group'
,
render
:
(
text
,
record
,
index
)
=>
(
render
:
(
text
,
record
,
index
)
=>
(
...
@@ -269,7 +201,6 @@ const ChannelsTable = () => {
...
@@ -269,7 +201,6 @@ const ChannelsTable = () => {
),
),
},
},
{
{
key
:
COLUMN_KEYS
.
TYPE
,
title
:
t
(
'类型'
),
title
:
t
(
'类型'
),
dataIndex
:
'type'
,
dataIndex
:
'type'
,
render
:
(
text
,
record
,
index
)
=>
{
render
:
(
text
,
record
,
index
)
=>
{
...
@@ -281,7 +212,6 @@ const ChannelsTable = () => {
...
@@ -281,7 +212,6 @@ const ChannelsTable = () => {
},
},
},
},
{
{
key
:
COLUMN_KEYS
.
STATUS
,
title
:
t
(
'状态'
),
title
:
t
(
'状态'
),
dataIndex
:
'status'
,
dataIndex
:
'status'
,
render
:
(
text
,
record
,
index
)
=>
{
render
:
(
text
,
record
,
index
)
=>
{
...
@@ -307,7 +237,6 @@ const ChannelsTable = () => {
...
@@ -307,7 +237,6 @@ const ChannelsTable = () => {
},
},
},
},
{
{
key
:
COLUMN_KEYS
.
RESPONSE_TIME
,
title
:
t
(
'响应时间'
),
title
:
t
(
'响应时间'
),
dataIndex
:
'response_time'
,
dataIndex
:
'response_time'
,
render
:
(
text
,
record
,
index
)
=>
(
render
:
(
text
,
record
,
index
)
=>
(
...
@@ -315,7 +244,6 @@ const ChannelsTable = () => {
...
@@ -315,7 +244,6 @@ const ChannelsTable = () => {
),
),
},
},
{
{
key
:
COLUMN_KEYS
.
BALANCE
,
title
:
t
(
'已用/剩余'
),
title
:
t
(
'已用/剩余'
),
dataIndex
:
'expired_time'
,
dataIndex
:
'expired_time'
,
render
:
(
text
,
record
,
index
)
=>
{
render
:
(
text
,
record
,
index
)
=>
{
...
@@ -324,7 +252,7 @@ const ChannelsTable = () => {
...
@@ -324,7 +252,7 @@ const ChannelsTable = () => {
<
div
>
<
div
>
<
Space
spacing
=
{
1
}
>
<
Space
spacing
=
{
1
}
>
<
Tooltip
content
=
{
t
(
'已用额度'
)}
>
<
Tooltip
content
=
{
t
(
'已用额度'
)}
>
<
Tag
color
=
'white'
type
=
'ghost'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'white'
type
=
'ghost'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Coins
size
=
{
14
}
/>}
>
{
renderQuota
(
record
.
used_quota
)}
{
renderQuota
(
record
.
used_quota
)}
<
/Tag
>
<
/Tag
>
<
/Tooltip
>
<
/Tooltip
>
...
@@ -334,6 +262,7 @@ const ChannelsTable = () => {
...
@@ -334,6 +262,7 @@ const ChannelsTable = () => {
type
=
'ghost'
type
=
'ghost'
size
=
'large'
size
=
'large'
shape
=
'circle'
shape
=
'circle'
prefixIcon
=
{
<
Coins
size
=
{
14
}
/>
}
onClick
=
{()
=>
updateChannelBalance
(
record
)}
onClick
=
{()
=>
updateChannelBalance
(
record
)}
>
>
{
renderQuotaWithAmount
(
record
.
balance
)}
{
renderQuotaWithAmount
(
record
.
balance
)}
...
@@ -345,7 +274,7 @@ const ChannelsTable = () => {
...
@@ -345,7 +274,7 @@ const ChannelsTable = () => {
}
else
{
}
else
{
return
(
return
(
<
Tooltip
content
=
{
t
(
'已用额度'
)}
>
<
Tooltip
content
=
{
t
(
'已用额度'
)}
>
<
Tag
color
=
'white'
type
=
'ghost'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'white'
type
=
'ghost'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Coins
size
=
{
14
}
/>}
>
{
renderQuota
(
record
.
used_quota
)}
{
renderQuota
(
record
.
used_quota
)}
<
/Tag
>
<
/Tag
>
<
/Tooltip
>
<
/Tooltip
>
...
@@ -354,7 +283,6 @@ const ChannelsTable = () => {
...
@@ -354,7 +283,6 @@ const ChannelsTable = () => {
},
},
},
},
{
{
key
:
COLUMN_KEYS
.
PRIORITY
,
title
:
t
(
'优先级'
),
title
:
t
(
'优先级'
),
dataIndex
:
'priority'
,
dataIndex
:
'priority'
,
render
:
(
text
,
record
,
index
)
=>
{
render
:
(
text
,
record
,
index
)
=>
{
...
@@ -406,7 +334,6 @@ const ChannelsTable = () => {
...
@@ -406,7 +334,6 @@ const ChannelsTable = () => {
},
},
},
},
{
{
key
:
COLUMN_KEYS
.
WEIGHT
,
title
:
t
(
'权重'
),
title
:
t
(
'权重'
),
dataIndex
:
'weight'
,
dataIndex
:
'weight'
,
render
:
(
text
,
record
,
index
)
=>
{
render
:
(
text
,
record
,
index
)
=>
{
...
@@ -458,7 +385,6 @@ const ChannelsTable = () => {
...
@@ -458,7 +385,6 @@ const ChannelsTable = () => {
},
},
},
},
{
{
key
:
COLUMN_KEYS
.
OPERATE
,
title
:
''
,
title
:
''
,
dataIndex
:
'operate'
,
dataIndex
:
'operate'
,
fixed
:
'right'
,
fixed
:
'right'
,
...
@@ -631,96 +557,10 @@ const ChannelsTable = () => {
...
@@ -631,96 +557,10 @@ const ChannelsTable = () => {
},
},
];
];
// Filter columns based on visibility settings
const
getVisibleColumns
=
()
=>
{
return
allColumns
.
filter
((
column
)
=>
visibleColumns
[
column
.
key
]);
};
// Column selector modal
const
renderColumnSelector
=
()
=>
{
return
(
<
Modal
title
=
{
t
(
'列设置'
)}
visible
=
{
showColumnSelector
}
onCancel
=
{()
=>
setShowColumnSelector
(
false
)}
footer
=
{
<
div
className
=
"flex justify-end"
>
<
Button
theme
=
"light"
onClick
=
{()
=>
initDefaultColumns
()}
className
=
"!rounded-full"
>
{
t
(
'重置'
)}
<
/Button
>
<
Button
theme
=
"light"
onClick
=
{()
=>
setShowColumnSelector
(
false
)}
className
=
"!rounded-full"
>
{
t
(
'取消'
)}
<
/Button
>
<
Button
type
=
'primary'
onClick
=
{()
=>
setShowColumnSelector
(
false
)}
className
=
"!rounded-full"
>
{
t
(
'确定'
)}
<
/Button
>
<
/div
>
}
size
=
"middle"
centered
=
{
true
}
>
<
div
style
=
{{
marginBottom
:
20
}}
>
<
Checkbox
checked
=
{
Object
.
values
(
visibleColumns
).
every
((
v
)
=>
v
===
true
)}
indeterminate
=
{
Object
.
values
(
visibleColumns
).
some
((
v
)
=>
v
===
true
)
&&
!
Object
.
values
(
visibleColumns
).
every
((
v
)
=>
v
===
true
)
}
onChange
=
{(
e
)
=>
handleSelectAll
(
e
.
target
.
checked
)}
>
{
t
(
'全选'
)}
<
/Checkbox
>
<
/div
>
<
div
className
=
"flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4"
style
=
{{
border
:
'1px solid var(--semi-color-border)'
}}
>
{
allColumns
.
map
((
column
)
=>
{
// Skip columns without title
if
(
!
column
.
title
)
{
return
null
;
}
return
(
<
div
key
=
{
column
.
key
}
className
=
"w-1/2 mb-4 pr-2"
>
<
Checkbox
checked
=
{
!!
visibleColumns
[
column
.
key
]}
onChange
=
{(
e
)
=>
handleColumnVisibilityChange
(
column
.
key
,
e
.
target
.
checked
)
}
>
{
column
.
title
}
<
/Checkbox
>
<
/div
>
);
})}
<
/div
>
<
/Modal
>
);
};
const
[
channels
,
setChannels
]
=
useState
([]);
const
[
channels
,
setChannels
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
const
[
idSort
,
setIdSort
]
=
useState
(
false
);
const
[
idSort
,
setIdSort
]
=
useState
(
false
);
const
[
searchKeyword
,
setSearchKeyword
]
=
useState
(
''
);
const
[
searchGroup
,
setSearchGroup
]
=
useState
(
''
);
const
[
searchModel
,
setSearchModel
]
=
useState
(
''
);
const
[
searching
,
setSearching
]
=
useState
(
false
);
const
[
searching
,
setSearching
]
=
useState
(
false
);
const
[
pageSize
,
setPageSize
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
pageSize
,
setPageSize
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
channelCount
,
setChannelCount
]
=
useState
(
pageSize
);
const
[
channelCount
,
setChannelCount
]
=
useState
(
pageSize
);
...
@@ -745,6 +585,16 @@ const ChannelsTable = () => {
...
@@ -745,6 +585,16 @@ const ChannelsTable = () => {
const
[
testQueue
,
setTestQueue
]
=
useState
([]);
const
[
testQueue
,
setTestQueue
]
=
useState
([]);
const
[
isProcessingQueue
,
setIsProcessingQueue
]
=
useState
(
false
);
const
[
isProcessingQueue
,
setIsProcessingQueue
]
=
useState
(
false
);
// Form API 引用
const
[
formApi
,
setFormApi
]
=
useState
(
null
);
// Form 初始值
const
formInitValues
=
{
searchKeyword
:
''
,
searchGroup
:
''
,
searchModel
:
''
,
};
const
removeRecord
=
(
record
)
=>
{
const
removeRecord
=
(
record
)
=>
{
let
newDataSource
=
[...
channels
];
let
newDataSource
=
[...
channels
];
if
(
record
.
id
!=
null
)
{
if
(
record
.
id
!=
null
)
{
...
@@ -896,15 +746,11 @@ const ChannelsTable = () => {
...
@@ -896,15 +746,11 @@ const ChannelsTable = () => {
};
};
const
refresh
=
async
()
=>
{
const
refresh
=
async
()
=>
{
const
{
searchKeyword
,
searchGroup
,
searchModel
}
=
getFormValues
();
if
(
searchKeyword
===
''
&&
searchGroup
===
''
&&
searchModel
===
''
)
{
if
(
searchKeyword
===
''
&&
searchGroup
===
''
&&
searchModel
===
''
)
{
await
loadChannels
(
activePage
-
1
,
pageSize
,
idSort
,
enableTagMode
);
await
loadChannels
(
activePage
-
1
,
pageSize
,
idSort
,
enableTagMode
);
}
else
{
}
else
{
await
searchChannels
(
await
searchChannels
(
enableTagMode
);
searchKeyword
,
searchGroup
,
searchModel
,
enableTagMode
,
);
}
}
};
};
...
@@ -1010,29 +856,40 @@ const ChannelsTable = () => {
...
@@ -1010,29 +856,40 @@ const ChannelsTable = () => {
}
}
};
};
const
searchChannels
=
async
(
// 获取表单值的辅助函数,确保所有值都是字符串
searchKeyword
,
const
getFormValues
=
()
=>
{
searchGroup
,
const
formValues
=
formApi
?
formApi
.
getValues
()
:
{};
searchModel
,
return
{
enableTagMode
,
searchKeyword
:
formValues
.
searchKeyword
||
''
,
)
=>
{
searchGroup
:
formValues
.
searchGroup
||
''
,
if
(
searchKeyword
===
''
&&
searchGroup
===
''
&&
searchModel
===
''
)
{
searchModel
:
formValues
.
searchModel
||
''
,
await
loadChannels
(
activePage
-
1
,
pageSize
,
idSort
,
enableTagMode
);
};
// setActivePage(1);
};
return
;
}
const
searchChannels
=
async
(
enableTagMode
)
=>
{
const
{
searchKeyword
,
searchGroup
,
searchModel
}
=
getFormValues
();
setSearching
(
true
);
setSearching
(
true
);
const
res
=
await
API
.
get
(
try
{
`/api/channel/search?keyword=
${
searchKeyword
}
&group=
${
searchGroup
}
&model=
${
searchModel
}
&id_sort=
${
idSort
}
&tag_mode=
${
enableTagMode
}
`
,
if
(
searchKeyword
===
''
&&
searchGroup
===
''
&&
searchModel
===
''
)
{
);
await
loadChannels
(
activePage
-
1
,
pageSize
,
idSort
,
enableTagMode
);
const
{
success
,
message
,
data
}
=
res
.
data
;
// setActivePage(1);
if
(
success
)
{
return
;
setChannelFormat
(
data
,
enableTagMode
);
}
setActivePage
(
1
);
}
else
{
const
res
=
await
API
.
get
(
showError
(
message
);
`/api/channel/search?keyword=
${
searchKeyword
}
&group=
${
searchGroup
}
&model=
${
searchModel
}
&id_sort=
${
idSort
}
&tag_mode=
${
enableTagMode
}
`
,
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
setChannelFormat
(
data
,
enableTagMode
);
setActivePage
(
1
);
}
else
{
showError
(
message
);
}
}
finally
{
setSearching
(
false
);
}
}
setSearching
(
false
);
};
};
const
updateChannelProperty
=
(
channelId
,
updateFn
)
=>
{
const
updateChannelProperty
=
(
channelId
,
updateFn
)
=>
{
...
@@ -1540,71 +1397,83 @@ const ChannelsTable = () => {
...
@@ -1540,71 +1397,83 @@ const ChannelsTable = () => {
>
>
{
t
(
'刷新'
)}
{
t
(
'刷新'
)}
<
/Button
>
<
/Button
>
<
Button
theme
=
'light'
type
=
'tertiary'
icon
=
{
<
IconSetting
/>
}
onClick
=
{()
=>
setShowColumnSelector
(
true
)}
className
=
"!rounded-full w-full md:w-auto"
>
{
t
(
'列设置'
)}
<
/Button
>
<
/div
>
<
/div
>
<
div
className
=
"flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2"
>
<
div
className
=
"flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2"
>
<
div
className
=
"relative w-full md:w-64"
>
<
Form
<
Input
initValues
=
{
formInitValues
}
prefix
=
{
<
IconSearch
/>
}
getFormApi
=
{(
api
)
=>
setFormApi
(
api
)}
placeholder
=
{
t
(
'搜索渠道的 ID,名称,密钥和API地址 ...'
)}
onSubmit
=
{()
=>
searchChannels
(
enableTagMode
)}
value
=
{
searchKeyword
}
allowEmpty
=
{
true
}
loading
=
{
searching
}
autoComplete
=
"off"
onChange
=
{(
v
)
=>
{
layout
=
"horizontal"
setSearchKeyword
(
v
.
trim
());
trigger
=
"change"
}}
stopValidateWithError
=
{
false
}
className
=
"!rounded-full"
className
=
"flex flex-col md:flex-row items-center gap-4 w-full"
showClear
/>
<
/div
>
<
div
className
=
"w-full md:w-48"
>
<
Input
prefix
=
{
<
IconFilter
/>
}
placeholder
=
{
t
(
'模型关键字'
)}
value
=
{
searchModel
}
loading
=
{
searching
}
onChange
=
{(
v
)
=>
{
setSearchModel
(
v
.
trim
());
}}
className
=
"!rounded-full"
showClear
/>
<
/div
>
<
div
className
=
"w-full md:w-48"
>
<
Select
placeholder
=
{
t
(
'选择分组'
)}
optionList
=
{[
{
label
:
t
(
'选择分组'
),
value
:
null
},
...
groupOptions
,
]}
value
=
{
searchGroup
}
onChange
=
{(
v
)
=>
{
setSearchGroup
(
v
);
searchChannels
(
searchKeyword
,
v
,
searchModel
,
enableTagMode
);
}}
className
=
"!rounded-full w-full"
showClear
/>
<
/div
>
<
Button
type
=
"primary"
onClick
=
{()
=>
{
searchChannels
(
searchKeyword
,
searchGroup
,
searchModel
,
enableTagMode
);
}}
loading
=
{
searching
}
className
=
"!rounded-full w-full md:w-auto"
>
>
{
t
(
'查询'
)}
<
div
className
=
"relative w-full md:w-64"
>
<
/Button
>
<
Form
.
Input
field
=
"searchKeyword"
prefix
=
{
<
IconSearch
/>
}
placeholder
=
{
t
(
'搜索渠道的 ID,名称,密钥和API地址 ...'
)}
className
=
"!rounded-full"
showClear
pure
/>
<
/div
>
<
div
className
=
"w-full md:w-48"
>
<
Form
.
Input
field
=
"searchModel"
prefix
=
{
<
IconFilter
/>
}
placeholder
=
{
t
(
'模型关键字'
)}
className
=
"!rounded-full"
showClear
pure
/>
<
/div
>
<
div
className
=
"w-full md:w-48"
>
<
Form
.
Select
field
=
"searchGroup"
placeholder
=
{
t
(
'选择分组'
)}
optionList
=
{[
{
label
:
t
(
'选择分组'
),
value
:
null
},
...
groupOptions
,
]}
className
=
"!rounded-full w-full"
showClear
pure
onChange
=
{()
=>
{
// 延迟执行搜索,让表单值先更新
setTimeout
(()
=>
{
searchChannels
(
enableTagMode
);
},
0
);
}}
/
>
<
/div
>
<
Button
type
=
"primary"
htmlType
=
"submit"
loading
=
{
loading
||
searching
}
className
=
"!rounded-full w-full md:w-auto"
>
{
t
(
'查询'
)}
<
/Button
>
<
Button
theme
=
'light'
onClick
=
{()
=>
{
if
(
formApi
)
{
formApi
.
reset
();
// 重置后立即查询,使用setTimeout确保表单重置完成
setTimeout
(()
=>
{
refresh
();
},
100
);
}
}}
className
=
"!rounded-full w-full md:w-auto"
>
{
t
(
'重置'
)}
<
/Button
>
<
/Form
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
...
@@ -1612,7 +1481,6 @@ const ChannelsTable = () => {
...
@@ -1612,7 +1481,6 @@ const ChannelsTable = () => {
return
(
return
(
<>
<>
{
renderColumnSelector
()}
<
EditTagModal
<
EditTagModal
visible
=
{
showEditTag
}
visible
=
{
showEditTag
}
tag
=
{
editingTag
}
tag
=
{
editingTag
}
...
@@ -1633,7 +1501,7 @@ const ChannelsTable = () => {
...
@@ -1633,7 +1501,7 @@ const ChannelsTable = () => {
bordered
=
{
false
}
bordered
=
{
false
}
>
>
<
Table
<
Table
columns
=
{
getVisibleColumns
()
}
columns
=
{
columns
}
dataSource
=
{
pageData
}
dataSource
=
{
pageData
}
scroll
=
{{
x
:
'max-content'
}}
scroll
=
{{
x
:
'max-content'
}}
pagination
=
{{
pagination
=
{{
...
@@ -1663,6 +1531,14 @@ const ChannelsTable = () => {
...
@@ -1663,6 +1531,14 @@ const ChannelsTable = () => {
}
}
:
null
:
null
}
}
empty
=
{
<
Empty
image
=
{
<
IllustrationNoResult
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
darkModeImage
=
{
<
IllustrationNoResultDark
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
description
=
{
t
(
'搜索无结果'
)}
style
=
{{
padding
:
30
}}
/
>
}
className
=
"rounded-xl overflow-hidden"
className
=
"rounded-xl overflow-hidden"
size
=
"middle"
size
=
"middle"
loading
=
{
loading
}
loading
=
{
loading
}
...
...
web/src/components/table/LogsTable.js
View file @
c821b0ed
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
import
{
CreditCard
,
ShoppingCart
,
Settings
,
Server
,
AlertTriangle
,
HelpCircle
,
Zap
,
Play
,
Clock
,
Hash
,
Key
}
from
'lucide-react'
;
import
{
API
,
API
,
copy
,
copy
,
getTodayStartTimestamp
,
getTodayStartTimestamp
,
...
@@ -20,16 +33,16 @@ import {
...
@@ -20,16 +33,16 @@ import {
renderQuota
,
renderQuota
,
stringToColor
,
stringToColor
,
getLogOther
,
getLogOther
,
renderModelTag
,
renderModelTag
}
from
'../../helpers'
;
}
from
'../../helpers'
;
import
{
import
{
Avatar
,
Avatar
,
Button
,
Button
,
Descriptions
,
Descriptions
,
Empty
,
Modal
,
Modal
,
Popover
,
Popover
,
Select
,
Space
,
Space
,
Spin
,
Spin
,
Table
,
Table
,
...
@@ -39,24 +52,18 @@ import {
...
@@ -39,24 +52,18 @@ import {
Card
,
Card
,
Typography
,
Typography
,
Divider
,
Divider
,
Input
,
Form
DatePicker
,
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
IllustrationNoResult
,
IllustrationNoResultDark
}
from
'@douyinfe/semi-illustrations'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
Paragraph
from
'@douyinfe/semi-ui/lib/es/typography/paragraph'
;
import
Paragraph
from
'@douyinfe/semi-ui/lib/es/typography/paragraph'
;
import
{
IconSetting
,
IconSearch
,
IconForward
}
from
'@douyinfe/semi-icons'
;
import
{
IconSetting
,
IconSearch
,
IconForward
}
from
'@douyinfe/semi-icons'
;
const
{
Text
}
=
Typography
;
const
{
Text
}
=
Typography
;
function
renderTimestamp
(
timestamp
)
{
return
<>
{
timestamp2string
(
timestamp
)}
<
/>
;
}
const
MODE_OPTIONS
=
[
{
key
:
'all'
,
text
:
'all'
,
value
:
'all'
},
{
key
:
'self'
,
text
:
'current user'
,
value
:
'self'
},
];
const
colors
=
[
const
colors
=
[
'amber'
,
'amber'
,
'blue'
,
'blue'
,
...
@@ -238,11 +245,6 @@ const LogsTable = () => {
...
@@ -238,11 +245,6 @@ const LogsTable = () => {
onClick
:
(
event
)
=>
{
onClick
:
(
event
)
=>
{
copyText
(
event
,
record
.
model_name
).
then
((
r
)
=>
{
});
copyText
(
event
,
record
.
model_name
).
then
((
r
)
=>
{
});
},
},
suffixIcon
:
(
<
IconForward
style
=
{{
width
:
'0.9em'
,
height
:
'0.9em'
,
opacity
:
0.75
}}
/
>
),
})}
})}
<
/Popover
>
<
/Popover
>
<
/Space
>
<
/Space
>
...
@@ -737,39 +739,67 @@ const LogsTable = () => {
...
@@ -737,39 +739,67 @@ const LogsTable = () => {
const
[
logType
,
setLogType
]
=
useState
(
0
);
const
[
logType
,
setLogType
]
=
useState
(
0
);
const
isAdminUser
=
isAdmin
();
const
isAdminUser
=
isAdmin
();
let
now
=
new
Date
();
let
now
=
new
Date
();
// 初始化start_timestamp为今天0点
const
[
inputs
,
setInputs
]
=
useState
({
// Form 初始值
const
formInitValues
=
{
username
:
''
,
username
:
''
,
token_name
:
''
,
token_name
:
''
,
model_name
:
''
,
model_name
:
''
,
start_timestamp
:
timestamp2string
(
getTodayStartTimestamp
()),
end_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
),
channel
:
''
,
channel
:
''
,
group
:
''
,
group
:
''
,
});
dateRange
:
[
const
{
timestamp2string
(
getTodayStartTimestamp
()),
username
,
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
)
token_name
,
],
model_name
,
logType
:
'0'
,
start_timestamp
,
};
end_timestamp
,
channel
,
group
,
}
=
inputs
;
const
[
stat
,
setStat
]
=
useState
({
const
[
stat
,
setStat
]
=
useState
({
quota
:
0
,
quota
:
0
,
token
:
0
,
token
:
0
,
});
});
const
handleInputChange
=
(
value
,
name
)
=>
{
// Form API 引用
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
const
[
formApi
,
setFormApi
]
=
useState
(
null
);
// 获取表单值的辅助函数,确保所有值都是字符串
const
getFormValues
=
()
=>
{
const
formValues
=
formApi
?
formApi
.
getValues
()
:
{};
// 处理时间范围
let
start_timestamp
=
timestamp2string
(
getTodayStartTimestamp
());
let
end_timestamp
=
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
);
if
(
formValues
.
dateRange
&&
Array
.
isArray
(
formValues
.
dateRange
)
&&
formValues
.
dateRange
.
length
===
2
)
{
start_timestamp
=
formValues
.
dateRange
[
0
];
end_timestamp
=
formValues
.
dateRange
[
1
];
}
return
{
username
:
formValues
.
username
||
''
,
token_name
:
formValues
.
token_name
||
''
,
model_name
:
formValues
.
model_name
||
''
,
start_timestamp
,
end_timestamp
,
channel
:
formValues
.
channel
||
''
,
group
:
formValues
.
group
||
''
,
logType
:
formValues
.
logType
?
parseInt
(
formValues
.
logType
)
:
0
,
};
};
};
const
getLogSelfStat
=
async
()
=>
{
const
getLogSelfStat
=
async
()
=>
{
const
{
token_name
,
model_name
,
start_timestamp
,
end_timestamp
,
group
,
logType
:
formLogType
,
}
=
getFormValues
();
const
currentLogType
=
formLogType
!==
undefined
?
formLogType
:
logType
;
let
localStartTimestamp
=
Date
.
parse
(
start_timestamp
)
/
1000
;
let
localStartTimestamp
=
Date
.
parse
(
start_timestamp
)
/
1000
;
let
localEndTimestamp
=
Date
.
parse
(
end_timestamp
)
/
1000
;
let
localEndTimestamp
=
Date
.
parse
(
end_timestamp
)
/
1000
;
let
url
=
`/api/log/self/stat?type=
${
l
ogType
}
&token_name=
${
token_name
}
&model_name=
${
model_name
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&group=
${
group
}
`
;
let
url
=
`/api/log/self/stat?type=
${
currentL
ogType
}
&token_name=
${
token_name
}
&model_name=
${
model_name
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&group=
${
group
}
`
;
url
=
encodeURI
(
url
);
url
=
encodeURI
(
url
);
let
res
=
await
API
.
get
(
url
);
let
res
=
await
API
.
get
(
url
);
const
{
success
,
message
,
data
}
=
res
.
data
;
const
{
success
,
message
,
data
}
=
res
.
data
;
...
@@ -781,9 +811,20 @@ const LogsTable = () => {
...
@@ -781,9 +811,20 @@ const LogsTable = () => {
};
};
const
getLogStat
=
async
()
=>
{
const
getLogStat
=
async
()
=>
{
const
{
username
,
token_name
,
model_name
,
start_timestamp
,
end_timestamp
,
channel
,
group
,
logType
:
formLogType
,
}
=
getFormValues
();
const
currentLogType
=
formLogType
!==
undefined
?
formLogType
:
logType
;
let
localStartTimestamp
=
Date
.
parse
(
start_timestamp
)
/
1000
;
let
localStartTimestamp
=
Date
.
parse
(
start_timestamp
)
/
1000
;
let
localEndTimestamp
=
Date
.
parse
(
end_timestamp
)
/
1000
;
let
localEndTimestamp
=
Date
.
parse
(
end_timestamp
)
/
1000
;
let
url
=
`/api/log/stat?type=
${
l
ogType
}
&username=
${
username
}
&token_name=
${
token_name
}
&model_name=
${
model_name
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&channel=
${
channel
}
&group=
${
group
}
`
;
let
url
=
`/api/log/stat?type=
${
currentL
ogType
}
&username=
${
username
}
&token_name=
${
token_name
}
&model_name=
${
model_name
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&channel=
${
channel
}
&group=
${
group
}
`
;
url
=
encodeURI
(
url
);
url
=
encodeURI
(
url
);
let
res
=
await
API
.
get
(
url
);
let
res
=
await
API
.
get
(
url
);
const
{
success
,
message
,
data
}
=
res
.
data
;
const
{
success
,
message
,
data
}
=
res
.
data
;
...
@@ -1016,16 +1057,30 @@ const LogsTable = () => {
...
@@ -1016,16 +1057,30 @@ const LogsTable = () => {
setLogs
(
logs
);
setLogs
(
logs
);
};
};
const
loadLogs
=
async
(
startIdx
,
pageSize
,
logType
=
0
)
=>
{
const
loadLogs
=
async
(
startIdx
,
pageSize
,
customLogType
=
null
)
=>
{
setLoading
(
true
);
setLoading
(
true
);
let
url
=
''
;
let
url
=
''
;
const
{
username
,
token_name
,
model_name
,
start_timestamp
,
end_timestamp
,
channel
,
group
,
logType
:
formLogType
,
}
=
getFormValues
();
// 使用传入的 logType 或者表单中的 logType 或者状态中的 logType
const
currentLogType
=
customLogType
!==
null
?
customLogType
:
formLogType
!==
undefined
?
formLogType
:
logType
;
let
localStartTimestamp
=
Date
.
parse
(
start_timestamp
)
/
1000
;
let
localStartTimestamp
=
Date
.
parse
(
start_timestamp
)
/
1000
;
let
localEndTimestamp
=
Date
.
parse
(
end_timestamp
)
/
1000
;
let
localEndTimestamp
=
Date
.
parse
(
end_timestamp
)
/
1000
;
if
(
isAdminUser
)
{
if
(
isAdminUser
)
{
url
=
`/api/log/?p=
${
startIdx
}
&page_size=
${
pageSize
}
&type=
${
l
ogType
}
&username=
${
username
}
&token_name=
${
token_name
}
&model_name=
${
model_name
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&channel=
${
channel
}
&group=
${
group
}
`
;
url
=
`/api/log/?p=
${
startIdx
}
&page_size=
${
pageSize
}
&type=
${
currentL
ogType
}
&username=
${
username
}
&token_name=
${
token_name
}
&model_name=
${
model_name
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&channel=
${
channel
}
&group=
${
group
}
`
;
}
else
{
}
else
{
url
=
`/api/log/self/?p=
${
startIdx
}
&page_size=
${
pageSize
}
&type=
${
l
ogType
}
&token_name=
${
token_name
}
&model_name=
${
model_name
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&group=
${
group
}
`
;
url
=
`/api/log/self/?p=
${
startIdx
}
&page_size=
${
pageSize
}
&type=
${
currentL
ogType
}
&token_name=
${
token_name
}
&model_name=
${
model_name
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&group=
${
group
}
`
;
}
}
url
=
encodeURI
(
url
);
url
=
encodeURI
(
url
);
const
res
=
await
API
.
get
(
url
);
const
res
=
await
API
.
get
(
url
);
...
@@ -1045,7 +1100,7 @@ const LogsTable = () => {
...
@@ -1045,7 +1100,7 @@ const LogsTable = () => {
const
handlePageChange
=
(
page
)
=>
{
const
handlePageChange
=
(
page
)
=>
{
setActivePage
(
page
);
setActivePage
(
page
);
loadLogs
(
page
,
pageSize
,
logType
).
then
((
r
)
=>
{
});
loadLogs
(
page
,
pageSize
).
then
((
r
)
=>
{
});
// 不传入logType,让其从表单获取最新值
};
};
const
handlePageSizeChange
=
async
(
size
)
=>
{
const
handlePageSizeChange
=
async
(
size
)
=>
{
...
@@ -1062,7 +1117,7 @@ const LogsTable = () => {
...
@@ -1062,7 +1117,7 @@ const LogsTable = () => {
const
refresh
=
async
()
=>
{
const
refresh
=
async
()
=>
{
setActivePage
(
1
);
setActivePage
(
1
);
handleEyeClick
();
handleEyeClick
();
await
loadLogs
(
activePage
,
pageSize
,
logType
);
await
loadLogs
(
1
,
pageSize
);
// 不传入logType,让其从表单获取最新值
};
};
const
copyText
=
async
(
e
,
text
)
=>
{
const
copyText
=
async
(
e
,
text
)
=>
{
...
@@ -1083,9 +1138,15 @@ const LogsTable = () => {
...
@@ -1083,9 +1138,15 @@ const LogsTable = () => {
.
catch
((
reason
)
=>
{
.
catch
((
reason
)
=>
{
showError
(
reason
);
showError
(
reason
);
});
});
handleEyeClick
();
},
[]);
},
[]);
// 当 formApi 可用时,初始化统计
useEffect
(()
=>
{
if
(
formApi
)
{
handleEyeClick
();
}
},
[
formApi
]);
const
expandRowRender
=
(
record
,
index
)
=>
{
const
expandRowRender
=
(
record
,
index
)
=>
{
return
<
Descriptions
data
=
{
expandData
[
record
.
key
]}
/>
;
return
<
Descriptions
data
=
{
expandData
[
record
.
key
]}
/>
;
};
};
...
@@ -1149,115 +1210,144 @@ const LogsTable = () => {
...
@@ -1149,115 +1210,144 @@ const LogsTable = () => {
<
Divider
margin
=
'12px'
/>
<
Divider
margin
=
'12px'
/>
{
/* 搜索表单区域 */
}
{
/* 搜索表单区域 */
}
<
div
className
=
'flex flex-col gap-4'
>
<
Form
<
div
className
=
'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'
>
initValues
=
{
formInitValues
}
{
/* 时间选择器 */
}
getFormApi
=
{(
api
)
=>
setFormApi
(
api
)}
<
div
className
=
'col-span-1 lg:col-span-2'
>
onSubmit
=
{
refresh
}
<
DatePicker
allowEmpty
=
{
true
}
className
=
'w-full'
autoComplete
=
"off"
value
=
{[
start_timestamp
,
end_timestamp
]}
layout
=
"vertical"
type
=
'dateTimeRange'
trigger
=
"change"
onChange
=
{(
value
)
=>
{
stopValidateWithError
=
{
false
}
if
(
Array
.
isArray
(
value
)
&&
value
.
length
===
2
)
{
>
handleInputChange
(
value
[
0
],
'start_timestamp'
);
<
div
className
=
'flex flex-col gap-4'
>
handleInputChange
(
value
[
1
],
'end_timestamp'
);
<
div
className
=
'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'
>
}
{
/* 时间选择器 */
}
}}
<
div
className
=
'col-span-1 lg:col-span-2'
>
<
Form
.
DatePicker
field
=
'dateRange'
className
=
'w-full'
type
=
'dateTimeRange'
placeholder
=
{[
t
(
'开始时间'
),
t
(
'结束时间'
)]}
showClear
pure
/>
<
/div
>
{
/* 其他搜索字段 */
}
<
Form
.
Input
field
=
'token_name'
prefix
=
{
<
IconSearch
/>
}
placeholder
=
{
t
(
'令牌名称'
)}
className
=
'!rounded-full'
showClear
pure
/>
/>
<
/div
>
{
/* 日志类型选择器 */
}
<
Form
.
Input
<
Select
field
=
'model_name'
value
=
{
logType
.
toString
()}
prefix
=
{
<
IconSearch
/>
}
placeholder
=
{
t
(
'日志类型'
)}
placeholder
=
{
t
(
'模型名称'
)}
className
=
'!rounded-full'
className
=
'!rounded-full'
onChange
=
{(
value
)
=>
{
showClear
setLogType
(
parseInt
(
value
));
pure
loadLogs
(
0
,
pageSize
,
parseInt
(
value
));
/>
}}
>
<
Select
.
Option
value
=
'0'
>
{
t
(
'全部'
)}
<
/Select.Option
>
<
Select
.
Option
value
=
'1'
>
{
t
(
'充值'
)}
<
/Select.Option
>
<
Select
.
Option
value
=
'2'
>
{
t
(
'消费'
)}
<
/Select.Option
>
<
Select
.
Option
value
=
'3'
>
{
t
(
'管理'
)}
<
/Select.Option
>
<
Select
.
Option
value
=
'4'
>
{
t
(
'系统'
)}
<
/Select.Option
>
<
Select
.
Option
value
=
'5'
>
{
t
(
'错误'
)}
<
/Select.Option
>
<
/Select
>
{
/* 其他搜索字段 */
}
<
Form
.
Input
<
Input
field
=
'group'
prefix
=
{
<
IconSearch
/>
}
prefix
=
{
<
IconSearch
/>
}
placeholder
=
{
t
(
'令牌名称'
)}
placeholder
=
{
t
(
'分组'
)}
value
=
{
token_name
}
className
=
'!rounded-full'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'token_name'
)}
showClear
className
=
'!rounded-full'
pure
showClear
/>
/>
<
Input
{
isAdminUser
&&
(
prefix
=
{
<
IconSearch
/>
}
<>
placeholder
=
{
t
(
'模型名称'
)}
<
Form
.
Input
value
=
{
model_name
}
field
=
'channel'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'model_name'
)}
prefix
=
{
<
IconSearch
/>
}
className
=
'!rounded-full'
placeholder
=
{
t
(
'渠道 ID'
)}
showClear
className
=
'!rounded-full'
/>
showClear
pure
/>
<
Form
.
Input
field
=
'username'
prefix
=
{
<
IconSearch
/>
}
placeholder
=
{
t
(
'用户名称'
)}
className
=
'!rounded-full'
showClear
pure
/>
<
/
>
)}
<
/div
>
<
Input
{
/* 操作按钮区域 */
}
prefix
=
{
<
IconSearch
/>
}
<
div
className
=
'flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3'
>
placeholder
=
{
t
(
'分组'
)}
{
/* 日志类型选择器 */
}
value
=
{
group
}
<
div
className
=
'w-full sm:w-auto'
>
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'group'
)}
<
Form
.
Select
className
=
'!rounded-full'
field
=
'logType'
showClear
placeholder
=
{
t
(
'日志类型'
)}
/>
className
=
'!rounded-full w-full sm:w-auto min-w-[120px]'
showClear
pure
onChange
=
{()
=>
{
// 延迟执行搜索,让表单值先更新
setTimeout
(()
=>
{
refresh
();
},
0
);
}}
>
<
Form
.
Select
.
Option
value
=
'0'
>
{
t
(
'全部'
)}
<
/Form.Select.Option
>
<
Form
.
Select
.
Option
value
=
'1'
>
{
t
(
'充值'
)}
<
/Form.Select.Option
>
<
Form
.
Select
.
Option
value
=
'2'
>
{
t
(
'消费'
)}
<
/Form.Select.Option
>
<
Form
.
Select
.
Option
value
=
'3'
>
{
t
(
'管理'
)}
<
/Form.Select.Option
>
<
Form
.
Select
.
Option
value
=
'4'
>
{
t
(
'系统'
)}
<
/Form.Select.Option
>
<
Form
.
Select
.
Option
value
=
'5'
>
{
t
(
'错误'
)}
<
/Form.Select.Option
>
<
/Form.Select
>
<
/div
>
{
isAdminUser
&&
(
<
div
className
=
'flex gap-2 w-full sm:w-auto justify-end'
>
<>
<
Button
<
Input
type
=
'primary'
prefix
=
{
<
IconSearch
/>
}
htmlType
=
'submit'
placeholder
=
{
t
(
'渠道 ID'
)}
loading
=
{
loading
}
value
=
{
channel
}
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'channel'
)}
className
=
'!rounded-full'
className
=
'!rounded-full'
showClear
>
/>
{
t
(
'查询'
)}
<
Input
<
/Button
>
prefix
=
{
<
IconSearch
/>
}
<
Button
placeholder
=
{
t
(
'用户名称'
)}
theme
=
'light'
value
=
{
username
}
onClick
=
{()
=>
{
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'username'
)}
if
(
formApi
)
{
formApi
.
reset
();
setLogType
(
0
);
// 重置后立即查询,使用setTimeout确保表单重置完成
setTimeout
(()
=>
{
refresh
();
},
100
);
}
}}
className
=
'!rounded-full'
className
=
'!rounded-full'
showClear
>
/>
{
t
(
'重置'
)}
<
/
>
<
/Button
>
)}
<
Button
<
/div
>
theme
=
'light'
type
=
'tertiary'
{
/* 操作按钮区域 */
}
icon
=
{
<
IconSetting
/>
}
<
div
className
=
'flex justify-between items-center pt-2'
>
onClick
=
{()
=>
setShowColumnSelector
(
true
)}
<
div
><
/div
>
className
=
'!rounded-full'
<
div
className
=
'flex gap-2'
>
>
<
Button
{
t
(
'列设置'
)}
type
=
'primary'
<
/Button
>
onClick
=
{
refresh
}
<
/div
>
loading
=
{
loading
}
className
=
'!rounded-full'
>
{
t
(
'查询'
)}
<
/Button
>
<
Button
theme
=
'light'
type
=
'tertiary'
icon
=
{
<
IconSetting
/>
}
onClick
=
{()
=>
setShowColumnSelector
(
true
)}
className
=
'!rounded-full'
>
{
t
(
'列设置'
)}
<
/Button
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/
div
>
<
/
Form
>
<
/div
>
<
/div
>
}
}
shadows
=
'always'
shadows
=
'always'
...
@@ -1276,6 +1366,14 @@ const LogsTable = () => {
...
@@ -1276,6 +1366,14 @@ const LogsTable = () => {
scroll
=
{{
x
:
'max-content'
}}
scroll
=
{{
x
:
'max-content'
}}
className
=
'rounded-xl overflow-hidden'
className
=
'rounded-xl overflow-hidden'
size
=
'middle'
size
=
'middle'
empty
=
{
<
Empty
image
=
{
<
IllustrationNoResult
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
darkModeImage
=
{
<
IllustrationNoResultDark
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
description
=
{
t
(
'搜索无结果'
)}
style
=
{{
padding
:
30
}}
/
>
}
pagination
=
{{
pagination
=
{{
formatPageText
:
(
page
)
=>
formatPageText
:
(
page
)
=>
t
(
'第 {{start}} - {{end}} 条,共 {{total}} 条'
,
{
t
(
'第 {{start}} - {{end}} 条,共 {{total}} 条'
,
{
...
...
web/src/components/table/MjLogsTable.js
View file @
c821b0ed
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
import
{
Palette
,
ZoomIn
,
Shuffle
,
Move
,
FileText
,
Blend
,
Upload
,
Minimize2
,
RotateCcw
,
PaintBucket
,
Focus
,
Move3D
,
Monitor
,
UserCheck
,
HelpCircle
,
CheckCircle
,
Clock
,
Copy
,
FileX
,
Pause
,
XCircle
,
Loader
,
AlertCircle
,
Hash
}
from
'lucide-react'
;
import
{
API
,
API
,
copy
,
copy
,
isAdmin
,
isAdmin
,
showError
,
showError
,
showSuccess
,
showSuccess
,
timestamp2string
,
timestamp2string
}
from
'../../helpers'
;
}
from
'../../helpers'
;
import
{
import
{
Button
,
Button
,
Card
,
Card
,
Checkbox
,
Checkbox
,
DatePicker
,
Divider
,
Divider
,
Empty
,
Form
,
ImagePreview
,
ImagePreview
,
Input
,
Layout
,
Layout
,
Modal
,
Modal
,
Progress
,
Progress
,
Skeleton
,
Skeleton
,
Table
,
Table
,
Tag
,
Tag
,
Typography
,
Typography
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
IllustrationNoResult
,
IllustrationNoResultDark
}
from
'@douyinfe/semi-illustrations'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
{
import
{
IconEyeOpened
,
IconEyeOpened
,
IconSearch
,
IconSearch
,
IconSetting
,
IconSetting
}
from
'@douyinfe/semi-icons'
;
}
from
'@douyinfe/semi-icons'
;
const
{
Text
}
=
Typography
;
const
{
Text
}
=
Typography
;
...
@@ -154,103 +184,103 @@ const LogsTable = () => {
...
@@ -154,103 +184,103 @@ const LogsTable = () => {
switch
(
type
)
{
switch
(
type
)
{
case
'IMAGINE'
:
case
'IMAGINE'
:
return
(
return
(
<
Tag
color
=
'blue'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'blue'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Palette
size
=
{
14
}
/>}
>
{
t
(
'绘图'
)}
{
t
(
'绘图'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'UPSCALE'
:
case
'UPSCALE'
:
return
(
return
(
<
Tag
color
=
'orange'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'orange'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
ZoomIn
size
=
{
14
}
/>}
>
{
t
(
'放大'
)}
{
t
(
'放大'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'VARIATION'
:
case
'VARIATION'
:
return
(
return
(
<
Tag
color
=
'purple'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'purple'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Shuffle
size
=
{
14
}
/>}
>
{
t
(
'变换'
)}
{
t
(
'变换'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'HIGH_VARIATION'
:
case
'HIGH_VARIATION'
:
return
(
return
(
<
Tag
color
=
'purple'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'purple'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Shuffle
size
=
{
14
}
/>}
>
{
t
(
'强变换'
)}
{
t
(
'强变换'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'LOW_VARIATION'
:
case
'LOW_VARIATION'
:
return
(
return
(
<
Tag
color
=
'purple'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'purple'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Shuffle
size
=
{
14
}
/>}
>
{
t
(
'弱变换'
)}
{
t
(
'弱变换'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'PAN'
:
case
'PAN'
:
return
(
return
(
<
Tag
color
=
'cyan'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'cyan'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Move
size
=
{
14
}
/>}
>
{
t
(
'平移'
)}
{
t
(
'平移'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'DESCRIBE'
:
case
'DESCRIBE'
:
return
(
return
(
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
FileText
size
=
{
14
}
/>}
>
{
t
(
'图生文'
)}
{
t
(
'图生文'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'BLEND'
:
case
'BLEND'
:
return
(
return
(
<
Tag
color
=
'lime'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'lime'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Blend
size
=
{
14
}
/>}
>
{
t
(
'图混合'
)}
{
t
(
'图混合'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'UPLOAD'
:
case
'UPLOAD'
:
return
(
return
(
<
Tag
color
=
'blue'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'blue'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Upload
size
=
{
14
}
/>}
>
上传文件
上传文件
<
/Tag
>
<
/Tag
>
);
);
case
'SHORTEN'
:
case
'SHORTEN'
:
return
(
return
(
<
Tag
color
=
'pink'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'pink'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Minimize2
size
=
{
14
}
/>}
>
{
t
(
'缩词'
)}
{
t
(
'缩词'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'REROLL'
:
case
'REROLL'
:
return
(
return
(
<
Tag
color
=
'indigo'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'indigo'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
RotateCcw
size
=
{
14
}
/>}
>
{
t
(
'重绘'
)}
{
t
(
'重绘'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'INPAINT'
:
case
'INPAINT'
:
return
(
return
(
<
Tag
color
=
'violet'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'violet'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
PaintBucket
size
=
{
14
}
/>}
>
{
t
(
'局部重绘-提交'
)}
{
t
(
'局部重绘-提交'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'ZOOM'
:
case
'ZOOM'
:
return
(
return
(
<
Tag
color
=
'teal'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'teal'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Focus
size
=
{
14
}
/>}
>
{
t
(
'变焦'
)}
{
t
(
'变焦'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'CUSTOM_ZOOM'
:
case
'CUSTOM_ZOOM'
:
return
(
return
(
<
Tag
color
=
'teal'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'teal'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Move3D
size
=
{
14
}
/>}
>
{
t
(
'自定义变焦-提交'
)}
{
t
(
'自定义变焦-提交'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'MODAL'
:
case
'MODAL'
:
return
(
return
(
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Monitor
size
=
{
14
}
/>}
>
{
t
(
'窗口处理'
)}
{
t
(
'窗口处理'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'SWAP_FACE'
:
case
'SWAP_FACE'
:
return
(
return
(
<
Tag
color
=
'light-green'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'light-green'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
UserCheck
size
=
{
14
}
/>}
>
{
t
(
'换脸'
)}
{
t
(
'换脸'
)}
<
/Tag
>
<
/Tag
>
);
);
default
:
default
:
return
(
return
(
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
HelpCircle
size
=
{
14
}
/>}
>
{
t
(
'未知'
)}
{
t
(
'未知'
)}
<
/Tag
>
<
/Tag
>
);
);
...
@@ -261,31 +291,31 @@ const LogsTable = () => {
...
@@ -261,31 +291,31 @@ const LogsTable = () => {
switch
(
code
)
{
switch
(
code
)
{
case
1
:
case
1
:
return
(
return
(
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
CheckCircle
size
=
{
14
}
/>}
>
{
t
(
'已提交'
)}
{
t
(
'已提交'
)}
<
/Tag
>
<
/Tag
>
);
);
case
21
:
case
21
:
return
(
return
(
<
Tag
color
=
'lime'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'lime'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Clock
size
=
{
14
}
/>}
>
{
t
(
'等待中'
)}
{
t
(
'等待中'
)}
<
/Tag
>
<
/Tag
>
);
);
case
22
:
case
22
:
return
(
return
(
<
Tag
color
=
'orange'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'orange'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Copy
size
=
{
14
}
/>}
>
{
t
(
'重复提交'
)}
{
t
(
'重复提交'
)}
<
/Tag
>
<
/Tag
>
);
);
case
0
:
case
0
:
return
(
return
(
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
FileX
size
=
{
14
}
/>}
>
{
t
(
'未提交'
)}
{
t
(
'未提交'
)}
<
/Tag
>
<
/Tag
>
);
);
default
:
default
:
return
(
return
(
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
HelpCircle
size
=
{
14
}
/>}
>
{
t
(
'未知'
)}
{
t
(
'未知'
)}
<
/Tag
>
<
/Tag
>
);
);
...
@@ -296,43 +326,43 @@ const LogsTable = () => {
...
@@ -296,43 +326,43 @@ const LogsTable = () => {
switch
(
type
)
{
switch
(
type
)
{
case
'SUCCESS'
:
case
'SUCCESS'
:
return
(
return
(
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
CheckCircle
size
=
{
14
}
/>}
>
{
t
(
'成功'
)}
{
t
(
'成功'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'NOT_START'
:
case
'NOT_START'
:
return
(
return
(
<
Tag
color
=
'grey'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'grey'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Pause
size
=
{
14
}
/>}
>
{
t
(
'未启动'
)}
{
t
(
'未启动'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'SUBMITTED'
:
case
'SUBMITTED'
:
return
(
return
(
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Clock
size
=
{
14
}
/>}
>
{
t
(
'队列中'
)}
{
t
(
'队列中'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'IN_PROGRESS'
:
case
'IN_PROGRESS'
:
return
(
return
(
<
Tag
color
=
'blue'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'blue'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Loader
size
=
{
14
}
/>}
>
{
t
(
'执行中'
)}
{
t
(
'执行中'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'FAILURE'
:
case
'FAILURE'
:
return
(
return
(
<
Tag
color
=
'red'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'red'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
XCircle
size
=
{
14
}
/>}
>
{
t
(
'失败'
)}
{
t
(
'失败'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'MODAL'
:
case
'MODAL'
:
return
(
return
(
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
AlertCircle
size
=
{
14
}
/>}
>
{
t
(
'窗口等待'
)}
{
t
(
'窗口等待'
)}
<
/Tag
>
<
/Tag
>
);
);
default
:
default
:
return
(
return
(
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
HelpCircle
size
=
{
14
}
/>}
>
{
t
(
'未知'
)}
{
t
(
'未知'
)}
<
/Tag
>
<
/Tag
>
);
);
...
@@ -362,7 +392,7 @@ const LogsTable = () => {
...
@@ -362,7 +392,7 @@ const LogsTable = () => {
const
color
=
durationSec
>
60
?
'red'
:
'green'
;
const
color
=
durationSec
>
60
?
'red'
:
'green'
;
return
(
return
(
<
Tag
color
=
{
color
}
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
{
color
}
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Clock
size
=
{
14
}
/>}
>
{
durationSec
}
{
t
(
'秒'
)}
{
durationSec
}
{
t
(
'秒'
)}
<
/Tag
>
<
/Tag
>
);
);
...
@@ -398,6 +428,7 @@ const LogsTable = () => {
...
@@ -398,6 +428,7 @@ const LogsTable = () => {
color
=
{
colors
[
parseInt
(
text
)
%
colors
.
length
]}
color
=
{
colors
[
parseInt
(
text
)
%
colors
.
length
]}
size
=
'large'
size
=
'large'
shape
=
'circle'
shape
=
'circle'
prefixIcon
=
{
<
Hash
size
=
{
14
}
/>
}
onClick
=
{()
=>
{
onClick
=
{()
=>
{
copyText
(
text
);
copyText
(
text
);
}}
}}
...
@@ -462,7 +493,7 @@ const LogsTable = () => {
...
@@ -462,7 +493,7 @@ const LogsTable = () => {
percent
=
{
text
?
parseInt
(
text
.
replace
(
'%'
,
''
))
:
0
}
percent
=
{
text
?
parseInt
(
text
.
replace
(
'%'
,
''
))
:
0
}
showInfo
=
{
true
}
showInfo
=
{
true
}
aria
-
label
=
'drawing progress'
aria
-
label
=
'drawing progress'
style
=
{{
minWidth
:
'
20
0px'
}}
style
=
{{
minWidth
:
'
16
0px'
}}
/
>
/
>
}
}
<
/div
>
<
/div
>
...
@@ -483,6 +514,7 @@ const LogsTable = () => {
...
@@ -483,6 +514,7 @@ const LogsTable = () => {
setModalImageUrl
(
text
);
setModalImageUrl
(
text
);
setIsModalOpenurl
(
true
);
setIsModalOpenurl
(
true
);
}}
}}
className
=
"!rounded-full"
>
>
{
t
(
'查看图片'
)}
{
t
(
'查看图片'
)}
<
/Button
>
<
/Button
>
...
@@ -570,7 +602,6 @@ const LogsTable = () => {
...
@@ -570,7 +602,6 @@ const LogsTable = () => {
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
const
[
logCount
,
setLogCount
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
logCount
,
setLogCount
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
logType
,
setLogType
]
=
useState
(
0
);
const
[
pageSize
,
setPageSize
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
pageSize
,
setPageSize
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
isModalOpenurl
,
setIsModalOpenurl
]
=
useState
(
false
);
const
[
isModalOpenurl
,
setIsModalOpenurl
]
=
useState
(
false
);
const
[
showBanner
,
setShowBanner
]
=
useState
(
false
);
const
[
showBanner
,
setShowBanner
]
=
useState
(
false
);
...
@@ -578,22 +609,44 @@ const LogsTable = () => {
...
@@ -578,22 +609,44 @@ const LogsTable = () => {
// 定义模态框图片URL的状态和更新函数
// 定义模态框图片URL的状态和更新函数
const
[
modalImageUrl
,
setModalImageUrl
]
=
useState
(
''
);
const
[
modalImageUrl
,
setModalImageUrl
]
=
useState
(
''
);
let
now
=
new
Date
();
let
now
=
new
Date
();
// 初始化start_timestamp为前一天
const
[
inputs
,
setInputs
]
=
useState
({
// Form 初始值
const
formInitValues
=
{
channel_id
:
''
,
channel_id
:
''
,
mj_id
:
''
,
mj_id
:
''
,
start_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
-
2592000
),
dateRange
:
[
end_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
),
timestamp2string
(
now
.
getTime
()
/
1000
-
2592000
),
});
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
)
const
{
channel_id
,
mj_id
,
start_timestamp
,
end_timestamp
}
=
inputs
;
],
};
// Form API 引用
const
[
formApi
,
setFormApi
]
=
useState
(
null
);
const
[
stat
,
setStat
]
=
useState
({
const
[
stat
,
setStat
]
=
useState
({
quota
:
0
,
quota
:
0
,
token
:
0
,
token
:
0
,
});
});
const
handleInputChange
=
(
value
,
name
)
=>
{
// 获取表单值的辅助函数
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
const
getFormValues
=
()
=>
{
const
formValues
=
formApi
?
formApi
.
getValues
()
:
{};
// 处理时间范围
let
start_timestamp
=
timestamp2string
(
now
.
getTime
()
/
1000
-
2592000
);
let
end_timestamp
=
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
);
if
(
formValues
.
dateRange
&&
Array
.
isArray
(
formValues
.
dateRange
)
&&
formValues
.
dateRange
.
length
===
2
)
{
start_timestamp
=
formValues
.
dateRange
[
0
];
end_timestamp
=
formValues
.
dateRange
[
1
];
}
return
{
channel_id
:
formValues
.
channel_id
||
''
,
mj_id
:
formValues
.
mj_id
||
''
,
start_timestamp
,
end_timestamp
,
};
};
};
const
setLogsFormat
=
(
logs
)
=>
{
const
setLogsFormat
=
(
logs
)
=>
{
...
@@ -611,6 +664,7 @@ const LogsTable = () => {
...
@@ -611,6 +664,7 @@ const LogsTable = () => {
setLoading
(
true
);
setLoading
(
true
);
let
url
=
''
;
let
url
=
''
;
const
{
channel_id
,
mj_id
,
start_timestamp
,
end_timestamp
}
=
getFormValues
();
let
localStartTimestamp
=
Date
.
parse
(
start_timestamp
);
let
localStartTimestamp
=
Date
.
parse
(
start_timestamp
);
let
localEndTimestamp
=
Date
.
parse
(
end_timestamp
);
let
localEndTimestamp
=
Date
.
parse
(
end_timestamp
);
if
(
isAdminUser
)
{
if
(
isAdminUser
)
{
...
@@ -673,7 +727,7 @@ const LogsTable = () => {
...
@@ -673,7 +727,7 @@ const LogsTable = () => {
const
localPageSize
=
parseInt
(
localStorage
.
getItem
(
'mj-page-size'
))
||
ITEMS_PER_PAGE
;
const
localPageSize
=
parseInt
(
localStorage
.
getItem
(
'mj-page-size'
))
||
ITEMS_PER_PAGE
;
setPageSize
(
localPageSize
);
setPageSize
(
localPageSize
);
loadLogs
(
0
,
localPageSize
).
then
();
loadLogs
(
0
,
localPageSize
).
then
();
},
[
logType
]);
},
[]);
useEffect
(()
=>
{
useEffect
(()
=>
{
const
mjNotifyEnabled
=
localStorage
.
getItem
(
'mj_notify_enabled'
);
const
mjNotifyEnabled
=
localStorage
.
getItem
(
'mj_notify_enabled'
);
...
@@ -788,70 +842,93 @@ const LogsTable = () => {
...
@@ -788,70 +842,93 @@ const LogsTable = () => {
<
Divider
margin
=
"12px"
/>
<
Divider
margin
=
"12px"
/>
{
/* 搜索表单区域 */
}
{
/* 搜索表单区域 */
}
<
div
className
=
"flex flex-col gap-4"
>
<
Form
<
div
className
=
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
>
initValues
=
{
formInitValues
}
{
/* 时间选择器 */
}
getFormApi
=
{(
api
)
=>
setFormApi
(
api
)}
<
div
className
=
"col-span-1 lg:col-span-2"
>
onSubmit
=
{
refresh
}
<
DatePicker
allowEmpty
=
{
true
}
className
=
"w-full"
autoComplete
=
"off"
value
=
{[
start_timestamp
,
end_timestamp
]}
layout
=
"vertical"
type
=
'dateTimeRange'
trigger
=
"change"
onChange
=
{(
value
)
=>
{
stopValidateWithError
=
{
false
}
if
(
Array
.
isArray
(
value
)
&&
value
.
length
===
2
)
{
>
handleInputChange
(
value
[
0
],
'start_timestamp'
);
<
div
className
=
"flex flex-col gap-4"
>
handleInputChange
(
value
[
1
],
'end_timestamp'
);
<
div
className
=
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
>
}
{
/* 时间选择器 */
}
}}
<
div
className
=
"col-span-1 lg:col-span-2"
>
/
>
<
Form
.
DatePicker
<
/div
>
field
=
'dateRange'
className
=
"w-full"
{
/* 任务 ID */
}
type
=
'dateTimeRange'
<
Input
placeholder
=
{[
t
(
'开始时间'
),
t
(
'结束时间'
)]}
prefix
=
{
<
IconSearch
/>
}
showClear
placeholder
=
{
t
(
'任务 ID'
)}
pure
value
=
{
mj_id
}
/>
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'mj_id'
)}
<
/div
>
className
=
"!rounded-full"
showClear
{
/* 任务 ID */
}
/>
<
Form
.
Input
field
=
'mj_id'
{
/* 渠道 ID - 仅管理员可见 */
}
{
isAdminUser
&&
(
<
Input
prefix
=
{
<
IconSearch
/>
}
prefix
=
{
<
IconSearch
/>
}
placeholder
=
{
t
(
'渠道 ID'
)}
placeholder
=
{
t
(
'任务 ID'
)}
value
=
{
channel_id
}
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'channel_id'
)}
className
=
"!rounded-full"
className
=
"!rounded-full"
showClear
showClear
pure
/>
/>
)}
<
/div
>
{
/* 操作按钮区域 */
}
{
/* 渠道 ID - 仅管理员可见 */
}
<
div
className
=
"flex justify-between items-center pt-2"
>
{
isAdminUser
&&
(
<
div
><
/div
>
<
Form
.
Input
<
div
className
=
"flex gap-2"
>
field
=
'channel_id'
<
Button
prefix
=
{
<
IconSearch
/>
}
type
=
'primary'
placeholder
=
{
t
(
'渠道 ID'
)}
onClick
=
{
refresh
}
className
=
"!rounded-full"
loading
=
{
loading
}
showClear
className
=
"!rounded-full"
pure
>
/>
{
t
(
'查询'
)}
)}
<
/Button
>
<
/div
>
<
Button
theme
=
'light'
{
/* 操作按钮区域 */
}
type
=
'tertiary'
<
div
className
=
"flex justify-between items-center"
>
icon
=
{
<
IconSetting
/>
}
<
div
><
/div
>
onClick
=
{()
=>
setShowColumnSelector
(
true
)}
<
div
className
=
"flex gap-2"
>
className
=
"!rounded-full"
<
Button
>
type
=
'primary'
{
t
(
'列设置'
)}
htmlType
=
'submit'
<
/Button
>
loading
=
{
loading
}
className
=
"!rounded-full"
>
{
t
(
'查询'
)}
<
/Button
>
<
Button
theme
=
'light'
onClick
=
{()
=>
{
if
(
formApi
)
{
formApi
.
reset
();
// 重置后立即查询,使用setTimeout确保表单重置完成
setTimeout
(()
=>
{
refresh
();
},
100
);
}
}}
className
=
"!rounded-full"
>
{
t
(
'重置'
)}
<
/Button
>
<
Button
theme
=
'light'
type
=
'tertiary'
icon
=
{
<
IconSetting
/>
}
onClick
=
{()
=>
setShowColumnSelector
(
true
)}
className
=
"!rounded-full"
>
{
t
(
'列设置'
)}
<
/Button
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/
div
>
<
/
Form
>
<
/div
>
<
/div
>
}
}
shadows
=
'always'
shadows
=
'always'
...
@@ -865,6 +942,14 @@ const LogsTable = () => {
...
@@ -865,6 +942,14 @@ const LogsTable = () => {
scroll
=
{{
x
:
'max-content'
}}
scroll
=
{{
x
:
'max-content'
}}
className
=
"rounded-xl overflow-hidden"
className
=
"rounded-xl overflow-hidden"
size
=
"middle"
size
=
"middle"
empty
=
{
<
Empty
image
=
{
<
IllustrationNoResult
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
darkModeImage
=
{
<
IllustrationNoResultDark
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
description
=
{
t
(
'搜索无结果'
)}
style
=
{{
padding
:
30
}}
/
>
}
pagination
=
{{
pagination
=
{{
formatPageText
:
(
page
)
=>
formatPageText
:
(
page
)
=>
t
(
'第 {{start}} - {{end}} 条,共 {{total}} 条'
,
{
t
(
'第 {{start}} - {{end}} 条,共 {{total}} 条'
,
{
...
...
web/src/components/table/ModelPricing.js
View file @
c821b0ed
...
@@ -17,14 +17,19 @@ import {
...
@@ -17,14 +17,19 @@ import {
Tabs
,
Tabs
,
TabPane
,
TabPane
,
Dropdown
,
Dropdown
,
Empty
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
import
{
IllustrationNoResult
,
IllustrationNoResultDark
}
from
'@douyinfe/semi-illustrations'
;
import
{
IconVerify
,
IconVerify
,
IconHelpCircle
,
IconHelpCircle
,
IconSearch
,
IconSearch
,
IconCopy
,
IconCopy
,
IconInfoCircle
,
IconInfoCircle
,
IconLayers
,
IconLayers
}
from
'@douyinfe/semi-icons'
;
}
from
'@douyinfe/semi-icons'
;
import
{
UserContext
}
from
'../../context/User/index.js'
;
import
{
UserContext
}
from
'../../context/User/index.js'
;
import
{
AlertCircle
}
from
'lucide-react'
;
import
{
AlertCircle
}
from
'lucide-react'
;
...
@@ -489,6 +494,14 @@ const ModelPricing = () => {
...
@@ -489,6 +494,14 @@ const ModelPricing = () => {
loading
=
{
loading
}
loading
=
{
loading
}
rowSelection
=
{
rowSelection
}
rowSelection
=
{
rowSelection
}
className
=
"custom-table"
className
=
"custom-table"
empty
=
{
<
Empty
image
=
{
<
IllustrationNoResult
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
darkModeImage
=
{
<
IllustrationNoResultDark
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
description
=
{
t
(
'搜索无结果'
)}
style
=
{{
padding
:
30
}}
/
>
}
pagination
=
{{
pagination
=
{{
defaultPageSize
:
10
,
defaultPageSize
:
10
,
pageSize
:
pageSize
,
pageSize
:
pageSize
,
...
...
web/src/components/table/RedemptionsTable.js
View file @
c821b0ed
...
@@ -8,21 +8,34 @@ import {
...
@@ -8,21 +8,34 @@ import {
renderQuota
renderQuota
}
from
'../../helpers'
;
}
from
'../../helpers'
;
import
{
CheckCircle
,
XCircle
,
Minus
,
HelpCircle
,
Coins
}
from
'lucide-react'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
{
import
{
Button
,
Button
,
Card
,
Card
,
Divider
,
Divider
,
Dropdown
,
Dropdown
,
Input
,
Empty
,
Form
,
Modal
,
Modal
,
Popover
,
Popover
,
Space
,
Space
,
Table
,
Table
,
Tag
,
Tag
,
Typography
,
Typography
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
import
{
IllustrationNoResult
,
IllustrationNoResultDark
}
from
'@douyinfe/semi-illustrations'
;
import
{
IconPlus
,
IconPlus
,
IconCopy
,
IconCopy
,
IconSearch
,
IconSearch
,
...
@@ -31,7 +44,7 @@ import {
...
@@ -31,7 +44,7 @@ import {
IconDelete
,
IconDelete
,
IconStop
,
IconStop
,
IconPlay
,
IconPlay
,
IconMore
,
IconMore
}
from
'@douyinfe/semi-icons'
;
}
from
'@douyinfe/semi-icons'
;
import
EditRedemption
from
'../../pages/Redemption/EditRedemption'
;
import
EditRedemption
from
'../../pages/Redemption/EditRedemption'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useTranslation
}
from
'react-i18next'
;
...
@@ -49,25 +62,25 @@ const RedemptionsTable = () => {
...
@@ -49,25 +62,25 @@ const RedemptionsTable = () => {
switch
(
status
)
{
switch
(
status
)
{
case
1
:
case
1
:
return
(
return
(
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
CheckCircle
size
=
{
14
}
/>}
>
{
t
(
'未使用'
)}
{
t
(
'未使用'
)}
<
/Tag
>
<
/Tag
>
);
);
case
2
:
case
2
:
return
(
return
(
<
Tag
color
=
'red'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'red'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
XCircle
size
=
{
14
}
/>}
>
{
t
(
'已禁用'
)}
{
t
(
'已禁用'
)}
<
/Tag
>
<
/Tag
>
);
);
case
3
:
case
3
:
return
(
return
(
<
Tag
color
=
'grey'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'grey'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Minus
size
=
{
14
}
/>}
>
{
t
(
'已使用'
)}
{
t
(
'已使用'
)}
<
/Tag
>
<
/Tag
>
);
);
default
:
default
:
return
(
return
(
<
Tag
color
=
'black'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'black'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
HelpCircle
size
=
{
14
}
/>}
>
{
t
(
'未知状态'
)}
{
t
(
'未知状态'
)}
<
/Tag
>
<
/Tag
>
);
);
...
@@ -95,7 +108,13 @@ const RedemptionsTable = () => {
...
@@ -95,7 +108,13 @@ const RedemptionsTable = () => {
title
:
t
(
'额度'
),
title
:
t
(
'额度'
),
dataIndex
:
'quota'
,
dataIndex
:
'quota'
,
render
:
(
text
,
record
,
index
)
=>
{
render
:
(
text
,
record
,
index
)
=>
{
return
<
div
>
{
renderQuota
(
parseInt
(
text
))}
<
/div>
;
return
(
<
div
>
<
Tag
size
=
{
'large'
}
color
=
{
'grey'
}
shape
=
'circle'
prefixIcon
=
{
<
Coins
size
=
{
14
}
/>}
>
{
renderQuota
(
parseInt
(
text
))}
<
/Tag
>
<
/div
>
);
},
},
},
},
{
{
...
@@ -223,7 +242,6 @@ const RedemptionsTable = () => {
...
@@ -223,7 +242,6 @@ const RedemptionsTable = () => {
const
[
redemptions
,
setRedemptions
]
=
useState
([]);
const
[
redemptions
,
setRedemptions
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
const
[
searchKeyword
,
setSearchKeyword
]
=
useState
(
''
);
const
[
searching
,
setSearching
]
=
useState
(
false
);
const
[
searching
,
setSearching
]
=
useState
(
false
);
const
[
tokenCount
,
setTokenCount
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
tokenCount
,
setTokenCount
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
selectedKeys
,
setSelectedKeys
]
=
useState
([]);
const
[
selectedKeys
,
setSelectedKeys
]
=
useState
([]);
...
@@ -233,6 +251,22 @@ const RedemptionsTable = () => {
...
@@ -233,6 +251,22 @@ const RedemptionsTable = () => {
});
});
const
[
showEdit
,
setShowEdit
]
=
useState
(
false
);
const
[
showEdit
,
setShowEdit
]
=
useState
(
false
);
// Form 初始值
const
formInitValues
=
{
searchKeyword
:
''
,
};
// Form API 引用
const
[
formApi
,
setFormApi
]
=
useState
(
null
);
// 获取表单值的辅助函数
const
getFormValues
=
()
=>
{
const
formValues
=
formApi
?
formApi
.
getValues
()
:
{};
return
{
searchKeyword
:
formValues
.
searchKeyword
||
''
,
};
};
const
closeEdit
=
()
=>
{
const
closeEdit
=
()
=>
{
setShowEdit
(
false
);
setShowEdit
(
false
);
setTimeout
(()
=>
{
setTimeout
(()
=>
{
...
@@ -340,8 +374,14 @@ const RedemptionsTable = () => {
...
@@ -340,8 +374,14 @@ const RedemptionsTable = () => {
setLoading
(
false
);
setLoading
(
false
);
};
};
const
searchRedemptions
=
async
(
keyword
,
page
,
pageSize
)
=>
{
const
searchRedemptions
=
async
(
keyword
=
null
,
page
,
pageSize
)
=>
{
if
(
searchKeyword
===
''
)
{
// 如果没有传递keyword参数,从表单获取值
if
(
keyword
===
null
)
{
const
formValues
=
getFormValues
();
keyword
=
formValues
.
searchKeyword
;
}
if
(
keyword
===
''
)
{
await
loadRedemptions
(
page
,
pageSize
);
await
loadRedemptions
(
page
,
pageSize
);
return
;
return
;
}
}
...
@@ -361,10 +401,6 @@ const RedemptionsTable = () => {
...
@@ -361,10 +401,6 @@ const RedemptionsTable = () => {
setSearching
(
false
);
setSearching
(
false
);
};
};
const
handleKeywordChange
=
async
(
value
)
=>
{
setSearchKeyword
(
value
.
trim
());
};
const
sortRedemption
=
(
key
)
=>
{
const
sortRedemption
=
(
key
)
=>
{
if
(
redemptions
.
length
===
0
)
return
;
if
(
redemptions
.
length
===
0
)
return
;
setLoading
(
true
);
setLoading
(
true
);
...
@@ -381,6 +417,7 @@ const RedemptionsTable = () => {
...
@@ -381,6 +417,7 @@ const RedemptionsTable = () => {
const
handlePageChange
=
(
page
)
=>
{
const
handlePageChange
=
(
page
)
=>
{
setActivePage
(
page
);
setActivePage
(
page
);
const
{
searchKeyword
}
=
getFormValues
();
if
(
searchKeyword
===
''
)
{
if
(
searchKeyword
===
''
)
{
loadRedemptions
(
page
,
pageSize
).
then
();
loadRedemptions
(
page
,
pageSize
).
then
();
}
else
{
}
else
{
...
@@ -457,28 +494,59 @@ const RedemptionsTable = () => {
...
@@ -457,28 +494,59 @@ const RedemptionsTable = () => {
<
/Button
>
<
/Button
>
<
/div
>
<
/div
>
<
div
className
=
"flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2"
>
<
Form
<
div
className
=
"relative w-full md:w-64"
>
initValues
=
{
formInitValues
}
<
Input
getFormApi
=
{(
api
)
=>
setFormApi
(
api
)}
prefix
=
{
<
IconSearch
/>
}
onSubmit
=
{()
=>
{
placeholder
=
{
t
(
'关键字(id或者名称)'
)}
setActivePage
(
1
);
value
=
{
searchKeyword
}
searchRedemptions
(
null
,
1
,
pageSize
);
onChange
=
{
handleKeywordChange
}
}}
className
=
"!rounded-full"
allowEmpty
=
{
true
}
showClear
autoComplete
=
"off"
/>
layout
=
"horizontal"
trigger
=
"change"
stopValidateWithError
=
{
false
}
className
=
"w-full md:w-auto order-1 md:order-2"
>
<
div
className
=
"flex flex-col md:flex-row items-center gap-4 w-full md:w-auto"
>
<
div
className
=
"relative w-full md:w-64"
>
<
Form
.
Input
field
=
"searchKeyword"
prefix
=
{
<
IconSearch
/>
}
placeholder
=
{
t
(
'关键字(id或者名称)'
)}
className
=
"!rounded-full"
showClear
pure
/>
<
/div
>
<
div
className
=
"flex gap-2 w-full md:w-auto"
>
<
Button
type
=
"primary"
htmlType
=
"submit"
loading
=
{
loading
||
searching
}
className
=
"!rounded-full flex-1 md:flex-initial md:w-auto"
>
{
t
(
'查询'
)}
<
/Button
>
<
Button
theme
=
"light"
onClick
=
{()
=>
{
if
(
formApi
)
{
formApi
.
reset
();
// 重置后立即查询,使用setTimeout确保表单重置完成
setTimeout
(()
=>
{
setActivePage
(
1
);
loadRedemptions
(
1
,
pageSize
);
},
100
);
}
}}
className
=
"!rounded-full flex-1 md:flex-initial md:w-auto"
>
{
t
(
'重置'
)}
<
/Button
>
<
/div
>
<
/div
>
<
/div
>
<
Button
<
/Form
>
type
=
"primary"
onClick
=
{()
=>
{
searchRedemptions
(
searchKeyword
,
1
,
pageSize
).
then
();
}}
loading
=
{
searching
}
className
=
"!rounded-full w-full md:w-auto"
>
{
t
(
'查询'
)}
<
/Button
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
);
);
...
@@ -517,6 +585,7 @@ const RedemptionsTable = () => {
...
@@ -517,6 +585,7 @@ const RedemptionsTable = () => {
onPageSizeChange
:
(
size
)
=>
{
onPageSizeChange
:
(
size
)
=>
{
setPageSize
(
size
);
setPageSize
(
size
);
setActivePage
(
1
);
setActivePage
(
1
);
const
{
searchKeyword
}
=
getFormValues
();
if
(
searchKeyword
===
''
)
{
if
(
searchKeyword
===
''
)
{
loadRedemptions
(
1
,
size
).
then
();
loadRedemptions
(
1
,
size
).
then
();
}
else
{
}
else
{
...
@@ -528,6 +597,14 @@ const RedemptionsTable = () => {
...
@@ -528,6 +597,14 @@ const RedemptionsTable = () => {
loading
=
{
loading
}
loading
=
{
loading
}
rowSelection
=
{
rowSelection
}
rowSelection
=
{
rowSelection
}
onRow
=
{
handleRow
}
onRow
=
{
handleRow
}
empty
=
{
<
Empty
image
=
{
<
IllustrationNoResult
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
darkModeImage
=
{
<
IllustrationNoResultDark
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
description
=
{
t
(
'搜索无结果'
)}
style
=
{{
padding
:
30
}}
/
>
}
className
=
"rounded-xl overflow-hidden"
className
=
"rounded-xl overflow-hidden"
size
=
"middle"
size
=
"middle"
><
/Table
>
><
/Table
>
...
...
web/src/components/table/TaskLogsTable.js
View file @
c821b0ed
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
import
{
Music
,
FileText
,
HelpCircle
,
CheckCircle
,
Pause
,
Clock
,
Play
,
XCircle
,
Loader
,
List
,
Hash
}
from
'lucide-react'
;
import
{
API
,
API
,
copy
,
copy
,
isAdmin
,
isAdmin
,
showError
,
showError
,
showSuccess
,
showSuccess
,
timestamp2string
,
timestamp2string
}
from
'../../helpers'
;
}
from
'../../helpers'
;
import
{
import
{
Button
,
Button
,
Card
,
Card
,
Checkbox
,
Checkbox
,
DatePicker
,
Divider
,
Divider
,
Input
,
Empty
,
Form
,
Layout
,
Layout
,
Modal
,
Modal
,
Progress
,
Progress
,
Skeleton
,
Skeleton
,
Table
,
Table
,
Tag
,
Tag
,
Typography
,
Typography
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
IllustrationNoResult
,
IllustrationNoResultDark
}
from
'@douyinfe/semi-illustrations'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
{
import
{
IconEyeOpened
,
IconEyeOpened
,
IconSearch
,
IconSearch
,
IconSetting
,
IconSetting
}
from
'@douyinfe/semi-icons'
;
}
from
'@douyinfe/semi-icons'
;
const
{
Text
}
=
Typography
;
const
{
Text
}
=
Typography
;
...
@@ -97,7 +114,7 @@ function renderDuration(submit_time, finishTime) {
...
@@ -97,7 +114,7 @@ function renderDuration(submit_time, finishTime) {
// 返回带有样式的颜色标签
// 返回带有样式的颜色标签
return
(
return
(
<
Tag
color
=
{
color
}
size
=
'large'
>
<
Tag
color
=
{
color
}
size
=
'large'
prefixIcon
=
{
<
Clock
size
=
{
14
}
/>}
>
{
durationSec
}
秒
{
durationSec
}
秒
<
/Tag
>
<
/Tag
>
);
);
...
@@ -188,19 +205,19 @@ const LogsTable = () => {
...
@@ -188,19 +205,19 @@ const LogsTable = () => {
switch
(
type
)
{
switch
(
type
)
{
case
'MUSIC'
:
case
'MUSIC'
:
return
(
return
(
<
Tag
color
=
'grey'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'grey'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Music
size
=
{
14
}
/>}
>
{
t
(
'生成音乐'
)}
{
t
(
'生成音乐'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'LYRICS'
:
case
'LYRICS'
:
return
(
return
(
<
Tag
color
=
'pink'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'pink'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
FileText
size
=
{
14
}
/>}
>
{
t
(
'生成歌词'
)}
{
t
(
'生成歌词'
)}
<
/Tag
>
<
/Tag
>
);
);
default
:
default
:
return
(
return
(
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
HelpCircle
size
=
{
14
}
/>}
>
{
t
(
'未知'
)}
{
t
(
'未知'
)}
<
/Tag
>
<
/Tag
>
);
);
...
@@ -211,13 +228,13 @@ const LogsTable = () => {
...
@@ -211,13 +228,13 @@ const LogsTable = () => {
switch
(
type
)
{
switch
(
type
)
{
case
'suno'
:
case
'suno'
:
return
(
return
(
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Music
size
=
{
14
}
/>}
>
Suno
Suno
<
/Tag
>
<
/Tag
>
);
);
default
:
default
:
return
(
return
(
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
HelpCircle
size
=
{
14
}
/>}
>
{
t
(
'未知'
)}
{
t
(
'未知'
)}
<
/Tag
>
<
/Tag
>
);
);
...
@@ -228,55 +245,55 @@ const LogsTable = () => {
...
@@ -228,55 +245,55 @@ const LogsTable = () => {
switch
(
type
)
{
switch
(
type
)
{
case
'SUCCESS'
:
case
'SUCCESS'
:
return
(
return
(
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
CheckCircle
size
=
{
14
}
/>}
>
{
t
(
'成功'
)}
{
t
(
'成功'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'NOT_START'
:
case
'NOT_START'
:
return
(
return
(
<
Tag
color
=
'grey'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'grey'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Pause
size
=
{
14
}
/>}
>
{
t
(
'未启动'
)}
{
t
(
'未启动'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'SUBMITTED'
:
case
'SUBMITTED'
:
return
(
return
(
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Clock
size
=
{
14
}
/>}
>
{
t
(
'队列中'
)}
{
t
(
'队列中'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'IN_PROGRESS'
:
case
'IN_PROGRESS'
:
return
(
return
(
<
Tag
color
=
'blue'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'blue'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Play
size
=
{
14
}
/>}
>
{
t
(
'执行中'
)}
{
t
(
'执行中'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'FAILURE'
:
case
'FAILURE'
:
return
(
return
(
<
Tag
color
=
'red'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'red'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
XCircle
size
=
{
14
}
/>}
>
{
t
(
'失败'
)}
{
t
(
'失败'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'QUEUED'
:
case
'QUEUED'
:
return
(
return
(
<
Tag
color
=
'orange'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'orange'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
List
size
=
{
14
}
/>}
>
{
t
(
'排队中'
)}
{
t
(
'排队中'
)}
<
/Tag
>
<
/Tag
>
);
);
case
'UNKNOWN'
:
case
'UNKNOWN'
:
return
(
return
(
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
HelpCircle
size
=
{
14
}
/>}
>
{
t
(
'未知'
)}
{
t
(
'未知'
)}
<
/Tag
>
<
/Tag
>
);
);
case
''
:
case
''
:
return
(
return
(
<
Tag
color
=
'grey'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'grey'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Loader
size
=
{
14
}
/>}
>
{
t
(
'正在提交'
)}
{
t
(
'正在提交'
)}
<
/Tag
>
<
/Tag
>
);
);
default
:
default
:
return
(
return
(
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
HelpCircle
size
=
{
14
}
/>}
>
{
t
(
'未知'
)}
{
t
(
'未知'
)}
<
/Tag
>
<
/Tag
>
);
);
...
@@ -321,6 +338,7 @@ const LogsTable = () => {
...
@@ -321,6 +338,7 @@ const LogsTable = () => {
color
=
{
colors
[
parseInt
(
text
)
%
colors
.
length
]}
color
=
{
colors
[
parseInt
(
text
)
%
colors
.
length
]}
size
=
'large'
size
=
'large'
shape
=
'circle'
shape
=
'circle'
prefixIcon
=
{
<
Hash
size
=
{
14
}
/>
}
onClick
=
{()
=>
{
onClick
=
{()
=>
{
copyText
(
text
);
copyText
(
text
);
}}
}}
...
@@ -395,7 +413,7 @@ const LogsTable = () => {
...
@@ -395,7 +413,7 @@ const LogsTable = () => {
percent
=
{
text
?
parseInt
(
text
.
replace
(
'%'
,
''
))
:
0
}
percent
=
{
text
?
parseInt
(
text
.
replace
(
'%'
,
''
))
:
0
}
showInfo
=
{
true
}
showInfo
=
{
true
}
aria
-
label
=
'task progress'
aria
-
label
=
'task progress'
style
=
{{
minWidth
:
'
20
0px'
}}
style
=
{{
minWidth
:
'
16
0px'
}}
/
>
/
>
)
)
}
}
...
@@ -437,21 +455,43 @@ const LogsTable = () => {
...
@@ -437,21 +455,43 @@ const LogsTable = () => {
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
const
[
logCount
,
setLogCount
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
logCount
,
setLogCount
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
logType
]
=
useState
(
0
);
let
now
=
new
Date
();
let
now
=
new
Date
();
// 初始化start_timestamp为前一天
// 初始化start_timestamp为前一天
let
zeroNow
=
new
Date
(
now
.
getFullYear
(),
now
.
getMonth
(),
now
.
getDate
());
let
zeroNow
=
new
Date
(
now
.
getFullYear
(),
now
.
getMonth
(),
now
.
getDate
());
const
[
inputs
,
setInputs
]
=
useState
({
// Form 初始值
const
formInitValues
=
{
channel_id
:
''
,
channel_id
:
''
,
task_id
:
''
,
task_id
:
''
,
start_timestamp
:
timestamp2string
(
zeroNow
.
getTime
()
/
1000
),
dateRange
:
[
end_timestamp
:
''
,
timestamp2string
(
zeroNow
.
getTime
()
/
1000
),
});
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
)
const
{
channel_id
,
task_id
,
start_timestamp
,
end_timestamp
}
=
inputs
;
],
};
// Form API 引用
const
[
formApi
,
setFormApi
]
=
useState
(
null
);
// 获取表单值的辅助函数
const
getFormValues
=
()
=>
{
const
formValues
=
formApi
?
formApi
.
getValues
()
:
{};
const
handleInputChange
=
(
value
,
name
)
=>
{
// 处理时间范围
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
let
start_timestamp
=
timestamp2string
(
zeroNow
.
getTime
()
/
1000
);
let
end_timestamp
=
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
);
if
(
formValues
.
dateRange
&&
Array
.
isArray
(
formValues
.
dateRange
)
&&
formValues
.
dateRange
.
length
===
2
)
{
start_timestamp
=
formValues
.
dateRange
[
0
];
end_timestamp
=
formValues
.
dateRange
[
1
];
}
return
{
channel_id
:
formValues
.
channel_id
||
''
,
task_id
:
formValues
.
task_id
||
''
,
start_timestamp
,
end_timestamp
,
};
};
};
const
setLogsFormat
=
(
logs
)
=>
{
const
setLogsFormat
=
(
logs
)
=>
{
...
@@ -469,6 +509,7 @@ const LogsTable = () => {
...
@@ -469,6 +509,7 @@ const LogsTable = () => {
setLoading
(
true
);
setLoading
(
true
);
let
url
=
''
;
let
url
=
''
;
const
{
channel_id
,
task_id
,
start_timestamp
,
end_timestamp
}
=
getFormValues
();
let
localStartTimestamp
=
parseInt
(
Date
.
parse
(
start_timestamp
)
/
1000
);
let
localStartTimestamp
=
parseInt
(
Date
.
parse
(
start_timestamp
)
/
1000
);
let
localEndTimestamp
=
parseInt
(
Date
.
parse
(
end_timestamp
)
/
1000
);
let
localEndTimestamp
=
parseInt
(
Date
.
parse
(
end_timestamp
)
/
1000
);
if
(
isAdminUser
)
{
if
(
isAdminUser
)
{
...
@@ -528,7 +569,7 @@ const LogsTable = () => {
...
@@ -528,7 +569,7 @@ const LogsTable = () => {
const
localPageSize
=
parseInt
(
localStorage
.
getItem
(
'task-page-size'
))
||
ITEMS_PER_PAGE
;
const
localPageSize
=
parseInt
(
localStorage
.
getItem
(
'task-page-size'
))
||
ITEMS_PER_PAGE
;
setPageSize
(
localPageSize
);
setPageSize
(
localPageSize
);
loadLogs
(
0
,
localPageSize
).
then
();
loadLogs
(
0
,
localPageSize
).
then
();
},
[
logType
]);
},
[]);
// 列选择器模态框
// 列选择器模态框
const
renderColumnSelector
=
()
=>
{
const
renderColumnSelector
=
()
=>
{
...
@@ -628,70 +669,93 @@ const LogsTable = () => {
...
@@ -628,70 +669,93 @@ const LogsTable = () => {
<
Divider
margin
=
"12px"
/>
<
Divider
margin
=
"12px"
/>
{
/* 搜索表单区域 */
}
{
/* 搜索表单区域 */
}
<
div
className
=
"flex flex-col gap-4"
>
<
Form
<
div
className
=
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
>
initValues
=
{
formInitValues
}
{
/* 时间选择器 */
}
getFormApi
=
{(
api
)
=>
setFormApi
(
api
)}
<
div
className
=
"col-span-1 lg:col-span-2"
>
onSubmit
=
{
refresh
}
<
DatePicker
allowEmpty
=
{
true
}
className
=
"w-full"
autoComplete
=
"off"
value
=
{[
start_timestamp
,
end_timestamp
]}
layout
=
"vertical"
type
=
'dateTimeRange'
trigger
=
"change"
onChange
=
{(
value
)
=>
{
stopValidateWithError
=
{
false
}
if
(
Array
.
isArray
(
value
)
&&
value
.
length
===
2
)
{
>
handleInputChange
(
value
[
0
],
'start_timestamp'
);
<
div
className
=
"flex flex-col gap-4"
>
handleInputChange
(
value
[
1
],
'end_timestamp'
);
<
div
className
=
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
>
}
{
/* 时间选择器 */
}
}}
<
div
className
=
"col-span-1 lg:col-span-2"
>
/
>
<
Form
.
DatePicker
<
/div
>
field
=
'dateRange'
className
=
"w-full"
{
/* 任务 ID */
}
type
=
'dateTimeRange'
<
Input
placeholder
=
{[
t
(
'开始时间'
),
t
(
'结束时间'
)]}
prefix
=
{
<
IconSearch
/>
}
showClear
placeholder
=
{
t
(
'任务 ID'
)}
pure
value
=
{
task_id
}
/>
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'task_id'
)}
<
/div
>
className
=
"!rounded-full"
showClear
{
/* 任务 ID */
}
/>
<
Form
.
Input
field
=
'task_id'
{
/* 渠道 ID - 仅管理员可见 */
}
{
isAdminUser
&&
(
<
Input
prefix
=
{
<
IconSearch
/>
}
prefix
=
{
<
IconSearch
/>
}
placeholder
=
{
t
(
'渠道 ID'
)}
placeholder
=
{
t
(
'任务 ID'
)}
value
=
{
channel_id
}
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'channel_id'
)}
className
=
"!rounded-full"
className
=
"!rounded-full"
showClear
showClear
pure
/>
/>
)}
<
/div
>
{
/* 操作按钮区域 */
}
{
/* 渠道 ID - 仅管理员可见 */
}
<
div
className
=
"flex justify-between items-center pt-2"
>
{
isAdminUser
&&
(
<
div
><
/div
>
<
Form
.
Input
<
div
className
=
"flex gap-2"
>
field
=
'channel_id'
<
Button
prefix
=
{
<
IconSearch
/>
}
type
=
'primary'
placeholder
=
{
t
(
'渠道 ID'
)}
onClick
=
{
refresh
}
className
=
"!rounded-full"
loading
=
{
loading
}
showClear
className
=
"!rounded-full"
pure
>
/>
{
t
(
'查询'
)}
)}
<
/Button
>
<
/div
>
<
Button
theme
=
'light'
{
/* 操作按钮区域 */
}
type
=
'tertiary'
<
div
className
=
"flex justify-between items-center"
>
icon
=
{
<
IconSetting
/>
}
<
div
><
/div
>
onClick
=
{()
=>
setShowColumnSelector
(
true
)}
<
div
className
=
"flex gap-2"
>
className
=
"!rounded-full"
<
Button
>
type
=
'primary'
{
t
(
'列设置'
)}
htmlType
=
'submit'
<
/Button
>
loading
=
{
loading
}
className
=
"!rounded-full"
>
{
t
(
'查询'
)}
<
/Button
>
<
Button
theme
=
'light'
onClick
=
{()
=>
{
if
(
formApi
)
{
formApi
.
reset
();
// 重置后立即查询,使用setTimeout确保表单重置完成
setTimeout
(()
=>
{
refresh
();
},
100
);
}
}}
className
=
"!rounded-full"
>
{
t
(
'重置'
)}
<
/Button
>
<
Button
theme
=
'light'
type
=
'tertiary'
icon
=
{
<
IconSetting
/>
}
onClick
=
{()
=>
setShowColumnSelector
(
true
)}
className
=
"!rounded-full"
>
{
t
(
'列设置'
)}
<
/Button
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/
div
>
<
/
Form
>
<
/div
>
<
/div
>
}
}
shadows
=
'always'
shadows
=
'always'
...
@@ -705,6 +769,14 @@ const LogsTable = () => {
...
@@ -705,6 +769,14 @@ const LogsTable = () => {
scroll
=
{{
x
:
'max-content'
}}
scroll
=
{{
x
:
'max-content'
}}
className
=
"rounded-xl overflow-hidden"
className
=
"rounded-xl overflow-hidden"
size
=
"middle"
size
=
"middle"
empty
=
{
<
Empty
image
=
{
<
IllustrationNoResult
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
darkModeImage
=
{
<
IllustrationNoResultDark
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
description
=
{
t
(
'搜索无结果'
)}
style
=
{{
padding
:
30
}}
/
>
}
pagination
=
{{
pagination
=
{{
formatPageText
:
(
page
)
=>
formatPageText
:
(
page
)
=>
t
(
'第 {{start}} - {{end}} 条,共 {{total}} 条'
,
{
t
(
'第 {{start}} - {{end}} 条,共 {{total}} 条'
,
{
...
...
web/src/components/table/TokensTable.js
View file @
c821b0ed
...
@@ -6,7 +6,8 @@ import {
...
@@ -6,7 +6,8 @@ import {
showSuccess
,
showSuccess
,
timestamp2string
,
timestamp2string
,
renderGroup
,
renderGroup
,
renderQuota
renderQuota
,
getQuotaPerUnit
}
from
'../../helpers'
;
}
from
'../../helpers'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
...
@@ -14,13 +15,29 @@ import {
...
@@ -14,13 +15,29 @@ import {
Button
,
Button
,
Card
,
Card
,
Dropdown
,
Dropdown
,
Empty
,
Form
,
Modal
,
Modal
,
Space
,
Space
,
SplitButtonGroup
,
SplitButtonGroup
,
Table
,
Table
,
Tag
,
Tag
Input
,
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
IllustrationNoResult
,
IllustrationNoResultDark
}
from
'@douyinfe/semi-illustrations'
;
import
{
CheckCircle
,
Shield
,
XCircle
,
Clock
,
Gauge
,
HelpCircle
,
Infinity
,
Coins
}
from
'lucide-react'
;
import
{
import
{
IconPlus
,
IconPlus
,
...
@@ -32,7 +49,7 @@ import {
...
@@ -32,7 +49,7 @@ import {
IconDelete
,
IconDelete
,
IconStop
,
IconStop
,
IconPlay
,
IconPlay
,
IconMore
,
IconMore
}
from
'@douyinfe/semi-icons'
;
}
from
'@douyinfe/semi-icons'
;
import
EditToken
from
'../../pages/Token/EditToken'
;
import
EditToken
from
'../../pages/Token/EditToken'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useTranslation
}
from
'react-i18next'
;
...
@@ -49,38 +66,38 @@ const TokensTable = () => {
...
@@ -49,38 +66,38 @@ const TokensTable = () => {
case
1
:
case
1
:
if
(
model_limits_enabled
)
{
if
(
model_limits_enabled
)
{
return
(
return
(
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Shield
size
=
{
14
}
/>}
>
{
t
(
'已启用:限制模型'
)}
{
t
(
'已启用:限制模型'
)}
<
/Tag
>
<
/Tag
>
);
);
}
else
{
}
else
{
return
(
return
(
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'green'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
CheckCircle
size
=
{
14
}
/>}
>
{
t
(
'已启用'
)}
{
t
(
'已启用'
)}
<
/Tag
>
<
/Tag
>
);
);
}
}
case
2
:
case
2
:
return
(
return
(
<
Tag
color
=
'red'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'red'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
XCircle
size
=
{
14
}
/>}
>
{
t
(
'已禁用'
)}
{
t
(
'已禁用'
)}
<
/Tag
>
<
/Tag
>
);
);
case
3
:
case
3
:
return
(
return
(
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Clock
size
=
{
14
}
/>}
>
{
t
(
'已过期'
)}
{
t
(
'已过期'
)}
<
/Tag
>
<
/Tag
>
);
);
case
4
:
case
4
:
return
(
return
(
<
Tag
color
=
'grey'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'grey'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Gauge
size
=
{
14
}
/>}
>
{
t
(
'已耗尽'
)}
{
t
(
'已耗尽'
)}
<
/Tag
>
<
/Tag
>
);
);
default
:
default
:
return
(
return
(
<
Tag
color
=
'black'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'black'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
HelpCircle
size
=
{
14
}
/>}
>
{
t
(
'未知状态'
)}
{
t
(
'未知状态'
)}
<
/Tag
>
<
/Tag
>
);
);
...
@@ -111,21 +128,45 @@ const TokensTable = () => {
...
@@ -111,21 +128,45 @@ const TokensTable = () => {
title
:
t
(
'已用额度'
),
title
:
t
(
'已用额度'
),
dataIndex
:
'used_quota'
,
dataIndex
:
'used_quota'
,
render
:
(
text
,
record
,
index
)
=>
{
render
:
(
text
,
record
,
index
)
=>
{
return
<
div
>
{
renderQuota
(
parseInt
(
text
))}
<
/div>
;
return
(
<
div
>
<
Tag
size
=
{
'large'
}
color
=
{
'grey'
}
shape
=
'circle'
prefixIcon
=
{
<
Coins
size
=
{
14
}
/>}
>
{
renderQuota
(
parseInt
(
text
))}
<
/Tag
>
<
/div
>
);
},
},
},
},
{
{
title
:
t
(
'剩余额度'
),
title
:
t
(
'剩余额度'
),
dataIndex
:
'remain_quota'
,
dataIndex
:
'remain_quota'
,
render
:
(
text
,
record
,
index
)
=>
{
render
:
(
text
,
record
,
index
)
=>
{
const
getQuotaColor
=
(
quotaValue
)
=>
{
const
quotaPerUnit
=
getQuotaPerUnit
();
const
dollarAmount
=
quotaValue
/
quotaPerUnit
;
if
(
dollarAmount
<=
0
)
{
return
'red'
;
}
else
if
(
dollarAmount
<=
100
)
{
return
'yellow'
;
}
else
{
return
'green'
;
}
};
return
(
return
(
<
div
>
<
div
>
{
record
.
unlimited_quota
?
(
{
record
.
unlimited_quota
?
(
<
Tag
size
=
{
'large'
}
color
=
{
'white'
}
shape
=
'circle'
>
<
Tag
size
=
{
'large'
}
color
=
{
'white'
}
shape
=
'circle'
prefixIcon
=
{
<
Infinity
size
=
{
14
}
/>}
>
{
t
(
'无限制'
)}
{
t
(
'无限制'
)}
<
/Tag
>
<
/Tag
>
)
:
(
)
:
(
<
Tag
size
=
{
'large'
}
color
=
{
'light-blue'
}
shape
=
'circle'
>
<
Tag
size
=
{
'large'
}
color
=
{
getQuotaColor
(
parseInt
(
text
))}
shape
=
'circle'
prefixIcon
=
{
<
Coins
size
=
{
14
}
/>
}
>
{
renderQuota
(
parseInt
(
text
))}
{
renderQuota
(
parseInt
(
text
))}
<
/Tag
>
<
/Tag
>
)}
)}
...
@@ -335,14 +376,29 @@ const TokensTable = () => {
...
@@ -335,14 +376,29 @@ const TokensTable = () => {
const
[
tokenCount
,
setTokenCount
]
=
useState
(
pageSize
);
const
[
tokenCount
,
setTokenCount
]
=
useState
(
pageSize
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
const
[
searchKeyword
,
setSearchKeyword
]
=
useState
(
''
);
const
[
searchToken
,
setSearchToken
]
=
useState
(
''
);
const
[
searching
,
setSearching
]
=
useState
(
false
);
const
[
searching
,
setSearching
]
=
useState
(
false
);
const
[
chats
,
setChats
]
=
useState
([]);
const
[
editingToken
,
setEditingToken
]
=
useState
({
const
[
editingToken
,
setEditingToken
]
=
useState
({
id
:
undefined
,
id
:
undefined
,
});
});
// Form 初始值
const
formInitValues
=
{
searchKeyword
:
''
,
searchToken
:
''
,
};
// Form API 引用
const
[
formApi
,
setFormApi
]
=
useState
(
null
);
// 获取表单值的辅助函数
const
getFormValues
=
()
=>
{
const
formValues
=
formApi
?
formApi
.
getValues
()
:
{};
return
{
searchKeyword
:
formValues
.
searchKeyword
||
''
,
searchToken
:
formValues
.
searchToken
||
''
,
};
};
const
closeEdit
=
()
=>
{
const
closeEdit
=
()
=>
{
setShowEdit
(
false
);
setShowEdit
(
false
);
setTimeout
(()
=>
{
setTimeout
(()
=>
{
...
@@ -416,8 +472,6 @@ const TokensTable = () => {
...
@@ -416,8 +472,6 @@ const TokensTable = () => {
window
.
open
(
url
,
'_blank'
);
window
.
open
(
url
,
'_blank'
);
};
};
useEffect
(()
=>
{
useEffect
(()
=>
{
loadTokens
(
0
)
loadTokens
(
0
)
.
then
()
.
then
()
...
@@ -472,6 +526,7 @@ const TokensTable = () => {
...
@@ -472,6 +526,7 @@ const TokensTable = () => {
};
};
const
searchTokens
=
async
()
=>
{
const
searchTokens
=
async
()
=>
{
const
{
searchKeyword
,
searchToken
}
=
getFormValues
();
if
(
searchKeyword
===
''
&&
searchToken
===
''
)
{
if
(
searchKeyword
===
''
&&
searchToken
===
''
)
{
await
loadTokens
(
0
);
await
loadTokens
(
0
);
setActivePage
(
1
);
setActivePage
(
1
);
...
@@ -491,14 +546,6 @@ const TokensTable = () => {
...
@@ -491,14 +546,6 @@ const TokensTable = () => {
setSearching
(
false
);
setSearching
(
false
);
};
};
const
handleKeywordChange
=
async
(
value
)
=>
{
setSearchKeyword
(
value
.
trim
());
};
const
handleSearchTokenChange
=
async
(
value
)
=>
{
setSearchToken
(
value
.
trim
());
};
const
sortToken
=
(
key
)
=>
{
const
sortToken
=
(
key
)
=>
{
if
(
tokens
.
length
===
0
)
return
;
if
(
tokens
.
length
===
0
)
return
;
setLoading
(
true
);
setLoading
(
true
);
...
@@ -580,36 +627,65 @@ const TokensTable = () => {
...
@@ -580,36 +627,65 @@ const TokensTable = () => {
<
/Button
>
<
/Button
>
<
/div
>
<
/div
>
<
div
className
=
"flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2"
>
<
Form
<
div
className
=
"relative w-full md:w-56"
>
initValues
=
{
formInitValues
}
<
Input
getFormApi
=
{(
api
)
=>
setFormApi
(
api
)}
prefix
=
{
<
IconSearch
/>
}
onSubmit
=
{
searchTokens
}
placeholder
=
{
t
(
'搜索关键字'
)}
allowEmpty
=
{
true
}
value
=
{
searchKeyword
}
autoComplete
=
"off"
onChange
=
{
handleKeywordChange
}
layout
=
"horizontal"
className
=
"!rounded-full"
trigger
=
"change"
showClear
stopValidateWithError
=
{
false
}
/>
className
=
"w-full md:w-auto order-1 md:order-2"
<
/div
>
>
<
div
className
=
"relative w-full md:w-56"
>
<
div
className
=
"flex flex-col md:flex-row items-center gap-4 w-full md:w-auto"
>
<
Input
<
div
className
=
"relative w-full md:w-56"
>
prefix
=
{
<
IconSearch
/>
}
<
Form
.
Input
placeholder
=
{
t
(
'密钥'
)}
field
=
"searchKeyword"
value
=
{
searchToken
}
prefix
=
{
<
IconSearch
/>
}
onChange
=
{
handleSearchTokenChange
}
placeholder
=
{
t
(
'搜索关键字'
)}
className
=
"!rounded-full"
className
=
"!rounded-full"
showClear
showClear
/>
pure
/>
<
/div
>
<
div
className
=
"relative w-full md:w-56"
>
<
Form
.
Input
field
=
"searchToken"
prefix
=
{
<
IconSearch
/>
}
placeholder
=
{
t
(
'密钥'
)}
className
=
"!rounded-full"
showClear
pure
/>
<
/div
>
<
div
className
=
"flex gap-2 w-full md:w-auto"
>
<
Button
type
=
"primary"
htmlType
=
"submit"
loading
=
{
searching
}
className
=
"!rounded-full flex-1 md:flex-initial md:w-auto"
>
{
t
(
'查询'
)}
<
/Button
>
<
Button
theme
=
"light"
onClick
=
{()
=>
{
if
(
formApi
)
{
formApi
.
reset
();
// 重置后立即查询,使用setTimeout确保表单重置完成
setTimeout
(()
=>
{
searchTokens
();
},
100
);
}
}}
className
=
"!rounded-full flex-1 md:flex-initial md:w-auto"
>
{
t
(
'重置'
)}
<
/Button
>
<
/div
>
<
/div
>
<
/div
>
<
Button
<
/Form
>
type
=
"primary"
onClick
=
{
searchTokens
}
loading
=
{
searching
}
className
=
"!rounded-full w-full md:w-auto"
>
{
t
(
'查询'
)}
<
/Button
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
);
);
...
@@ -654,6 +730,14 @@ const TokensTable = () => {
...
@@ -654,6 +730,14 @@ const TokensTable = () => {
loading
=
{
loading
}
loading
=
{
loading
}
rowSelection
=
{
rowSelection
}
rowSelection
=
{
rowSelection
}
onRow
=
{
handleRow
}
onRow
=
{
handleRow
}
empty
=
{
<
Empty
image
=
{
<
IllustrationNoResult
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
darkModeImage
=
{
<
IllustrationNoResultDark
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
description
=
{
t
(
'搜索无结果'
)}
style
=
{{
padding
:
30
}}
/
>
}
className
=
"rounded-xl overflow-hidden"
className
=
"rounded-xl overflow-hidden"
size
=
"middle"
size
=
"middle"
><
/Table
>
><
/Table
>
...
...
web/src/components/table/UsersTable.js
View file @
c821b0ed
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
API
,
showError
,
showSuccess
,
renderGroup
,
renderNumber
,
renderQuota
}
from
'../../helpers'
;
import
{
API
,
showError
,
showSuccess
,
renderGroup
,
renderNumber
,
renderQuota
}
from
'../../helpers'
;
import
{
User
,
Shield
,
Crown
,
HelpCircle
,
CheckCircle
,
XCircle
,
Minus
,
Coins
,
Activity
,
Users
,
DollarSign
,
UserPlus
}
from
'lucide-react'
;
import
{
import
{
Button
,
Button
,
Card
,
Card
,
Divider
,
Divider
,
Dropdown
,
Dropdown
,
Input
,
Empty
,
Form
,
Modal
,
Modal
,
Select
,
Space
,
Space
,
Table
,
Table
,
Tag
,
Tag
,
Typography
,
Typography
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
import
{
IllustrationNoResult
,
IllustrationNoResultDark
}
from
'@douyinfe/semi-illustrations'
;
import
{
IconPlus
,
IconPlus
,
IconSearch
,
IconSearch
,
IconEdit
,
IconEdit
,
...
@@ -23,7 +42,7 @@ import {
...
@@ -23,7 +42,7 @@ import {
IconMore
,
IconMore
,
IconUserAdd
,
IconUserAdd
,
IconArrowUp
,
IconArrowUp
,
IconArrowDown
,
IconArrowDown
}
from
'@douyinfe/semi-icons'
;
}
from
'@douyinfe/semi-icons'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
{
ITEMS_PER_PAGE
}
from
'../../constants'
;
import
AddUser
from
'../../pages/User/AddUser'
;
import
AddUser
from
'../../pages/User/AddUser'
;
...
@@ -39,25 +58,25 @@ const UsersTable = () => {
...
@@ -39,25 +58,25 @@ const UsersTable = () => {
switch
(
role
)
{
switch
(
role
)
{
case
1
:
case
1
:
return
(
return
(
<
Tag
size
=
'large'
color
=
'blue'
shape
=
'circle'
>
<
Tag
size
=
'large'
color
=
'blue'
shape
=
'circle'
prefixIcon
=
{
<
User
size
=
{
14
}
/>}
>
{
t
(
'普通用户'
)}
{
t
(
'普通用户'
)}
<
/Tag
>
<
/Tag
>
);
);
case
10
:
case
10
:
return
(
return
(
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'yellow'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Shield
size
=
{
14
}
/>}
>
{
t
(
'管理员'
)}
{
t
(
'管理员'
)}
<
/Tag
>
<
/Tag
>
);
);
case
100
:
case
100
:
return
(
return
(
<
Tag
color
=
'orange'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'orange'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
Crown
size
=
{
14
}
/>}
>
{
t
(
'超级管理员'
)}
{
t
(
'超级管理员'
)}
<
/Tag
>
<
/Tag
>
);
);
default
:
default
:
return
(
return
(
<
Tag
color
=
'red'
size
=
'large'
shape
=
'circle'
>
<
Tag
color
=
'red'
size
=
'large'
shape
=
'circle'
prefixIcon
=
{
<
HelpCircle
size
=
{
14
}
/>}
>
{
t
(
'未知身份'
)}
{
t
(
'未知身份'
)}
<
/Tag
>
<
/Tag
>
);
);
...
@@ -67,16 +86,16 @@ const UsersTable = () => {
...
@@ -67,16 +86,16 @@ const UsersTable = () => {
const
renderStatus
=
(
status
)
=>
{
const
renderStatus
=
(
status
)
=>
{
switch
(
status
)
{
switch
(
status
)
{
case
1
:
case
1
:
return
<
Tag
size
=
'large'
color
=
'green'
shape
=
'circle'
>
{
t
(
'已激活'
)}
<
/Tag>
;
return
<
Tag
size
=
'large'
color
=
'green'
shape
=
'circle'
prefixIcon
=
{
<
CheckCircle
size
=
{
14
}
/>}
>{t
(
'已激活'
)
}</
Tag
>
;
case
2
:
case
2
:
return
(
return
(
<
Tag
size
=
'large'
color
=
'red'
shape
=
'circle'
>
<
Tag
size
=
'large'
color
=
'red'
shape
=
'circle'
prefixIcon
=
{
<
XCircle
size
=
{
14
}
/>}
>
{
t
(
'已封禁'
)}
{
t
(
'已封禁'
)}
<
/Tag
>
<
/Tag
>
);
);
default
:
default
:
return
(
return
(
<
Tag
size
=
'large'
color
=
'grey'
shape
=
'circle'
>
<
Tag
size
=
'large'
color
=
'grey'
shape
=
'circle'
prefixIcon
=
{
<
HelpCircle
size
=
{
14
}
/>}
>
{
t
(
'未知状态'
)}
{
t
(
'未知状态'
)}
<
/Tag
>
<
/Tag
>
);
);
...
@@ -106,13 +125,13 @@ const UsersTable = () => {
...
@@ -106,13 +125,13 @@ const UsersTable = () => {
return
(
return
(
<
div
>
<
div
>
<
Space
spacing
=
{
1
}
>
<
Space
spacing
=
{
1
}
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
className
=
"!text-xs"
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
className
=
"!text-xs"
prefixIcon
=
{
<
Coins
size
=
{
14
}
/>}
>
{
t
(
'剩余'
)}:
{
renderQuota
(
record
.
quota
)}
{
t
(
'剩余'
)}:
{
renderQuota
(
record
.
quota
)}
<
/Tag
>
<
/Tag
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
className
=
"!text-xs"
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
className
=
"!text-xs"
prefixIcon
=
{
<
Coins
size
=
{
14
}
/>}
>
{
t
(
'已用'
)}:
{
renderQuota
(
record
.
used_quota
)}
{
t
(
'已用'
)}:
{
renderQuota
(
record
.
used_quota
)}
<
/Tag
>
<
/Tag
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
className
=
"!text-xs"
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
className
=
"!text-xs"
prefixIcon
=
{
<
Activity
size
=
{
14
}
/>}
>
{
t
(
'调用'
)}:
{
renderNumber
(
record
.
request_count
)}
{
t
(
'调用'
)}:
{
renderNumber
(
record
.
request_count
)}
<
/Tag
>
<
/Tag
>
<
/Space
>
<
/Space
>
...
@@ -127,13 +146,13 @@ const UsersTable = () => {
...
@@ -127,13 +146,13 @@ const UsersTable = () => {
return
(
return
(
<
div
>
<
div
>
<
Space
spacing
=
{
1
}
>
<
Space
spacing
=
{
1
}
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
className
=
"!text-xs"
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
className
=
"!text-xs"
prefixIcon
=
{
<
Users
size
=
{
14
}
/>}
>
{
t
(
'邀请'
)}:
{
renderNumber
(
record
.
aff_count
)}
{
t
(
'邀请'
)}:
{
renderNumber
(
record
.
aff_count
)}
<
/Tag
>
<
/Tag
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
className
=
"!text-xs"
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
className
=
"!text-xs"
prefixIcon
=
{
<
DollarSign
size
=
{
14
}
/>}
>
{
t
(
'收益'
)}:
{
renderQuota
(
record
.
aff_history_quota
)}
{
t
(
'收益'
)}:
{
renderQuota
(
record
.
aff_history_quota
)}
<
/Tag
>
<
/Tag
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
className
=
"!text-xs"
>
<
Tag
color
=
'white'
size
=
'large'
shape
=
'circle'
className
=
"!text-xs"
prefixIcon
=
{
<
UserPlus
size
=
{
14
}
/>}
>
{
record
.
inviter_id
===
0
?
t
(
'无邀请人'
)
:
`邀请人:
${
record
.
inviter_id
}
`
}
{
record
.
inviter_id
===
0
?
t
(
'无邀请人'
)
:
`邀请人:
${
record
.
inviter_id
}
`
}
<
/Tag
>
<
/Tag
>
<
/Space
>
<
/Space
>
...
@@ -155,7 +174,7 @@ const UsersTable = () => {
...
@@ -155,7 +174,7 @@ const UsersTable = () => {
return
(
return
(
<
div
>
<
div
>
{
record
.
DeletedAt
!==
null
?
(
{
record
.
DeletedAt
!==
null
?
(
<
Tag
color
=
'red'
shape
=
'circle'
>
{
t
(
'已注销'
)}
<
/Tag
>
<
Tag
color
=
'red'
shape
=
'circle'
prefixIcon
=
{
<
Minus
size
=
{
14
}
/>}
>{t
(
'已注销'
)
}</
Tag
>
)
:
(
)
:
(
renderStatus
(
text
)
renderStatus
(
text
)
)}
)}
...
@@ -285,9 +304,7 @@ const UsersTable = () => {
...
@@ -285,9 +304,7 @@ const UsersTable = () => {
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
const
[
activePage
,
setActivePage
]
=
useState
(
1
);
const
[
pageSize
,
setPageSize
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
pageSize
,
setPageSize
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
searchKeyword
,
setSearchKeyword
]
=
useState
(
''
);
const
[
searching
,
setSearching
]
=
useState
(
false
);
const
[
searching
,
setSearching
]
=
useState
(
false
);
const
[
searchGroup
,
setSearchGroup
]
=
useState
(
''
);
const
[
groupOptions
,
setGroupOptions
]
=
useState
([]);
const
[
groupOptions
,
setGroupOptions
]
=
useState
([]);
const
[
userCount
,
setUserCount
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
userCount
,
setUserCount
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
showAddUser
,
setShowAddUser
]
=
useState
(
false
);
const
[
showAddUser
,
setShowAddUser
]
=
useState
(
false
);
...
@@ -296,6 +313,24 @@ const UsersTable = () => {
...
@@ -296,6 +313,24 @@ const UsersTable = () => {
id
:
undefined
,
id
:
undefined
,
});
});
// Form 初始值
const
formInitValues
=
{
searchKeyword
:
''
,
searchGroup
:
''
,
};
// Form API 引用
const
[
formApi
,
setFormApi
]
=
useState
(
null
);
// 获取表单值的辅助函数
const
getFormValues
=
()
=>
{
const
formValues
=
formApi
?
formApi
.
getValues
()
:
{};
return
{
searchKeyword
:
formValues
.
searchKeyword
||
''
,
searchGroup
:
formValues
.
searchGroup
||
''
,
};
};
const
removeRecord
=
(
key
)
=>
{
const
removeRecord
=
(
key
)
=>
{
let
newDataSource
=
[...
users
];
let
newDataSource
=
[...
users
];
if
(
key
!=
null
)
{
if
(
key
!=
null
)
{
...
@@ -363,9 +398,16 @@ const UsersTable = () => {
...
@@ -363,9 +398,16 @@ const UsersTable = () => {
const
searchUsers
=
async
(
const
searchUsers
=
async
(
startIdx
,
startIdx
,
pageSize
,
pageSize
,
searchKeyword
,
searchKeyword
=
null
,
searchGroup
,
searchGroup
=
null
,
)
=>
{
)
=>
{
// 如果没有传递参数,从表单获取值
if
(
searchKeyword
===
null
||
searchGroup
===
null
)
{
const
formValues
=
getFormValues
();
searchKeyword
=
formValues
.
searchKeyword
;
searchGroup
=
formValues
.
searchGroup
;
}
if
(
searchKeyword
===
''
&&
searchGroup
===
''
)
{
if
(
searchKeyword
===
''
&&
searchGroup
===
''
)
{
// if keyword is blank, load files instead.
// if keyword is blank, load files instead.
await
loadUsers
(
startIdx
,
pageSize
);
await
loadUsers
(
startIdx
,
pageSize
);
...
@@ -387,12 +429,9 @@ const UsersTable = () => {
...
@@ -387,12 +429,9 @@ const UsersTable = () => {
setSearching
(
false
);
setSearching
(
false
);
};
};
const
handleKeywordChange
=
async
(
value
)
=>
{
setSearchKeyword
(
value
.
trim
());
};
const
handlePageChange
=
(
page
)
=>
{
const
handlePageChange
=
(
page
)
=>
{
setActivePage
(
page
);
setActivePage
(
page
);
const
{
searchKeyword
,
searchGroup
}
=
getFormValues
();
if
(
searchKeyword
===
''
&&
searchGroup
===
''
)
{
if
(
searchKeyword
===
''
&&
searchGroup
===
''
)
{
loadUsers
(
page
,
pageSize
).
then
();
loadUsers
(
page
,
pageSize
).
then
();
}
else
{
}
else
{
...
@@ -413,10 +452,11 @@ const UsersTable = () => {
...
@@ -413,10 +452,11 @@ const UsersTable = () => {
const
refresh
=
async
()
=>
{
const
refresh
=
async
()
=>
{
setActivePage
(
1
);
setActivePage
(
1
);
if
(
searchKeyword
===
''
)
{
const
{
searchKeyword
,
searchGroup
}
=
getFormValues
();
await
loadUsers
(
activePage
,
pageSize
);
if
(
searchKeyword
===
''
&&
searchGroup
===
''
)
{
await
loadUsers
(
1
,
pageSize
);
}
else
{
}
else
{
await
searchUsers
(
activePage
,
pageSize
,
searchKeyword
,
searchGroup
);
await
searchUsers
(
1
,
pageSize
,
searchKeyword
,
searchGroup
);
}
}
};
};
...
@@ -488,41 +528,76 @@ const UsersTable = () => {
...
@@ -488,41 +528,76 @@ const UsersTable = () => {
<
/Button
>
<
/Button
>
<
/div
>
<
/div
>
<
div
className
=
"flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2"
>
<
Form
<
div
className
=
"relative w-full md:w-64"
>
initValues
=
{
formInitValues
}
<
Input
getFormApi
=
{(
api
)
=>
setFormApi
(
api
)}
prefix
=
{
<
IconSearch
/>
}
onSubmit
=
{()
=>
{
placeholder
=
{
t
(
'支持搜索用户的 ID、用户名、显示名称和邮箱地址'
)}
setActivePage
(
1
);
value
=
{
searchKeyword
}
searchUsers
(
1
,
pageSize
);
onChange
=
{
handleKeywordChange
}
}}
className
=
"!rounded-full"
allowEmpty
=
{
true
}
showClear
autoComplete
=
"off"
/>
layout
=
"horizontal"
<
/div
>
trigger
=
"change"
<
div
className
=
"w-full md:w-48"
>
stopValidateWithError
=
{
false
}
<
Select
className
=
"w-full md:w-auto order-1 md:order-2"
placeholder
=
{
t
(
'选择分组'
)}
>
optionList
=
{
groupOptions
}
<
div
className
=
"flex flex-col md:flex-row items-center gap-4 w-full md:w-auto"
>
value
=
{
searchGroup
}
<
div
className
=
"relative w-full md:w-64"
>
onChange
=
{(
value
)
=>
{
<
Form
.
Input
setSearchGroup
(
value
);
field
=
"searchKeyword"
searchUsers
(
activePage
,
pageSize
,
searchKeyword
,
value
);
prefix
=
{
<
IconSearch
/>
}
}}
placeholder
=
{
t
(
'支持搜索用户的 ID、用户名、显示名称和邮箱地址'
)}
className
=
"!rounded-full w-full"
className
=
"!rounded-full"
showClear
showClear
/>
pure
/>
<
/div
>
<
div
className
=
"w-full md:w-48"
>
<
Form
.
Select
field
=
"searchGroup"
placeholder
=
{
t
(
'选择分组'
)}
optionList
=
{
groupOptions
}
onChange
=
{(
value
)
=>
{
// 分组变化时自动搜索
setTimeout
(()
=>
{
setActivePage
(
1
);
searchUsers
(
1
,
pageSize
);
},
100
);
}}
className
=
"!rounded-full w-full"
showClear
pure
/>
<
/div
>
<
div
className
=
"flex gap-2 w-full md:w-auto"
>
<
Button
type
=
"primary"
htmlType
=
"submit"
loading
=
{
loading
||
searching
}
className
=
"!rounded-full flex-1 md:flex-initial md:w-auto"
>
{
t
(
'查询'
)}
<
/Button
>
<
Button
theme
=
"light"
onClick
=
{()
=>
{
if
(
formApi
)
{
formApi
.
reset
();
// 重置后立即查询,使用setTimeout确保表单重置完成
setTimeout
(()
=>
{
setActivePage
(
1
);
loadUsers
(
1
,
pageSize
);
},
100
);
}
}}
className
=
"!rounded-full flex-1 md:flex-initial md:w-auto"
>
{
t
(
'重置'
)}
<
/Button
>
<
/div
>
<
/div
>
<
/div
>
<
Button
<
/Form
>
type
=
"primary"
onClick
=
{()
=>
{
searchUsers
(
activePage
,
pageSize
,
searchKeyword
,
searchGroup
);
}}
loading
=
{
searching
}
className
=
"!rounded-full w-full md:w-auto"
>
{
t
(
'查询'
)}
<
/Button
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
);
);
...
@@ -570,6 +645,14 @@ const UsersTable = () => {
...
@@ -570,6 +645,14 @@ const UsersTable = () => {
}}
}}
loading
=
{
loading
}
loading
=
{
loading
}
onRow
=
{
handleRow
}
onRow
=
{
handleRow
}
empty
=
{
<
Empty
image
=
{
<
IllustrationNoResult
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
darkModeImage
=
{
<
IllustrationNoResultDark
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
description
=
{
t
(
'搜索无结果'
)}
style
=
{{
padding
:
30
}}
/
>
}
className
=
"rounded-xl overflow-hidden"
className
=
"rounded-xl overflow-hidden"
size
=
"middle"
size
=
"middle"
/>
/>
...
...
web/src/helpers/render.js
View file @
c821b0ed
...
@@ -24,6 +24,13 @@ import {
...
@@ -24,6 +24,13 @@ import {
XAI
,
XAI
,
Ollama
,
Ollama
,
Doubao
,
Doubao
,
Suno
,
Xinference
,
OpenRouter
,
Dify
,
Coze
,
SiliconCloud
,
FastGPT
}
from
'@lobehub/icons'
;
}
from
'@lobehub/icons'
;
import
{
import
{
...
@@ -40,6 +47,7 @@ import {
...
@@ -40,6 +47,7 @@ import {
User
,
User
,
Settings
,
Settings
,
CircleUser
,
CircleUser
,
Users
}
from
'lucide-react'
;
}
from
'lucide-react'
;
// 侧边栏图标颜色映射
// 侧边栏图标颜色映射
...
@@ -308,6 +316,88 @@ export const getModelCategories = (() => {
...
@@ -308,6 +316,88 @@ export const getModelCategories = (() => {
};
};
})();
})();
/**
* 根据渠道类型返回对应的厂商图标
* @param {number} channelType - 渠道类型值
* @returns {JSX.Element|null} - 对应的厂商图标组件
*/
export
function
getChannelIcon
(
channelType
)
{
const
iconSize
=
14
;
switch
(
channelType
)
{
case
1
:
// OpenAI
case
3
:
// Azure OpenAI
return
<
OpenAI
size
=
{
iconSize
}
/>
;
case
2
:
// Midjourney Proxy
case
5
:
// Midjourney Proxy Plus
return
<
Midjourney
size
=
{
iconSize
}
/>
;
case
36
:
// Suno API
return
<
Suno
size
=
{
iconSize
}
/>
;
case
4
:
// Ollama
return
<
Ollama
size
=
{
iconSize
}
/>
;
case
14
:
// Anthropic Claude
case
33
:
// AWS Claude
return
<
Claude
.
Color
size
=
{
iconSize
}
/>
;
case
41
:
// Vertex AI
return
<
Gemini
.
Color
size
=
{
iconSize
}
/>
;
case
34
:
// Cohere
return
<
Cohere
.
Color
size
=
{
iconSize
}
/>
;
case
39
:
// Cloudflare
return
<
Cloudflare
.
Color
size
=
{
iconSize
}
/>
;
case
43
:
// DeepSeek
return
<
DeepSeek
.
Color
size
=
{
iconSize
}
/>
;
case
15
:
// 百度文心千帆
case
46
:
// 百度文心千帆V2
return
<
Wenxin
.
Color
size
=
{
iconSize
}
/>
;
case
17
:
// 阿里通义千问
return
<
Qwen
.
Color
size
=
{
iconSize
}
/>
;
case
18
:
// 讯飞星火认知
return
<
Spark
.
Color
size
=
{
iconSize
}
/>
;
case
16
:
// 智谱 ChatGLM
case
26
:
// 智谱 GLM-4V
return
<
Zhipu
.
Color
size
=
{
iconSize
}
/>
;
case
24
:
// Google Gemini
case
11
:
// Google PaLM2
return
<
Gemini
.
Color
size
=
{
iconSize
}
/>
;
case
47
:
// Xinference
return
<
Xinference
.
Color
size
=
{
iconSize
}
/>
;
case
25
:
// Moonshot
return
<
Moonshot
size
=
{
iconSize
}
/>
;
case
20
:
// OpenRouter
return
<
OpenRouter
size
=
{
iconSize
}
/>
;
case
19
:
// 360 智脑
return
<
Ai360
.
Color
size
=
{
iconSize
}
/>
;
case
23
:
// 腾讯混元
return
<
Hunyuan
.
Color
size
=
{
iconSize
}
/>
;
case
31
:
// 零一万物
return
<
Yi
.
Color
size
=
{
iconSize
}
/>
;
case
35
:
// MiniMax
return
<
Minimax
.
Color
size
=
{
iconSize
}
/>
;
case
37
:
// Dify
return
<
Dify
.
Color
size
=
{
iconSize
}
/>
;
case
38
:
// Jina
return
<
Jina
size
=
{
iconSize
}
/>
;
case
40
:
// SiliconCloud
return
<
SiliconCloud
.
Color
size
=
{
iconSize
}
/>
;
case
42
:
// Mistral AI
return
<
Mistral
.
Color
size
=
{
iconSize
}
/>
;
case
45
:
// 字节火山方舟、豆包通用
return
<
Doubao
.
Color
size
=
{
iconSize
}
/>
;
case
48
:
// xAI
return
<
XAI
size
=
{
iconSize
}
/>
;
case
49
:
// Coze
return
<
Coze
size
=
{
iconSize
}
/>
;
case
8
:
// 自定义渠道
case
22
:
// 知识库:FastGPT
return
<
FastGPT
.
Color
size
=
{
iconSize
}
/>
;
case
21
:
// 知识库:AI Proxy
case
44
:
// 嵌入模型:MokaAI M3E
default
:
return
null
;
// 未知类型或自定义渠道不显示图标
}
}
// 颜色列表
// 颜色列表
const
colors
=
[
const
colors
=
[
'amber'
,
'amber'
,
...
@@ -519,7 +609,7 @@ export function renderGroup(group) {
...
@@ -519,7 +609,7 @@ export function renderGroup(group) {
showSuccess
(
i18next
.
t
(
'已复制:'
)
+
group
);
showSuccess
(
i18next
.
t
(
'已复制:'
)
+
group
);
}
else
{
}
else
{
Modal
.
error
({
Modal
.
error
({
title
:
t
(
'无法复制到剪贴板,请手动复制'
),
title
:
i18next
.
t
(
'无法复制到剪贴板,请手动复制'
),
content
:
group
,
content
:
group
,
});
});
}
}
...
@@ -956,23 +1046,23 @@ export function renderModelPrice(
...
@@ -956,23 +1046,23 @@ export function renderModelPrice(
const
extraServices
=
[
const
extraServices
=
[
webSearch
&&
webSearchCallCount
>
0
webSearch
&&
webSearchCallCount
>
0
?
i18next
.
t
(
?
i18next
.
t
(
' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}'
,
' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}'
,
{
{
count
:
webSearchCallCount
,
count
:
webSearchCallCount
,
price
:
webSearchPrice
,
price
:
webSearchPrice
,
ratio
:
groupRatio
,
ratio
:
groupRatio
,
},
},
)
)
:
''
,
:
''
,
fileSearch
&&
fileSearchCallCount
>
0
fileSearch
&&
fileSearchCallCount
>
0
?
i18next
.
t
(
?
i18next
.
t
(
' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}'
,
' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}'
,
{
{
count
:
fileSearchCallCount
,
count
:
fileSearchCallCount
,
price
:
fileSearchPrice
,
price
:
fileSearchPrice
,
ratio
:
groupRatio
,
ratio
:
groupRatio
,
},
},
)
)
:
''
,
:
''
,
].
join
(
''
);
].
join
(
''
);
...
@@ -1156,10 +1246,10 @@ export function renderAudioModelPrice(
...
@@ -1156,10 +1246,10 @@ export function renderAudioModelPrice(
let
audioPrice
=
let
audioPrice
=
(
audioInputTokens
/
1000000
)
*
inputRatioPrice
*
audioRatio
*
groupRatio
+
(
audioInputTokens
/
1000000
)
*
inputRatioPrice
*
audioRatio
*
groupRatio
+
(
audioCompletionTokens
/
1000000
)
*
(
audioCompletionTokens
/
1000000
)
*
inputRatioPrice
*
inputRatioPrice
*
audioRatio
*
audioRatio
*
audioCompletionRatio
*
audioCompletionRatio
*
groupRatio
;
groupRatio
;
let
price
=
textPrice
+
audioPrice
;
let
price
=
textPrice
+
audioPrice
;
return
(
return
(
<>
<>
...
@@ -1215,27 +1305,27 @@ export function renderAudioModelPrice(
...
@@ -1215,27 +1305,27 @@ export function renderAudioModelPrice(
<
p
>
<
p
>
{
cacheTokens
>
0
{
cacheTokens
>
0
?
i18next
.
t
(
?
i18next
.
t
(
'文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}'
,
'文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}'
,
{
{
nonCacheInput
:
inputTokens
-
cacheTokens
,
nonCacheInput
:
inputTokens
-
cacheTokens
,
cacheInput
:
cacheTokens
,
cacheInput
:
cacheTokens
,
cachePrice
:
inputRatioPrice
*
cacheRatio
,
cachePrice
:
inputRatioPrice
*
cacheRatio
,
price
:
inputRatioPrice
,
price
:
inputRatioPrice
,
completion
:
completionTokens
,
completion
:
completionTokens
,
compPrice
:
completionRatioPrice
,
compPrice
:
completionRatioPrice
,
total
:
textPrice
.
toFixed
(
6
),
total
:
textPrice
.
toFixed
(
6
),
},
},
)
)
:
i18next
.
t
(
:
i18next
.
t
(
'文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}'
,
'文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}'
,
{
{
input
:
inputTokens
,
input
:
inputTokens
,
price
:
inputRatioPrice
,
price
:
inputRatioPrice
,
completion
:
completionTokens
,
completion
:
completionTokens
,
compPrice
:
completionRatioPrice
,
compPrice
:
completionRatioPrice
,
total
:
textPrice
.
toFixed
(
6
),
total
:
textPrice
.
toFixed
(
6
),
},
},
)}
)}
<
/p
>
<
/p
>
<
p
>
<
p
>
{
i18next
.
t
(
{
i18next
.
t
(
...
@@ -1372,33 +1462,33 @@ export function renderClaudeModelPrice(
...
@@ -1372,33 +1462,33 @@ export function renderClaudeModelPrice(
<
p
>
<
p
>
{
cacheTokens
>
0
||
cacheCreationTokens
>
0
{
cacheTokens
>
0
||
cacheCreationTokens
>
0
?
i18next
.
t
(
?
i18next
.
t
(
'提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}'
,
'提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}'
,
{
{
nonCacheInput
:
nonCachedTokens
,
nonCacheInput
:
nonCachedTokens
,
cacheInput
:
cacheTokens
,
cacheInput
:
cacheTokens
,
cacheRatio
:
cacheRatio
,
cacheRatio
:
cacheRatio
,
cacheCreationInput
:
cacheCreationTokens
,
cacheCreationInput
:
cacheCreationTokens
,
cacheCreationRatio
:
cacheCreationRatio
,
cacheCreationRatio
:
cacheCreationRatio
,
cachePrice
:
cacheRatioPrice
,
cachePrice
:
cacheRatioPrice
,
cacheCreationPrice
:
cacheCreationRatioPrice
,
cacheCreationPrice
:
cacheCreationRatioPrice
,
price
:
inputRatioPrice
,
price
:
inputRatioPrice
,
completion
:
completionTokens
,
completion
:
completionTokens
,
compPrice
:
completionRatioPrice
,
compPrice
:
completionRatioPrice
,
ratio
:
groupRatio
,
ratio
:
groupRatio
,
total
:
price
.
toFixed
(
6
),
total
:
price
.
toFixed
(
6
),
},
},
)
)
:
i18next
.
t
(
:
i18next
.
t
(
'提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}'
,
'提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}'
,
{
{
input
:
inputTokens
,
input
:
inputTokens
,
price
:
inputRatioPrice
,
price
:
inputRatioPrice
,
completion
:
completionTokens
,
completion
:
completionTokens
,
compPrice
:
completionRatioPrice
,
compPrice
:
completionRatioPrice
,
ratio
:
groupRatio
,
ratio
:
groupRatio
,
total
:
price
.
toFixed
(
6
),
total
:
price
.
toFixed
(
6
),
},
},
)}
)}
<
/p
>
<
/p
>
<
p
>
{
i18next
.
t
(
'仅供参考,以实际扣费为准'
)}
<
/p
>
<
p
>
{
i18next
.
t
(
'仅供参考,以实际扣费为准'
)}
<
/p
>
<
/article
>
<
/article
>
...
...
web/src/i18n/locales/en.json
View file @
c821b0ed
...
@@ -1567,5 +1567,24 @@
...
@@ -1567,5 +1567,24 @@
"使用统计"
:
"Usage Statistics"
,
"使用统计"
:
"Usage Statistics"
,
"资源消耗"
:
"Resource Consumption"
,
"资源消耗"
:
"Resource Consumption"
,
"性能指标"
:
"Performance Indicators"
,
"性能指标"
:
"Performance Indicators"
,
"模型数据分析"
:
"Model Data Analysis"
"模型数据分析"
:
"Model Data Analysis"
,
"搜索无结果"
:
"No results found"
,
"仪表盘配置"
:
"Dashboard Configuration"
,
"API信息管理,可以配置多个API地址用于状态展示和负载均衡"
:
"API information management, you can configure multiple API addresses for status display and load balancing"
,
"线路描述"
:
"Route description"
,
"颜色"
:
"Color"
,
"标识颜色"
:
"Identifier color"
,
"添加API"
:
"Add API"
,
"保存配置"
:
"Save Configuration"
,
"API信息"
:
"API Information"
,
"暂无API信息配置"
:
"No API information configured"
,
"暂无API信息"
:
"No API information"
,
"请输入API地址"
:
"Please enter the API address"
,
"请输入线路描述"
:
"Please enter the route description"
,
"如:大带宽批量分析图片推荐"
:
"e.g. Large bandwidth batch analysis of image recommendations"
,
"请输入说明"
:
"Please enter the description"
,
"如:香港线路"
:
"e.g. Hong Kong line"
,
"请联系管理员在系统设置中配置API信息"
:
"Please contact the administrator to configure API information in the system settings."
,
"确定要删除此API信息吗?"
:
"Are you sure you want to delete this API information?"
,
"测速"
:
"Speed Test"
}
}
\ No newline at end of file
web/src/index.css
View file @
c821b0ed
...
@@ -73,6 +73,10 @@ code {
...
@@ -73,6 +73,10 @@ code {
.semi-page-item
,
.semi-page-item
,
.semi-navigation-item
,
.semi-navigation-item
,
.semi-tag-closable
,
.semi-tag-closable
,
.semi-input-wrapper
,
.semi-tabs-tab-button
,
.semi-select
,
.semi-button
,
.semi-datepicker-range-input
{
.semi-datepicker-range-input
{
border-radius
:
9999px
!important
;
border-radius
:
9999px
!important
;
}
}
...
@@ -322,6 +326,24 @@ code {
...
@@ -322,6 +326,24 @@ code {
font-size
:
1.1em
;
font-size
:
1.1em
;
}
}
/* API信息卡片样式 */
.api-info-container
{
position
:
relative
;
}
.api-info-fade-indicator
{
position
:
absolute
;
bottom
:
0
;
left
:
0
;
right
:
0
;
height
:
30px
;
background
:
linear-gradient
(
transparent
,
var
(
--semi-color-bg-1
));
pointer-events
:
none
;
z-index
:
1
;
opacity
:
0
;
transition
:
opacity
0.3s
ease
;
}
/* ==================== 调试面板特定样式 ==================== */
/* ==================== 调试面板特定样式 ==================== */
.debug-panel
.semi-tabs
{
.debug-panel
.semi-tabs
{
height
:
100%
!important
;
height
:
100%
!important
;
...
@@ -378,6 +400,7 @@ code {
...
@@ -378,6 +400,7 @@ code {
}
}
/* 隐藏模型设置区域的滚动条 */
/* 隐藏模型设置区域的滚动条 */
.api-info-scroll
::-webkit-scrollbar
,
.model-settings-scroll
::-webkit-scrollbar
,
.model-settings-scroll
::-webkit-scrollbar
,
.thinking-content-scroll
::-webkit-scrollbar
,
.thinking-content-scroll
::-webkit-scrollbar
,
.custom-request-textarea
.semi-input
::-webkit-scrollbar
,
.custom-request-textarea
.semi-input
::-webkit-scrollbar
,
...
@@ -385,6 +408,7 @@ code {
...
@@ -385,6 +408,7 @@ code {
display
:
none
;
display
:
none
;
}
}
.api-info-scroll
,
.model-settings-scroll
,
.model-settings-scroll
,
.thinking-content-scroll
,
.thinking-content-scroll
,
.custom-request-textarea
.semi-input
,
.custom-request-textarea
.semi-input
,
...
...
web/src/index.js
View file @
c821b0ed
import
React
from
'react'
;
import
React
from
'react'
;
import
ReactDOM
from
'react-dom/client'
;
import
ReactDOM
from
'react-dom/client'
;
import
{
BrowserRouter
}
from
'react-router-dom'
;
import
{
BrowserRouter
}
from
'react-router-dom'
;
import
'@douyinfe/semi-ui/dist/css/semi.css'
;
import
{
UserProvider
}
from
'./context/User'
;
import
{
UserProvider
}
from
'./context/User'
;
import
'react-toastify/dist/ReactToastify.css'
;
import
'react-toastify/dist/ReactToastify.css'
;
import
{
StatusProvider
}
from
'./context/Status'
;
import
{
StatusProvider
}
from
'./context/Status'
;
...
...
web/src/pages/Channel/EditTagModal.js
View file @
c821b0ed
...
@@ -194,6 +194,24 @@ const EditTagModal = (props) => {
...
@@ -194,6 +194,24 @@ const EditTagModal = (props) => {
},
[
originModelOptions
,
inputs
.
models
]);
},
[
originModelOptions
,
inputs
.
models
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
const
fetchTagModels
=
async
()
=>
{
if
(
!
tag
)
return
;
setLoading
(
true
);
try
{
const
res
=
await
API
.
get
(
`/api/channel/tag/models?tag=
${
tag
}
`
);
if
(
res
?.
data
?.
success
)
{
const
models
=
res
.
data
.
data
?
res
.
data
.
data
.
split
(
','
)
:
[];
setInputs
((
inputs
)
=>
({
...
inputs
,
models
:
models
}));
}
else
{
showError
(
res
.
data
.
message
);
}
}
catch
(
error
)
{
showError
(
error
.
message
);
}
finally
{
setLoading
(
false
);
}
};
setInputs
({
setInputs
({
...
originInputs
,
...
originInputs
,
tag
:
tag
,
tag
:
tag
,
...
@@ -201,7 +219,8 @@ const EditTagModal = (props) => {
...
@@ -201,7 +219,8 @@ const EditTagModal = (props) => {
});
});
fetchModels
().
then
();
fetchModels
().
then
();
fetchGroups
().
then
();
fetchGroups
().
then
();
},
[
visible
]);
fetchTagModels
().
then
();
// Call the new function
},
[
visible
,
tag
]);
// Add tag to dependency array
const
addCustomModels
=
()
=>
{
const
addCustomModels
=
()
=>
{
if
(
customModel
.
trim
()
===
''
)
return
;
if
(
customModel
.
trim
()
===
''
)
return
;
...
@@ -347,6 +366,11 @@ const EditTagModal = (props) => {
...
@@ -347,6 +366,11 @@ const EditTagModal = (props) => {
<
div
className
=
"space-y-4"
>
<
div
className
=
"space-y-4"
>
<
div
>
<
div
>
<
Text
strong
className
=
"block mb-2"
>
{
t
(
'模型'
)}
<
/Text
>
<
Text
strong
className
=
"block mb-2"
>
{
t
(
'模型'
)}
<
/Text
>
<
Banner
type
=
"info"
description
=
{
t
(
'当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。'
)}
className
=
"!rounded-lg mb-4"
/>
<
Select
<
Select
placeholder
=
{
t
(
'请选择该渠道所支持的模型,留空则不更改'
)}
placeholder
=
{
t
(
'请选择该渠道所支持的模型,留空则不更改'
)}
name
=
'models'
name
=
'models'
...
@@ -388,19 +412,19 @@ const EditTagModal = (props) => {
...
@@ -388,19 +412,19 @@ const EditTagModal = (props) => {
/>
/>
<
Space
className
=
"mt-2"
>
<
Space
className
=
"mt-2"
>
<
Text
<
Text
className
=
"
text-blue-500
cursor-pointer"
className
=
"
!text-semi-color-primary
cursor-pointer"
onClick
=
{()
=>
handleInputChange
(
'model_mapping'
,
JSON
.
stringify
(
MODEL_MAPPING_EXAMPLE
,
null
,
2
))}
onClick
=
{()
=>
handleInputChange
(
'model_mapping'
,
JSON
.
stringify
(
MODEL_MAPPING_EXAMPLE
,
null
,
2
))}
>
>
{
t
(
'填入模板'
)}
{
t
(
'填入模板'
)}
<
/Text
>
<
/Text
>
<
Text
<
Text
className
=
"
text-blue-500
cursor-pointer"
className
=
"
!text-semi-color-primary
cursor-pointer"
onClick
=
{()
=>
handleInputChange
(
'model_mapping'
,
JSON
.
stringify
({},
null
,
2
))}
onClick
=
{()
=>
handleInputChange
(
'model_mapping'
,
JSON
.
stringify
({},
null
,
2
))}
>
>
{
t
(
'清空重定向'
)}
{
t
(
'清空重定向'
)}
<
/Text
>
<
/Text
>
<
Text
<
Text
className
=
"
text-blue-500
cursor-pointer"
className
=
"
!text-semi-color-primary
cursor-pointer"
onClick
=
{()
=>
handleInputChange
(
'model_mapping'
,
''
)}
onClick
=
{()
=>
handleInputChange
(
'model_mapping'
,
''
)}
>
>
{
t
(
'不更改'
)}
{
t
(
'不更改'
)}
...
...
web/src/pages/Detail/index.js
View file @
c821b0ed
import
React
,
{
useContext
,
useEffect
,
useRef
,
useState
}
from
'react'
;
import
React
,
{
useContext
,
useEffect
,
useRef
,
useState
,
useMemo
,
useCallback
}
from
'react'
;
import
{
initVChartSemiTheme
}
from
'@visactor/vchart-semi-theme'
;
import
{
initVChartSemiTheme
}
from
'@visactor/vchart-semi-theme'
;
import
{
useNavigate
}
from
'react-router-dom'
;
import
{
useNavigate
}
from
'react-router-dom'
;
import
{
Wallet
,
Activity
,
Zap
,
Gauge
,
PieChart
}
from
'lucide-react'
;
import
{
import
{
Card
,
Card
,
...
@@ -9,6 +10,10 @@ import {
...
@@ -9,6 +10,10 @@ import {
IconButton
,
IconButton
,
Modal
,
Modal
,
Avatar
,
Avatar
,
Tabs
,
TabPane
,
Empty
,
Tag
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
import
{
IconRefresh
,
IconRefresh
,
...
@@ -21,7 +26,9 @@ import {
...
@@ -21,7 +26,9 @@ import {
IconPulse
,
IconPulse
,
IconStopwatchStroked
,
IconStopwatchStroked
,
IconTypograph
,
IconTypograph
,
IconPieChart2Stroked
}
from
'@douyinfe/semi-icons'
;
}
from
'@douyinfe/semi-icons'
;
import
{
IllustrationConstruction
,
IllustrationConstructionDark
}
from
'@douyinfe/semi-illustrations'
;
import
{
VChart
}
from
'@visactor/react-vchart'
;
import
{
VChart
}
from
'@visactor/react-vchart'
;
import
{
import
{
API
,
API
,
...
@@ -34,47 +41,165 @@ import {
...
@@ -34,47 +41,165 @@ import {
modelColorMap
,
modelColorMap
,
renderNumber
,
renderNumber
,
renderQuota
,
renderQuota
,
modelToColor
modelToColor
,
copy
,
showSuccess
}
from
'../../helpers'
;
}
from
'../../helpers'
;
import
{
UserContext
}
from
'../../context/User/index.js'
;
import
{
UserContext
}
from
'../../context/User/index.js'
;
import
{
StatusContext
}
from
'../../context/Status/index.js'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useTranslation
}
from
'react-i18next'
;
const
Detail
=
(
props
)
=>
{
const
Detail
=
(
props
)
=>
{
// ========== Hooks - Context ==========
const
[
userState
,
userDispatch
]
=
useContext
(
UserContext
);
const
[
statusState
,
statusDispatch
]
=
useContext
(
StatusContext
);
// ========== Hooks - Navigation & Translation ==========
const
{
t
}
=
useTranslation
();
const
{
t
}
=
useTranslation
();
const
navigate
=
useNavigate
();
const
navigate
=
useNavigate
();
// ========== Hooks - Refs ==========
const
formRef
=
useRef
();
const
formRef
=
useRef
();
const
initialized
=
useRef
(
false
);
const
apiScrollRef
=
useRef
(
null
);
// ========== Constants & Shared Configurations ==========
const
CHART_CONFIG
=
{
mode
:
'desktop-browser'
};
const
CARD_PROPS
=
{
shadows
:
'always'
,
bordered
:
false
,
headerLine
:
true
};
const
FORM_FIELD_PROPS
=
{
className
:
"w-full mb-2 !rounded-lg"
,
size
:
'large'
};
const
ICON_BUTTON_CLASS
=
"text-white hover:bg-opacity-80 !rounded-full"
;
const
FLEX_CENTER_GAP2
=
"flex items-center gap-2"
;
// ========== Constants ==========
let
now
=
new
Date
();
let
now
=
new
Date
();
const
[
userState
,
userDispatch
]
=
useContext
(
UserContext
);
const
isAdminUser
=
isAdmin
();
// ========== Helper Functions ==========
const
getDefaultTime
=
useCallback
(()
=>
{
return
localStorage
.
getItem
(
'data_export_default_time'
)
||
'hour'
;
},
[]);
const
getTimeInterval
=
useCallback
((
timeType
,
isSeconds
=
false
)
=>
{
const
intervals
=
{
hour
:
isSeconds
?
3600
:
60
,
day
:
isSeconds
?
86400
:
1440
,
week
:
isSeconds
?
604800
:
10080
};
return
intervals
[
timeType
]
||
intervals
.
hour
;
},
[]);
const
getInitialTimestamp
=
useCallback
(()
=>
{
const
defaultTime
=
getDefaultTime
();
const
now
=
new
Date
().
getTime
()
/
1000
;
switch
(
defaultTime
)
{
case
'hour'
:
return
timestamp2string
(
now
-
86400
);
case
'week'
:
return
timestamp2string
(
now
-
86400
*
30
);
default
:
return
timestamp2string
(
now
-
86400
*
7
);
}
},
[
getDefaultTime
]);
const
updateMapValue
=
useCallback
((
map
,
key
,
value
)
=>
{
if
(
!
map
.
has
(
key
))
{
map
.
set
(
key
,
0
);
}
map
.
set
(
key
,
map
.
get
(
key
)
+
value
);
},
[]);
const
initializeMaps
=
useCallback
((
key
,
...
maps
)
=>
{
maps
.
forEach
(
map
=>
{
if
(
!
map
.
has
(
key
))
{
map
.
set
(
key
,
0
);
}
});
},
[]);
const
updateChartSpec
=
useCallback
((
setterFunc
,
newData
,
subtitle
,
newColors
,
dataId
)
=>
{
setterFunc
(
prev
=>
({
...
prev
,
data
:
[{
id
:
dataId
,
values
:
newData
}],
title
:
{
...
prev
.
title
,
subtext
:
subtitle
,
},
color
:
{
specified
:
newColors
,
},
}));
},
[]);
const
createSectionTitle
=
useCallback
((
Icon
,
text
)
=>
(
<
div
className
=
{
FLEX_CENTER_GAP2
}
>
<
Icon
size
=
{
16
}
/
>
{
text
}
<
/div
>
),
[]);
const
createFormField
=
useCallback
((
Component
,
props
)
=>
(
<
Component
{...
FORM_FIELD_PROPS
}
{...
props
}
/
>
),
[]);
// ========== Time Options ==========
const
timeOptions
=
useMemo
(()
=>
[
{
label
:
t
(
'小时'
),
value
:
'hour'
},
{
label
:
t
(
'天'
),
value
:
'day'
},
{
label
:
t
(
'周'
),
value
:
'week'
},
],
[
t
]);
// ========== Hooks - State ==========
const
[
inputs
,
setInputs
]
=
useState
({
const
[
inputs
,
setInputs
]
=
useState
({
username
:
''
,
username
:
''
,
token_name
:
''
,
token_name
:
''
,
model_name
:
''
,
model_name
:
''
,
start_timestamp
:
start_timestamp
:
getInitialTimestamp
(),
localStorage
.
getItem
(
'data_export_default_time'
)
===
'hour'
?
timestamp2string
(
now
.
getTime
()
/
1000
-
86400
)
:
localStorage
.
getItem
(
'data_export_default_time'
)
===
'week'
?
timestamp2string
(
now
.
getTime
()
/
1000
-
86400
*
30
)
:
timestamp2string
(
now
.
getTime
()
/
1000
-
86400
*
7
),
end_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
),
end_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
),
channel
:
''
,
channel
:
''
,
data_export_default_time
:
''
,
data_export_default_time
:
''
,
});
});
const
{
username
,
model_name
,
start_timestamp
,
end_timestamp
,
channel
}
=
inputs
;
const
[
dataExportDefaultTime
,
setDataExportDefaultTime
]
=
useState
(
getDefaultTime
());
const
isAdminUser
=
isAdmin
();
const
initialized
=
useRef
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
quotaData
,
setQuotaData
]
=
useState
([]);
const
[
quotaData
,
setQuotaData
]
=
useState
([]);
const
[
consumeQuota
,
setConsumeQuota
]
=
useState
(
0
);
const
[
consumeQuota
,
setConsumeQuota
]
=
useState
(
0
);
const
[
consumeTokens
,
setConsumeTokens
]
=
useState
(
0
);
const
[
consumeTokens
,
setConsumeTokens
]
=
useState
(
0
);
const
[
times
,
setTimes
]
=
useState
(
0
);
const
[
times
,
setTimes
]
=
useState
(
0
);
const
[
dataExportDefaultTime
,
setDataExportDefaultTime
]
=
useState
(
localStorage
.
getItem
(
'data_export_default_time'
)
||
'hour'
,
);
const
[
pieData
,
setPieData
]
=
useState
([{
type
:
'null'
,
value
:
'0'
}]);
const
[
pieData
,
setPieData
]
=
useState
([{
type
:
'null'
,
value
:
'0'
}]);
const
[
lineData
,
setLineData
]
=
useState
([]);
const
[
lineData
,
setLineData
]
=
useState
([]);
const
[
apiInfoData
,
setApiInfoData
]
=
useState
([]);
const
[
modelColors
,
setModelColors
]
=
useState
({});
const
[
activeChartTab
,
setActiveChartTab
]
=
useState
(
'1'
);
const
[
showApiScrollHint
,
setShowApiScrollHint
]
=
useState
(
false
);
const
[
searchModalVisible
,
setSearchModalVisible
]
=
useState
(
false
);
const
[
searchModalVisible
,
setSearchModalVisible
]
=
useState
(
false
);
const
[
trendData
,
setTrendData
]
=
useState
({
balance
:
[],
usedQuota
:
[],
requestCount
:
[],
times
:
[],
consumeQuota
:
[],
tokens
:
[],
rpm
:
[],
tpm
:
[]
});
// ========== Props Destructuring ==========
const
{
username
,
model_name
,
start_timestamp
,
end_timestamp
,
channel
}
=
inputs
;
// ========== Chart Specs State ==========
const
[
spec_pie
,
setSpecPie
]
=
useState
({
const
[
spec_pie
,
setSpecPie
]
=
useState
({
type
:
'pie'
,
type
:
'pie'
,
data
:
[
data
:
[
...
@@ -131,6 +256,7 @@ const Detail = (props) => {
...
@@ -131,6 +256,7 @@ const Detail = (props) => {
specified
:
modelColorMap
,
specified
:
modelColorMap
,
},
},
});
});
const
[
spec_line
,
setSpecLine
]
=
useState
({
const
[
spec_line
,
setSpecLine
]
=
useState
({
type
:
'bar'
,
type
:
'bar'
,
data
:
[
data
:
[
...
@@ -205,23 +331,35 @@ const Detail = (props) => {
...
@@ -205,23 +331,35 @@ const Detail = (props) => {
},
},
});
});
// 添加一个新的状态来存储模型-颜色映射
// ========== Hooks - Memoized Values ==========
const
[
modelColors
,
setModelColors
]
=
useState
({});
const
performanceMetrics
=
useMemo
(()
=>
{
const
timeDiff
=
(
Date
.
parse
(
end_timestamp
)
-
Date
.
parse
(
start_timestamp
))
/
60000
;
const
avgRPM
=
(
times
/
timeDiff
).
toFixed
(
3
);
const
avgTPM
=
isNaN
(
consumeTokens
/
timeDiff
)
?
'0'
:
(
consumeTokens
/
timeDiff
).
toFixed
(
3
);
// 添加趋势数据状态
return
{
avgRPM
,
avgTPM
,
timeDiff
};
const
[
trendData
,
setTrendData
]
=
useState
({
},
[
times
,
consumeTokens
,
end_timestamp
,
start_timestamp
]);
balance
:
[],
usedQuota
:
[],
requestCount
:
[],
times
:
[],
consumeQuota
:
[],
tokens
:
[],
rpm
:
[],
tpm
:
[]
});
// 迷你趋势图配置
const
getGreeting
=
useMemo
(()
=>
{
const
getTrendSpec
=
(
data
,
color
)
=>
({
const
hours
=
new
Date
().
getHours
();
let
greeting
=
''
;
if
(
hours
>=
5
&&
hours
<
12
)
{
greeting
=
t
(
'早上好'
);
}
else
if
(
hours
>=
12
&&
hours
<
14
)
{
greeting
=
t
(
'中午好'
);
}
else
if
(
hours
>=
14
&&
hours
<
18
)
{
greeting
=
t
(
'下午好'
);
}
else
{
greeting
=
t
(
'晚上好'
);
}
const
username
=
userState
?.
user
?.
username
||
''
;
return
`👋
${
greeting
}
,
${
username
}
`
;
},
[
t
,
userState
?.
user
?.
username
]);
// ========== Hooks - Callbacks ==========
const
getTrendSpec
=
useCallback
((
data
,
color
)
=>
({
type
:
'line'
,
type
:
'line'
,
data
:
[{
id
:
'trend'
,
values
:
data
.
map
((
val
,
idx
)
=>
({
x
:
idx
,
y
:
val
}))
}],
data
:
[{
id
:
'trend'
,
values
:
data
.
map
((
val
,
idx
)
=>
({
x
:
idx
,
y
:
val
}))
}],
xField
:
'x'
,
xField
:
'x'
,
...
@@ -255,33 +393,124 @@ const Detail = (props) => {
...
@@ -255,33 +393,124 @@ const Detail = (props) => {
background
:
{
background
:
{
fill
:
'transparent'
fill
:
'transparent'
}
}
});
})
,
[])
;
// 显示搜索Modal
const
groupedStatsData
=
useMemo
(()
=>
[
const
showSearchModal
=
()
=>
{
{
setSearchModalVisible
(
true
);
title
:
createSectionTitle
(
Wallet
,
t
(
'账户数据'
)),
};
color
:
'bg-blue-50'
,
items
:
[
{
title
:
t
(
'当前余额'
),
value
:
renderQuota
(
userState
?.
user
?.
quota
),
icon
:
<
IconMoneyExchangeStroked
size
=
"large"
/>
,
avatarColor
:
'blue'
,
onClick
:
()
=>
navigate
(
'/console/topup'
),
trendData
:
[],
trendColor
:
'#3b82f6'
},
{
title
:
t
(
'历史消耗'
),
value
:
renderQuota
(
userState
?.
user
?.
used_quota
),
icon
:
<
IconHistogram
size
=
"large"
/>
,
avatarColor
:
'purple'
,
trendData
:
[],
trendColor
:
'#8b5cf6'
}
]
},
{
title
:
createSectionTitle
(
Activity
,
t
(
'使用统计'
)),
color
:
'bg-green-50'
,
items
:
[
{
title
:
t
(
'请求次数'
),
value
:
userState
.
user
?.
request_count
,
icon
:
<
IconRotate
size
=
"large"
/>
,
avatarColor
:
'green'
,
trendData
:
[],
trendColor
:
'#10b981'
},
{
title
:
t
(
'统计次数'
),
value
:
times
,
icon
:
<
IconPulse
size
=
"large"
/>
,
avatarColor
:
'cyan'
,
trendData
:
trendData
.
times
,
trendColor
:
'#06b6d4'
}
]
},
{
title
:
createSectionTitle
(
Zap
,
t
(
'资源消耗'
)),
color
:
'bg-yellow-50'
,
items
:
[
{
title
:
t
(
'统计额度'
),
value
:
renderQuota
(
consumeQuota
),
icon
:
<
IconCoinMoneyStroked
size
=
"large"
/>
,
avatarColor
:
'yellow'
,
trendData
:
trendData
.
consumeQuota
,
trendColor
:
'#f59e0b'
},
{
title
:
t
(
'统计Tokens'
),
value
:
isNaN
(
consumeTokens
)
?
0
:
consumeTokens
,
icon
:
<
IconTextStroked
size
=
"large"
/>
,
avatarColor
:
'pink'
,
trendData
:
trendData
.
tokens
,
trendColor
:
'#ec4899'
}
]
},
{
title
:
createSectionTitle
(
Gauge
,
t
(
'性能指标'
)),
color
:
'bg-indigo-50'
,
items
:
[
{
title
:
t
(
'平均RPM'
),
value
:
performanceMetrics
.
avgRPM
,
icon
:
<
IconStopwatchStroked
size
=
"large"
/>
,
avatarColor
:
'indigo'
,
trendData
:
trendData
.
rpm
,
trendColor
:
'#6366f1'
},
{
title
:
t
(
'平均TPM'
),
value
:
performanceMetrics
.
avgTPM
,
icon
:
<
IconTypograph
size
=
"large"
/>
,
avatarColor
:
'orange'
,
trendData
:
trendData
.
tpm
,
trendColor
:
'#f97316'
}
]
}
],
[
createSectionTitle
,
t
,
userState
?.
user
?.
quota
,
userState
?.
user
?.
used_quota
,
userState
?.
user
?.
request_count
,
times
,
consumeQuota
,
consumeTokens
,
trendData
,
performanceMetrics
,
navigate
]);
// 关闭搜索Modal
const
handleCopyUrl
=
useCallback
(
async
(
url
)
=>
{
const
handleCloseModal
=
()
=>
{
if
(
await
copy
(
url
))
{
setSearchModalVisible
(
false
);
showSuccess
(
t
(
'复制成功'
));
};
}
},
[
t
]);
// 搜索Modal确认按钮
const
handleSpeedTest
=
useCallback
((
apiUrl
)
=>
{
const
handleSearchConfirm
=
()
=>
{
const
encodedUrl
=
encodeURIComponent
(
apiUrl
);
refresh
()
;
const
speedTestUrl
=
`https://www.tcptest.cn/http/
${
encodedUrl
}
`
;
setSearchModalVisible
(
false
);
window
.
open
(
speedTestUrl
,
'_blank'
);
};
}
,
[])
;
const
handleInputChange
=
(
value
,
name
)
=>
{
const
handleInputChange
=
useCallback
(
(
value
,
name
)
=>
{
if
(
name
===
'data_export_default_time'
)
{
if
(
name
===
'data_export_default_time'
)
{
setDataExportDefaultTime
(
value
);
setDataExportDefaultTime
(
value
);
return
;
return
;
}
}
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
};
}
,
[])
;
const
loadQuotaData
=
async
()
=>
{
const
loadQuotaData
=
useCallback
(
async
()
=>
{
setLoading
(
true
);
setLoading
(
true
);
try
{
try
{
let
url
=
''
;
let
url
=
''
;
...
@@ -304,7 +533,6 @@ const Detail = (props) => {
...
@@ -304,7 +533,6 @@ const Detail = (props) => {
created_at
:
now
.
getTime
()
/
1000
,
created_at
:
now
.
getTime
()
/
1000
,
});
});
}
}
// sort created_at
data
.
sort
((
a
,
b
)
=>
a
.
created_at
-
b
.
created_at
);
data
.
sort
((
a
,
b
)
=>
a
.
created_at
-
b
.
created_at
);
updateChartData
(
data
);
updateChartData
(
data
);
}
else
{
}
else
{
...
@@ -313,72 +541,97 @@ const Detail = (props) => {
...
@@ -313,72 +541,97 @@ const Detail = (props) => {
}
finally
{
}
finally
{
setLoading
(
false
);
setLoading
(
false
);
}
}
};
}
,
[
start_timestamp
,
end_timestamp
,
username
,
dataExportDefaultTime
,
isAdminUser
])
;
const
refresh
=
async
()
=>
{
const
refresh
=
useCallback
(
async
()
=>
{
await
loadQuotaData
();
await
loadQuotaData
();
};
},
[
loadQuotaData
]);
const
handleSearchConfirm
=
useCallback
(()
=>
{
refresh
();
setSearchModalVisible
(
false
);
},
[
refresh
]);
const
initChart
=
async
()
=>
{
const
initChart
=
useCallback
(
async
()
=>
{
await
loadQuotaData
();
await
loadQuotaData
();
},
[
loadQuotaData
]);
const
showSearchModal
=
useCallback
(()
=>
{
setSearchModalVisible
(
true
);
},
[]);
const
handleCloseModal
=
useCallback
(()
=>
{
setSearchModalVisible
(
false
);
},
[]);
// ========== Regular Functions ==========
const
checkApiScrollable
=
()
=>
{
if
(
apiScrollRef
.
current
)
{
const
element
=
apiScrollRef
.
current
;
const
isScrollable
=
element
.
scrollHeight
>
element
.
clientHeight
;
const
isAtBottom
=
element
.
scrollTop
+
element
.
clientHeight
>=
element
.
scrollHeight
-
5
;
setShowApiScrollHint
(
isScrollable
&&
!
isAtBottom
);
}
};
};
const
updateChartData
=
(
data
)
=>
{
const
handleApiScroll
=
()
=>
{
let
newPieData
=
[];
checkApiScrollable
();
let
newLineData
=
[];
};
let
totalQuota
=
0
;
let
totalTimes
=
0
;
const
getUserData
=
async
()
=>
{
let
uniqueModels
=
new
Set
();
let
res
=
await
API
.
get
(
`/api/user/self`
);
let
totalTokens
=
0
;
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
// 趋势数据处理
userDispatch
({
type
:
'login'
,
payload
:
data
});
let
timePoints
=
[];
}
else
{
let
timeQuotaMap
=
new
Map
();
showError
(
message
);
let
timeTokensMap
=
new
Map
();
}
let
timeCountMap
=
new
Map
();
};
// 收集所有唯一的模型名称和时间点
// ========== Data Processing Functions ==========
const
processRawData
=
useCallback
((
data
)
=>
{
const
result
=
{
totalQuota
:
0
,
totalTimes
:
0
,
totalTokens
:
0
,
uniqueModels
:
new
Set
(),
timePoints
:
[],
timeQuotaMap
:
new
Map
(),
timeTokensMap
:
new
Map
(),
timeCountMap
:
new
Map
()
};
data
.
forEach
((
item
)
=>
{
data
.
forEach
((
item
)
=>
{
uniqueModels
.
add
(
item
.
model_name
);
result
.
uniqueModels
.
add
(
item
.
model_name
);
totalTokens
+=
item
.
token_used
;
result
.
totalTokens
+=
item
.
token_used
;
totalQuota
+=
item
.
quota
;
result
.
totalQuota
+=
item
.
quota
;
totalTimes
+=
item
.
count
;
result
.
totalTimes
+=
item
.
count
;
// 记录时间点
const
timeKey
=
timestamp2string1
(
item
.
created_at
,
dataExportDefaultTime
);
const
timeKey
=
timestamp2string1
(
item
.
created_at
,
dataExportDefaultTime
);
if
(
!
timePoints
.
includes
(
timeKey
))
{
if
(
!
result
.
timePoints
.
includes
(
timeKey
))
{
timePoints
.
push
(
timeKey
);
result
.
timePoints
.
push
(
timeKey
);
}
}
// 按时间点累加数据
initializeMaps
(
timeKey
,
result
.
timeQuotaMap
,
result
.
timeTokensMap
,
result
.
timeCountMap
);
if
(
!
timeQuotaMap
.
has
(
timeKey
))
{
updateMapValue
(
result
.
timeQuotaMap
,
timeKey
,
item
.
quota
);
timeQuotaMap
.
set
(
timeKey
,
0
);
updateMapValue
(
result
.
timeTokensMap
,
timeKey
,
item
.
token_used
);
timeTokensMap
.
set
(
timeKey
,
0
);
updateMapValue
(
result
.
timeCountMap
,
timeKey
,
item
.
count
);
timeCountMap
.
set
(
timeKey
,
0
);
}
timeQuotaMap
.
set
(
timeKey
,
timeQuotaMap
.
get
(
timeKey
)
+
item
.
quota
);
timeTokensMap
.
set
(
timeKey
,
timeTokensMap
.
get
(
timeKey
)
+
item
.
token_used
);
timeCountMap
.
set
(
timeKey
,
timeCountMap
.
get
(
timeKey
)
+
item
.
count
);
});
});
// 确保时间点有序
result
.
timePoints
.
sort
();
timePoints
.
sort
();
return
result
;
},
[
dataExportDefaultTime
,
initializeMaps
,
updateMapValue
]);
// 生成趋势数据
const
calculateTrendData
=
useCallback
((
timePoints
,
timeQuotaMap
,
timeTokensMap
,
timeCountMap
)
=>
{
const
quotaTrend
=
timePoints
.
map
(
time
=>
timeQuotaMap
.
get
(
time
)
||
0
);
const
quotaTrend
=
timePoints
.
map
(
time
=>
timeQuotaMap
.
get
(
time
)
||
0
);
const
tokensTrend
=
timePoints
.
map
(
time
=>
timeTokensMap
.
get
(
time
)
||
0
);
const
tokensTrend
=
timePoints
.
map
(
time
=>
timeTokensMap
.
get
(
time
)
||
0
);
const
countTrend
=
timePoints
.
map
(
time
=>
timeCountMap
.
get
(
time
)
||
0
);
const
countTrend
=
timePoints
.
map
(
time
=>
timeCountMap
.
get
(
time
)
||
0
);
// 计算RPM和TPM趋势
const
rpmTrend
=
[];
const
rpmTrend
=
[];
const
tpmTrend
=
[];
const
tpmTrend
=
[];
if
(
timePoints
.
length
>=
2
)
{
if
(
timePoints
.
length
>=
2
)
{
const
interval
=
dataExportDefaultTime
===
'hour'
const
interval
=
getTimeInterval
(
dataExportDefaultTime
);
?
60
// 分钟/小时
:
dataExportDefaultTime
===
'day'
?
1440
// 分钟/天
:
10080
;
// 分钟/周
for
(
let
i
=
0
;
i
<
timePoints
.
length
;
i
++
)
{
for
(
let
i
=
0
;
i
<
timePoints
.
length
;
i
++
)
{
rpmTrend
.
push
(
timeCountMap
.
get
(
timePoints
[
i
])
/
interval
);
rpmTrend
.
push
(
timeCountMap
.
get
(
timePoints
[
i
])
/
interval
);
...
@@ -386,23 +639,19 @@ const Detail = (props) => {
...
@@ -386,23 +639,19 @@ const Detail = (props) => {
}
}
}
}
// 更新趋势数据状态
return
{
setTrendData
({
// 账户数据不在API返回中,保持空数组
balance
:
[],
balance
:
[],
usedQuota
:
[],
usedQuota
:
[],
// 使用统计
requestCount
:
[],
requestCount
:
[],
// 没有总请求次数趋势数据
times
:
countTrend
,
times
:
countTrend
,
// 资源消耗
consumeQuota
:
quotaTrend
,
consumeQuota
:
quotaTrend
,
tokens
:
tokensTrend
,
tokens
:
tokensTrend
,
// 性能指标
rpm
:
rpmTrend
,
rpm
:
rpmTrend
,
tpm
:
tpmTrend
tpm
:
tpmTrend
});
};
},
[
dataExportDefaultTime
,
getTimeInterval
]);
// 处理颜色映射
const
generateModelColors
=
useCallback
((
uniqueModels
)
=>
{
const
newModelColors
=
{};
const
newModelColors
=
{};
Array
.
from
(
uniqueModels
).
forEach
((
modelName
)
=>
{
Array
.
from
(
uniqueModels
).
forEach
((
modelName
)
=>
{
newModelColors
[
modelName
]
=
newModelColors
[
modelName
]
=
...
@@ -410,10 +659,12 @@ const Detail = (props) => {
...
@@ -410,10 +659,12 @@ const Detail = (props) => {
modelColors
[
modelName
]
||
modelColors
[
modelName
]
||
modelToColor
(
modelName
);
modelToColor
(
modelName
);
});
});
setModelColors
(
newModelColors
);
return
newModelColors
;
},
[
modelColors
]);
const
aggregateDataByTimeAndModel
=
useCallback
((
data
)
=>
{
const
aggregatedData
=
new
Map
();
// 按时间和模型聚合数据
let
aggregatedData
=
new
Map
();
data
.
forEach
((
item
)
=>
{
data
.
forEach
((
item
)
=>
{
const
timeKey
=
timestamp2string1
(
item
.
created_at
,
dataExportDefaultTime
);
const
timeKey
=
timestamp2string1
(
item
.
created_at
,
dataExportDefaultTime
);
const
modelKey
=
item
.
model_name
;
const
modelKey
=
item
.
model_name
;
...
@@ -433,41 +684,52 @@ const Detail = (props) => {
...
@@ -433,41 +684,52 @@ const Detail = (props) => {
existing
.
count
+=
item
.
count
;
existing
.
count
+=
item
.
count
;
});
});
// 处理饼图数据
return
aggregatedData
;
let
modelTotals
=
new
Map
();
},
[
dataExportDefaultTime
]);
for
(
let
[
_
,
value
]
of
aggregatedData
)
{
if
(
!
modelTotals
.
has
(
value
.
model
))
{
modelTotals
.
set
(
value
.
model
,
0
);
}
modelTotals
.
set
(
value
.
model
,
modelTotals
.
get
(
value
.
model
)
+
value
.
count
);
}
newPieData
=
Array
.
from
(
modelTotals
).
map
(([
model
,
count
])
=>
({
type
:
model
,
value
:
count
,
}));
// 生成时间点序列
const
generateChartTimePoints
=
useCallback
((
aggregatedData
,
data
)
=>
{
let
chartTimePoints
=
Array
.
from
(
let
chartTimePoints
=
Array
.
from
(
new
Set
([...
aggregatedData
.
values
()].
map
((
d
)
=>
d
.
time
)),
new
Set
([...
aggregatedData
.
values
()].
map
((
d
)
=>
d
.
time
)),
);
);
if
(
chartTimePoints
.
length
<
7
)
{
if
(
chartTimePoints
.
length
<
7
)
{
const
lastTime
=
Math
.
max
(...
data
.
map
((
item
)
=>
item
.
created_at
));
const
lastTime
=
Math
.
max
(...
data
.
map
((
item
)
=>
item
.
created_at
));
const
interval
=
const
interval
=
getTimeInterval
(
dataExportDefaultTime
,
true
);
dataExportDefaultTime
===
'hour'
?
3600
:
dataExportDefaultTime
===
'day'
?
86400
:
604800
;
chartTimePoints
=
Array
.
from
({
length
:
7
},
(
_
,
i
)
=>
chartTimePoints
=
Array
.
from
({
length
:
7
},
(
_
,
i
)
=>
timestamp2string1
(
lastTime
-
(
6
-
i
)
*
interval
,
dataExportDefaultTime
),
timestamp2string1
(
lastTime
-
(
6
-
i
)
*
interval
,
dataExportDefaultTime
),
);
);
}
}
// 生成柱状图数据
return
chartTimePoints
;
},
[
dataExportDefaultTime
,
getTimeInterval
]);
const
updateChartData
=
useCallback
((
data
)
=>
{
const
processedData
=
processRawData
(
data
);
const
{
totalQuota
,
totalTimes
,
totalTokens
,
uniqueModels
,
timePoints
,
timeQuotaMap
,
timeTokensMap
,
timeCountMap
}
=
processedData
;
const
trendDataResult
=
calculateTrendData
(
timePoints
,
timeQuotaMap
,
timeTokensMap
,
timeCountMap
);
setTrendData
(
trendDataResult
);
const
newModelColors
=
generateModelColors
(
uniqueModels
);
setModelColors
(
newModelColors
);
const
aggregatedData
=
aggregateDataByTimeAndModel
(
data
);
const
modelTotals
=
new
Map
();
for
(
let
[
_
,
value
]
of
aggregatedData
)
{
updateMapValue
(
modelTotals
,
value
.
model
,
value
.
count
);
}
const
newPieData
=
Array
.
from
(
modelTotals
).
map
(([
model
,
count
])
=>
({
type
:
model
,
value
:
count
,
})).
sort
((
a
,
b
)
=>
b
.
value
-
a
.
value
);
const
chartTimePoints
=
generateChartTimePoints
(
aggregatedData
,
data
);
let
newLineData
=
[];
chartTimePoints
.
forEach
((
time
)
=>
{
chartTimePoints
.
forEach
((
time
)
=>
{
// 为每个时间点收集所有模型的数据
let
timeData
=
Array
.
from
(
uniqueModels
).
map
((
model
)
=>
{
let
timeData
=
Array
.
from
(
uniqueModels
).
map
((
model
)
=>
{
const
key
=
`
${
time
}
-
${
model
}
`
;
const
key
=
`
${
time
}
-
${
model
}
`
;
const
aggregated
=
aggregatedData
.
get
(
key
);
const
aggregated
=
aggregatedData
.
get
(
key
);
...
@@ -479,68 +741,41 @@ const Detail = (props) => {
...
@@ -479,68 +741,41 @@ const Detail = (props) => {
};
};
});
});
// 计算该时间点的总计
const
timeSum
=
timeData
.
reduce
((
sum
,
item
)
=>
sum
+
item
.
rawQuota
,
0
);
const
timeSum
=
timeData
.
reduce
((
sum
,
item
)
=>
sum
+
item
.
rawQuota
,
0
);
// 按照 rawQuota 从大到小排序
timeData
.
sort
((
a
,
b
)
=>
b
.
rawQuota
-
a
.
rawQuota
);
timeData
.
sort
((
a
,
b
)
=>
b
.
rawQuota
-
a
.
rawQuota
);
timeData
=
timeData
.
map
((
item
)
=>
({
...
item
,
TimeSum
:
timeSum
}));
// 为每个数据点添加该时间的总计
timeData
=
timeData
.
map
((
item
)
=>
({
...
item
,
TimeSum
:
timeSum
,
}));
// 将排序后的数据添加到 newLineData
newLineData
.
push
(...
timeData
);
newLineData
.
push
(...
timeData
);
});
});
// 排序
newPieData
.
sort
((
a
,
b
)
=>
b
.
value
-
a
.
value
);
newLineData
.
sort
((
a
,
b
)
=>
a
.
Time
.
localeCompare
(
b
.
Time
));
newLineData
.
sort
((
a
,
b
)
=>
a
.
Time
.
localeCompare
(
b
.
Time
));
// 更新图表配置和数据
updateChartSpec
(
setSpecPie
((
prev
)
=>
({
setSpecPie
,
...
prev
,
newPieData
,
data
:
[{
id
:
'id0'
,
values
:
newPieData
}],
`
${
t
(
'总计'
)}
:
${
renderNumber
(
totalTimes
)}
`
,
title
:
{
newModelColors
,
...
prev
.
title
,
'id0'
subtext
:
`
${
t
(
'总计'
)}
:
${
renderNumber
(
totalTimes
)}
`
,
);
},
color
:
{
specified
:
newModelColors
,
},
}));
setSpecLine
((
prev
)
=>
({
updateChartSpec
(
...
prev
,
setSpecLine
,
data
:
[{
id
:
'barData'
,
values
:
newLineData
}],
newLineData
,
title
:
{
`
${
t
(
'总计'
)}
:
${
renderQuota
(
totalQuota
,
2
)}
`
,
...
prev
.
title
,
newModelColors
,
subtext
:
`
${
t
(
'总计'
)}
:
${
renderQuota
(
totalQuota
,
2
)}
`
,
'barData'
},
);
color
:
{
specified
:
newModelColors
,
},
}));
setPieData
(
newPieData
);
setPieData
(
newPieData
);
setLineData
(
newLineData
);
setLineData
(
newLineData
);
setConsumeQuota
(
totalQuota
);
setConsumeQuota
(
totalQuota
);
setTimes
(
totalTimes
);
setTimes
(
totalTimes
);
setConsumeTokens
(
totalTokens
);
setConsumeTokens
(
totalTokens
);
};
},
[
processRawData
,
calculateTrendData
,
generateModelColors
,
aggregateDataByTimeAndModel
,
const
getUserData
=
async
()
=>
{
generateChartTimePoints
,
updateChartSpec
,
updateMapValue
,
t
let
res
=
await
API
.
get
(
`/api/user/self`
);
]);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
userDispatch
({
type
:
'login'
,
payload
:
data
});
}
else
{
showError
(
message
);
}
};
// ========== Hooks - Effects ==========
useEffect
(()
=>
{
useEffect
(()
=>
{
getUserData
();
getUserData
();
if
(
!
initialized
.
current
)
{
if
(
!
initialized
.
current
)
{
...
@@ -552,140 +787,34 @@ const Detail = (props) => {
...
@@ -552,140 +787,34 @@ const Detail = (props) => {
}
}
},
[]);
},
[]);
// 数据卡片信息
useEffect
(()
=>
{
const
groupedStatsData
=
[
if
(
statusState
?.
status
?.
api_info
)
{
{
setApiInfoData
(
statusState
.
status
.
api_info
);
title
:
t
(
'账户数据'
),
color
:
'bg-blue-50'
,
items
:
[
{
title
:
t
(
'当前余额'
),
value
:
renderQuota
(
userState
?.
user
?.
quota
),
icon
:
<
IconMoneyExchangeStroked
size
=
"large"
/>
,
avatarColor
:
'blue'
,
onClick
:
()
=>
navigate
(
'/console/topup'
),
trendData
:
[],
// 当前余额没有趋势数据
trendColor
:
'#3b82f6'
},
{
title
:
t
(
'历史消耗'
),
value
:
renderQuota
(
userState
?.
user
?.
used_quota
),
icon
:
<
IconHistogram
size
=
"large"
/>
,
avatarColor
:
'purple'
,
trendData
:
[],
// 历史消耗没有趋势数据
trendColor
:
'#8b5cf6'
}
]
},
{
title
:
t
(
'使用统计'
),
color
:
'bg-green-50'
,
items
:
[
{
title
:
t
(
'请求次数'
),
value
:
userState
.
user
?.
request_count
,
icon
:
<
IconRotate
size
=
"large"
/>
,
avatarColor
:
'green'
,
trendData
:
[],
// 请求次数没有趋势数据
trendColor
:
'#10b981'
},
{
title
:
t
(
'统计次数'
),
value
:
times
,
icon
:
<
IconPulse
size
=
"large"
/>
,
avatarColor
:
'cyan'
,
trendData
:
trendData
.
times
,
trendColor
:
'#06b6d4'
}
]
},
{
title
:
t
(
'资源消耗'
),
color
:
'bg-yellow-50'
,
items
:
[
{
title
:
t
(
'统计额度'
),
value
:
renderQuota
(
consumeQuota
),
icon
:
<
IconCoinMoneyStroked
size
=
"large"
/>
,
avatarColor
:
'yellow'
,
trendData
:
trendData
.
consumeQuota
,
trendColor
:
'#f59e0b'
},
{
title
:
t
(
'统计Tokens'
),
value
:
isNaN
(
consumeTokens
)
?
0
:
consumeTokens
,
icon
:
<
IconTextStroked
size
=
"large"
/>
,
avatarColor
:
'pink'
,
trendData
:
trendData
.
tokens
,
trendColor
:
'#ec4899'
}
]
},
{
title
:
t
(
'性能指标'
),
color
:
'bg-indigo-50'
,
items
:
[
{
title
:
t
(
'平均RPM'
),
value
:
(
times
/
((
Date
.
parse
(
end_timestamp
)
-
Date
.
parse
(
start_timestamp
))
/
60000
)
).
toFixed
(
3
),
icon
:
<
IconStopwatchStroked
size
=
"large"
/>
,
avatarColor
:
'indigo'
,
trendData
:
trendData
.
rpm
,
trendColor
:
'#6366f1'
},
{
title
:
t
(
'平均TPM'
),
value
:
(()
=>
{
const
tpm
=
consumeTokens
/
((
Date
.
parse
(
end_timestamp
)
-
Date
.
parse
(
start_timestamp
))
/
60000
);
return
isNaN
(
tpm
)
?
'0'
:
tpm
.
toFixed
(
3
);
})(),
icon
:
<
IconTypograph
size
=
"large"
/>
,
avatarColor
:
'orange'
,
trendData
:
trendData
.
tpm
,
trendColor
:
'#f97316'
}
]
}
];
// 获取问候语
const
getGreeting
=
()
=>
{
const
hours
=
new
Date
().
getHours
();
let
greeting
=
''
;
if
(
hours
>=
5
&&
hours
<
12
)
{
greeting
=
t
(
'早上好'
);
}
else
if
(
hours
>=
12
&&
hours
<
14
)
{
greeting
=
t
(
'中午好'
);
}
else
if
(
hours
>=
14
&&
hours
<
18
)
{
greeting
=
t
(
'下午好'
);
}
else
{
greeting
=
t
(
'晚上好'
);
}
}
},
[
statusState
?.
status
?.
api_info
]);
const
username
=
userState
?.
user
?.
username
||
''
;
useEffect
(()
=>
{
return
`👋
${
greeting
}
,
${
username
}
`
;
const
timer
=
setTimeout
(()
=>
{
};
checkApiScrollable
();
},
100
);
return
()
=>
clearTimeout
(
timer
);
},
[]);
return
(
return
(
<
div
className
=
"bg-gray-50 h-full"
>
<
div
className
=
"bg-gray-50 h-full"
>
<
div
className
=
"flex items-center justify-between mb-4"
>
<
div
className
=
"flex items-center justify-between mb-4"
>
<
h2
className
=
"text-2xl font-semibold text-gray-800"
>
{
getGreeting
()
}
<
/h2
>
<
h2
className
=
"text-2xl font-semibold text-gray-800"
>
{
getGreeting
}
<
/h2
>
<
div
className
=
"flex gap-3"
>
<
div
className
=
"flex gap-3"
>
<
IconButton
<
IconButton
icon
=
{
<
IconSearch
/>
}
icon
=
{
<
IconSearch
/>
}
onClick
=
{
showSearchModal
}
onClick
=
{
showSearchModal
}
className
=
"bg-green-500 text-white hover:bg-green-600 !rounded-full"
className
=
{
`bg-green-500 hover:bg-green-600
${
ICON_BUTTON_CLASS
}
`
}
/
>
/
>
<
IconButton
<
IconButton
icon
=
{
<
IconRefresh
/>
}
icon
=
{
<
IconRefresh
/>
}
onClick
=
{
refresh
}
onClick
=
{
refresh
}
loading
=
{
loading
}
loading
=
{
loading
}
className
=
"bg-blue-500 text-white hover:bg-blue-600 !rounded-full"
className
=
{
`bg-blue-500 hover:bg-blue-600
${
ICON_BUTTON_CLASS
}
`
}
/
>
/
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
...
@@ -701,55 +830,44 @@ const Detail = (props) => {
...
@@ -701,55 +830,44 @@ const Detail = (props) => {
centered
centered
>
>
<
Form
ref
=
{
formRef
}
layout
=
'vertical'
className
=
"w-full"
>
<
Form
ref
=
{
formRef
}
layout
=
'vertical'
className
=
"w-full"
>
<
Form
.
DatePicker
{
createFormField
(
Form
.
DatePicker
,
{
field
=
'start_timestamp'
field
:
'start_timestamp'
,
label
=
{
t
(
'起始时间'
)}
label
:
t
(
'起始时间'
),
className
=
"w-full mb-2 !rounded-lg"
initValue
:
start_timestamp
,
initValue
=
{
start_timestamp
}
value
:
start_timestamp
,
value
=
{
start_timestamp
}
type
:
'dateTime'
,
type
=
'dateTime'
name
:
'start_timestamp'
,
name
=
'start_timestamp'
onChange
:
(
value
)
=>
handleInputChange
(
value
,
'start_timestamp'
)
size
=
'large'
})}
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'start_timestamp'
)}
/
>
{
createFormField
(
Form
.
DatePicker
,
{
<
Form
.
DatePicker
field
:
'end_timestamp'
,
field
=
'end_timestamp'
label
:
t
(
'结束时间'
),
label
=
{
t
(
'结束时间'
)}
initValue
:
end_timestamp
,
className
=
"w-full mb-2 !rounded-lg"
value
:
end_timestamp
,
initValue
=
{
end_timestamp
}
type
:
'dateTime'
,
value
=
{
end_timestamp
}
name
:
'end_timestamp'
,
type
=
'dateTime'
onChange
:
(
value
)
=>
handleInputChange
(
value
,
'end_timestamp'
)
name
=
'end_timestamp'
})}
size
=
'large'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'end_timestamp'
)}
{
createFormField
(
Form
.
Select
,
{
/
>
field
:
'data_export_default_time'
,
<
Form
.
Select
label
:
t
(
'时间粒度'
),
field
=
'data_export_default_time'
initValue
:
dataExportDefaultTime
,
label
=
{
t
(
'时间粒度'
)}
placeholder
:
t
(
'时间粒度'
),
className
=
"w-full mb-2 !rounded-lg"
name
:
'data_export_default_time'
,
initValue
=
{
dataExportDefaultTime
}
optionList
:
timeOptions
,
placeholder
=
{
t
(
'时间粒度'
)}
onChange
:
(
value
)
=>
handleInputChange
(
value
,
'data_export_default_time'
)
name
=
'data_export_default_time'
})}
size
=
'large'
optionList
=
{[
{
isAdminUser
&&
createFormField
(
Form
.
Input
,
{
{
label
:
t
(
'小时'
),
value
:
'hour'
},
field
:
'username'
,
{
label
:
t
(
'天'
),
value
:
'day'
},
label
:
t
(
'用户名称'
),
{
label
:
t
(
'周'
),
value
:
'week'
},
value
:
username
,
]}
placeholder
:
t
(
'可选值'
),
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'data_export_default_time'
)}
name
:
'username'
,
/
>
onChange
:
(
value
)
=>
handleInputChange
(
value
,
'username'
)
{
isAdminUser
&&
(
})}
<
Form
.
Input
field
=
'username'
label
=
{
t
(
'用户名称'
)}
className
=
"w-full mb-2 !rounded-lg"
value
=
{
username
}
placeholder
=
{
t
(
'可选值'
)}
name
=
'username'
size
=
'large'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'username'
)}
/
>
)}
<
/Form
>
<
/Form
>
<
/Modal
>
<
/Modal
>
...
@@ -759,23 +877,9 @@ const Detail = (props) => {
...
@@ -759,23 +877,9 @@ const Detail = (props) => {
{
groupedStatsData
.
map
((
group
,
idx
)
=>
(
{
groupedStatsData
.
map
((
group
,
idx
)
=>
(
<
Card
<
Card
key
=
{
idx
}
key
=
{
idx
}
shadows
=
'always'
{...
CARD_PROPS
}
bordered
=
{
false
}
className
=
{
`
${
group
.
color
}
border-0 !rounded-2xl w-full`
}
className
=
{
`
${
group
.
color
}
border-0 !rounded-2xl w-full`
}
headerLine
=
{
true
}
title
=
{
group
.
title
}
header
=
{
<
div
style
=
{{
color
:
'white'
,
fontWeight
:
'bold'
,
fontSize
:
'16px'
}}
>
{
group
.
title
}
<
/div>
}
headerStyle
=
{{
background
:
idx
===
0
?
'linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)'
:
idx
===
1
?
'linear-gradient(135deg, #10b981 0%, #34d399 100%)'
:
idx
===
2
?
'linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)'
:
'linear-gradient(135deg, #ec4899 0%, #f472b6 100%)'
,
borderTopLeftRadius
:
'16px'
,
borderTopRightRadius
:
'16px'
,
padding
:
'12px 16px'
,
}}
>
>
<
div
className
=
"space-y-4"
>
<
div
className
=
"space-y-4"
>
{
group
.
items
.
map
((
item
,
itemIdx
)
=>
(
{
group
.
items
.
map
((
item
,
itemIdx
)
=>
(
...
@@ -801,7 +905,7 @@ const Detail = (props) => {
...
@@ -801,7 +905,7 @@ const Detail = (props) => {
<
div
className
=
"w-24 h-10"
>
<
div
className
=
"w-24 h-10"
>
<
VChart
<
VChart
spec
=
{
getTrendSpec
(
item
.
trendData
,
item
.
trendColor
)}
spec
=
{
getTrendSpec
(
item
.
trendData
,
item
.
trendColor
)}
option
=
{
{
mode
:
'desktop-browser'
}
}
option
=
{
CHART_CONFIG
}
/
>
/
>
<
/div
>
<
/div
>
)}
)}
...
@@ -813,29 +917,127 @@ const Detail = (props) => {
...
@@ -813,29 +917,127 @@ const Detail = (props) => {
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
className
=
"grid grid-cols-1 lg:grid-cols-1 gap-6 mb-6"
>
<
div
className
=
"mb-4"
>
<
Card
<
div
className
=
{
`grid grid-cols-1 gap-4
${
!
statusState
?.
status
?.
self_use_mode_enabled
?
'lg:grid-cols-4'
:
''
}
`}>
shadows
=
'always'
<Card
bordered
=
{
false
}
{...CARD_PROPS}
className
=
"shadow-sm !rounded-2xl"
className={`
shadow
-
sm
!
rounded
-
2
xl
$
{
!
statusState
?.
status
?.
self_use_mode_enabled
?
'lg:col-span-3'
:
''
}
`}
headerLine
=
{
true
}
title={
title
=
{
t
(
'模型数据分析'
)}
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3">
>
<div className={FLEX_CENTER_GAP2}>
<
div
className
=
"grid grid-cols-1 xl:grid-cols-2 gap-6"
>
<PieChart size={16} />
<
div
style
=
{{
height
:
400
}}
>
{t('模型数据分析')}
<
VChart
</div>
spec
=
{
spec_line
}
<Tabs
option
=
{{
mode
:
'desktop-browser'
}}
type="button"
/
>
activeKey={activeChartTab}
<
/div
>
onChange={setActiveChartTab}
>
<TabPane tab={
<span>
<IconHistogram />
{t('消耗分布')}
</span>
} itemKey="1" />
<TabPane tab={
<span>
<IconPieChart2Stroked />
{t('调用次数分布')}
</span>
} itemKey="2" />
</Tabs>
</div>
}
>
<div style={{ height: 400 }}>
<div style={{ height: 400 }}>
<
VChart
{activeChartTab === '1' ? (
spec
=
{
spec_pie
}
<VChart
option
=
{{
mode
:
'desktop-browser'
}}
spec={spec_line}
/
>
option={CHART_CONFIG}
/>
) : (
<VChart
spec={spec_pie}
option={CHART_CONFIG}
/>
)}
</div>
</div>
<
/div
>
</Card>
<
/Card
>
{!statusState?.status?.self_use_mode_enabled && (
<Card
{...CARD_PROPS}
className="bg-gray-50 border-0 !rounded-2xl"
title={
<div className={FLEX_CENTER_GAP2}>
<IconSearch size={16} />
{t('API信息')}
</div>
}
>
<div className="api-info-container">
<div
ref={apiScrollRef}
className="space-y-3 max-h-96 overflow-y-auto api-info-scroll"
onScroll={handleApiScroll}
>
{apiInfoData.length > 0 ? (
apiInfoData.map((api) => (
<div key={api.id} className="flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer">
<div className="flex-shrink-0 mr-3">
<Avatar
size="extra-small"
color={api.color}
>
{api.route.substring(0, 2)}
</Avatar>
</div>
<div className="flex-1">
<div className="text-sm font-medium text-gray-900 mb-1 !font-bold flex items-center gap-2">
<Tag
prefixIcon={<Gauge size={12} />}
size="small"
color="white"
shape='circle'
onClick={() => handleSpeedTest(api.url)}
className="cursor-pointer hover:opacity-80 text-xs"
>
{t('测速')}
</Tag>
{api.route}
</div>
<div
className="text-xs !text-semi-color-primary font-mono break-all cursor-pointer hover:underline mb-1"
onClick={() => handleCopyUrl(api.url)}
>
{api.url}
</div>
<div className="text-xs text-gray-500">
{api.description}
</div>
</div>
</div>
))
) : (
<div className="flex justify-center items-center py-8">
<Empty
image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
title={t('暂无API信息配置')}
description={t('请联系管理员在系统设置中配置API信息')}
style={{ padding: '12px' }}
/>
</div>
)}
</div>
<div
className="api-info-fade-indicator"
style={{ opacity: showApiScrollHint ? 1 : 0 }}
/>
</div>
</Card>
)}
</div>
</div>
</div>
</Spin>
</Spin>
</div>
</div>
...
...
web/src/pages/Home/index.js
View file @
c821b0ed
...
@@ -4,8 +4,7 @@ import { API, showError, isMobile } from '../../helpers';
...
@@ -4,8 +4,7 @@ import { API, showError, isMobile } from '../../helpers';
import
{
StatusContext
}
from
'../../context/Status'
;
import
{
StatusContext
}
from
'../../context/Status'
;
import
{
marked
}
from
'marked'
;
import
{
marked
}
from
'marked'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
IconGithubLogo
}
from
'@douyinfe/semi-icons'
;
import
{
IconGithubLogo
,
IconPlay
,
IconFile
}
from
'@douyinfe/semi-icons'
;
import
exampleImage
from
'/example.png'
;
import
{
Link
}
from
'react-router-dom'
;
import
{
Link
}
from
'react-router-dom'
;
import
NoticeModal
from
'../../components/layout/NoticeModal'
;
import
NoticeModal
from
'../../components/layout/NoticeModal'
;
import
{
Moonshot
,
OpenAI
,
XAI
,
Zhipu
,
Volcengine
,
Cohere
,
Claude
,
Gemini
,
Suno
,
Minimax
,
Wenxin
,
Spark
,
Qingyan
,
DeepSeek
,
Qwen
,
Midjourney
,
Grok
,
AzureAI
,
Hunyuan
,
Xinference
}
from
'@lobehub/icons'
;
import
{
Moonshot
,
OpenAI
,
XAI
,
Zhipu
,
Volcengine
,
Cohere
,
Claude
,
Gemini
,
Suno
,
Minimax
,
Wenxin
,
Spark
,
Qingyan
,
DeepSeek
,
Qwen
,
Midjourney
,
Grok
,
AzureAI
,
Hunyuan
,
Xinference
}
from
'@lobehub/icons'
;
...
@@ -20,6 +19,7 @@ const Home = () => {
...
@@ -20,6 +19,7 @@ const Home = () => {
const
[
noticeVisible
,
setNoticeVisible
]
=
useState
(
false
);
const
[
noticeVisible
,
setNoticeVisible
]
=
useState
(
false
);
const
isDemoSiteMode
=
statusState
?.
status
?.
demo_site_enabled
||
false
;
const
isDemoSiteMode
=
statusState
?.
status
?.
demo_site_enabled
||
false
;
const
docsLink
=
statusState
?.
status
?.
docs_link
||
''
;
useEffect
(()
=>
{
useEffect
(()
=>
{
const
checkNoticeAndShow
=
async
()
=>
{
const
checkNoticeAndShow
=
async
()
=>
{
...
@@ -85,132 +85,123 @@ const Home = () => {
...
@@ -85,132 +85,123 @@ const Home = () => {
{
homePageContentLoaded
&&
homePageContent
===
''
?
(
{
homePageContentLoaded
&&
homePageContent
===
''
?
(
<
div
className
=
"w-full overflow-x-hidden"
>
<
div
className
=
"w-full overflow-x-hidden"
>
{
/* Banner 部分 */
}
{
/* Banner 部分 */
}
<
div
className
=
"w-full border-b border-semi-color-border min-h-[500px] md:
h-[650px] lg:h-[75
0px] relative overflow-x-hidden"
>
<
div
className
=
"w-full border-b border-semi-color-border min-h-[500px] md:
min-h-[600px] lg:min-h-[70
0px] relative overflow-x-hidden"
>
<
div
className
=
"flex
flex-col md:flex-row items-center justify-center h-full px-4 py-8 md:py-
0"
>
<
div
className
=
"flex
items-center justify-center h-full px-4 py-12 md:py-16 lg:py-2
0"
>
{
/*
左侧
内容区 */
}
{
/*
居中
内容区 */
}
<
div
className
=
"flex
-shrink-0 w-full md:w-[480px] md:mr-[60px] lg:mr-[120px] mb-8 md:mb-0
"
>
<
div
className
=
"flex
flex-col items-center justify-center text-center max-w-4xl mx-auto
"
>
<
div
className
=
"flex
items-center gap-2 justify-center md:justify-start
"
>
<
div
className
=
"flex
flex-col items-center justify-center mb-6 md:mb-8
"
>
<
h1
className
=
"text-3xl md:text-4xl lg:text-5xl
font-semibold text-semi-color-text-0 w-auto leading-normal md:leading-[67px]
"
>
<
h1
className
=
"text-3xl md:text-4xl lg:text-5xl
xl:text-6xl font-semibold text-semi-color-text-0 leading-tight
"
>
{
statusState
?.
status
?.
system_name
||
'New API'
}
{
statusState
?.
status
?.
system_name
||
'New API'
}
<
/h1
>
<
/h1
>
{
statusState
?.
status
?.
version
&&
(
<
Tag
color
=
'light-blue'
size
=
'large'
shape
=
'circle'
className
=
"ml-1"
>
{
statusState
.
status
.
version
}
<
/Tag
>
)}
<
/div
>
<
/div
>
<
p
className
=
"text-base md:text-lg
text-semi-color-text-0 mt-4 md:mt-8 w-full md:w-[480px] leading-7 md:leading-8 text-center md:text-left
"
>
<
p
className
=
"text-base md:text-lg
lg:text-xl text-semi-color-text-0 leading-7 md:leading-8 lg:leading-9 max-w-2xl px-4
"
>
{
t
(
'新一代大模型网关与AI资产管理系统,一键接入主流大模型,轻松管理您的AI资产'
)}
{
t
(
'新一代大模型网关与AI资产管理系统,一键接入主流大模型,轻松管理您的AI资产'
)}
<
/p
>
<
/p
>
{
/* 操作按钮 */
}
{
/* 操作按钮 */
}
<
div
className
=
"mt-
6 md:mt-10 flex flex-wrap gap-4 justify-center md:justify-start
"
>
<
div
className
=
"mt-
8 md:mt-10 lg:mt-12 flex flex-row gap-4 justify-center items-center
"
>
<
Link
to
=
"/console"
>
<
Link
to
=
"/console"
>
<
Button
theme
=
"solid"
type
=
"primary"
size
=
"large"
className
=
"!rounded-3xl
"
>
<
Button
theme
=
"solid"
type
=
"primary"
size
=
"large"
className
=
"!rounded-3xl
px-8 py-2"
icon
=
{
<
IconPlay
/>
}
>
{
t
(
'开始使用'
)}
{
t
(
'开始使用'
)}
<
/Button
>
<
/Button
>
<
/Link
>
<
/Link
>
{
isDemoSiteMode
&&
(
{
isDemoSiteMode
&&
statusState
?.
status
?.
version
?
(
<
Button
<
Button
size
=
"large"
size
=
"large"
className
=
"flex items-center !rounded-3xl"
className
=
"flex items-center !rounded-3xl
px-6 py-2
"
icon
=
{
<
IconGithubLogo
/>
}
icon
=
{
<
IconGithubLogo
/>
}
onClick
=
{()
=>
window
.
open
(
'https://github.com/QuantumNous/new-api'
,
'_blank'
)}
onClick
=
{()
=>
window
.
open
(
'https://github.com/QuantumNous/new-api'
,
'_blank'
)}
>
>
GitHub
{
statusState
.
status
.
version
}
<
/Button
>
<
/Button
>
)
:
(
docsLink
&&
(
<
Button
size
=
"large"
className
=
"flex items-center !rounded-3xl px-6 py-2"
icon
=
{
<
IconFile
/>
}
onClick
=
{()
=>
window
.
open
(
docsLink
,
'_blank'
)}
>
{
t
(
'文档'
)}
<
/Button
>
)
)}
)}
<
/div
>
<
/div
>
{
/* 框架兼容性图标 */
}
{
/* 框架兼容性图标 */
}
<
div
className
=
"mt-
8 md:mt-16
"
>
<
div
className
=
"mt-
12 md:mt-16 lg:mt-20 w-full
"
>
<
div
className
=
"flex items-center mb-
3 justify-center md:justify-start
"
>
<
div
className
=
"flex items-center mb-
6 md:mb-8 justify-center
"
>
<
Text
type
=
"tertiary"
className
=
"text-lg md:text-xl font-light"
>
<
Text
type
=
"tertiary"
className
=
"text-lg md:text-xl
lg:text-2xl
font-light"
>
{
t
(
'支持众多的大模型供应商'
)}
{
t
(
'支持众多的大模型供应商'
)}
<
/Text
>
<
/Text
>
<
/div
>
<
/div
>
<
div
className
=
"flex flex-wrap items-center
relative mt-6 md:mt-8 gap-6 md:gap-8 justify-center md:justify-start
"
>
<
div
className
=
"flex flex-wrap items-center
justify-center gap-3 sm:gap-4 md:gap-6 lg:gap-8 max-w-5xl mx-auto px-4
"
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Moonshot
size
=
{
40
}
/
>
<
Moonshot
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
OpenAI
size
=
{
40
}
/
>
<
OpenAI
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
XAI
size
=
{
40
}
/
>
<
XAI
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Zhipu
.
Color
size
=
{
40
}
/
>
<
Zhipu
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Volcengine
.
Color
size
=
{
40
}
/
>
<
Volcengine
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Cohere
.
Color
size
=
{
40
}
/
>
<
Cohere
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Claude
.
Color
size
=
{
40
}
/
>
<
Claude
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Gemini
.
Color
size
=
{
40
}
/
>
<
Gemini
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Suno
size
=
{
40
}
/
>
<
Suno
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Minimax
.
Color
size
=
{
40
}
/
>
<
Minimax
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Wenxin
.
Color
size
=
{
40
}
/
>
<
Wenxin
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Spark
.
Color
size
=
{
40
}
/
>
<
Spark
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Qingyan
.
Color
size
=
{
40
}
/
>
<
Qingyan
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
DeepSeek
.
Color
size
=
{
40
}
/
>
<
DeepSeek
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Qwen
.
Color
size
=
{
40
}
/
>
<
Qwen
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Midjourney
size
=
{
40
}
/
>
<
Midjourney
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Grok
size
=
{
40
}
/
>
<
Grok
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
AzureAI
.
Color
size
=
{
40
}
/
>
<
AzureAI
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Hunyuan
.
Color
size
=
{
40
}
/
>
<
Hunyuan
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Xinference
.
Color
size
=
{
40
}
/
>
<
Xinference
.
Color
size
=
{
40
}
/
>
<
/div
>
<
/div
>
<
div
className
=
"
relative w-8 md:w-10 h-8 md:h-10
flex items-center justify-center"
>
<
div
className
=
"
w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12
flex items-center justify-center"
>
<
Typography
.
Text
className
=
"!text-
2
xl font-bold"
>
30
+<
/Typography.Text
>
<
Typography
.
Text
className
=
"!text-
lg sm:!text-xl md:!text-2xl lg:!text-3
xl font-bold"
>
30
+<
/Typography.Text
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
{
/* 右侧图片区域 - 在小屏幕上隐藏或调整位置 */
}
<
div
className
=
"flex-shrink-0 relative md:mr-[-200px] lg:mr-[-400px] hidden md:block lg:min-w-[1100px]"
>
<
div
className
=
"absolute w-[320px] md:w-[500px] lg:w-[640px] h-[320px] md:h-[500px] lg:h-[640px] left-[-25px] md:left-[-40px] lg:left-[-50px] top-[-10px] md:top-[-15px] lg:top-[-20px] opacity-60"
style
=
{{
filter
:
'blur(120px)'
}}
>
<
div
className
=
"absolute w-[320px] md:w-[400px] lg:w-[474px] h-[320px] md:h-[400px] lg:h-[474px] top-[80px] md:top-[100px] lg:top-[132px] bg-semi-color-primary rounded-full opacity-30"
><
/div
>
<
div
className
=
"absolute w-[320px] md:w-[400px] lg:w-[474px] h-[320px] md:h-[400px] lg:h-[474px] left-[80px] md:left-[120px] lg:left-[166px] bg-semi-color-tertiary rounded-full opacity-30"
><
/div
>
<
/div
>
<
img
src
=
{
exampleImage
}
alt
=
"application demo"
className
=
"relative h-[400px] md:h-[600px] lg:h-[721px] ml-[-15px] md:ml-[-20px] lg:ml-[-30px] mt-[-15px] md:mt-[-20px] lg:mt-[-30px]"
/>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
...
@@ -223,7 +214,7 @@ const Home = () => {
...
@@ -223,7 +214,7 @@ const Home = () => {
/>
/>
)
:
(
)
:
(
<
div
<
div
className
=
"text-base md:text-lg p-4 md:p-6
overflow-x-hidden
"
className
=
"text-base md:text-lg p-4 md:p-6
lg:p-8 overflow-x-hidden max-w-6xl mx-auto
"
dangerouslySetInnerHTML
=
{{
__html
:
homePageContent
}}
dangerouslySetInnerHTML
=
{{
__html
:
homePageContent
}}
><
/div
>
><
/div
>
)}
)}
...
@@ -234,3 +225,4 @@ const Home = () => {
...
@@ -234,3 +225,4 @@ const Home = () => {
};
};
export
default
Home
;
export
default
Home
;
web/src/pages/Setting/Dashboard/SettingsAPIInfo.js
0 → 100644
View file @
c821b0ed
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
Button
,
Space
,
Table
,
Form
,
Typography
,
Empty
,
Divider
,
Avatar
,
Modal
,
Tag
}
from
'@douyinfe/semi-ui'
;
import
{
IllustrationNoResult
,
IllustrationNoResultDark
}
from
'@douyinfe/semi-illustrations'
;
import
{
Plus
,
Edit
,
Trash2
,
Save
,
Settings
}
from
'lucide-react'
;
import
{
API
,
showError
,
showSuccess
}
from
'../../../helpers'
;
import
{
useTranslation
}
from
'react-i18next'
;
const
{
Text
}
=
Typography
;
const
SettingsAPIInfo
=
({
options
,
refresh
})
=>
{
const
{
t
}
=
useTranslation
();
const
[
apiInfoList
,
setApiInfoList
]
=
useState
([]);
const
[
showApiModal
,
setShowApiModal
]
=
useState
(
false
);
const
[
showDeleteModal
,
setShowDeleteModal
]
=
useState
(
false
);
const
[
deletingApi
,
setDeletingApi
]
=
useState
(
null
);
const
[
editingApi
,
setEditingApi
]
=
useState
(
null
);
const
[
modalLoading
,
setModalLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
hasChanges
,
setHasChanges
]
=
useState
(
false
);
const
[
apiForm
,
setApiForm
]
=
useState
({
url
:
''
,
description
:
''
,
route
:
''
,
color
:
'blue'
});
const
colorOptions
=
[
{
value
:
'blue'
,
label
:
'blue'
},
{
value
:
'green'
,
label
:
'green'
},
{
value
:
'cyan'
,
label
:
'cyan'
},
{
value
:
'purple'
,
label
:
'purple'
},
{
value
:
'pink'
,
label
:
'pink'
},
{
value
:
'red'
,
label
:
'red'
},
{
value
:
'orange'
,
label
:
'orange'
},
{
value
:
'amber'
,
label
:
'amber'
},
{
value
:
'yellow'
,
label
:
'yellow'
},
{
value
:
'lime'
,
label
:
'lime'
},
{
value
:
'light-green'
,
label
:
'light-green'
},
{
value
:
'teal'
,
label
:
'teal'
},
{
value
:
'light-blue'
,
label
:
'light-blue'
},
{
value
:
'indigo'
,
label
:
'indigo'
},
{
value
:
'violet'
,
label
:
'violet'
},
{
value
:
'grey'
,
label
:
'grey'
}
];
const
updateOption
=
async
(
key
,
value
)
=>
{
const
res
=
await
API
.
put
(
'/api/option/'
,
{
key
,
value
,
});
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
showSuccess
(
'API信息已更新'
);
if
(
refresh
)
refresh
();
}
else
{
showError
(
message
);
}
};
const
submitApiInfo
=
async
()
=>
{
try
{
setLoading
(
true
);
const
apiInfoJson
=
JSON
.
stringify
(
apiInfoList
);
await
updateOption
(
'ApiInfo'
,
apiInfoJson
);
setHasChanges
(
false
);
}
catch
(
error
)
{
console
.
error
(
'API信息更新失败'
,
error
);
showError
(
'API信息更新失败'
);
}
finally
{
setLoading
(
false
);
}
};
const
handleAddApi
=
()
=>
{
setEditingApi
(
null
);
setApiForm
({
url
:
''
,
description
:
''
,
route
:
''
,
color
:
'blue'
});
setShowApiModal
(
true
);
};
const
handleEditApi
=
(
api
)
=>
{
setEditingApi
(
api
);
setApiForm
({
url
:
api
.
url
,
description
:
api
.
description
,
route
:
api
.
route
,
color
:
api
.
color
});
setShowApiModal
(
true
);
};
const
handleDeleteApi
=
(
api
)
=>
{
setDeletingApi
(
api
);
setShowDeleteModal
(
true
);
};
const
confirmDeleteApi
=
()
=>
{
if
(
deletingApi
)
{
const
newList
=
apiInfoList
.
filter
(
api
=>
api
.
id
!==
deletingApi
.
id
);
setApiInfoList
(
newList
);
setHasChanges
(
true
);
showSuccess
(
'API信息已删除,请及时点击“保存配置”进行保存'
);
}
setShowDeleteModal
(
false
);
setDeletingApi
(
null
);
};
const
handleSaveApi
=
async
()
=>
{
if
(
!
apiForm
.
url
||
!
apiForm
.
route
||
!
apiForm
.
description
)
{
showError
(
'请填写完整的API信息'
);
return
;
}
try
{
setModalLoading
(
true
);
let
newList
;
if
(
editingApi
)
{
newList
=
apiInfoList
.
map
(
api
=>
api
.
id
===
editingApi
.
id
?
{
...
api
,
...
apiForm
}
:
api
);
}
else
{
const
newId
=
Math
.
max
(...
apiInfoList
.
map
(
api
=>
api
.
id
),
0
)
+
1
;
const
newApi
=
{
id
:
newId
,
...
apiForm
};
newList
=
[...
apiInfoList
,
newApi
];
}
setApiInfoList
(
newList
);
setHasChanges
(
true
);
setShowApiModal
(
false
);
showSuccess
(
editingApi
?
'API信息已更新,请及时点击“保存配置”进行保存'
:
'API信息已添加,请及时点击“保存配置”进行保存'
);
}
catch
(
error
)
{
showError
(
'操作失败: '
+
error
.
message
);
}
finally
{
setModalLoading
(
false
);
}
};
const
parseApiInfo
=
(
apiInfoStr
)
=>
{
if
(
!
apiInfoStr
)
{
setApiInfoList
([]);
return
;
}
try
{
const
parsed
=
JSON
.
parse
(
apiInfoStr
);
setApiInfoList
(
Array
.
isArray
(
parsed
)
?
parsed
:
[]);
}
catch
(
error
)
{
console
.
error
(
'解析API信息失败:'
,
error
);
setApiInfoList
([]);
}
};
useEffect
(()
=>
{
if
(
options
.
ApiInfo
!==
undefined
)
{
parseApiInfo
(
options
.
ApiInfo
);
}
},
[
options
.
ApiInfo
]);
const
columns
=
[
{
title
:
'ID'
,
dataIndex
:
'id'
,
},
{
title
:
t
(
'API地址'
),
dataIndex
:
'url'
,
render
:
(
text
,
record
)
=>
(
<
Tag
color
=
{
record
.
color
}
className
=
"!rounded-full"
style
=
{{
maxWidth
:
'280px'
}}
>
{
text
}
<
/Tag
>
),
},
{
title
:
t
(
'线路描述'
),
dataIndex
:
'route'
,
render
:
(
text
,
record
)
=>
(
<
Tag
shape
=
'circle'
>
{
text
}
<
/Tag
>
),
},
{
title
:
t
(
'说明'
),
dataIndex
:
'description'
,
ellipsis
:
true
,
render
:
(
text
,
record
)
=>
(
<
Tag
shape
=
'circle'
>
{
text
||
'-'
}
<
/Tag
>
),
},
{
title
:
t
(
'颜色'
),
dataIndex
:
'color'
,
render
:
(
color
)
=>
(
<
Avatar
size
=
"extra-extra-small"
color
=
{
color
}
/
>
),
},
{
title
:
t
(
'操作'
),
fixed
:
'right'
,
render
:
(
_
,
record
)
=>
(
<
Space
>
<
Button
icon
=
{
<
Edit
size
=
{
14
}
/>
}
theme
=
'light'
type
=
'tertiary'
size
=
'small'
className
=
"!rounded-full"
onClick
=
{()
=>
handleEditApi
(
record
)}
>
{
t
(
'编辑'
)}
<
/Button
>
<
Button
icon
=
{
<
Trash2
size
=
{
14
}
/>
}
type
=
'danger'
theme
=
'light'
size
=
'small'
className
=
"!rounded-full"
onClick
=
{()
=>
handleDeleteApi
(
record
)}
>
{
t
(
'删除'
)}
<
/Button
>
<
/Space
>
),
},
];
const
renderHeader
=
()
=>
(
<
div
className
=
"flex flex-col w-full"
>
<
div
className
=
"mb-2"
>
<
div
className
=
"flex items-center text-blue-500"
>
<
Settings
size
=
{
16
}
className
=
"mr-2"
/>
<
Text
>
{
t
(
'API信息管理,可以配置多个API地址用于状态展示和负载均衡'
)}
<
/Text
>
<
/div
>
<
/div
>
<
Divider
margin
=
"12px"
/>
<
div
className
=
"flex flex-col md:flex-row justify-between items-center gap-4 w-full"
>
<
div
className
=
"flex gap-2 w-full md:w-auto order-2 md:order-1"
>
<
Button
theme
=
'light'
type
=
'primary'
icon
=
{
<
Plus
size
=
{
14
}
/>
}
className
=
"!rounded-full w-full md:w-auto"
onClick
=
{
handleAddApi
}
>
{
t
(
'添加API'
)}
<
/Button
>
<
Button
icon
=
{
<
Save
size
=
{
14
}
/>
}
onClick
=
{
submitApiInfo
}
loading
=
{
loading
}
disabled
=
{
!
hasChanges
}
type
=
'secondary'
className
=
"!rounded-full w-full md:w-auto"
>
{
t
(
'保存配置'
)}
<
/Button
>
<
/div
>
<
/div
>
<
/div
>
);
return
(
<>
<
Form
.
Section
text
=
{
renderHeader
()}
>
<
Table
columns
=
{
columns
}
dataSource
=
{
apiInfoList
}
scroll
=
{{
x
:
'max-content'
}}
pagination
=
{
false
}
size
=
'middle'
loading
=
{
loading
}
empty
=
{
<
Empty
image
=
{
<
IllustrationNoResult
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
darkModeImage
=
{
<
IllustrationNoResultDark
style
=
{{
width
:
150
,
height
:
150
}}
/>
}
description
=
{
t
(
'暂无API信息'
)}
style
=
{{
padding
:
30
}}
/
>
}
className
=
"rounded-xl overflow-hidden"
/>
<
/Form.Section
>
<
Modal
title
=
{
editingApi
?
t
(
'编辑API'
)
:
t
(
'添加API'
)}
visible
=
{
showApiModal
}
onOk
=
{
handleSaveApi
}
onCancel
=
{()
=>
setShowApiModal
(
false
)}
okText
=
{
t
(
'保存'
)}
cancelText
=
{
t
(
'取消'
)}
className
=
"rounded-xl"
confirmLoading
=
{
modalLoading
}
>
<
Form
layout
=
'vertical'
initValues
=
{
apiForm
}
key
=
{
editingApi
?
editingApi
.
id
:
'new'
}
>
<
Form
.
Input
field
=
'url'
label
=
{
t
(
'API地址'
)}
placeholder
=
'https://api.example.com'
rules
=
{[{
required
:
true
,
message
:
t
(
'请输入API地址'
)
}]}
onChange
=
{(
value
)
=>
setApiForm
({
...
apiForm
,
url
:
value
})}
/
>
<
Form
.
Input
field
=
'route'
label
=
{
t
(
'线路描述'
)}
placeholder
=
{
t
(
'如:香港线路'
)}
rules
=
{[{
required
:
true
,
message
:
t
(
'请输入线路描述'
)
}]}
onChange
=
{(
value
)
=>
setApiForm
({
...
apiForm
,
route
:
value
})}
/
>
<
Form
.
Input
field
=
'description'
label
=
{
t
(
'说明'
)}
placeholder
=
{
t
(
'如:大带宽批量分析图片推荐'
)}
rules
=
{[{
required
:
true
,
message
:
t
(
'请输入说明'
)
}]}
onChange
=
{(
value
)
=>
setApiForm
({
...
apiForm
,
description
:
value
})}
/
>
<
Form
.
Select
field
=
'color'
label
=
{
t
(
'标识颜色'
)}
optionList
=
{
colorOptions
}
onChange
=
{(
value
)
=>
setApiForm
({
...
apiForm
,
color
:
value
})}
render
=
{(
option
)
=>
(
<
div
style
=
{{
display
:
'flex'
,
alignItems
:
'center'
,
gap
:
8
}}
>
<
Avatar
size
=
"extra-extra-small"
color
=
{
option
.
value
}
/
>
{
option
.
label
}
<
/div
>
)}
/
>
<
/Form
>
<
/Modal
>
<
Modal
title
=
{
t
(
'确认删除'
)}
visible
=
{
showDeleteModal
}
onOk
=
{
confirmDeleteApi
}
onCancel
=
{()
=>
{
setShowDeleteModal
(
false
);
setDeletingApi
(
null
);
}}
okText
=
{
t
(
'确认删除'
)}
cancelText
=
{
t
(
'取消'
)}
type
=
"warning"
className
=
"rounded-xl"
okButtonProps
=
{{
type
:
'danger'
,
theme
:
'solid'
}}
>
<
Text
>
{
t
(
'确定要删除此API信息吗?'
)}
<
/Text
>
<
/Modal
>
<
/
>
);
};
export
default
SettingsAPIInfo
;
\ No newline at end of file
web/src/pages/Setting/index.js
View file @
c821b0ed
...
@@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next';
...
@@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next';
import
SystemSetting
from
'../../components/settings/SystemSetting.js'
;
import
SystemSetting
from
'../../components/settings/SystemSetting.js'
;
import
{
isRoot
}
from
'../../helpers'
;
import
{
isRoot
}
from
'../../helpers'
;
import
OtherSetting
from
'../../components/settings/OtherSetting'
;
import
OtherSetting
from
'../../components/settings/OtherSetting'
;
import
PersonalSetting
from
'../../components/settings/PersonalSetting.js'
;
import
OperationSetting
from
'../../components/settings/OperationSetting.js'
;
import
OperationSetting
from
'../../components/settings/OperationSetting.js'
;
import
RateLimitSetting
from
'../../components/settings/RateLimitSetting.js'
;
import
RateLimitSetting
from
'../../components/settings/RateLimitSetting.js'
;
import
ModelSetting
from
'../../components/settings/ModelSetting.js'
;
import
ModelSetting
from
'../../components/settings/ModelSetting.js'
;
import
DashboardSetting
from
'../../components/settings/DashboardSetting.js'
;
const
Setting
=
()
=>
{
const
Setting
=
()
=>
{
const
{
t
}
=
useTranslation
();
const
{
t
}
=
useTranslation
();
...
@@ -44,6 +44,11 @@ const Setting = () => {
...
@@ -44,6 +44,11 @@ const Setting = () => {
content
:
<
OtherSetting
/>
,
content
:
<
OtherSetting
/>
,
itemKey
:
'other'
,
itemKey
:
'other'
,
});
});
panes
.
push
({
tab
:
t
(
'仪表盘配置'
),
content
:
<
DashboardSetting
/>
,
itemKey
:
'dashboard'
,
});
}
}
const
onChangeTab
=
(
key
)
=>
{
const
onChangeTab
=
(
key
)
=>
{
setTabActiveKey
(
key
);
setTabActiveKey
(
key
);
...
...
web/src/pages/Setup/index.js
View file @
c821b0ed
...
@@ -133,7 +133,7 @@ const Setup = () => {
...
@@ -133,7 +133,7 @@ const Setup = () => {
};
};
return
(
return
(
<
div
className
=
"
min-h-screen
bg-gray-50"
>
<
div
className
=
"bg-gray-50"
>
<
Layout
>
<
Layout
>
<
Layout
.
Content
>
<
Layout
.
Content
>
<
div
className
=
"flex justify-center px-4 py-8"
>
<
div
className
=
"flex justify-center px-4 py-8"
>
...
...
web/src/pages/Token/EditToken.js
View file @
c821b0ed
...
@@ -219,9 +219,15 @@ const EditToken = (props) => {
...
@@ -219,9 +219,15 @@ const EditToken = (props) => {
let
successCount
=
0
;
// 记录成功创建的令牌数量
let
successCount
=
0
;
// 记录成功创建的令牌数量
for
(
let
i
=
0
;
i
<
tokenCount
;
i
++
)
{
for
(
let
i
=
0
;
i
<
tokenCount
;
i
++
)
{
let
localInputs
=
{
...
inputs
};
let
localInputs
=
{
...
inputs
};
if
(
i
!==
0
)
{
// 如果用户想要创建多个令牌,则给每个令牌一个序号后缀
// 检查用户是否填写了令牌名称
localInputs
.
name
=
`
${
inputs
.
name
}
-
${
generateRandomSuffix
()}
`
;
const
baseName
=
inputs
.
name
.
trim
()
===
''
?
'default'
:
inputs
.
name
;
if
(
i
!==
0
||
inputs
.
name
.
trim
()
===
''
)
{
// 如果创建多个令牌(i !== 0)或者用户没有填写名称,则添加随机后缀
localInputs
.
name
=
`
${
baseName
}
-
${
generateRandomSuffix
()}
`
;
}
else
{
localInputs
.
name
=
baseName
;
}
}
localInputs
.
remain_quota
=
parseInt
(
localInputs
.
remain_quota
);
localInputs
.
remain_quota
=
parseInt
(
localInputs
.
remain_quota
);
...
...
web/src/pages/TopUp/index.js
View file @
c821b0ed
...
@@ -55,6 +55,7 @@ const TopUp = () => {
...
@@ -55,6 +55,7 @@ const TopUp = () => {
const
[
amountLoading
,
setAmountLoading
]
=
useState
(
false
);
const
[
amountLoading
,
setAmountLoading
]
=
useState
(
false
);
const
[
paymentLoading
,
setPaymentLoading
]
=
useState
(
false
);
const
[
paymentLoading
,
setPaymentLoading
]
=
useState
(
false
);
const
[
confirmLoading
,
setConfirmLoading
]
=
useState
(
false
);
const
[
confirmLoading
,
setConfirmLoading
]
=
useState
(
false
);
const
[
isDarkMode
,
setIsDarkMode
]
=
useState
(
false
);
// 邀请相关状态
// 邀请相关状态
const
[
affLink
,
setAffLink
]
=
useState
(
''
);
const
[
affLink
,
setAffLink
]
=
useState
(
''
);
...
@@ -256,6 +257,32 @@ const TopUp = () => {
...
@@ -256,6 +257,32 @@ const TopUp = () => {
showSuccess
(
t
(
'邀请链接已复制到剪切板'
));
showSuccess
(
t
(
'邀请链接已复制到剪切板'
));
};
};
// 检测暗色模式
useEffect
(()
=>
{
const
checkDarkMode
=
()
=>
{
const
isDark
=
document
.
documentElement
.
classList
.
contains
(
'dark'
)
||
window
.
matchMedia
(
'(prefers-color-scheme: dark)'
).
matches
;
setIsDarkMode
(
isDark
);
};
checkDarkMode
();
// 监听主题变化
const
observer
=
new
MutationObserver
(
checkDarkMode
);
observer
.
observe
(
document
.
documentElement
,
{
attributes
:
true
,
attributeFilter
:
[
'class'
]
});
const
mediaQuery
=
window
.
matchMedia
(
'(prefers-color-scheme: dark)'
);
mediaQuery
.
addListener
(
checkDarkMode
);
return
()
=>
{
observer
.
disconnect
();
mediaQuery
.
removeListener
(
checkDarkMode
);
};
},
[]);
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
userState
?.
user
?.
id
)
{
if
(
userState
?.
user
?.
id
)
{
setUserDataLoading
(
false
);
setUserDataLoading
(
false
);
...
@@ -398,48 +425,45 @@ const TopUp = () => {
...
@@ -398,48 +425,45 @@ const TopUp = () => {
<
div
className
=
"w-full"
>
<
div
className
=
"w-full"
>
<
Card
className
=
"!rounded-2xl shadow-lg border-0"
>
<
Card
className
=
"!rounded-2xl shadow-lg border-0"
>
<
Card
<
Card
className
=
"!rounded-2xl !border-0 !shadow-
2xl
overflow-hidden"
className
=
"!rounded-2xl !border-0 !shadow-
lg
overflow-hidden"
style
=
{{
style
=
{{
background
:
'linear-gradient(135deg, #1e3a8a 0%, #1e40af 25%, #2563eb 50%, #3b82f6 75%, #60a5fa 100%)'
,
background
:
isDarkMode
?
'linear-gradient(135deg, #1e293b 0%, #334155 50%, #475569 100%)'
:
'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%)'
,
position
:
'relative'
position
:
'relative'
}}
}}
bodyStyle
=
{{
padding
:
0
}}
bodyStyle
=
{{
padding
:
0
}}
>
>
<
div
className
=
"absolute inset-0 overflow-hidden"
>
<
div
className
=
"absolute inset-0 overflow-hidden"
>
<
div
className
=
"absolute -top-10 -right-10 w-40 h-40 bg-
white
opacity-5 rounded-full"
><
/div
>
<
div
className
=
"absolute -top-10 -right-10 w-40 h-40 bg-
slate-400
opacity-5 rounded-full"
><
/div
>
<
div
className
=
"absolute -bottom-16 -left-16 w-48 h-48 bg-
white opacity-3
rounded-full"
><
/div
>
<
div
className
=
"absolute -bottom-16 -left-16 w-48 h-48 bg-
slate-300 opacity-8
rounded-full"
><
/div
>
<
div
className
=
"absolute top-1/2 right-1/4 w-24 h-24 bg-
yellow-400 opacity-10
rounded-full"
><
/div
>
<
div
className
=
"absolute top-1/2 right-1/4 w-24 h-24 bg-
slate-400 opacity-6
rounded-full"
><
/div
>
<
/div
>
<
/div
>
<
div
className
=
"relative p-4 sm:p-6 md:p-8
"
style
=
{{
color
:
'white'
}}
>
<
div
className
=
"relative p-4 sm:p-6 md:p-8
text-gray-600 dark:text-gray-300"
>
<
div
className
=
"flex justify-between items-start mb-4 sm:mb-6"
>
<
div
className
=
"flex justify-between items-start mb-4 sm:mb-6"
>
<
div
className
=
"flex-1 min-w-0"
>
<
div
className
=
"flex-1 min-w-0"
>
{
userDataLoading
?
(
{
userDataLoading
?
(
<
Skeleton
.
Title
style
=
{{
width
:
'200px'
,
height
:
'20px'
}}
/
>
<
Skeleton
.
Title
style
=
{{
width
:
'200px'
,
height
:
'20px'
}}
/
>
)
:
(
)
:
(
<
div
className
=
"text-base sm:text-lg font-semibold truncate
"
style
=
{{
color
:
'white'
}}
>
<
div
className
=
"text-base sm:text-lg font-semibold truncate
text-gray-800 dark:text-gray-100"
>
{
t
(
'尊敬的'
)}
{
getUsername
()}
{
t
(
'尊敬的'
)}
{
getUsername
()}
<
/div
>
<
/div
>
)}
)}
<
/div
>
<
/div
>
<
div
<
div
className
=
"w-10 h-10 sm:w-12 sm:h-12 rounded-lg flex items-center justify-center shadow-md flex-shrink-0 ml-2 bg-slate-400 dark:bg-slate-500"
>
className
=
"w-10 h-10 sm:w-12 sm:h-12 rounded-lg flex items-center justify-center shadow-lg flex-shrink-0 ml-2"
<
IconCreditCard
size
=
"default"
className
=
"text-white"
/>
style
=
{{
background
:
`linear-gradient(135deg,
${
stringToColor
(
getUsername
())}
0%, #f59e0b 100%)`
}}
>
<
IconCreditCard
size
=
"default"
style
=
{{
color
:
'white'
}}
/
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
className
=
"mb-4 sm:mb-6"
>
<
div
className
=
"mb-4 sm:mb-6"
>
<
div
className
=
"text-xs sm:text-sm mb-1 sm:mb-2
"
style
=
{{
color
:
'rgba(255, 255, 255, 0.7)'
}}
>
<
div
className
=
"text-xs sm:text-sm mb-1 sm:mb-2
text-gray-500 dark:text-gray-400"
>
{
t
(
'当前余额'
)}
{
t
(
'当前余额'
)}
<
/div
>
<
/div
>
{
userDataLoading
?
(
{
userDataLoading
?
(
<
Skeleton
.
Title
style
=
{{
width
:
'180px'
,
height
:
'32px'
}}
/
>
<
Skeleton
.
Title
style
=
{{
width
:
'180px'
,
height
:
'32px'
}}
/
>
)
:
(
)
:
(
<
div
className
=
"text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide
"
style
=
{{
color
:
'white'
}}
>
<
div
className
=
"text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide
text-gray-900 dark:text-gray-100"
>
{
renderQuota
(
userState
?.
user
?.
quota
||
userQuota
)}
{
renderQuota
(
userState
?.
user
?.
quota
||
userQuota
)}
<
/div
>
<
/div
>
)}
)}
...
@@ -448,37 +472,37 @@ const TopUp = () => {
...
@@ -448,37 +472,37 @@ const TopUp = () => {
<
div
className
=
"flex flex-col sm:flex-row sm:justify-between sm:items-end"
>
<
div
className
=
"flex flex-col sm:flex-row sm:justify-between sm:items-end"
>
<
div
className
=
"grid grid-cols-3 gap-2 sm:flex sm:space-x-6 lg:space-x-8 mb-3 sm:mb-0"
>
<
div
className
=
"grid grid-cols-3 gap-2 sm:flex sm:space-x-6 lg:space-x-8 mb-3 sm:mb-0"
>
<
div
className
=
"text-center sm:text-left"
>
<
div
className
=
"text-center sm:text-left"
>
<
div
className
=
"text-xs
"
style
=
{{
color
:
'rgba(255, 255, 255, 0.6)'
}}
>
<
div
className
=
"text-xs
text-gray-400 dark:text-gray-500"
>
{
t
(
'历史消耗'
)}
{
t
(
'历史消耗'
)}
<
/div
>
<
/div
>
{
userDataLoading
?
(
{
userDataLoading
?
(
<
Skeleton
.
Title
style
=
{{
width
:
'60px'
,
height
:
'14px'
}}
/
>
<
Skeleton
.
Title
style
=
{{
width
:
'60px'
,
height
:
'14px'
}}
/
>
)
:
(
)
:
(
<
div
className
=
"text-xs sm:text-sm font-medium truncate
"
style
=
{{
color
:
'white'
}}
>
<
div
className
=
"text-xs sm:text-sm font-medium truncate
text-gray-600 dark:text-gray-300"
>
{
renderQuota
(
userState
?.
user
?.
used_quota
||
0
)}
{
renderQuota
(
userState
?.
user
?.
used_quota
||
0
)}
<
/div
>
<
/div
>
)}
)}
<
/div
>
<
/div
>
<
div
className
=
"text-center sm:text-left"
>
<
div
className
=
"text-center sm:text-left"
>
<
div
className
=
"text-xs
"
style
=
{{
color
:
'rgba(255, 255, 255, 0.6)'
}}
>
<
div
className
=
"text-xs
text-gray-400 dark:text-gray-500"
>
{
t
(
'用户分组'
)}
{
t
(
'用户分组'
)}
<
/div
>
<
/div
>
{
userDataLoading
?
(
{
userDataLoading
?
(
<
Skeleton
.
Title
style
=
{{
width
:
'50px'
,
height
:
'14px'
}}
/
>
<
Skeleton
.
Title
style
=
{{
width
:
'50px'
,
height
:
'14px'
}}
/
>
)
:
(
)
:
(
<
div
className
=
"text-xs sm:text-sm font-medium truncate
"
style
=
{{
color
:
'white'
}}
>
<
div
className
=
"text-xs sm:text-sm font-medium truncate
text-gray-600 dark:text-gray-300"
>
{
userState
?.
user
?.
group
||
t
(
'默认'
)}
{
userState
?.
user
?.
group
||
t
(
'默认'
)}
<
/div
>
<
/div
>
)}
)}
<
/div
>
<
/div
>
<
div
className
=
"text-center sm:text-left"
>
<
div
className
=
"text-center sm:text-left"
>
<
div
className
=
"text-xs
"
style
=
{{
color
:
'rgba(255, 255, 255, 0.6)'
}}
>
<
div
className
=
"text-xs
text-gray-400 dark:text-gray-500"
>
{
t
(
'用户角色'
)}
{
t
(
'用户角色'
)}
<
/div
>
<
/div
>
{
userDataLoading
?
(
{
userDataLoading
?
(
<
Skeleton
.
Title
style
=
{{
width
:
'60px'
,
height
:
'14px'
}}
/
>
<
Skeleton
.
Title
style
=
{{
width
:
'60px'
,
height
:
'14px'
}}
/
>
)
:
(
)
:
(
<
div
className
=
"text-xs sm:text-sm font-medium truncate
"
style
=
{{
color
:
'white'
}}
>
<
div
className
=
"text-xs sm:text-sm font-medium truncate
text-gray-600 dark:text-gray-300"
>
{
getUserRole
()}
{
getUserRole
()}
<
/div
>
<
/div
>
)}
)}
...
@@ -489,32 +513,187 @@ const TopUp = () => {
...
@@ -489,32 +513,187 @@ const TopUp = () => {
{
userDataLoading
?
(
{
userDataLoading
?
(
<
Skeleton
.
Title
style
=
{{
width
:
'50px'
,
height
:
'24px'
}}
/
>
<
Skeleton
.
Title
style
=
{{
width
:
'50px'
,
height
:
'24px'
}}
/
>
)
:
(
)
:
(
<
div
<
div
className
=
"px-2 py-1 sm:px-3 rounded-md text-xs sm:text-sm font-medium inline-block bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-600"
>
className
=
"px-2 py-1 sm:px-3 rounded-md text-xs sm:text-sm font-medium inline-block"
style
=
{{
backgroundColor
:
'rgba(255, 255, 255, 0.2)'
,
color
:
'white'
,
backdropFilter
:
'blur(10px)'
}}
>
ID
:
{
userState
?.
user
?.
id
||
'---'
}
ID
:
{
userState
?.
user
?.
id
||
'---'
}
<
/div
>
<
/div
>
)}
)}
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
className
=
"absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-
yellow-400 via-orange-400 to-red-400"
style
=
{{
opacity
:
0.6
}}
><
/div
>
<
div
className
=
"absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-
slate-300 via-slate-400 to-slate-500 dark:from-slate-600 dark:via-slate-500 dark:to-slate-400 opacity-40"
><
/div
>
<
/div
>
<
/div
>
<
/Card
>
<
/Card
>
<
div
className
=
"p-6"
>
<
div
className
=
"p-6"
>
<
div
className
=
"grid grid-cols-1 lg:grid-cols-3 gap-8"
>
<
div
className
=
"grid grid-cols-1 lg:grid-cols-3 gap-8"
>
{
/* 邀请信息部分 */
}
{
/* 左侧:在线充值和兑换余额 */
}
<
div
>
<
div
className
=
"lg:col-span-2 space-y-8"
>
{
/* 在线充值部分 */
}
<
div
>
<
div
className
=
"flex items-center mb-6"
>
<
div
className
=
"w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-4"
>
<
IconPlus
size
=
"large"
className
=
"text-slate-600 dark:text-slate-300"
/>
<
/div
>
<
div
>
<
Text
className
=
"text-xl font-semibold"
>
{
t
(
'在线充值'
)}
<
/Text
>
<
div
className
=
"text-gray-500 text-sm"
>
{
t
(
'支持多种支付方式'
)}
<
/div
>
<
/div
>
<
/div
>
<
div
className
=
"space-y-4"
>
<
div
>
<
div
className
=
"flex justify-between mb-2"
>
<
Text
strong
>
{
t
(
'充值数量'
)}
<
/Text
>
{
amountLoading
?
(
<
Skeleton
.
Title
style
=
{{
width
:
'80px'
,
height
:
'14px'
}}
/
>
)
:
(
<
Text
type
=
"tertiary"
>
{
t
(
'实付金额:'
)
+
' '
+
renderAmount
()}
<
/Text
>
)}
<
/div
>
<
InputNumber
disabled
=
{
!
enableOnlineTopUp
}
placeholder
=
{
t
(
'充值数量,最低 '
)
+
renderQuotaWithAmount
(
minTopUp
)
}
value
=
{
topUpCount
}
min
=
{
minTopUp
}
max
=
{
999999999
}
step
=
{
1
}
precision
=
{
0
}
onChange
=
{
async
(
value
)
=>
{
if
(
value
&&
value
>=
1
)
{
setTopUpCount
(
value
);
await
getAmount
(
value
);
}
}}
onBlur
=
{(
e
)
=>
{
const
value
=
parseInt
(
e
.
target
.
value
);
if
(
!
value
||
value
<
1
)
{
setTopUpCount
(
1
);
getAmount
(
1
);
}
}}
size
=
"large"
className
=
"!rounded-lg w-full"
prefix
=
{
<
IconCreditCard
/>
}
formatter
=
{(
value
)
=>
value
?
`
${
value
}
`
:
''
}
parser
=
{(
value
)
=>
value
?
parseInt
(
value
.
replace
(
/
[^\d]
/g
,
''
))
:
0
}
/
>
<
/div
>
<
div
className
=
"grid grid-cols-1 sm:grid-cols-2 gap-4"
>
<
Button
type
=
"primary"
theme
=
"solid"
onClick
=
{
async
()
=>
{
preTopUp
(
'zfb'
);
}}
size
=
"large"
className
=
"!rounded-lg !bg-slate-600 hover:!bg-slate-700 h-14"
disabled
=
{
!
enableOnlineTopUp
}
loading
=
{
paymentLoading
}
icon
=
{
<
SiAlipay
size
=
{
20
}
/>
}
>
<
span
className
=
"ml-2"
>
{
t
(
'支付宝'
)}
<
/span
>
<
/Button
>
<
Button
type
=
"primary"
theme
=
"solid"
onClick
=
{
async
()
=>
{
preTopUp
(
'wx'
);
}}
size
=
"large"
className
=
"!rounded-lg !bg-slate-600 hover:!bg-slate-700 h-14"
disabled
=
{
!
enableOnlineTopUp
}
loading
=
{
paymentLoading
}
icon
=
{
<
SiWechat
size
=
{
20
}
/>
}
>
<
span
className
=
"ml-2"
>
{
t
(
'微信'
)}
<
/span
>
<
/Button
>
<
/div
>
{
!
enableOnlineTopUp
&&
(
<
Banner
fullMode
=
{
false
}
type
=
"warning"
icon
=
{
null
}
closeIcon
=
{
null
}
className
=
"!rounded-lg"
title
=
{
<
div
style
=
{{
fontWeight
:
600
,
fontSize
:
'14px'
,
lineHeight
:
'20px'
}}
>
{
t
(
'在线充值功能未开启'
)}
<
/div
>
}
description
=
{
<
div
>
{
t
(
'管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。'
)}
<
/div
>
}
/
>
)}
<
/div
>
<
/div
>
{
/* 兑换余额部分 */
}
<
div
>
<
div
className
=
"flex items-center mb-6"
>
<
div
className
=
"w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-4"
>
<
IconGift
size
=
"large"
className
=
"text-slate-600 dark:text-slate-300"
/>
<
/div
>
<
div
>
<
Text
className
=
"text-xl font-semibold"
>
{
t
(
'兑换余额'
)}
<
/Text
>
<
div
className
=
"text-gray-500 text-sm"
>
{
t
(
'使用兑换码充值余额'
)}
<
/div
>
<
/div
>
<
/div
>
<
div
className
=
"space-y-4"
>
<
div
>
<
Text
strong
className
=
"block mb-2"
>
{
t
(
'兑换码'
)}
<
/Text
>
<
Input
placeholder
=
{
t
(
'请输入兑换码'
)}
value
=
{
redemptionCode
}
onChange
=
{(
value
)
=>
setRedemptionCode
(
value
)}
size
=
"large"
className
=
"!rounded-lg"
prefix
=
{
<
IconGift
/>
}
/
>
<
/div
>
<
div
className
=
"flex flex-col sm:flex-row gap-3"
>
{
topUpLink
&&
(
<
Button
type
=
"primary"
theme
=
"solid"
onClick
=
{
openTopUpLink
}
size
=
"large"
className
=
"!rounded-lg flex-1"
icon
=
{
<
IconLink
/>
}
>
{
t
(
'获取兑换码'
)}
<
/Button
>
)}
<
Button
type
=
"warning"
theme
=
"solid"
onClick
=
{
topUp
}
disabled
=
{
isSubmitting
}
loading
=
{
isSubmitting
}
size
=
"large"
className
=
"!rounded-lg flex-1"
>
{
isSubmitting
?
t
(
'兑换中...'
)
:
t
(
'兑换'
)}
<
/Button
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
{
/* 右侧:邀请信息部分 */
}
<
div
className
=
"lg:col-span-1"
>
<
div
className
=
"flex items-center justify-between mb-6"
>
<
div
className
=
"flex items-center justify-between mb-6"
>
<
div
className
=
"flex items-center"
>
<
div
className
=
"flex items-center"
>
<
div
className
=
"w-12 h-12 rounded-full bg-
orange-5
0 flex items-center justify-center mr-4"
>
<
div
className
=
"w-12 h-12 rounded-full bg-
slate-100 dark:bg-slate-70
0 flex items-center justify-center mr-4"
>
<
IconLink
size
=
"large"
className
=
"text-
orange-5
00"
/>
<
IconLink
size
=
"large"
className
=
"text-
slate-600 dark:text-slate-3
00"
/>
<
/div
>
<
/div
>
<
div
>
<
div
>
<
div
className
=
"flex items-center gap-3"
>
<
div
className
=
"flex items-center gap-3"
>
...
@@ -524,7 +703,7 @@ const TopUp = () => {
...
@@ -524,7 +703,7 @@ const TopUp = () => {
theme
=
"solid"
theme
=
"solid"
onClick
=
{()
=>
setOpenTransfer
(
true
)}
onClick
=
{()
=>
setOpenTransfer
(
true
)}
size
=
"small"
size
=
"small"
className
=
"!rounded-lg !bg-
blue-500 hover:!bg-blue-6
00"
className
=
"!rounded-lg !bg-
slate-600 hover:!bg-slate-7
00"
icon
=
{
<
IconCreditCard
/>
}
icon
=
{
<
IconCreditCard
/>
}
>
>
{
t
(
'划转'
)}
{
t
(
'划转'
)}
...
@@ -536,7 +715,7 @@ const TopUp = () => {
...
@@ -536,7 +715,7 @@ const TopUp = () => {
<
/div
>
<
/div
>
<
div
className
=
"space-y-4"
>
<
div
className
=
"space-y-4"
>
<
div
className
=
"grid grid-cols-1
sm:grid-cols-3
gap-3"
>
<
div
className
=
"grid grid-cols-1 gap-3"
>
<
Card
<
Card
className
=
"!rounded-2xl text-center"
className
=
"!rounded-2xl text-center"
bodyStyle
=
{{
padding
:
'16px'
}}
bodyStyle
=
{{
padding
:
'16px'
}}
...
@@ -546,7 +725,6 @@ const TopUp = () => {
...
@@ -546,7 +725,6 @@ const TopUp = () => {
<
div
className
=
"text-gray-900 text-lg font-bold mt-1"
>
<
div
className
=
"text-gray-900 text-lg font-bold mt-1"
>
{
renderQuota
(
userState
?.
user
?.
aff_quota
)}
{
renderQuota
(
userState
?.
user
?.
aff_quota
)}
<
/div
>
<
/div
>
<
/Card
>
<
/Card
>
<
Card
<
Card
className
=
"!rounded-2xl text-center"
className
=
"!rounded-2xl text-center"
...
@@ -583,162 +761,6 @@ const TopUp = () => {
...
@@ -583,162 +761,6 @@ const TopUp = () => {
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
>
<
div
className
=
"flex items-center mb-6"
>
<
div
className
=
"w-12 h-12 rounded-full bg-green-50 flex items-center justify-center mr-4"
>
<
IconGift
size
=
"large"
className
=
"text-green-500"
/>
<
/div
>
<
div
>
<
Text
className
=
"text-xl font-semibold"
>
{
t
(
'兑换余额'
)}
<
/Text
>
<
div
className
=
"text-gray-500 text-sm"
>
{
t
(
'使用兑换码充值余额'
)}
<
/div
>
<
/div
>
<
/div
>
<
div
className
=
"space-y-4"
>
<
div
>
<
Text
strong
className
=
"block mb-2"
>
{
t
(
'兑换码'
)}
<
/Text
>
<
Input
placeholder
=
{
t
(
'请输入兑换码'
)}
value
=
{
redemptionCode
}
onChange
=
{(
value
)
=>
setRedemptionCode
(
value
)}
size
=
"large"
className
=
"!rounded-lg"
prefix
=
{
<
IconGift
/>
}
/
>
<
/div
>
<
div
className
=
"flex flex-col sm:flex-row gap-3"
>
{
topUpLink
&&
(
<
Button
type
=
"primary"
theme
=
"solid"
onClick
=
{
openTopUpLink
}
size
=
"large"
className
=
"!rounded-lg flex-1"
icon
=
{
<
IconLink
/>
}
>
{
t
(
'获取兑换码'
)}
<
/Button
>
)}
<
Button
type
=
"warning"
theme
=
"solid"
onClick
=
{
topUp
}
disabled
=
{
isSubmitting
}
loading
=
{
isSubmitting
}
size
=
"large"
className
=
"!rounded-lg flex-1"
>
{
isSubmitting
?
t
(
'兑换中...'
)
:
t
(
'兑换'
)}
<
/Button
>
<
/div
>
<
/div
>
<
/div
>
<
div
>
<
div
className
=
"flex items-center mb-6"
>
<
div
className
=
"w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center mr-4"
>
<
IconPlus
size
=
"large"
className
=
"text-blue-500"
/>
<
/div
>
<
div
>
<
Text
className
=
"text-xl font-semibold"
>
{
t
(
'在线充值'
)}
<
/Text
>
<
div
className
=
"text-gray-500 text-sm"
>
{
t
(
'支持多种支付方式'
)}
<
/div
>
<
/div
>
<
/div
>
<
div
className
=
"space-y-4"
>
<
div
>
<
div
className
=
"flex justify-between mb-2"
>
<
Text
strong
>
{
t
(
'充值数量'
)}
<
/Text
>
{
amountLoading
?
(
<
Skeleton
.
Title
style
=
{{
width
:
'80px'
,
height
:
'14px'
}}
/
>
)
:
(
<
Text
type
=
"tertiary"
>
{
t
(
'实付金额:'
)
+
' '
+
renderAmount
()}
<
/Text
>
)}
<
/div
>
<
InputNumber
disabled
=
{
!
enableOnlineTopUp
}
placeholder
=
{
t
(
'充值数量,最低 '
)
+
renderQuotaWithAmount
(
minTopUp
)
}
value
=
{
topUpCount
}
min
=
{
minTopUp
}
max
=
{
999999999
}
step
=
{
1
}
precision
=
{
0
}
onChange
=
{
async
(
value
)
=>
{
if
(
value
&&
value
>=
1
)
{
setTopUpCount
(
value
);
await
getAmount
(
value
);
}
}}
onBlur
=
{(
e
)
=>
{
const
value
=
parseInt
(
e
.
target
.
value
);
if
(
!
value
||
value
<
1
)
{
setTopUpCount
(
1
);
getAmount
(
1
);
}
}}
size
=
"large"
className
=
"!rounded-lg w-full"
prefix
=
{
<
IconCreditCard
/>
}
formatter
=
{(
value
)
=>
value
?
`
${
value
}
`
:
''
}
parser
=
{(
value
)
=>
value
?
parseInt
(
value
.
replace
(
/
[^\d]
/g
,
''
))
:
0
}
/
>
<
/div
>
<
div
className
=
"grid grid-cols-1 sm:grid-cols-2 gap-4"
>
<
Button
type
=
"primary"
theme
=
"solid"
onClick
=
{
async
()
=>
{
preTopUp
(
'zfb'
);
}}
size
=
"large"
className
=
"!rounded-lg !bg-blue-500 hover:!bg-blue-600 h-14"
disabled
=
{
!
enableOnlineTopUp
}
loading
=
{
paymentLoading
}
icon
=
{
<
SiAlipay
size
=
{
20
}
/>
}
>
<
span
className
=
"ml-2"
>
{
t
(
'支付宝'
)}
<
/span
>
<
/Button
>
<
Button
type
=
"primary"
theme
=
"solid"
onClick
=
{
async
()
=>
{
preTopUp
(
'wx'
);
}}
size
=
"large"
className
=
"!rounded-lg !bg-green-500 hover:!bg-green-600 h-14"
disabled
=
{
!
enableOnlineTopUp
}
loading
=
{
paymentLoading
}
icon
=
{
<
SiWechat
size
=
{
20
}
/>
}
>
<
span
className
=
"ml-2"
>
{
t
(
'微信'
)}
<
/span
>
<
/Button
>
<
/div
>
{
!
enableOnlineTopUp
&&
(
<
Banner
fullMode
=
{
false
}
type
=
"warning"
icon
=
{
null
}
closeIcon
=
{
null
}
className
=
"!rounded-lg"
title
=
{
<
div
style
=
{{
fontWeight
:
600
,
fontSize
:
'14px'
,
lineHeight
:
'20px'
}}
>
{
t
(
'在线充值功能未开启'
)}
<
/div
>
}
description
=
{
<
div
>
{
t
(
'管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。'
)}
<
/div
>
}
/
>
)}
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/Card
>
<
/Card
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment