Merge branch 'alpha' into fix-balance-unit-sync
This commit is contained in:
@@ -92,12 +92,12 @@ func RedisDel(key string) error {
|
|||||||
return RDB.Del(ctx, key).Err()
|
return RDB.Del(ctx, key).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func RedisHDelObj(key string) error {
|
func RedisDelKey(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.HDel(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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
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"`
|
||||||
@@ -23,7 +26,7 @@ type ClaudeMediaMessage struct {
|
|||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,17 +84,13 @@ 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 {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -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 c.Content.(string)
|
return ""
|
||||||
}
|
}
|
||||||
|
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 ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ type GeneralOpenAIRequest struct {
|
|||||||
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"`
|
||||||
@@ -50,10 +50,10 @@ type GeneralOpenAIRequest struct {
|
|||||||
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"`
|
||||||
@@ -108,7 +108,7 @@ 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"`
|
||||||
@@ -116,7 +116,7 @@ type Message struct {
|
|||||||
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,22 +132,51 @@ type MediaContent struct {
|
|||||||
|
|
||||||
func (m *MediaContent) GetImageMedia() *MessageImageUrl {
|
func (m *MediaContent) GetImageMedia() *MessageImageUrl {
|
||||||
if m.ImageUrl != nil {
|
if m.ImageUrl != nil {
|
||||||
|
if _, ok := m.ImageUrl.(*MessageImageUrl); ok {
|
||||||
return m.ImageUrl.(*MessageImageUrl)
|
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 {
|
||||||
|
if _, ok := m.InputAudio.(*MessageInputAudio); ok {
|
||||||
return m.InputAudio.(*MessageInputAudio)
|
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 {
|
||||||
|
if _, ok := m.File.(*MessageFile); ok {
|
||||||
return m.File.(*MessageFile)
|
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) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
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"`
|
||||||
|
|||||||
2
makefile
2
makefile
@@ -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..."
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.RedisHDelObj(fmt.Sprintf("token:%s", key))
|
err := common.RedisDelKey(fmt.Sprintf("token:%s", key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
|||||||
if !common.RedisEnabled {
|
if !common.RedisEnabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return common.RedisHDelObj(getUserCacheKey(userId))
|
return common.RedisDelKey(getUserCacheKey(userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateUserCache updates all user cache fields using hash
|
// updateUserCache updates all user cache fields using hash
|
||||||
|
|||||||
@@ -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: content,
|
Content: response.Output.Text,
|
||||||
},
|
},
|
||||||
FinishReason: response.Output.FinishReason,
|
FinishReason: response.Output.FinishReason,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: content,
|
Content: response.Result,
|
||||||
},
|
},
|
||||||
FinishReason: "stop",
|
FinishReason: "stop",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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\nHuman: %s", message.Content)
|
prompt += fmt.Sprintf("\n\nHuman: %s", message.StringContent())
|
||||||
} else if message.Role == "assistant" {
|
} else if message.Role == "assistant" {
|
||||||
prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content)
|
prompt += fmt.Sprintf("\n\nAssistant: %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
|
|||||||
}
|
}
|
||||||
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
|
|||||||
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),
|
||||||
|
|||||||
@@ -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: content, Role: "assistant"},
|
Message: dto.Message{Content: cohereResp.Text, Role: "assistant"},
|
||||||
FinishReason: stopReasonCohere2OpenAI(cohereResp.FinishReason),
|
FinishReason: stopReasonCohere2OpenAI(cohereResp.FinishReason),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|||||||
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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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: candidate.Content,
|
||||||
},
|
},
|
||||||
FinishReason: "stop",
|
FinishReason: "stop",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: content,
|
Content: response.Choices[0].Messages.Content,
|
||||||
},
|
},
|
||||||
FinishReason: response.Choices[0].FinishReason,
|
FinishReason: response.Choices[0].FinishReason,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: content,
|
Content: response.Payload.Choices.Text[0].Content,
|
||||||
},
|
},
|
||||||
FinishReason: constant.FinishReasonStop,
|
FinishReason: constant.FinishReasonStop,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: "",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -261,16 +261,20 @@ func CountTokenClaudeMessages(messages []dto.ClaudeMessage, model string, stream
|
|||||||
//}
|
//}
|
||||||
tokenNum += 1000
|
tokenNum += 1000
|
||||||
case "tool_use":
|
case "tool_use":
|
||||||
|
if mediaMessage.Input != nil {
|
||||||
tokenNum += getTokenNum(tokenEncoder, mediaMessage.Name)
|
tokenNum += getTokenNum(tokenEncoder, mediaMessage.Name)
|
||||||
inputJSON, _ := json.Marshal(mediaMessage.Input)
|
inputJSON, _ := json.Marshal(mediaMessage.Input)
|
||||||
tokenNum += getTokenNum(tokenEncoder, string(inputJSON))
|
tokenNum += getTokenNum(tokenEncoder, string(inputJSON))
|
||||||
|
}
|
||||||
case "tool_result":
|
case "tool_result":
|
||||||
|
if mediaMessage.Content != nil {
|
||||||
contentJSON, _ := json.Marshal(mediaMessage.Content)
|
contentJSON, _ := json.Marshal(mediaMessage.Content)
|
||||||
tokenNum += getTokenNum(tokenEncoder, string(contentJSON))
|
tokenNum += getTokenNum(tokenEncoder, string(contentJSON))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add a constant for message formatting (this may need adjustment based on Claude's exact formatting)
|
// Add a constant for message formatting (this may need adjustment based on Claude's exact formatting)
|
||||||
tokenNum += len(messages) * 2 // Assuming 2 tokens per message for formatting
|
tokenNum += len(messages) * 2 // Assuming 2 tokens per message for formatting
|
||||||
@@ -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)
|
||||||
|
|||||||
124
setting/api_info.go
Normal file
124
setting/api_info.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 550 KiB |
@@ -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 = () => {
|
|||||||
<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 = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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()}
|
||||||
|
|||||||
@@ -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 = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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">
|
||||||
|
|||||||
@@ -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 = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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">
|
||||||
|
|||||||
@@ -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 = () => {
|
|||||||
<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 = () => {
|
|||||||
<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 = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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()}
|
||||||
|
|||||||
57
web/src/components/settings/DashboardSetting.js
Normal file
57
web/src/components/settings/DashboardSetting.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
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;
|
||||||
@@ -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 = () => {
|
|||||||
<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 = () => {
|
|||||||
<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 = () => {
|
|||||||
>
|
>
|
||||||
<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-50 flex items-center justify-center mr-3">
|
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||||
<Settings size={20} className="text-purple-500" />
|
<Settings size={20} className="text-slate-600 dark:text-slate-300" />
|
||||||
</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 = () => {
|
|||||||
</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);
|
||||||
@@ -737,8 +738,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-red-50 flex items-center justify-center mr-3">
|
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||||
<IconMail size="default" className="text-red-500" />
|
<IconMail size="default" className="text-slate-600 dark:text-slate-300" />
|
||||||
</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 = () => {
|
|||||||
>
|
>
|
||||||
<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-50 flex items-center justify-center mr-3">
|
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||||
<SiWechat size={20} className="text-green-500" />
|
<SiWechat size={20} className="text-slate-600 dark:text-slate-300" />
|
||||||
</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 = () => {
|
|||||||
>
|
>
|
||||||
<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-100 flex items-center justify-center mr-3">
|
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||||
<IconGithubLogo size="default" className="text-gray-700" />
|
<IconGithubLogo size="default" className="text-slate-600 dark:text-slate-300" />
|
||||||
</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 = () => {
|
|||||||
>
|
>
|
||||||
<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-50 flex items-center justify-center mr-3">
|
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||||
<IconShield size="default" className="text-indigo-500" />
|
<IconShield size="default" className="text-slate-600 dark:text-slate-300" />
|
||||||
</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 = () => {
|
|||||||
>
|
>
|
||||||
<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-50 flex items-center justify-center mr-3">
|
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||||
<SiTelegram size={20} className="text-blue-500" />
|
<SiTelegram size={20} className="text-slate-600 dark:text-slate-300" />
|
||||||
</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 = () => {
|
|||||||
>
|
>
|
||||||
<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-50 flex items-center justify-center mr-3">
|
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||||
<SiLinux size={20} className="text-orange-500" />
|
<SiLinux size={20} className="text-slate-600 dark:text-slate-300" />
|
||||||
</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 = () => {
|
|||||||
>
|
>
|
||||||
<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-50 flex items-center justify-center mr-4 flex-shrink-0">
|
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
<IconKey size="large" className="text-blue-500" />
|
<IconKey size="large" className="text-slate-600" />
|
||||||
</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 = () => {
|
|||||||
type="primary"
|
type="primary"
|
||||||
theme="solid"
|
theme="solid"
|
||||||
onClick={generateAccessToken}
|
onClick={generateAccessToken}
|
||||||
className="!rounded-lg !bg-blue-500 hover:!bg-blue-600 w-full sm:w-auto"
|
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
|
||||||
icon={<IconKey />}
|
icon={<IconKey />}
|
||||||
>
|
>
|
||||||
{systemToken ? t('重新生成') : t('生成令牌')}
|
{systemToken ? t('重新生成') : t('生成令牌')}
|
||||||
@@ -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-50 flex items-center justify-center mr-4 flex-shrink-0">
|
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
<IconLock size="large" className="text-orange-500" />
|
<IconLock size="large" className="text-slate-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title heading={6} className="mb-1">
|
<Typography.Title heading={6} className="mb-1">
|
||||||
@@ -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-600 w-full sm:w-auto"
|
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
|
||||||
icon={<IconLock />}
|
icon={<IconLock />}
|
||||||
>
|
>
|
||||||
{t('修改密码')}
|
{t('修改密码')}
|
||||||
@@ -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-50 flex items-center justify-center mr-4 flex-shrink-0">
|
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
<IconDelete size="large" className="text-red-500" />
|
<IconDelete size="large" className="text-slate-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title heading={6} className="mb-1 text-red-600">
|
<Typography.Title heading={6} className="mb-1 text-slate-700">
|
||||||
{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 = () => {
|
|||||||
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 = () => {
|
|||||||
>
|
>
|
||||||
<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-500" />
|
<IconMail className="mr-2 text-slate-600" />
|
||||||
<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 = () => {
|
|||||||
</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-500" />
|
<Webhook size={16} className="mr-2 text-slate-600" />
|
||||||
<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 = () => {
|
|||||||
</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-800">
|
<Typography.Text strong className="text-slate-700">
|
||||||
{t('Webhook请求结构')}
|
{t('Webhook请求结构')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -1256,8 +1257,8 @@ const PersonalSetting = () => {
|
|||||||
<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-50 flex items-center justify-center mt-1">
|
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center mt-1">
|
||||||
<Shield size={20} className="text-orange-500" />
|
<Shield size={20} className="text-slate-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -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-600"
|
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
||||||
icon={<IconSetting />}
|
icon={<IconSetting />}
|
||||||
>
|
>
|
||||||
{t('保存设置')}
|
{t('保存设置')}
|
||||||
@@ -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-600"
|
className="!rounded-lg w-full !bg-slate-600 hover:!bg-slate-700"
|
||||||
icon={<SiWechat size={16} />}
|
icon={<SiWechat size={16} />}
|
||||||
>
|
>
|
||||||
{t('绑定')}
|
{t('绑定')}
|
||||||
|
|||||||
@@ -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 {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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 = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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 = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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 = () => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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 = () => {
|
|||||||
<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 = () => {
|
|||||||
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 = () => {
|
|||||||
} 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 = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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 = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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 = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: COLUMN_KEYS.OPERATE,
|
|
||||||
title: '',
|
title: '',
|
||||||
dataIndex: 'operate',
|
dataIndex: 'operate',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
@@ -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 = () => {
|
|||||||
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 = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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,18 +856,27 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchChannels = async (
|
// 获取表单值的辅助函数,确保所有值都是字符串
|
||||||
searchKeyword,
|
const getFormValues = () => {
|
||||||
searchGroup,
|
const formValues = formApi ? formApi.getValues() : {};
|
||||||
searchModel,
|
return {
|
||||||
enableTagMode,
|
searchKeyword: formValues.searchKeyword || '',
|
||||||
) => {
|
searchGroup: formValues.searchGroup || '',
|
||||||
|
searchModel: formValues.searchModel || '',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchChannels = async (enableTagMode) => {
|
||||||
|
const { searchKeyword, searchGroup, searchModel } = getFormValues();
|
||||||
|
|
||||||
|
setSearching(true);
|
||||||
|
try {
|
||||||
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
|
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
|
||||||
await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
|
await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
|
||||||
// setActivePage(1);
|
// setActivePage(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSearching(true);
|
|
||||||
const res = await API.get(
|
const res = await API.get(
|
||||||
`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}`,
|
`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}`,
|
||||||
);
|
);
|
||||||
@@ -1032,7 +887,9 @@ const ChannelsTable = () => {
|
|||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
setSearching(false);
|
setSearching(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateChannelProperty = (channelId, updateFn) => {
|
const updateChannelProperty = (channelId, updateFn) => {
|
||||||
@@ -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">
|
||||||
|
<Form
|
||||||
|
initValues={formInitValues}
|
||||||
|
getFormApi={(api) => setFormApi(api)}
|
||||||
|
onSubmit={() => searchChannels(enableTagMode)}
|
||||||
|
allowEmpty={true}
|
||||||
|
autoComplete="off"
|
||||||
|
layout="horizontal"
|
||||||
|
trigger="change"
|
||||||
|
stopValidateWithError={false}
|
||||||
|
className="flex flex-col md:flex-row items-center gap-4 w-full"
|
||||||
|
>
|
||||||
<div className="relative w-full md:w-64">
|
<div className="relative w-full md:w-64">
|
||||||
<Input
|
<Form.Input
|
||||||
|
field="searchKeyword"
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('搜索渠道的 ID,名称,密钥和API地址 ...')}
|
placeholder={t('搜索渠道的 ID,名称,密钥和API地址 ...')}
|
||||||
value={searchKeyword}
|
|
||||||
loading={searching}
|
|
||||||
onChange={(v) => {
|
|
||||||
setSearchKeyword(v.trim());
|
|
||||||
}}
|
|
||||||
className="!rounded-full"
|
className="!rounded-full"
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full md:w-48">
|
<div className="w-full md:w-48">
|
||||||
<Input
|
<Form.Input
|
||||||
|
field="searchModel"
|
||||||
prefix={<IconFilter />}
|
prefix={<IconFilter />}
|
||||||
placeholder={t('模型关键字')}
|
placeholder={t('模型关键字')}
|
||||||
value={searchModel}
|
|
||||||
loading={searching}
|
|
||||||
onChange={(v) => {
|
|
||||||
setSearchModel(v.trim());
|
|
||||||
}}
|
|
||||||
className="!rounded-full"
|
className="!rounded-full"
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full md:w-48">
|
<div className="w-full md:w-48">
|
||||||
<Select
|
<Form.Select
|
||||||
|
field="searchGroup"
|
||||||
placeholder={t('选择分组')}
|
placeholder={t('选择分组')}
|
||||||
optionList={[
|
optionList={[
|
||||||
{ label: t('选择分组'), value: null },
|
{ label: t('选择分组'), value: null },
|
||||||
...groupOptions,
|
...groupOptions,
|
||||||
]}
|
]}
|
||||||
value={searchGroup}
|
|
||||||
onChange={(v) => {
|
|
||||||
setSearchGroup(v);
|
|
||||||
searchChannels(searchKeyword, v, searchModel, enableTagMode);
|
|
||||||
}}
|
|
||||||
className="!rounded-full w-full"
|
className="!rounded-full w-full"
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
|
onChange={() => {
|
||||||
|
// 延迟执行搜索,让表单值先更新
|
||||||
|
setTimeout(() => {
|
||||||
|
searchChannels(enableTagMode);
|
||||||
|
}, 0);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => {
|
htmlType="submit"
|
||||||
searchChannels(searchKeyword, searchGroup, searchModel, enableTagMode);
|
loading={loading || searching}
|
||||||
}}
|
|
||||||
loading={searching}
|
|
||||||
className="!rounded-full w-full md:w-auto"
|
className="!rounded-full w-full md:w-auto"
|
||||||
>
|
>
|
||||||
{t('查询')}
|
{t('查询')}
|
||||||
</Button>
|
</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 = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderColumnSelector()}
|
|
||||||
<EditTagModal
|
<EditTagModal
|
||||||
visible={showEditTag}
|
visible={showEditTag}
|
||||||
tag={editingTag}
|
tag={editingTag}
|
||||||
@@ -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 = () => {
|
|||||||
}
|
}
|
||||||
: 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}
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
CreditCard,
|
||||||
|
ShoppingCart,
|
||||||
|
Settings,
|
||||||
|
Server,
|
||||||
|
AlertTriangle,
|
||||||
|
HelpCircle,
|
||||||
|
Zap,
|
||||||
|
Play,
|
||||||
|
Clock,
|
||||||
|
Hash,
|
||||||
|
Key
|
||||||
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
API,
|
API,
|
||||||
copy,
|
copy,
|
||||||
@@ -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 {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
|
let url = `/api/log/self/stat?type=${currentLogType}&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 = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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=${logType}&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=${currentLogType}&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 = () => {
|
|||||||
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=${logType}&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=${currentLogType}&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=${logType}&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=${currentLogType}&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 = () => {
|
|||||||
|
|
||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
.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,103 +1210,131 @@ const LogsTable = () => {
|
|||||||
<Divider margin='12px' />
|
<Divider margin='12px' />
|
||||||
|
|
||||||
{/* 搜索表单区域 */}
|
{/* 搜索表单区域 */}
|
||||||
|
<Form
|
||||||
|
initValues={formInitValues}
|
||||||
|
getFormApi={(api) => setFormApi(api)}
|
||||||
|
onSubmit={refresh}
|
||||||
|
allowEmpty={true}
|
||||||
|
autoComplete="off"
|
||||||
|
layout="vertical"
|
||||||
|
trigger="change"
|
||||||
|
stopValidateWithError={false}
|
||||||
|
>
|
||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-4'>
|
||||||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'>
|
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'>
|
||||||
{/* 时间选择器 */}
|
{/* 时间选择器 */}
|
||||||
<div className='col-span-1 lg:col-span-2'>
|
<div className='col-span-1 lg:col-span-2'>
|
||||||
<DatePicker
|
<Form.DatePicker
|
||||||
|
field='dateRange'
|
||||||
className='w-full'
|
className='w-full'
|
||||||
value={[start_timestamp, end_timestamp]}
|
|
||||||
type='dateTimeRange'
|
type='dateTimeRange'
|
||||||
onChange={(value) => {
|
placeholder={[t('开始时间'), t('结束时间')]}
|
||||||
if (Array.isArray(value) && value.length === 2) {
|
showClear
|
||||||
handleInputChange(value[0], 'start_timestamp');
|
pure
|
||||||
handleInputChange(value[1], 'end_timestamp');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 日志类型选择器 */}
|
|
||||||
<Select
|
|
||||||
value={logType.toString()}
|
|
||||||
placeholder={t('日志类型')}
|
|
||||||
className='!rounded-full'
|
|
||||||
onChange={(value) => {
|
|
||||||
setLogType(parseInt(value));
|
|
||||||
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>
|
|
||||||
|
|
||||||
{/* 其他搜索字段 */}
|
{/* 其他搜索字段 */}
|
||||||
<Input
|
<Form.Input
|
||||||
|
field='token_name'
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('令牌名称')}
|
placeholder={t('令牌名称')}
|
||||||
value={token_name}
|
|
||||||
onChange={(value) => handleInputChange(value, 'token_name')}
|
|
||||||
className='!rounded-full'
|
className='!rounded-full'
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Form.Input
|
||||||
|
field='model_name'
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('模型名称')}
|
placeholder={t('模型名称')}
|
||||||
value={model_name}
|
|
||||||
onChange={(value) => handleInputChange(value, 'model_name')}
|
|
||||||
className='!rounded-full'
|
className='!rounded-full'
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Form.Input
|
||||||
|
field='group'
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('分组')}
|
placeholder={t('分组')}
|
||||||
value={group}
|
|
||||||
onChange={(value) => handleInputChange(value, 'group')}
|
|
||||||
className='!rounded-full'
|
className='!rounded-full'
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isAdminUser && (
|
{isAdminUser && (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Form.Input
|
||||||
|
field='channel'
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('渠道 ID')}
|
placeholder={t('渠道 ID')}
|
||||||
value={channel}
|
|
||||||
onChange={(value) => handleInputChange(value, 'channel')}
|
|
||||||
className='!rounded-full'
|
className='!rounded-full'
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
<Input
|
<Form.Input
|
||||||
|
field='username'
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('用户名称')}
|
placeholder={t('用户名称')}
|
||||||
value={username}
|
|
||||||
onChange={(value) => handleInputChange(value, 'username')}
|
|
||||||
className='!rounded-full'
|
className='!rounded-full'
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 操作按钮区域 */}
|
{/* 操作按钮区域 */}
|
||||||
<div className='flex justify-between items-center pt-2'>
|
<div className='flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3'>
|
||||||
<div></div>
|
{/* 日志类型选择器 */}
|
||||||
<div className='flex gap-2'>
|
<div className='w-full sm:w-auto'>
|
||||||
|
<Form.Select
|
||||||
|
field='logType'
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div className='flex gap-2 w-full sm:w-auto justify-end'>
|
||||||
<Button
|
<Button
|
||||||
type='primary'
|
type='primary'
|
||||||
onClick={refresh}
|
htmlType='submit'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
className='!rounded-full'
|
className='!rounded-full'
|
||||||
>
|
>
|
||||||
{t('查询')}
|
{t('查询')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
theme='light'
|
||||||
|
onClick={() => {
|
||||||
|
if (formApi) {
|
||||||
|
formApi.reset();
|
||||||
|
setLogType(0);
|
||||||
|
// 重置后立即查询,使用setTimeout确保表单重置完成
|
||||||
|
setTimeout(() => {
|
||||||
|
refresh();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className='!rounded-full'
|
||||||
|
>
|
||||||
|
{t('重置')}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
theme='light'
|
theme='light'
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
@@ -1258,6 +1347,7 @@ const LogsTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
shadows='always'
|
shadows='always'
|
||||||
@@ -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}} 条', {
|
||||||
|
|||||||
@@ -1,35 +1,65 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
Palette,
|
||||||
|
ZoomIn,
|
||||||
|
Shuffle,
|
||||||
|
Move,
|
||||||
|
FileText,
|
||||||
|
Blend,
|
||||||
|
Upload,
|
||||||
|
Minimize2,
|
||||||
|
RotateCcw,
|
||||||
|
PaintBucket,
|
||||||
|
Focus,
|
||||||
|
Move3D,
|
||||||
|
Monitor,
|
||||||
|
UserCheck,
|
||||||
|
HelpCircle,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
Copy,
|
||||||
|
FileX,
|
||||||
|
Pause,
|
||||||
|
XCircle,
|
||||||
|
Loader,
|
||||||
|
AlertCircle,
|
||||||
|
Hash
|
||||||
|
} from 'lucide-react';
|
||||||
import {
|
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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: '200px' }}
|
style={{ minWidth: '160px' }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -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 = () => {
|
|||||||
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 = () => {
|
|||||||
// 定义模态框图片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 = () => {
|
|||||||
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 = () => {
|
|||||||
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,58 +842,80 @@ const LogsTable = () => {
|
|||||||
<Divider margin="12px" />
|
<Divider margin="12px" />
|
||||||
|
|
||||||
{/* 搜索表单区域 */}
|
{/* 搜索表单区域 */}
|
||||||
|
<Form
|
||||||
|
initValues={formInitValues}
|
||||||
|
getFormApi={(api) => setFormApi(api)}
|
||||||
|
onSubmit={refresh}
|
||||||
|
allowEmpty={true}
|
||||||
|
autoComplete="off"
|
||||||
|
layout="vertical"
|
||||||
|
trigger="change"
|
||||||
|
stopValidateWithError={false}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{/* 时间选择器 */}
|
{/* 时间选择器 */}
|
||||||
<div className="col-span-1 lg:col-span-2">
|
<div className="col-span-1 lg:col-span-2">
|
||||||
<DatePicker
|
<Form.DatePicker
|
||||||
|
field='dateRange'
|
||||||
className="w-full"
|
className="w-full"
|
||||||
value={[start_timestamp, end_timestamp]}
|
|
||||||
type='dateTimeRange'
|
type='dateTimeRange'
|
||||||
onChange={(value) => {
|
placeholder={[t('开始时间'), t('结束时间')]}
|
||||||
if (Array.isArray(value) && value.length === 2) {
|
showClear
|
||||||
handleInputChange(value[0], 'start_timestamp');
|
pure
|
||||||
handleInputChange(value[1], 'end_timestamp');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 任务 ID */}
|
{/* 任务 ID */}
|
||||||
<Input
|
<Form.Input
|
||||||
|
field='mj_id'
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('任务 ID')}
|
placeholder={t('任务 ID')}
|
||||||
value={mj_id}
|
|
||||||
onChange={(value) => handleInputChange(value, 'mj_id')}
|
|
||||||
className="!rounded-full"
|
className="!rounded-full"
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 渠道 ID - 仅管理员可见 */}
|
{/* 渠道 ID - 仅管理员可见 */}
|
||||||
{isAdminUser && (
|
{isAdminUser && (
|
||||||
<Input
|
<Form.Input
|
||||||
|
field='channel_id'
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
{/* 操作按钮区域 */}
|
{/* 操作按钮区域 */}
|
||||||
<div className="flex justify-between items-center pt-2">
|
<div className="flex justify-between items-center">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
type='primary'
|
type='primary'
|
||||||
onClick={refresh}
|
htmlType='submit'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
className="!rounded-full"
|
className="!rounded-full"
|
||||||
>
|
>
|
||||||
{t('查询')}
|
{t('查询')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
theme='light'
|
||||||
|
onClick={() => {
|
||||||
|
if (formApi) {
|
||||||
|
formApi.reset();
|
||||||
|
// 重置后立即查询,使用setTimeout确保表单重置完成
|
||||||
|
setTimeout(() => {
|
||||||
|
refresh();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="!rounded-full"
|
||||||
|
>
|
||||||
|
{t('重置')}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
theme='light'
|
theme='light'
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
@@ -852,6 +928,7 @@ const LogsTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
shadows='always'
|
shadows='always'
|
||||||
@@ -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}} 条', {
|
||||||
|
|||||||
@@ -17,14 +17,19 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
TabPane,
|
TabPane,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
Empty
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
IllustrationNoResult,
|
||||||
|
IllustrationNoResultDark
|
||||||
|
} from '@douyinfe/semi-illustrations';
|
||||||
import {
|
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 = () => {
|
|||||||
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,
|
||||||
|
|||||||
@@ -8,20 +8,33 @@ 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 {
|
||||||
|
IllustrationNoResult,
|
||||||
|
IllustrationNoResultDark
|
||||||
|
} from '@douyinfe/semi-illustrations';
|
||||||
import {
|
import {
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
@@ -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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
});
|
});
|
||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
|
|
||||||
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,29 +494,60 @@ 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
|
||||||
|
initValues={formInitValues}
|
||||||
|
getFormApi={(api) => setFormApi(api)}
|
||||||
|
onSubmit={() => {
|
||||||
|
setActivePage(1);
|
||||||
|
searchRedemptions(null, 1, pageSize);
|
||||||
|
}}
|
||||||
|
allowEmpty={true}
|
||||||
|
autoComplete="off"
|
||||||
|
layout="horizontal"
|
||||||
|
trigger="change"
|
||||||
|
stopValidateWithError={false}
|
||||||
|
className="w-full md:w-auto order-1 md:order-2"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
|
||||||
<div className="relative w-full md:w-64">
|
<div className="relative w-full md:w-64">
|
||||||
<Input
|
<Form.Input
|
||||||
|
field="searchKeyword"
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('关键字(id或者名称)')}
|
placeholder={t('关键字(id或者名称)')}
|
||||||
value={searchKeyword}
|
|
||||||
onChange={handleKeywordChange}
|
|
||||||
className="!rounded-full"
|
className="!rounded-full"
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-2 w-full md:w-auto">
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => {
|
htmlType="submit"
|
||||||
searchRedemptions(searchKeyword, 1, pageSize).then();
|
loading={loading || searching}
|
||||||
}}
|
className="!rounded-full flex-1 md:flex-initial md:w-auto"
|
||||||
loading={searching}
|
|
||||||
className="!rounded-full w-full md:w-auto"
|
|
||||||
>
|
>
|
||||||
{t('查询')}
|
{t('查询')}
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -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 = () => {
|
|||||||
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>
|
||||||
|
|||||||
@@ -1,34 +1,51 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
Music,
|
||||||
|
FileText,
|
||||||
|
HelpCircle,
|
||||||
|
CheckCircle,
|
||||||
|
Pause,
|
||||||
|
Clock,
|
||||||
|
Play,
|
||||||
|
XCircle,
|
||||||
|
Loader,
|
||||||
|
List,
|
||||||
|
Hash
|
||||||
|
} from 'lucide-react';
|
||||||
import {
|
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) {
|
|||||||
|
|
||||||
// 返回带有样式的颜色标签
|
// 返回带有样式的颜色标签
|
||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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: '200px' }}
|
style={{ minWidth: '160px' }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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;
|
],
|
||||||
|
};
|
||||||
|
|
||||||
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(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 = () => {
|
|||||||
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 = () => {
|
|||||||
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,58 +669,80 @@ const LogsTable = () => {
|
|||||||
<Divider margin="12px" />
|
<Divider margin="12px" />
|
||||||
|
|
||||||
{/* 搜索表单区域 */}
|
{/* 搜索表单区域 */}
|
||||||
|
<Form
|
||||||
|
initValues={formInitValues}
|
||||||
|
getFormApi={(api) => setFormApi(api)}
|
||||||
|
onSubmit={refresh}
|
||||||
|
allowEmpty={true}
|
||||||
|
autoComplete="off"
|
||||||
|
layout="vertical"
|
||||||
|
trigger="change"
|
||||||
|
stopValidateWithError={false}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{/* 时间选择器 */}
|
{/* 时间选择器 */}
|
||||||
<div className="col-span-1 lg:col-span-2">
|
<div className="col-span-1 lg:col-span-2">
|
||||||
<DatePicker
|
<Form.DatePicker
|
||||||
|
field='dateRange'
|
||||||
className="w-full"
|
className="w-full"
|
||||||
value={[start_timestamp, end_timestamp]}
|
|
||||||
type='dateTimeRange'
|
type='dateTimeRange'
|
||||||
onChange={(value) => {
|
placeholder={[t('开始时间'), t('结束时间')]}
|
||||||
if (Array.isArray(value) && value.length === 2) {
|
showClear
|
||||||
handleInputChange(value[0], 'start_timestamp');
|
pure
|
||||||
handleInputChange(value[1], 'end_timestamp');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 任务 ID */}
|
{/* 任务 ID */}
|
||||||
<Input
|
<Form.Input
|
||||||
|
field='task_id'
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('任务 ID')}
|
placeholder={t('任务 ID')}
|
||||||
value={task_id}
|
|
||||||
onChange={(value) => handleInputChange(value, 'task_id')}
|
|
||||||
className="!rounded-full"
|
className="!rounded-full"
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 渠道 ID - 仅管理员可见 */}
|
{/* 渠道 ID - 仅管理员可见 */}
|
||||||
{isAdminUser && (
|
{isAdminUser && (
|
||||||
<Input
|
<Form.Input
|
||||||
|
field='channel_id'
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
{/* 操作按钮区域 */}
|
{/* 操作按钮区域 */}
|
||||||
<div className="flex justify-between items-center pt-2">
|
<div className="flex justify-between items-center">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
type='primary'
|
type='primary'
|
||||||
onClick={refresh}
|
htmlType='submit'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
className="!rounded-full"
|
className="!rounded-full"
|
||||||
>
|
>
|
||||||
{t('查询')}
|
{t('查询')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
theme='light'
|
||||||
|
onClick={() => {
|
||||||
|
if (formApi) {
|
||||||
|
formApi.reset();
|
||||||
|
// 重置后立即查询,使用setTimeout确保表单重置完成
|
||||||
|
setTimeout(() => {
|
||||||
|
refresh();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="!rounded-full"
|
||||||
|
>
|
||||||
|
{t('重置')}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
theme='light'
|
theme='light'
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
@@ -692,6 +755,7 @@ const LogsTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
shadows='always'
|
shadows='always'
|
||||||
@@ -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}} 条', {
|
||||||
|
|||||||
@@ -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 {
|
|||||||
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 {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadTokens(0)
|
loadTokens(0)
|
||||||
.then()
|
.then()
|
||||||
@@ -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 = () => {
|
|||||||
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,37 +627,66 @@ 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
|
||||||
|
initValues={formInitValues}
|
||||||
|
getFormApi={(api) => setFormApi(api)}
|
||||||
|
onSubmit={searchTokens}
|
||||||
|
allowEmpty={true}
|
||||||
|
autoComplete="off"
|
||||||
|
layout="horizontal"
|
||||||
|
trigger="change"
|
||||||
|
stopValidateWithError={false}
|
||||||
|
className="w-full md:w-auto order-1 md:order-2"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
|
||||||
<div className="relative w-full md:w-56">
|
<div className="relative w-full md:w-56">
|
||||||
<Input
|
<Form.Input
|
||||||
|
field="searchKeyword"
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('搜索关键字')}
|
placeholder={t('搜索关键字')}
|
||||||
value={searchKeyword}
|
|
||||||
onChange={handleKeywordChange}
|
|
||||||
className="!rounded-full"
|
className="!rounded-full"
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative w-full md:w-56">
|
<div className="relative w-full md:w-56">
|
||||||
<Input
|
<Form.Input
|
||||||
|
field="searchToken"
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('密钥')}
|
placeholder={t('密钥')}
|
||||||
value={searchToken}
|
|
||||||
onChange={handleSearchTokenChange}
|
|
||||||
className="!rounded-full"
|
className="!rounded-full"
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-2 w-full md:w-auto">
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={searchTokens}
|
htmlType="submit"
|
||||||
loading={searching}
|
loading={searching}
|
||||||
className="!rounded-full w-full md:w-auto"
|
className="!rounded-full flex-1 md:flex-initial md:w-auto"
|
||||||
>
|
>
|
||||||
{t('查询')}
|
{t('查询')}
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,18 +1,37 @@
|
|||||||
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 {
|
||||||
|
IllustrationNoResult,
|
||||||
|
IllustrationNoResultDark
|
||||||
|
} from '@douyinfe/semi-illustrations';
|
||||||
import {
|
import {
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
@@ -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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
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 = () => {
|
|||||||
|
|
||||||
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,42 +528,77 @@ 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
|
||||||
|
initValues={formInitValues}
|
||||||
|
getFormApi={(api) => setFormApi(api)}
|
||||||
|
onSubmit={() => {
|
||||||
|
setActivePage(1);
|
||||||
|
searchUsers(1, pageSize);
|
||||||
|
}}
|
||||||
|
allowEmpty={true}
|
||||||
|
autoComplete="off"
|
||||||
|
layout="horizontal"
|
||||||
|
trigger="change"
|
||||||
|
stopValidateWithError={false}
|
||||||
|
className="w-full md:w-auto order-1 md:order-2"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
|
||||||
<div className="relative w-full md:w-64">
|
<div className="relative w-full md:w-64">
|
||||||
<Input
|
<Form.Input
|
||||||
|
field="searchKeyword"
|
||||||
prefix={<IconSearch />}
|
prefix={<IconSearch />}
|
||||||
placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
|
placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
|
||||||
value={searchKeyword}
|
|
||||||
onChange={handleKeywordChange}
|
|
||||||
className="!rounded-full"
|
className="!rounded-full"
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full md:w-48">
|
<div className="w-full md:w-48">
|
||||||
<Select
|
<Form.Select
|
||||||
|
field="searchGroup"
|
||||||
placeholder={t('选择分组')}
|
placeholder={t('选择分组')}
|
||||||
optionList={groupOptions}
|
optionList={groupOptions}
|
||||||
value={searchGroup}
|
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSearchGroup(value);
|
// 分组变化时自动搜索
|
||||||
searchUsers(activePage, pageSize, searchKeyword, value);
|
setTimeout(() => {
|
||||||
|
setActivePage(1);
|
||||||
|
searchUsers(1, pageSize);
|
||||||
|
}, 100);
|
||||||
}}
|
}}
|
||||||
className="!rounded-full w-full"
|
className="!rounded-full w-full"
|
||||||
showClear
|
showClear
|
||||||
|
pure
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-2 w-full md:w-auto">
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => {
|
htmlType="submit"
|
||||||
searchUsers(activePage, pageSize, searchKeyword, searchGroup);
|
loading={loading || searching}
|
||||||
}}
|
className="!rounded-full flex-1 md:flex-initial md:w-auto"
|
||||||
loading={searching}
|
|
||||||
className="!rounded-full w-full md:w-auto"
|
|
||||||
>
|
>
|
||||||
{t('查询')}
|
{t('查询')}
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 {
|
|||||||
User,
|
User,
|
||||||
Settings,
|
Settings,
|
||||||
CircleUser,
|
CircleUser,
|
||||||
|
Users
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// 侧边栏图标颜色映射
|
// 侧边栏图标颜色映射
|
||||||
@@ -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) {
|
|||||||
showSuccess(i18next.t('已复制:') + group);
|
showSuccess(i18next.t('已复制:') + group);
|
||||||
} else {
|
} else {
|
||||||
Modal.error({
|
Modal.error({
|
||||||
title: t('无法复制到剪贴板,请手动复制'),
|
title: i18next.t('无法复制到剪贴板,请手动复制'),
|
||||||
content: group,
|
content: group,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 隐藏模型设置区域的滚动条 */
|
/* 隐藏模型设置区域的滚动条 */
|
||||||
|
.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 {
|
|||||||
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,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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';
|
||||||
|
|||||||
@@ -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) => {
|
|||||||
});
|
});
|
||||||
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) => {
|
|||||||
<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) => {
|
|||||||
/>
|
/>
|
||||||
<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('不更改')}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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 = () => {
|
|||||||
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 = () => {
|
|||||||
{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-[750px] relative overflow-x-hidden">
|
<div className="w-full border-b border-semi-color-border min-h-[500px] md:min-h-[600px] lg:min-h-[700px] 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-20">
|
||||||
{/* 左侧内容区 */}
|
{/* 居中内容区 */}
|
||||||
<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-2xl font-bold">30+</Typography.Text>
|
<Typography.Text className="!text-lg sm:!text-xl md:!text-2xl lg:!text-3xl 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 = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<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 = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default Home;
|
export default Home;
|
||||||
|
|
||||||
|
|||||||
399
web/src/pages/Setting/Dashboard/SettingsAPIInfo.js
Normal file
399
web/src/pages/Setting/Dashboard/SettingsAPIInfo.js
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
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;
|
||||||
@@ -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 = () => {
|
|||||||
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);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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 = () => {
|
|||||||
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 = () => {
|
|||||||
<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 = () => {
|
|||||||
<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,156 +513,26 @@ 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 className="flex items-center justify-between mb-6">
|
{/* 在线充值部分 */}
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="w-12 h-12 rounded-full bg-orange-50 flex items-center justify-center mr-4">
|
|
||||||
<IconLink size="large" className="text-orange-500" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Text className="text-xl font-semibold">{t('邀请信息')}</Text>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
theme="solid"
|
|
||||||
onClick={() => setOpenTransfer(true)}
|
|
||||||
size="small"
|
|
||||||
className="!rounded-lg !bg-blue-500 hover:!bg-blue-600"
|
|
||||||
icon={<IconCreditCard />}
|
|
||||||
>
|
|
||||||
{t('划转')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-500 text-sm">{t('管理您的邀请链接和收益')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
||||||
<Card
|
|
||||||
className="!rounded-2xl text-center"
|
|
||||||
bodyStyle={{ padding: '16px' }}
|
|
||||||
shadows='hover'
|
|
||||||
>
|
|
||||||
<div className="text-gray-600 text-xs font-medium">{t('待使用收益')}</div>
|
|
||||||
<div className="text-gray-900 text-lg font-bold mt-1">
|
|
||||||
{renderQuota(userState?.user?.aff_quota)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</Card>
|
|
||||||
<Card
|
|
||||||
className="!rounded-2xl text-center"
|
|
||||||
bodyStyle={{ padding: '16px' }}
|
|
||||||
shadows='hover'
|
|
||||||
>
|
|
||||||
<div className="text-gray-600 text-xs font-medium">{t('总收益')}</div>
|
|
||||||
<div className="text-gray-900 text-lg font-bold mt-1">
|
|
||||||
{renderQuota(userState?.user?.aff_history_quota)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card
|
|
||||||
className="!rounded-2xl text-center"
|
|
||||||
bodyStyle={{ padding: '16px' }}
|
|
||||||
shadows='hover'
|
|
||||||
>
|
|
||||||
<div className="text-gray-600 text-xs font-medium">{t('邀请人数')}</div>
|
|
||||||
<div className="text-gray-900 text-lg font-bold mt-1">
|
|
||||||
{userState?.user?.aff_count || 0}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg p-3">
|
|
||||||
<Typography.Text strong className="block mb-2 text-sm">{t('邀请链接')}</Typography.Text>
|
|
||||||
<Input
|
|
||||||
value={affLink}
|
|
||||||
onClick={handleAffLinkClick}
|
|
||||||
readOnly
|
|
||||||
size="large"
|
|
||||||
className="!rounded-lg"
|
|
||||||
prefix={<IconLink />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center mb-6">
|
<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">
|
<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-green-500" />
|
<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>
|
|
||||||
<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>
|
||||||
<div>
|
<div>
|
||||||
<Text className="text-xl font-semibold">{t('在线充值')}</Text>
|
<Text className="text-xl font-semibold">{t('在线充值')}</Text>
|
||||||
@@ -695,7 +589,7 @@ const TopUp = () => {
|
|||||||
preTopUp('zfb');
|
preTopUp('zfb');
|
||||||
}}
|
}}
|
||||||
size="large"
|
size="large"
|
||||||
className="!rounded-lg !bg-blue-500 hover:!bg-blue-600 h-14"
|
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 h-14"
|
||||||
disabled={!enableOnlineTopUp}
|
disabled={!enableOnlineTopUp}
|
||||||
loading={paymentLoading}
|
loading={paymentLoading}
|
||||||
icon={<SiAlipay size={20} />}
|
icon={<SiAlipay size={20} />}
|
||||||
@@ -709,7 +603,7 @@ const TopUp = () => {
|
|||||||
preTopUp('wx');
|
preTopUp('wx');
|
||||||
}}
|
}}
|
||||||
size="large"
|
size="large"
|
||||||
className="!rounded-lg !bg-green-500 hover:!bg-green-600 h-14"
|
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 h-14"
|
||||||
disabled={!enableOnlineTopUp}
|
disabled={!enableOnlineTopUp}
|
||||||
loading={paymentLoading}
|
loading={paymentLoading}
|
||||||
icon={<SiWechat size={20} />}
|
icon={<SiWechat size={20} />}
|
||||||
@@ -739,6 +633,134 @@ const TopUp = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-4">
|
||||||
|
<IconLink size="large" className="text-slate-600 dark:text-slate-300" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Text className="text-xl font-semibold">{t('邀请信息')}</Text>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
theme="solid"
|
||||||
|
onClick={() => setOpenTransfer(true)}
|
||||||
|
size="small"
|
||||||
|
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
||||||
|
icon={<IconCreditCard />}
|
||||||
|
>
|
||||||
|
{t('划转')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-500 text-sm">{t('管理您的邀请链接和收益')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 gap-3">
|
||||||
|
<Card
|
||||||
|
className="!rounded-2xl text-center"
|
||||||
|
bodyStyle={{ padding: '16px' }}
|
||||||
|
shadows='hover'
|
||||||
|
>
|
||||||
|
<div className="text-gray-600 text-xs font-medium">{t('待使用收益')}</div>
|
||||||
|
<div className="text-gray-900 text-lg font-bold mt-1">
|
||||||
|
{renderQuota(userState?.user?.aff_quota)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
className="!rounded-2xl text-center"
|
||||||
|
bodyStyle={{ padding: '16px' }}
|
||||||
|
shadows='hover'
|
||||||
|
>
|
||||||
|
<div className="text-gray-600 text-xs font-medium">{t('总收益')}</div>
|
||||||
|
<div className="text-gray-900 text-lg font-bold mt-1">
|
||||||
|
{renderQuota(userState?.user?.aff_history_quota)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
className="!rounded-2xl text-center"
|
||||||
|
bodyStyle={{ padding: '16px' }}
|
||||||
|
shadows='hover'
|
||||||
|
>
|
||||||
|
<div className="text-gray-600 text-xs font-medium">{t('邀请人数')}</div>
|
||||||
|
<div className="text-gray-900 text-lg font-bold mt-1">
|
||||||
|
{userState?.user?.aff_count || 0}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg p-3">
|
||||||
|
<Typography.Text strong className="block mb-2 text-sm">{t('邀请链接')}</Typography.Text>
|
||||||
|
<Input
|
||||||
|
value={affLink}
|
||||||
|
onClick={handleAffLinkClick}
|
||||||
|
readOnly
|
||||||
|
size="large"
|
||||||
|
className="!rounded-lg"
|
||||||
|
prefix={<IconLink />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user