Browse Source

Merge branch 'alpha' into fix-balance-unit-sync

Calcium-Ion 7 months ago
parent
commit
4759cda8f7
53 changed files with 3245 additions and 1742 deletions
  1. 3 3
      common/redis.go
  2. 2 2
      controller/channel-test.go
  3. 41 0
      controller/channel.go
  4. 1 0
      controller/misc.go
  5. 9 1
      controller/option.go
  6. 67 36
      dto/claude.go
  7. 260 51
      dto/openai_request.go
  8. 1 1
      makefile
  9. 1 0
      model/option.go
  10. 1 1
      model/token_cache.go
  11. 3 2
      model/user_cache.go
  12. 1 2
      relay/channel/ali/text.go
  13. 1 2
      relay/channel/baidu/relay-baidu.go
  14. 5 8
      relay/channel/claude/relay-claude.go
  15. 1 2
      relay/channel/cohere/relay-cohere.go
  16. 1 1
      relay/channel/coze/dto.go
  17. 1 2
      relay/channel/dify/relay-dify.go
  18. 1 8
      relay/channel/gemini/relay-gemini.go
  19. 1 1
      relay/channel/mistral/text.go
  20. 1 2
      relay/channel/palm/relay-palm.go
  21. 1 2
      relay/channel/tencent/relay-tencent.go
  22. 1 2
      relay/channel/xunfei/relay-xunfei.go
  23. 1 2
      relay/channel/zhipu/relay-zhipu.go
  24. 1 0
      router/api-router.go
  25. 10 6
      service/token_counter.go
  26. 124 0
      setting/api_info.go
  27. BIN
      web/public/example.png
  28. 3 15
      web/src/components/auth/LoginForm.js
  29. 3 15
      web/src/components/auth/PasswordResetConfirm.js
  30. 3 15
      web/src/components/auth/PasswordResetForm.js
  31. 4 14
      web/src/components/auth/RegisterForm.js
  32. 57 0
      web/src/components/settings/DashboardSetting.js
  33. 95 94
      web/src/components/settings/PersonalSetting.js
  34. 170 294
      web/src/components/table/ChannelsTable.js
  35. 239 141
      web/src/components/table/LogsTable.js
  36. 188 103
      web/src/components/table/MjLogsTable.js
  37. 14 1
      web/src/components/table/ModelPricing.js
  38. 113 36
      web/src/components/table/RedemptionsTable.js
  39. 159 87
      web/src/components/table/TaskLogsTable.js
  40. 139 55
      web/src/components/table/TokensTable.js
  41. 146 63
      web/src/components/table/UsersTable.js
  42. 155 65
      web/src/helpers/render.js
  43. 20 1
      web/src/i18n/locales/en.json
  44. 24 0
      web/src/index.css
  45. 1 0
      web/src/index.js
  46. 28 4
      web/src/pages/Channel/EditTagModal.js
  47. 460 342
      web/src/pages/Detail/index.js
  48. 53 61
      web/src/pages/Home/index.js
  49. 399 0
      web/src/pages/Setting/Dashboard/SettingsAPIInfo.js
  50. 6 1
      web/src/pages/Setting/index.js
  51. 1 1
      web/src/pages/Setup/index.js
  52. 9 3
      web/src/pages/Token/EditToken.js
  53. 216 194
      web/src/pages/TopUp/index.js

+ 3 - 3
common/redis.go

@@ -92,12 +92,12 @@ func RedisDel(key string) error {
 	return RDB.Del(ctx, key).Err()
 }
 
-func RedisHDelObj(key string) error {
+func RedisDelKey(key string) error {
 	if DebugEnabled {
-		SysLog(fmt.Sprintf("Redis HDEL: key=%s", key))
+		SysLog(fmt.Sprintf("Redis DEL Key: key=%s", key))
 	}
 	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 {

+ 2 - 2
controller/channel-test.go

@@ -200,10 +200,10 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
 	} else {
 		testRequest.MaxTokens = 10
 	}
-	content, _ := json.Marshal("hi")
+
 	testMessage := dto.Message{
 		Role:    "user",
-		Content: content,
+		Content: "hi",
 	}
 	testRequest.Model = model
 	testRequest.Messages = append(testRequest.Messages, testMessage)

+ 41 - 0
controller/channel.go

@@ -623,3 +623,44 @@ func BatchSetChannelTag(c *gin.Context) {
 	})
 	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
+}

+ 1 - 0
controller/misc.go

@@ -74,6 +74,7 @@ func GetStatus(c *gin.Context) {
 			"oidc_client_id":              system_setting.GetOIDCSettings().ClientId,
 			"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
 			"setup":                       constant.Setup,
+			"api_info":                    setting.GetApiInfo(),
 		},
 	})
 	return

+ 9 - 1
controller/option.go

@@ -119,7 +119,15 @@ func UpdateOption(c *gin.Context) {
 			})
 			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)
 	if err != nil {

+ 67 - 36
dto/claude.go

@@ -1,6 +1,9 @@
 package dto
 
-import "encoding/json"
+import (
+	"encoding/json"
+	"one-api/common"
+)
 
 type ClaudeMetadata struct {
 	UserId string `json:"user_id"`
@@ -20,11 +23,11 @@ type ClaudeMediaMessage struct {
 	Delta        string               `json:"delta,omitempty"`
 	CacheControl json.RawMessage      `json:"cache_control,omitempty"`
 	// tool_calls
-	Id        string          `json:"id,omitempty"`
-	Name      string          `json:"name,omitempty"`
-	Input     any             `json:"input,omitempty"`
-	Content   json.RawMessage `json:"content,omitempty"`
-	ToolUseId string          `json:"tool_use_id,omitempty"`
+	Id        string `json:"id,omitempty"`
+	Name      string `json:"name,omitempty"`
+	Input     any    `json:"input,omitempty"`
+	Content   any    `json:"content,omitempty"`
+	ToolUseId string `json:"tool_use_id,omitempty"`
 }
 
 func (c *ClaudeMediaMessage) SetText(s string) {
@@ -39,15 +42,39 @@ func (c *ClaudeMediaMessage) GetText() string {
 }
 
 func (c *ClaudeMediaMessage) IsStringContent() bool {
-	var content string
-	return json.Unmarshal(c.Content, &content) == nil
+	if c.Content == nil {
+		return false
+	}
+	_, ok := c.Content.(string)
+	if ok {
+		return true
+	}
+	return false
 }
 
 func (c *ClaudeMediaMessage) GetStringContent() string {
-	var content string
-	if err := json.Unmarshal(c.Content, &content); err == nil {
-		return content
+	if c.Content == nil {
+		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 ""
 }
 
@@ -57,16 +84,12 @@ func (c *ClaudeMediaMessage) GetJsonRowString() string {
 }
 
 func (c *ClaudeMediaMessage) SetContent(content any) {
-	jsonContent, _ := json.Marshal(content)
-	c.Content = jsonContent
+	c.Content = content
 }
 
 func (c *ClaudeMediaMessage) ParseMediaContent() []ClaudeMediaMessage {
-	var mediaContent []ClaudeMediaMessage
-	if err := json.Unmarshal(c.Content, &mediaContent); err == nil {
-		return mediaContent
-	}
-	return make([]ClaudeMediaMessage, 0)
+	mediaContent, _ := common.Any2Type[[]ClaudeMediaMessage](c.Content)
+	return mediaContent
 }
 
 type ClaudeMessageSource struct {
@@ -82,14 +105,36 @@ type ClaudeMessage struct {
 }
 
 func (c *ClaudeMessage) IsStringContent() bool {
+	if c.Content == nil {
+		return false
+	}
 	_, ok := c.Content.(string)
 	return ok
 }
 
 func (c *ClaudeMessage) GetStringContent() string {
-	if c.IsStringContent() {
+	if c.Content == nil {
+		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 ""
 }
 
@@ -98,15 +143,7 @@ func (c *ClaudeMessage) SetStringContent(content string) {
 }
 
 func (c *ClaudeMessage) ParseContent() ([]ClaudeMediaMessage, error) {
-	// map content to []ClaudeMediaMessage
-	// 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
+	return common.Any2Type[[]ClaudeMediaMessage](c.Content)
 }
 
 type Tool struct {
@@ -161,14 +198,8 @@ func (c *ClaudeRequest) SetStringSystem(system string) {
 }
 
 func (c *ClaudeRequest) ParseSystem() []ClaudeMediaMessage {
-	// map content to []ClaudeMediaMessage
-	// parse to json
-	jsonContent, _ := json.Marshal(c.System)
-	var contentList []ClaudeMediaMessage
-	if err := json.Unmarshal(jsonContent, &contentList); err == nil {
-		return contentList
-	}
-	return make([]ClaudeMediaMessage, 0)
+	mediaContent, _ := common.Any2Type[[]ClaudeMediaMessage](c.System)
+	return mediaContent
 }
 
 type ClaudeError struct {

+ 260 - 51
dto/openai_request.go

@@ -19,43 +19,43 @@ type FormatJsonSchema struct {
 }
 
 type GeneralOpenAIRequest struct {
-	Model               string         `json:"model,omitempty"`
-	Messages            []Message      `json:"messages,omitempty"`
-	Prompt              any            `json:"prompt,omitempty"`
-	Prefix              any            `json:"prefix,omitempty"`
-	Suffix              any            `json:"suffix,omitempty"`
-	Stream              bool           `json:"stream,omitempty"`
-	StreamOptions       *StreamOptions `json:"stream_options,omitempty"`
-	MaxTokens           uint           `json:"max_tokens,omitempty"`
-	MaxCompletionTokens uint           `json:"max_completion_tokens,omitempty"`
-	ReasoningEffort     string         `json:"reasoning_effort,omitempty"`
-	Temperature      *float64          `json:"temperature,omitempty"`
-	TopP             float64           `json:"top_p,omitempty"`
-	TopK             int               `json:"top_k,omitempty"`
-	Stop             any               `json:"stop,omitempty"`
-	N                int               `json:"n,omitempty"`
-	Input            any               `json:"input,omitempty"`
-	Instruction      string            `json:"instruction,omitempty"`
-	Size             string            `json:"size,omitempty"`
-	Functions        any               `json:"functions,omitempty"`
-	FrequencyPenalty float64           `json:"frequency_penalty,omitempty"`
-	PresencePenalty  float64           `json:"presence_penalty,omitempty"`
-	ResponseFormat   *ResponseFormat   `json:"response_format,omitempty"`
-	EncodingFormat   any               `json:"encoding_format,omitempty"`
-	Seed             float64           `json:"seed,omitempty"`
-	ParallelTooCalls *bool             `json:"parallel_tool_calls,omitempty"`
-	Tools            []ToolCallRequest `json:"tools,omitempty"`
-	ToolChoice       any               `json:"tool_choice,omitempty"`
-	User             string            `json:"user,omitempty"`
-	LogProbs         bool              `json:"logprobs,omitempty"`
-	TopLogProbs      int               `json:"top_logprobs,omitempty"`
-	Dimensions       int               `json:"dimensions,omitempty"`
-	Modalities       any               `json:"modalities,omitempty"`
-	Audio            any               `json:"audio,omitempty"`
-	EnableThinking   any               `json:"enable_thinking,omitempty"` // ali
-	ExtraBody        any               `json:"extra_body,omitempty"`
-	WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"`
-  // OpenRouter Params
+	Model               string            `json:"model,omitempty"`
+	Messages            []Message         `json:"messages,omitempty"`
+	Prompt              any               `json:"prompt,omitempty"`
+	Prefix              any               `json:"prefix,omitempty"`
+	Suffix              any               `json:"suffix,omitempty"`
+	Stream              bool              `json:"stream,omitempty"`
+	StreamOptions       *StreamOptions    `json:"stream_options,omitempty"`
+	MaxTokens           uint              `json:"max_tokens,omitempty"`
+	MaxCompletionTokens uint              `json:"max_completion_tokens,omitempty"`
+	ReasoningEffort     string            `json:"reasoning_effort,omitempty"`
+	Temperature         *float64          `json:"temperature,omitempty"`
+	TopP                float64           `json:"top_p,omitempty"`
+	TopK                int               `json:"top_k,omitempty"`
+	Stop                any               `json:"stop,omitempty"`
+	N                   int               `json:"n,omitempty"`
+	Input               any               `json:"input,omitempty"`
+	Instruction         string            `json:"instruction,omitempty"`
+	Size                string            `json:"size,omitempty"`
+	Functions           json.RawMessage   `json:"functions,omitempty"`
+	FrequencyPenalty    float64           `json:"frequency_penalty,omitempty"`
+	PresencePenalty     float64           `json:"presence_penalty,omitempty"`
+	ResponseFormat      *ResponseFormat   `json:"response_format,omitempty"`
+	EncodingFormat      json.RawMessage   `json:"encoding_format,omitempty"`
+	Seed                float64           `json:"seed,omitempty"`
+	ParallelTooCalls    *bool             `json:"parallel_tool_calls,omitempty"`
+	Tools               []ToolCallRequest `json:"tools,omitempty"`
+	ToolChoice          any               `json:"tool_choice,omitempty"`
+	User                string            `json:"user,omitempty"`
+	LogProbs            bool              `json:"logprobs,omitempty"`
+	TopLogProbs         int               `json:"top_logprobs,omitempty"`
+	Dimensions          int               `json:"dimensions,omitempty"`
+	Modalities          json.RawMessage   `json:"modalities,omitempty"`
+	Audio               json.RawMessage   `json:"audio,omitempty"`
+	EnableThinking      any               `json:"enable_thinking,omitempty"` // ali
+	ExtraBody           json.RawMessage   `json:"extra_body,omitempty"`
+	WebSearchOptions    *WebSearchOptions `json:"web_search_options,omitempty"`
+	// OpenRouter Params
 	Reasoning json.RawMessage `json:"reasoning,omitempty"`
 }
 
@@ -107,16 +107,16 @@ func (r *GeneralOpenAIRequest) ParseInput() []string {
 }
 
 type Message struct {
-	Role                string          `json:"role"`
-	Content             json.RawMessage `json:"content"`
-	Name                *string         `json:"name,omitempty"`
-	Prefix              *bool           `json:"prefix,omitempty"`
-	ReasoningContent    string          `json:"reasoning_content,omitempty"`
-	Reasoning           string          `json:"reasoning,omitempty"`
-	ToolCalls           json.RawMessage `json:"tool_calls,omitempty"`
-	ToolCallId          string          `json:"tool_call_id,omitempty"`
-	parsedContent       []MediaContent
-	parsedStringContent *string
+	Role             string          `json:"role"`
+	Content          any             `json:"content"`
+	Name             *string         `json:"name,omitempty"`
+	Prefix           *bool           `json:"prefix,omitempty"`
+	ReasoningContent string          `json:"reasoning_content,omitempty"`
+	Reasoning        string          `json:"reasoning,omitempty"`
+	ToolCalls        json.RawMessage `json:"tool_calls,omitempty"`
+	ToolCallId       string          `json:"tool_call_id,omitempty"`
+	parsedContent    []MediaContent
+	//parsedStringContent *string
 }
 
 type MediaContent struct {
@@ -132,21 +132,50 @@ type MediaContent struct {
 
 func (m *MediaContent) GetImageMedia() *MessageImageUrl {
 	if m.ImageUrl != nil {
-		return m.ImageUrl.(*MessageImageUrl)
+		if _, ok := m.ImageUrl.(*MessageImageUrl); ok {
+			return m.ImageUrl.(*MessageImageUrl)
+		}
+		if itemMap, ok := m.ImageUrl.(map[string]any); ok {
+			out := &MessageImageUrl{
+				Url:      common.Interface2String(itemMap["url"]),
+				Detail:   common.Interface2String(itemMap["detail"]),
+				MimeType: common.Interface2String(itemMap["mime_type"]),
+			}
+			return out
+		}
 	}
 	return nil
 }
 
 func (m *MediaContent) GetInputAudio() *MessageInputAudio {
 	if m.InputAudio != nil {
-		return m.InputAudio.(*MessageInputAudio)
+		if _, ok := m.InputAudio.(*MessageInputAudio); ok {
+			return m.InputAudio.(*MessageInputAudio)
+		}
+		if itemMap, ok := m.InputAudio.(map[string]any); ok {
+			out := &MessageInputAudio{
+				Data:   common.Interface2String(itemMap["data"]),
+				Format: common.Interface2String(itemMap["format"]),
+			}
+			return out
+		}
 	}
 	return nil
 }
 
 func (m *MediaContent) GetFile() *MessageFile {
 	if m.File != nil {
-		return m.File.(*MessageFile)
+		if _, ok := m.File.(*MessageFile); ok {
+			return m.File.(*MessageFile)
+		}
+		if itemMap, ok := m.File.(map[string]any); ok {
+			out := &MessageFile{
+				FileName: common.Interface2String(itemMap["file_name"]),
+				FileData: common.Interface2String(itemMap["file_data"]),
+				FileId:   common.Interface2String(itemMap["file_id"]),
+			}
+			return out
+		}
 	}
 	return nil
 }
@@ -212,6 +241,186 @@ func (m *Message) SetToolCalls(toolCalls any) {
 }
 
 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 {
 		return *m.parsedStringContent
 	}
@@ -382,7 +591,7 @@ func (m *Message) ParseContent() []MediaContent {
 		m.parsedContent = contentList
 	}
 	return contentList
-}
+}*/
 
 type WebSearchOptions struct {
 	SearchContextSize string          `json:"search_context_size,omitempty"`

+ 1 - 1
makefile

@@ -7,7 +7,7 @@ all: build-frontend start-backend
 
 build-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:
 	@echo "Starting backend dev server..."

+ 1 - 0
model/option.go

@@ -122,6 +122,7 @@ func InitOptionMap() {
 	common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
 	common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
 	common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
+	common.OptionMap["ApiInfo"] = ""
 
 	// 自动添加所有注册的模型配置
 	modelConfigs := config.GlobalConfig.ExportAllConfigs()

+ 1 - 1
model/token_cache.go

@@ -19,7 +19,7 @@ func cacheSetToken(token Token) error {
 
 func cacheDeleteToken(key string) error {
 	key = common.GenerateHMAC(key)
-	err := common.RedisHDelObj(fmt.Sprintf("token:%s", key))
+	err := common.RedisDelKey(fmt.Sprintf("token:%s", key))
 	if err != nil {
 		return err
 	}

+ 3 - 2
model/user_cache.go

@@ -3,11 +3,12 @@ package model
 import (
 	"encoding/json"
 	"fmt"
-	"github.com/gin-gonic/gin"
 	"one-api/common"
 	"one-api/constant"
 	"time"
 
+	"github.com/gin-gonic/gin"
+
 	"github.com/bytedance/gopkg/util/gopool"
 )
 
@@ -57,7 +58,7 @@ func invalidateUserCache(userId int) error {
 	if !common.RedisEnabled {
 		return nil
 	}
-	return common.RedisHDelObj(getUserCacheKey(userId))
+	return common.RedisDelKey(getUserCacheKey(userId))
 }
 
 // updateUserCache updates all user cache fields using hash

+ 1 - 2
relay/channel/ali/text.go

@@ -96,12 +96,11 @@ func embeddingResponseAli2OpenAI(response *AliEmbeddingResponse, model string) *
 }
 
 func responseAli2OpenAI(response *AliResponse) *dto.OpenAITextResponse {
-	content, _ := json.Marshal(response.Output.Text)
 	choice := dto.OpenAITextResponseChoice{
 		Index: 0,
 		Message: dto.Message{
 			Role:    "assistant",
-			Content: content,
+			Content: response.Output.Text,
 		},
 		FinishReason: response.Output.FinishReason,
 	}

+ 1 - 2
relay/channel/baidu/relay-baidu.go

@@ -53,12 +53,11 @@ func requestOpenAI2Baidu(request dto.GeneralOpenAIRequest) *BaiduChatRequest {
 }
 
 func responseBaidu2OpenAI(response *BaiduChatResponse) *dto.OpenAITextResponse {
-	content, _ := json.Marshal(response.Result)
 	choice := dto.OpenAITextResponseChoice{
 		Index: 0,
 		Message: dto.Message{
 			Role:    "assistant",
-			Content: content,
+			Content: response.Result,
 		},
 		FinishReason: "stop",
 	}

+ 5 - 8
relay/channel/claude/relay-claude.go

@@ -48,9 +48,9 @@ func RequestOpenAI2ClaudeComplete(textRequest dto.GeneralOpenAIRequest) *dto.Cla
 	prompt := ""
 	for _, message := range textRequest.Messages {
 		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" {
-			prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content)
+			prompt += fmt.Sprintf("\n\nAssistant: %s", message.StringContent())
 		} else if message.Role == "system" {
 			if prompt == "" {
 				prompt = message.StringContent()
@@ -155,15 +155,13 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.Cla
 		}
 		if lastMessage.Role == message.Role && lastMessage.Role != "tool" {
 			if lastMessage.IsStringContent() && message.IsStringContent() {
-				content, _ := json.Marshal(strings.Trim(fmt.Sprintf("%s %s", lastMessage.StringContent(), message.StringContent()), "\""))
-				fmtMessage.Content = content
+				fmtMessage.SetStringContent(strings.Trim(fmt.Sprintf("%s %s", lastMessage.StringContent(), message.StringContent()), "\""))
 				// delete last message
 				formatMessages = formatMessages[:len(formatMessages)-1]
 			}
 		}
 		if fmtMessage.Content == nil {
-			content, _ := json.Marshal("...")
-			fmtMessage.Content = content
+			fmtMessage.SetStringContent("...")
 		}
 		formatMessages = append(formatMessages, fmtMessage)
 		lastMessage = fmtMessage
@@ -397,12 +395,11 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse) *dto
 	thinkingContent := ""
 
 	if reqMode == RequestModeCompletion {
-		content, _ := json.Marshal(strings.TrimPrefix(claudeResponse.Completion, " "))
 		choice := dto.OpenAITextResponseChoice{
 			Index: 0,
 			Message: dto.Message{
 				Role:    "assistant",
-				Content: content,
+				Content: strings.TrimPrefix(claudeResponse.Completion, " "),
 				Name:    nil,
 			},
 			FinishReason: stopReasonClaude2OpenAI(claudeResponse.StopReason),

+ 1 - 2
relay/channel/cohere/relay-cohere.go

@@ -195,11 +195,10 @@ func cohereHandler(c *gin.Context, resp *http.Response, modelName string, prompt
 	openaiResp.Model = modelName
 	openaiResp.Usage = usage
 
-	content, _ := json.Marshal(cohereResp.Text)
 	openaiResp.Choices = []dto.OpenAITextResponseChoice{
 		{
 			Index:        0,
-			Message:      dto.Message{Content: content, Role: "assistant"},
+			Message:      dto.Message{Content: cohereResp.Text, Role: "assistant"},
 			FinishReason: stopReasonCohere2OpenAI(cohereResp.FinishReason),
 		},
 	}

+ 1 - 1
relay/channel/coze/dto.go

@@ -10,7 +10,7 @@ type CozeError struct {
 type CozeEnterMessage struct {
 	Role        string          `json:"role"`
 	Type        string          `json:"type,omitempty"`
-	Content     json.RawMessage `json:"content,omitempty"`
+	Content     any             `json:"content,omitempty"`
 	MetaData    json.RawMessage `json:"meta_data,omitempty"`
 	ContentType string          `json:"content_type,omitempty"`
 }

+ 1 - 2
relay/channel/dify/relay-dify.go

@@ -278,12 +278,11 @@ func difyHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInf
 		Created: common.GetTimestamp(),
 		Usage:   difyResponse.MetaData.Usage,
 	}
-	content, _ := json.Marshal(difyResponse.Answer)
 	choice := dto.OpenAITextResponseChoice{
 		Index: 0,
 		Message: dto.Message{
 			Role:    "assistant",
-			Content: content,
+			Content: difyResponse.Answer,
 		},
 		FinishReason: "stop",
 	}

+ 1 - 8
relay/channel/gemini/relay-gemini.go

@@ -175,12 +175,6 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
 		// common.SysLog("tools: " + fmt.Sprintf("%+v", geminiRequest.Tools))
 		// json_data, _ := json.Marshal(geminiRequest.Tools)
 		// 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") {
@@ -609,14 +603,13 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
 		Created: common.GetTimestamp(),
 		Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)),
 	}
-	content, _ := json.Marshal("")
 	isToolCall := false
 	for _, candidate := range response.Candidates {
 		choice := dto.OpenAITextResponseChoice{
 			Index: int(candidate.Index),
 			Message: dto.Message{
 				Role:    "assistant",
-				Content: content,
+				Content: "",
 			},
 			FinishReason: constant.FinishReasonStop,
 		}

+ 1 - 1
relay/channel/mistral/text.go

@@ -47,7 +47,7 @@ func requestOpenAI2Mistral(request *dto.GeneralOpenAIRequest) *dto.GeneralOpenAI
 		}
 
 		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{}
 		}
 		for j, mediaMessage := range mediaMessages {

+ 1 - 2
relay/channel/palm/relay-palm.go

@@ -45,12 +45,11 @@ func responsePaLM2OpenAI(response *PaLMChatResponse) *dto.OpenAITextResponse {
 		Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)),
 	}
 	for i, candidate := range response.Candidates {
-		content, _ := json.Marshal(candidate.Content)
 		choice := dto.OpenAITextResponseChoice{
 			Index: i,
 			Message: dto.Message{
 				Role:    "assistant",
-				Content: content,
+				Content: candidate.Content,
 			},
 			FinishReason: "stop",
 		}

+ 1 - 2
relay/channel/tencent/relay-tencent.go

@@ -56,12 +56,11 @@ func responseTencent2OpenAI(response *TencentChatResponse) *dto.OpenAITextRespon
 		},
 	}
 	if len(response.Choices) > 0 {
-		content, _ := json.Marshal(response.Choices[0].Messages.Content)
 		choice := dto.OpenAITextResponseChoice{
 			Index: 0,
 			Message: dto.Message{
 				Role:    "assistant",
-				Content: content,
+				Content: response.Choices[0].Messages.Content,
 			},
 			FinishReason: response.Choices[0].FinishReason,
 		}

+ 1 - 2
relay/channel/xunfei/relay-xunfei.go

@@ -61,12 +61,11 @@ func responseXunfei2OpenAI(response *XunfeiChatResponse) *dto.OpenAITextResponse
 			},
 		}
 	}
-	content, _ := json.Marshal(response.Payload.Choices.Text[0].Content)
 	choice := dto.OpenAITextResponseChoice{
 		Index: 0,
 		Message: dto.Message{
 			Role:    "assistant",
-			Content: content,
+			Content: response.Payload.Choices.Text[0].Content,
 		},
 		FinishReason: constant.FinishReasonStop,
 	}

+ 1 - 2
relay/channel/zhipu/relay-zhipu.go

@@ -108,12 +108,11 @@ func responseZhipu2OpenAI(response *ZhipuResponse) *dto.OpenAITextResponse {
 		Usage:   response.Data.Usage,
 	}
 	for i, choice := range response.Data.Choices {
-		content, _ := json.Marshal(strings.Trim(choice.Content, "\""))
 		openaiChoice := dto.OpenAITextResponseChoice{
 			Index: i,
 			Message: dto.Message{
 				Role:    choice.Role,
-				Content: content,
+				Content: strings.Trim(choice.Content, "\""),
 			},
 			FinishReason: "",
 		}

+ 1 - 0
router/api-router.go

@@ -105,6 +105,7 @@ func SetApiRouter(router *gin.Engine) {
 			channelRoute.GET("/fetch_models/:id", controller.FetchUpstreamModels)
 			channelRoute.POST("/fetch_models", controller.FetchModels)
 			channelRoute.POST("/batch/tag", controller.BatchSetChannelTag)
+			channelRoute.GET("/tag/models", controller.GetTagModels)
 		}
 		tokenRoute := apiRouter.Group("/token")
 		tokenRoute.Use(middleware.UserAuth())

+ 10 - 6
service/token_counter.go

@@ -261,12 +261,16 @@ func CountTokenClaudeMessages(messages []dto.ClaudeMessage, model string, stream
 					//}
 					tokenNum += 1000
 				case "tool_use":
-					tokenNum += getTokenNum(tokenEncoder, mediaMessage.Name)
-					inputJSON, _ := json.Marshal(mediaMessage.Input)
-					tokenNum += getTokenNum(tokenEncoder, string(inputJSON))
+					if mediaMessage.Input != nil {
+						tokenNum += getTokenNum(tokenEncoder, mediaMessage.Name)
+						inputJSON, _ := json.Marshal(mediaMessage.Input)
+						tokenNum += getTokenNum(tokenEncoder, string(inputJSON))
+					}
 				case "tool_result":
-					contentJSON, _ := json.Marshal(mediaMessage.Content)
-					tokenNum += getTokenNum(tokenEncoder, string(contentJSON))
+					if mediaMessage.Content != nil {
+						contentJSON, _ := json.Marshal(mediaMessage.Content)
+						tokenNum += getTokenNum(tokenEncoder, string(contentJSON))
+					}
 				}
 			}
 		}
@@ -386,7 +390,7 @@ func CountTokenMessages(info *relaycommon.RelayInfo, messages []dto.Message, mod
 	for _, message := range messages {
 		tokenNum += tokensPerMessage
 		tokenNum += getTokenNum(tokenEncoder, message.Role)
-		if len(message.Content) > 0 {
+		if message.Content != nil {
 			if message.Name != nil {
 				tokenNum += tokensPerName
 				tokenNum += getTokenNum(tokenEncoder, *message.Name)

+ 124 - 0
setting/api_info.go

@@ -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
+} 

BIN
web/public/example.png


+ 3 - 15
web/src/components/auth/LoginForm.js

@@ -32,7 +32,6 @@ import OIDCIcon from '../common/logo/OIDCIcon.js';
 import WeChatIcon from '../common/logo/WeChatIcon.js';
 import LinuxDoIcon from '../common/logo/LinuxDoIcon.js';
 import { useTranslation } from 'react-i18next';
-import Background from '/example.png';
 
 const LoginForm = () => {
   const [inputs, setInputs] = useState({
@@ -266,7 +265,7 @@ const LoginForm = () => {
         <div className="w-full max-w-md">
           <div className="flex items-center justify-center mb-6 gap-2">
             <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>
 
           <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
@@ -500,19 +499,8 @@ const LoginForm = () => {
   };
 
   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="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="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">
         {showEmailLogin || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
           ? renderEmailLoginForm()
           : renderOAuthOptions()}

+ 3 - 15
web/src/components/auth/PasswordResetConfirm.js

@@ -4,7 +4,6 @@ import { useSearchParams, Link } from 'react-router-dom';
 import { Button, Card, Form, Typography, Banner } from '@douyinfe/semi-ui';
 import { IconMail, IconLock, IconCopy } from '@douyinfe/semi-icons';
 import { useTranslation } from 'react-i18next';
-import Background from '/example.png';
 
 const { Text, Title } = Typography;
 
@@ -79,24 +78,13 @@ const PasswordResetConfirm = () => {
   }
 
   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="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="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="flex flex-col items-center">
           <div className="w-full max-w-md">
             <div className="flex items-center justify-center mb-6 gap-2">
               <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>
 
             <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">

+ 3 - 15
web/src/components/auth/PasswordResetForm.js

@@ -5,7 +5,6 @@ import { Button, Card, Form, Typography } from '@douyinfe/semi-ui';
 import { IconMail } from '@douyinfe/semi-icons';
 import { Link } from 'react-router-dom';
 import { useTranslation } from 'react-i18next';
-import Background from '/example.png';
 
 const { Text, Title } = Typography;
 
@@ -79,24 +78,13 @@ const PasswordResetForm = () => {
   }
 
   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="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="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="flex flex-col items-center">
           <div className="w-full max-w-md">
             <div className="flex items-center justify-center mb-6 gap-2">
               <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>
 
             <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">

+ 4 - 14
web/src/components/auth/RegisterForm.js

@@ -33,7 +33,6 @@ import WeChatIcon from '../common/logo/WeChatIcon.js';
 import TelegramLoginButton from 'react-telegram-login/src';
 import { UserContext } from '../../context/User/index.js';
 import { useTranslation } from 'react-i18next';
-import Background from '/example.png';
 
 const RegisterForm = () => {
   const { t } = useTranslation();
@@ -272,7 +271,7 @@ const RegisterForm = () => {
         <div className="w-full max-w-md">
           <div className="flex items-center justify-center mb-6 gap-2">
             <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>
 
           <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="flex items-center justify-center mb-6 gap-2">
             <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>
 
           <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
@@ -542,17 +541,8 @@ const RegisterForm = () => {
   };
 
   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="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="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">
         {showEmailRegister || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
           ? renderEmailRegisterForm()
           : renderOAuthOptions()}

+ 57 - 0
web/src/components/settings/DashboardSetting.js

@@ -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; 

+ 95 - 94
web/src/components/settings/PersonalSetting.js

@@ -104,6 +104,33 @@ const PersonalSetting = () => {
   });
   const [modelsLoading, setModelsLoading] = 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(() => {
     let status = localStorage.getItem('status');
@@ -384,107 +411,81 @@ const PersonalSetting = () => {
               <Card className="!rounded-2xl shadow-lg border-0">
                 {/* 顶部用户信息区域 */}
                 <Card
-                  className="!rounded-2xl !border-0 !shadow-2xl overflow-hidden"
+                  className="!rounded-2xl !border-0 !shadow-lg overflow-hidden"
                   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'
                   }}
                   bodyStyle={{ padding: 0 }}
                 >
                   {/* 装饰性背景元素 */}
                   <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 -bottom-16 -left-16 w-48 h-48 bg-white opacity-3 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-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-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-slate-400 dark:bg-slate-500 opacity-6 rounded-full"></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 items-center flex-1 min-w-0">
                         <Avatar
                           size='large'
-                          color={stringToColor(getUsername())}
-                          border={{ motion: true }}
-                          contentMotion={true}
-                          className="mr-3 sm:mr-4 shadow-lg flex-shrink-0"
+                          className="mr-3 sm:mr-4 shadow-md flex-shrink-0 bg-slate-500 dark:bg-slate-400"
                         >
                           {getAvatarText()}
                         </Avatar>
                         <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()}
                           </div>
                           <div className="mt-1 flex flex-wrap gap-1 sm:gap-2">
                             {isRoot() ? (
                               <Tag
-                                color='red'
                                 size='small'
-                                style={{
-                                  backgroundColor: 'rgba(255, 255, 255, 0.95)',
-                                  color: '#dc2626',
-                                  fontWeight: '600'
-                                }}
-                                className="!rounded-full"
+                                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"
+                                style={{ fontWeight: '500' }}
                               >
                                 {t('超级管理员')}
                               </Tag>
                             ) : isAdmin() ? (
                               <Tag
-                                color='orange'
                                 size='small'
-                                style={{
-                                  backgroundColor: 'rgba(255, 255, 255, 0.95)',
-                                  color: '#ea580c',
-                                  fontWeight: '600'
-                                }}
-                                className="!rounded-full"
+                                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"
+                                style={{ fontWeight: '500' }}
                               >
                                 {t('管理员')}
                               </Tag>
                             ) : (
                               <Tag
-                                color='blue'
                                 size='small'
-                                style={{
-                                  backgroundColor: 'rgba(255, 255, 255, 0.95)',
-                                  color: '#2563eb',
-                                  fontWeight: '600'
-                                }}
-                                className="!rounded-full"
+                                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"
+                                style={{ fontWeight: '500' }}
                               >
                                 {t('普通用户')}
                               </Tag>
                             )}
                             <Tag
-                              color='green'
                               size='small'
-                              className="!rounded-full"
-                              style={{
-                                backgroundColor: 'rgba(255, 255, 255, 0.95)',
-                                color: '#16a34a',
-                                fontWeight: '600'
-                              }}
+                              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={{ fontWeight: '500' }}
                             >
                               ID: {userState?.user?.id}
                             </Tag>
                           </div>
                         </div>
                       </div>
-                      <div
-                        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"
-                        style={{
-                          background: `linear-gradient(135deg, ${stringToColor(getUsername())} 0%, #f59e0b 100%)`
-                        }}
-                      >
-                        <IconUser size="default" style={{ color: 'white' }} />
+                      <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">
+                        <IconUser size="default" className="text-white" />
                       </div>
                     </div>
 
                     <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('当前余额')}
                       </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)}
                       </div>
                     </div>
@@ -492,33 +493,33 @@ const PersonalSetting = () => {
                     <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="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('历史消耗')}
                           </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)}
                           </div>
                         </div>
                         <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('请求次数')}
                           </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}
                           </div>
                         </div>
                         <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('用户分组')}
                           </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('默认')}
                           </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>
                 </Card>
 
@@ -537,10 +538,10 @@ const PersonalSetting = () => {
                     >
                       <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="w-10 h-10 rounded-full bg-purple-50 flex items-center justify-center mr-3">
-                              <Settings size={20} className="text-purple-500" />
+                            <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-slate-600 dark:text-slate-300" />
                             </div>
                             <div>
                               <Typography.Title heading={6} className="mb-0">{t('模型列表')}</Typography.Title>
@@ -629,7 +630,7 @@ const PersonalSetting = () => {
                                 </Tabs>
                               </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);
@@ -736,9 +737,9 @@ const PersonalSetting = () => {
                             shadows='hover'
                           >
                             <div className="flex items-center justify-between">
-                              <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">
-                                  <IconMail size="default" className="text-red-500" />
+                                                              <div className="flex items-center flex-1">
+                                <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-slate-600 dark:text-slate-300" />
                                 </div>
                                 <div className="flex-1 min-w-0">
                                   <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 flex-1">
-                                <div className="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center mr-3">
-                                  <SiWechat size={20} className="text-green-500" />
+                                <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-slate-600 dark:text-slate-300" />
                                 </div>
                                 <div className="flex-1 min-w-0">
                                   <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 flex-1">
-                                <div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center mr-3">
-                                  <IconGithubLogo size="default" className="text-gray-700" />
+                                <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-slate-600 dark:text-slate-300" />
                                 </div>
                                 <div className="flex-1 min-w-0">
                                   <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 flex-1">
-                                <div className="w-10 h-10 rounded-full bg-indigo-50 flex items-center justify-center mr-3">
-                                  <IconShield size="default" className="text-indigo-500" />
+                                <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-slate-600 dark:text-slate-300" />
                                 </div>
                                 <div className="flex-1 min-w-0">
                                   <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 flex-1">
-                                <div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center mr-3">
-                                  <SiTelegram size={20} className="text-blue-500" />
+                                <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-slate-600 dark:text-slate-300" />
                                 </div>
                                 <div className="flex-1 min-w-0">
                                   <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 flex-1">
-                                <div className="w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center mr-3">
-                                  <SiLinux size={20} className="text-orange-500" />
+                                <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-slate-600 dark:text-slate-300" />
                                 </div>
                                 <div className="flex-1 min-w-0">
                                   <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 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">
-                                    <IconKey size="large" className="text-blue-500" />
+                                  <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-slate-600" />
                                   </div>
                                   <div className="flex-1">
                                     <Typography.Title heading={6} className="mb-1">
@@ -1006,7 +1007,7 @@ const PersonalSetting = () => {
                                   type="primary"
                                   theme="solid"
                                   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 />}
                                 >
                                   {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 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">
-                                    <IconLock size="large" className="text-orange-500" />
+                                  <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-slate-600" />
                                   </div>
                                   <div>
                                     <Typography.Title heading={6} className="mb-1">
@@ -1038,7 +1039,7 @@ const PersonalSetting = () => {
                                   type="primary"
                                   theme="solid"
                                   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 />}
                                 >
                                   {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 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">
-                                    <IconDelete size="large" className="text-red-500" />
+                                  <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-slate-600" />
                                   </div>
                                   <div>
-                                    <Typography.Title heading={6} className="mb-1 text-red-600">
+                                    <Typography.Title heading={6} className="mb-1 text-slate-700">
                                       {t('删除账户')}
                                     </Typography.Title>
                                     <Typography.Text type="tertiary" className="text-sm">
@@ -1070,7 +1071,7 @@ const PersonalSetting = () => {
                                   type="danger"
                                   theme="solid"
                                   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 />}
                                 >
                                   {t('删除账户')}
@@ -1111,7 +1112,7 @@ const PersonalSetting = () => {
                                 >
                                   <Radio value='email' className="!p-4 !rounded-lg">
                                     <div className="flex items-center">
-                                      <IconMail className="mr-2 text-blue-500" />
+                                      <IconMail className="mr-2 text-slate-600" />
                                       <div>
                                         <div className="font-medium">{t('邮件通知')}</div>
                                         <div className="text-sm text-gray-500">{t('通过邮件接收通知')}</div>
@@ -1120,7 +1121,7 @@ const PersonalSetting = () => {
                                   </Radio>
                                   <Radio value='webhook' className="!p-4 !rounded-lg">
                                     <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 className="font-medium">{t('Webhook通知')}</div>
                                         <div className="text-sm text-gray-500">{t('通过HTTP请求接收通知')}</div>
@@ -1167,11 +1168,11 @@ const PersonalSetting = () => {
                                     </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">
-                                        <Globe size={16} className="mr-2 text-yellow-600" />
-                                        <Typography.Text strong className="text-yellow-800">
+                                        <Globe size={16} className="mr-2 text-slate-600" />
+                                        <Typography.Text strong className="text-slate-700">
                                           {t('Webhook请求结构')}
                                         </Typography.Text>
                                       </div>
@@ -1254,11 +1255,11 @@ const PersonalSetting = () => {
                             itemKey='price'
                           >
                             <div className="py-4">
-                              <div className="bg-white rounded-xl">
-                                <div className="flex items-start">
-                                  <div className="w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center mt-1">
-                                    <Shield size={20} className="text-orange-500" />
-                                  </div>
+                                                              <div className="bg-white rounded-xl">
+                                  <div className="flex items-start">
+                                    <div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center mt-1">
+                                      <Shield size={20} className="text-slate-600" />
+                                    </div>
                                   <div className="flex-1">
                                     <div className="flex items-center justify-between">
                                       <div>
@@ -1292,7 +1293,7 @@ const PersonalSetting = () => {
                             type='primary'
                             onClick={saveNotificationSettings}
                             size="large"
-                            className="!rounded-lg !bg-purple-500 hover:!bg-purple-600"
+                            className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
                             icon={<IconSetting />}
                           >
                             {t('保存设置')}
@@ -1408,7 +1409,7 @@ const PersonalSetting = () => {
             theme="solid"
             size='large'
             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} />}
           >
             {t('绑定')}

+ 170 - 294
web/src/components/table/ChannelsTable.js

@@ -6,15 +6,31 @@ import {
   showSuccess,
   timestamp2string,
   renderGroup,
-  renderQuotaWithAmount,
-  renderQuota
+  renderNumberWithPoint,
+  renderQuota,
+  getChannelIcon
 } 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 {
   Button,
   Divider,
   Dropdown,
+  Empty,
   Input,
   InputNumber,
   Modal,
@@ -25,13 +41,15 @@ import {
   Tag,
   Tooltip,
   Typography,
-  Checkbox,
   Card,
-  Select
+  Form
 } from '@douyinfe/semi-ui';
+import {
+  IllustrationNoResult,
+  IllustrationNoResultDark
+} from '@douyinfe/semi-illustrations';
 import EditChannel from '../../pages/Channel/EditChannel.js';
 import {
-  IconList,
   IconTreeTriangleDown,
   IconFilter,
   IconPlus,
@@ -64,7 +82,12 @@ const ChannelsTable = () => {
       type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' };
     }
     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}
       </Tag>
     );
@@ -74,7 +97,7 @@ const ChannelsTable = () => {
     return (
       <Tag
         color='light-blue'
-        prefixIcon={<IconList />}
+        prefixIcon={<Tags size={14} />}
         size='large'
         shape='circle'
         type='light'
@@ -88,25 +111,25 @@ const ChannelsTable = () => {
     switch (status) {
       case 1:
         return (
-          <Tag size='large' color='green' shape='circle'>
+          <Tag size='large' color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
             {t('已启用')}
           </Tag>
         );
       case 2:
         return (
-          <Tag size='large' color='yellow' shape='circle'>
+          <Tag size='large' color='yellow' shape='circle' prefixIcon={<XCircle size={14} />}>
             {t('已禁用')}
           </Tag>
         );
       case 3:
         return (
-          <Tag size='large' color='yellow' shape='circle'>
+          <Tag size='large' color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
             {t('自动禁用')}
           </Tag>
         );
       default:
         return (
-          <Tag size='large' color='grey' shape='circle'>
+          <Tag size='large' color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
             {t('未知状态')}
           </Tag>
         );
@@ -118,139 +141,48 @@ const ChannelsTable = () => {
     time = time.toFixed(2) + t(' 秒');
     if (responseTime === 0) {
       return (
-        <Tag size='large' color='grey' shape='circle'>
+        <Tag size='large' color='grey' shape='circle' prefixIcon={<TestTube size={14} />}>
           {t('未测试')}
         </Tag>
       );
     } else if (responseTime <= 1000) {
       return (
-        <Tag size='large' color='green' shape='circle'>
+        <Tag size='large' color='green' shape='circle' prefixIcon={<Zap size={14} />}>
           {time}
         </Tag>
       );
     } else if (responseTime <= 3000) {
       return (
-        <Tag size='large' color='lime' shape='circle'>
+        <Tag size='large' color='lime' shape='circle' prefixIcon={<Timer size={14} />}>
           {time}
         </Tag>
       );
     } else if (responseTime <= 5000) {
       return (
-        <Tag size='large' color='yellow' shape='circle'>
+        <Tag size='large' color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
           {time}
         </Tag>
       );
     } else {
       return (
-        <Tag size='large' color='red' shape='circle'>
+        <Tag size='large' color='red' shape='circle' prefixIcon={<AlertTriangle size={14} />}>
           {time}
         </Tag>
       );
     }
   };
 
-  // Define column keys for selection
-  const COLUMN_KEYS = {
-    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 = [
+  // Define all columns
+  const columns = [
     {
-      key: COLUMN_KEYS.ID,
       title: t('ID'),
       dataIndex: 'id',
     },
     {
-      key: COLUMN_KEYS.NAME,
       title: t('名称'),
       dataIndex: 'name',
     },
     {
-      key: COLUMN_KEYS.GROUP,
       title: t('分组'),
       dataIndex: 'group',
       render: (text, record, index) => (
@@ -269,7 +201,6 @@ const ChannelsTable = () => {
       ),
     },
     {
-      key: COLUMN_KEYS.TYPE,
       title: t('类型'),
       dataIndex: 'type',
       render: (text, record, index) => {
@@ -281,7 +212,6 @@ const ChannelsTable = () => {
       },
     },
     {
-      key: COLUMN_KEYS.STATUS,
       title: t('状态'),
       dataIndex: 'status',
       render: (text, record, index) => {
@@ -307,7 +237,6 @@ const ChannelsTable = () => {
       },
     },
     {
-      key: COLUMN_KEYS.RESPONSE_TIME,
       title: t('响应时间'),
       dataIndex: 'response_time',
       render: (text, record, index) => (
@@ -315,7 +244,6 @@ const ChannelsTable = () => {
       ),
     },
     {
-      key: COLUMN_KEYS.BALANCE,
       title: t('已用/剩余'),
       dataIndex: 'expired_time',
       render: (text, record, index) => {
@@ -324,7 +252,7 @@ const ChannelsTable = () => {
             <div>
               <Space spacing={1}>
                 <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)}
                   </Tag>
                 </Tooltip>
@@ -334,6 +262,7 @@ const ChannelsTable = () => {
                     type='ghost'
                     size='large'
                     shape='circle'
+                    prefixIcon={<Coins size={14} />}
                     onClick={() => updateChannelBalance(record)}
                   >
                     {renderQuotaWithAmount(record.balance)}
@@ -345,7 +274,7 @@ const ChannelsTable = () => {
         } else {
           return (
             <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)}
               </Tag>
             </Tooltip>
@@ -354,7 +283,6 @@ const ChannelsTable = () => {
       },
     },
     {
-      key: COLUMN_KEYS.PRIORITY,
       title: t('优先级'),
       dataIndex: 'priority',
       render: (text, record, index) => {
@@ -406,7 +334,6 @@ const ChannelsTable = () => {
       },
     },
     {
-      key: COLUMN_KEYS.WEIGHT,
       title: t('权重'),
       dataIndex: 'weight',
       render: (text, record, index) => {
@@ -458,7 +385,6 @@ const ChannelsTable = () => {
       },
     },
     {
-      key: COLUMN_KEYS.OPERATE,
       title: '',
       dataIndex: 'operate',
       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 [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
   const [idSort, setIdSort] = useState(false);
-  const [searchKeyword, setSearchKeyword] = useState('');
-  const [searchGroup, setSearchGroup] = useState('');
-  const [searchModel, setSearchModel] = useState('');
   const [searching, setSearching] = useState(false);
   const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
   const [channelCount, setChannelCount] = useState(pageSize);
@@ -745,6 +585,16 @@ const ChannelsTable = () => {
   const [testQueue, setTestQueue] = useState([]);
   const [isProcessingQueue, setIsProcessingQueue] = useState(false);
 
+  // Form API 引用
+  const [formApi, setFormApi] = useState(null);
+
+  // Form 初始值
+  const formInitValues = {
+    searchKeyword: '',
+    searchGroup: '',
+    searchModel: '',
+  };
+
   const removeRecord = (record) => {
     let newDataSource = [...channels];
     if (record.id != null) {
@@ -896,15 +746,11 @@ const ChannelsTable = () => {
   };
 
   const refresh = async () => {
+    const { searchKeyword, searchGroup, searchModel } = getFormValues();
     if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
       await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
     } else {
-      await searchChannels(
-        searchKeyword,
-        searchGroup,
-        searchModel,
-        enableTagMode,
-      );
+      await searchChannels(enableTagMode);
     }
   };
 
@@ -1010,29 +856,40 @@ const ChannelsTable = () => {
     }
   };
 
-  const searchChannels = async (
-    searchKeyword,
-    searchGroup,
-    searchModel,
-    enableTagMode,
-  ) => {
-    if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
-      await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
-      // setActivePage(1);
-      return;
-    }
+  // 获取表单值的辅助函数,确保所有值都是字符串
+  const getFormValues = () => {
+    const formValues = formApi ? formApi.getValues() : {};
+    return {
+      searchKeyword: formValues.searchKeyword || '',
+      searchGroup: formValues.searchGroup || '',
+      searchModel: formValues.searchModel || '',
+    };
+  };
+
+  const searchChannels = async (enableTagMode) => {
+    const { searchKeyword, searchGroup, searchModel } = getFormValues();
+
     setSearching(true);
-    const res = await API.get(
-      `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}`,
-    );
-    const { success, message, data } = res.data;
-    if (success) {
-      setChannelFormat(data, enableTagMode);
-      setActivePage(1);
-    } else {
-      showError(message);
+    try {
+      if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
+        await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
+        // setActivePage(1);
+        return;
+      }
+
+      const res = await API.get(
+        `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}`,
+      );
+      const { success, message, data } = res.data;
+      if (success) {
+        setChannelFormat(data, enableTagMode);
+        setActivePage(1);
+      } else {
+        showError(message);
+      }
+    } finally {
+      setSearching(false);
     }
-    setSearching(false);
   };
 
   const updateChannelProperty = (channelId, updateFn) => {
@@ -1540,71 +1397,83 @@ const ChannelsTable = () => {
           >
             {t('刷新')}
           </Button>
-
-          <Button
-            theme='light'
-            type='tertiary'
-            icon={<IconSetting />}
-            onClick={() => setShowColumnSelector(true)}
-            className="!rounded-full w-full md:w-auto"
-          >
-            {t('列设置')}
-          </Button>
         </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="relative w-full md:w-64">
-            <Input
-              prefix={<IconSearch />}
-              placeholder={t('搜索渠道的 ID,名称,密钥和API地址 ...')}
-              value={searchKeyword}
-              loading={searching}
-              onChange={(v) => {
-                setSearchKeyword(v.trim());
-              }}
-              className="!rounded-full"
-              showClear
-            />
-          </div>
-          <div className="w-full md:w-48">
-            <Input
-              prefix={<IconFilter />}
-              placeholder={t('模型关键字')}
-              value={searchModel}
-              loading={searching}
-              onChange={(v) => {
-                setSearchModel(v.trim());
-              }}
-              className="!rounded-full"
-              showClear
-            />
-          </div>
-          <div className="w-full md:w-48">
-            <Select
-              placeholder={t('选择分组')}
-              optionList={[
-                { label: t('选择分组'), value: null },
-                ...groupOptions,
-              ]}
-              value={searchGroup}
-              onChange={(v) => {
-                setSearchGroup(v);
-                searchChannels(searchKeyword, v, searchModel, enableTagMode);
-              }}
-              className="!rounded-full w-full"
-              showClear
-            />
-          </div>
-          <Button
-            type="primary"
-            onClick={() => {
-              searchChannels(searchKeyword, searchGroup, searchModel, enableTagMode);
-            }}
-            loading={searching}
-            className="!rounded-full w-full md:w-auto"
+          <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"
           >
-            {t('查询')}
-          </Button>
+            <div className="relative w-full md:w-64">
+              <Form.Input
+                field="searchKeyword"
+                prefix={<IconSearch />}
+                placeholder={t('搜索渠道的 ID,名称,密钥和API地址 ...')}
+                className="!rounded-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="w-full md:w-48">
+              <Form.Input
+                field="searchModel"
+                prefix={<IconFilter />}
+                placeholder={t('模型关键字')}
+                className="!rounded-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="w-full md:w-48">
+              <Form.Select
+                field="searchGroup"
+                placeholder={t('选择分组')}
+                optionList={[
+                  { label: t('选择分组'), value: null },
+                  ...groupOptions,
+                ]}
+                className="!rounded-full w-full"
+                showClear
+                pure
+                onChange={() => {
+                  // 延迟执行搜索,让表单值先更新
+                  setTimeout(() => {
+                    searchChannels(enableTagMode);
+                  }, 0);
+                }}
+              />
+            </div>
+            <Button
+              type="primary"
+              htmlType="submit"
+              loading={loading || searching}
+              className="!rounded-full w-full md:w-auto"
+            >
+              {t('查询')}
+            </Button>
+            <Button
+              theme='light'
+              onClick={() => {
+                if (formApi) {
+                  formApi.reset();
+                  // 重置后立即查询,使用setTimeout确保表单重置完成
+                  setTimeout(() => {
+                    refresh();
+                  }, 100);
+                }
+              }}
+              className="!rounded-full w-full md:w-auto"
+            >
+              {t('重置')}
+            </Button>
+          </Form>
         </div>
       </div>
     </div>
@@ -1612,7 +1481,6 @@ const ChannelsTable = () => {
 
   return (
     <>
-      {renderColumnSelector()}
       <EditTagModal
         visible={showEditTag}
         tag={editingTag}
@@ -1633,7 +1501,7 @@ const ChannelsTable = () => {
         bordered={false}
       >
         <Table
-          columns={getVisibleColumns()}
+          columns={columns}
           dataSource={pageData}
           scroll={{ x: 'max-content' }}
           pagination={{
@@ -1663,6 +1531,14 @@ const ChannelsTable = () => {
               }
               : null
           }
+          empty={
+            <Empty
+              image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
+              darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+              description={t('搜索无结果')}
+              style={{ padding: 30 }}
+            />
+          }
           className="rounded-xl overflow-hidden"
           size="middle"
           loading={loading}

+ 239 - 141
web/src/components/table/LogsTable.js

@@ -1,5 +1,18 @@
 import React, { useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
+import {
+  CreditCard,
+  ShoppingCart,
+  Settings,
+  Server,
+  AlertTriangle,
+  HelpCircle,
+  Zap,
+  Play,
+  Clock,
+  Hash,
+  Key
+} from 'lucide-react';
 import {
   API,
   copy,
@@ -20,16 +33,16 @@ import {
   renderQuota,
   stringToColor,
   getLogOther,
-  renderModelTag,
+  renderModelTag
 } from '../../helpers';
 
 import {
   Avatar,
   Button,
   Descriptions,
+  Empty,
   Modal,
   Popover,
-  Select,
   Space,
   Spin,
   Table,
@@ -39,24 +52,18 @@ import {
   Card,
   Typography,
   Divider,
-  Input,
-  DatePicker,
+  Form
 } from '@douyinfe/semi-ui';
+import {
+  IllustrationNoResult,
+  IllustrationNoResultDark
+} from '@douyinfe/semi-illustrations';
 import { ITEMS_PER_PAGE } from '../../constants';
 import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
 import { IconSetting, IconSearch, IconForward } from '@douyinfe/semi-icons';
 
 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 = [
   'amber',
   'blue',
@@ -238,11 +245,6 @@ const LogsTable = () => {
                 onClick: (event) => {
                   copyText(event, record.model_name).then((r) => { });
                 },
-                suffixIcon: (
-                  <IconForward
-                    style={{ width: '0.9em', height: '0.9em', opacity: 0.75 }}
-                  />
-                ),
               })}
             </Popover>
           </Space>
@@ -737,39 +739,67 @@ const LogsTable = () => {
   const [logType, setLogType] = useState(0);
   const isAdminUser = isAdmin();
   let now = new Date();
-  // 初始化start_timestamp为今天0点
-  const [inputs, setInputs] = useState({
+
+  // Form 初始值
+  const formInitValues = {
     username: '',
     token_name: '',
     model_name: '',
-    start_timestamp: timestamp2string(getTodayStartTimestamp()),
-    end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
     channel: '',
     group: '',
-  });
-  const {
-    username,
-    token_name,
-    model_name,
-    start_timestamp,
-    end_timestamp,
-    channel,
-    group,
-  } = inputs;
+    dateRange: [
+      timestamp2string(getTodayStartTimestamp()),
+      timestamp2string(now.getTime() / 1000 + 3600)
+    ],
+    logType: '0',
+  };
 
   const [stat, setStat] = useState({
     quota: 0,
     token: 0,
   });
 
-  const handleInputChange = (value, name) => {
-    setInputs((inputs) => ({ ...inputs, [name]: value }));
+  // Form API 引用
+  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 {
+      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 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);
     let res = await API.get(url);
     const { success, message, data } = res.data;
@@ -781,9 +811,20 @@ const LogsTable = () => {
   };
 
   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 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);
     let res = await API.get(url);
     const { success, message, data } = res.data;
@@ -1016,16 +1057,30 @@ const LogsTable = () => {
     setLogs(logs);
   };
 
-  const loadLogs = async (startIdx, pageSize, logType = 0) => {
+  const loadLogs = async (startIdx, pageSize, customLogType = null) => {
     setLoading(true);
 
     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 localEndTimestamp = Date.parse(end_timestamp) / 1000;
     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 {
-      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);
     const res = await API.get(url);
@@ -1045,7 +1100,7 @@ const LogsTable = () => {
 
   const handlePageChange = (page) => {
     setActivePage(page);
-    loadLogs(page, pageSize, logType).then((r) => { });
+    loadLogs(page, pageSize).then((r) => { }); // 不传入logType,让其从表单获取最新值
   };
 
   const handlePageSizeChange = async (size) => {
@@ -1062,7 +1117,7 @@ const LogsTable = () => {
   const refresh = async () => {
     setActivePage(1);
     handleEyeClick();
-    await loadLogs(activePage, pageSize, logType);
+    await loadLogs(1, pageSize); // 不传入logType,让其从表单获取最新值
   };
 
   const copyText = async (e, text) => {
@@ -1083,9 +1138,15 @@ const LogsTable = () => {
       .catch((reason) => {
         showError(reason);
       });
-    handleEyeClick();
   }, []);
 
+  // 当 formApi 可用时,初始化统计
+  useEffect(() => {
+    if (formApi) {
+      handleEyeClick();
+    }
+  }, [formApi]);
+
   const expandRowRender = (record, index) => {
     return <Descriptions data={expandData[record.key]} />;
   };
@@ -1149,115 +1210,144 @@ const LogsTable = () => {
             <Divider margin='12px' />
 
             {/* 搜索表单区域 */}
-            <div className='flex flex-col gap-4'>
-              <div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'>
-                {/* 时间选择器 */}
-                <div className='col-span-1 lg:col-span-2'>
-                  <DatePicker
-                    className='w-full'
-                    value={[start_timestamp, end_timestamp]}
-                    type='dateTimeRange'
-                    onChange={(value) => {
-                      if (Array.isArray(value) && value.length === 2) {
-                        handleInputChange(value[0], 'start_timestamp');
-                        handleInputChange(value[1], 'end_timestamp');
-                      }
-                    }}
+            <Form
+              initValues={formInitValues}
+              getFormApi={(api) => setFormApi(api)}
+              onSubmit={refresh}
+              allowEmpty={true}
+              autoComplete="off"
+              layout="vertical"
+              trigger="change"
+              stopValidateWithError={false}
+            >
+              <div className='flex flex-col gap-4'>
+                <div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'>
+                  {/* 时间选择器 */}
+                  <div className='col-span-1 lg:col-span-2'>
+                    <Form.DatePicker
+                      field='dateRange'
+                      className='w-full'
+                      type='dateTimeRange'
+                      placeholder={[t('开始时间'), t('结束时间')]}
+                      showClear
+                      pure
+                    />
+                  </div>
+
+                  {/* 其他搜索字段 */}
+                  <Form.Input
+                    field='token_name'
+                    prefix={<IconSearch />}
+                    placeholder={t('令牌名称')}
+                    className='!rounded-full'
+                    showClear
+                    pure
                   />
-                </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>
+                  <Form.Input
+                    field='model_name'
+                    prefix={<IconSearch />}
+                    placeholder={t('模型名称')}
+                    className='!rounded-full'
+                    showClear
+                    pure
+                  />
 
-                {/* 其他搜索字段 */}
-                <Input
-                  prefix={<IconSearch />}
-                  placeholder={t('令牌名称')}
-                  value={token_name}
-                  onChange={(value) => handleInputChange(value, 'token_name')}
-                  className='!rounded-full'
-                  showClear
-                />
+                  <Form.Input
+                    field='group'
+                    prefix={<IconSearch />}
+                    placeholder={t('分组')}
+                    className='!rounded-full'
+                    showClear
+                    pure
+                  />
 
-                <Input
-                  prefix={<IconSearch />}
-                  placeholder={t('模型名称')}
-                  value={model_name}
-                  onChange={(value) => handleInputChange(value, 'model_name')}
-                  className='!rounded-full'
-                  showClear
-                />
+                  {isAdminUser && (
+                    <>
+                      <Form.Input
+                        field='channel'
+                        prefix={<IconSearch />}
+                        placeholder={t('渠道 ID')}
+                        className='!rounded-full'
+                        showClear
+                        pure
+                      />
+                      <Form.Input
+                        field='username'
+                        prefix={<IconSearch />}
+                        placeholder={t('用户名称')}
+                        className='!rounded-full'
+                        showClear
+                        pure
+                      />
+                    </>
+                  )}
+                </div>
 
-                <Input
-                  prefix={<IconSearch />}
-                  placeholder={t('分组')}
-                  value={group}
-                  onChange={(value) => handleInputChange(value, 'group')}
-                  className='!rounded-full'
-                  showClear
-                />
+                {/* 操作按钮区域 */}
+                <div className='flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3'>
+                  {/* 日志类型选择器 */}
+                  <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>
 
-                {isAdminUser && (
-                  <>
-                    <Input
-                      prefix={<IconSearch />}
-                      placeholder={t('渠道 ID')}
-                      value={channel}
-                      onChange={(value) => handleInputChange(value, 'channel')}
+                  <div className='flex gap-2 w-full sm:w-auto justify-end'>
+                    <Button
+                      type='primary'
+                      htmlType='submit'
+                      loading={loading}
                       className='!rounded-full'
-                      showClear
-                    />
-                    <Input
-                      prefix={<IconSearch />}
-                      placeholder={t('用户名称')}
-                      value={username}
-                      onChange={(value) => handleInputChange(value, 'username')}
+                    >
+                      {t('查询')}
+                    </Button>
+                    <Button
+                      theme='light'
+                      onClick={() => {
+                        if (formApi) {
+                          formApi.reset();
+                          setLogType(0);
+                          // 重置后立即查询,使用setTimeout确保表单重置完成
+                          setTimeout(() => {
+                            refresh();
+                          }, 100);
+                        }
+                      }}
                       className='!rounded-full'
-                      showClear
-                    />
-                  </>
-                )}
-              </div>
-
-              {/* 操作按钮区域 */}
-              <div className='flex justify-between items-center pt-2'>
-                <div></div>
-                <div className='flex gap-2'>
-                  <Button
-                    type='primary'
-                    onClick={refresh}
-                    loading={loading}
-                    className='!rounded-full'
-                  >
-                    {t('查询')}
-                  </Button>
-                  <Button
-                    theme='light'
-                    type='tertiary'
-                    icon={<IconSetting />}
-                    onClick={() => setShowColumnSelector(true)}
-                    className='!rounded-full'
-                  >
-                    {t('列设置')}
-                  </Button>
+                    >
+                      {t('重置')}
+                    </Button>
+                    <Button
+                      theme='light'
+                      type='tertiary'
+                      icon={<IconSetting />}
+                      onClick={() => setShowColumnSelector(true)}
+                      className='!rounded-full'
+                    >
+                      {t('列设置')}
+                    </Button>
+                  </div>
                 </div>
               </div>
-            </div>
+            </Form>
           </div>
         }
         shadows='always'
@@ -1276,6 +1366,14 @@ const LogsTable = () => {
           scroll={{ x: 'max-content' }}
           className='rounded-xl overflow-hidden'
           size='middle'
+          empty={
+            <Empty
+              image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
+              darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+              description={t('搜索无结果')}
+              style={{ padding: 30 }}
+            />
+          }
           pagination={{
             formatPageText: (page) =>
               t('第 {{start}} - {{end}} 条,共 {{total}} 条', {

+ 188 - 103
web/src/components/table/MjLogsTable.js

@@ -1,35 +1,65 @@
 import React, { useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
+import {
+  Palette,
+  ZoomIn,
+  Shuffle,
+  Move,
+  FileText,
+  Blend,
+  Upload,
+  Minimize2,
+  RotateCcw,
+  PaintBucket,
+  Focus,
+  Move3D,
+  Monitor,
+  UserCheck,
+  HelpCircle,
+  CheckCircle,
+  Clock,
+  Copy,
+  FileX,
+  Pause,
+  XCircle,
+  Loader,
+  AlertCircle,
+  Hash
+} from 'lucide-react';
 import {
   API,
   copy,
   isAdmin,
   showError,
   showSuccess,
-  timestamp2string,
+  timestamp2string
 } from '../../helpers';
 
 import {
   Button,
   Card,
   Checkbox,
-  DatePicker,
   Divider,
+  Empty,
+  Form,
   ImagePreview,
-  Input,
   Layout,
   Modal,
   Progress,
   Skeleton,
   Table,
   Tag,
-  Typography,
+  Typography
 } from '@douyinfe/semi-ui';
+import {
+  IllustrationNoResult,
+  IllustrationNoResultDark
+} from '@douyinfe/semi-illustrations';
 import { ITEMS_PER_PAGE } from '../../constants';
 import {
   IconEyeOpened,
   IconSearch,
-  IconSetting,
+  IconSetting
 } from '@douyinfe/semi-icons';
 
 const { Text } = Typography;
@@ -154,103 +184,103 @@ const LogsTable = () => {
     switch (type) {
       case 'IMAGINE':
         return (
-          <Tag color='blue' size='large' shape='circle'>
+          <Tag color='blue' size='large' shape='circle' prefixIcon={<Palette size={14} />}>
             {t('绘图')}
           </Tag>
         );
       case 'UPSCALE':
         return (
-          <Tag color='orange' size='large' shape='circle'>
+          <Tag color='orange' size='large' shape='circle' prefixIcon={<ZoomIn size={14} />}>
             {t('放大')}
           </Tag>
         );
       case 'VARIATION':
         return (
-          <Tag color='purple' size='large' shape='circle'>
+          <Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
             {t('变换')}
           </Tag>
         );
       case 'HIGH_VARIATION':
         return (
-          <Tag color='purple' size='large' shape='circle'>
+          <Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
             {t('强变换')}
           </Tag>
         );
       case 'LOW_VARIATION':
         return (
-          <Tag color='purple' size='large' shape='circle'>
+          <Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
             {t('弱变换')}
           </Tag>
         );
       case 'PAN':
         return (
-          <Tag color='cyan' size='large' shape='circle'>
+          <Tag color='cyan' size='large' shape='circle' prefixIcon={<Move size={14} />}>
             {t('平移')}
           </Tag>
         );
       case 'DESCRIBE':
         return (
-          <Tag color='yellow' size='large' shape='circle'>
+          <Tag color='yellow' size='large' shape='circle' prefixIcon={<FileText size={14} />}>
             {t('图生文')}
           </Tag>
         );
       case 'BLEND':
         return (
-          <Tag color='lime' size='large' shape='circle'>
+          <Tag color='lime' size='large' shape='circle' prefixIcon={<Blend size={14} />}>
             {t('图混合')}
           </Tag>
         );
       case 'UPLOAD':
         return (
-          <Tag color='blue' size='large' shape='circle'>
+          <Tag color='blue' size='large' shape='circle' prefixIcon={<Upload size={14} />}>
             上传文件
           </Tag>
         );
       case 'SHORTEN':
         return (
-          <Tag color='pink' size='large' shape='circle'>
+          <Tag color='pink' size='large' shape='circle' prefixIcon={<Minimize2 size={14} />}>
             {t('缩词')}
           </Tag>
         );
       case 'REROLL':
         return (
-          <Tag color='indigo' size='large' shape='circle'>
+          <Tag color='indigo' size='large' shape='circle' prefixIcon={<RotateCcw size={14} />}>
             {t('重绘')}
           </Tag>
         );
       case 'INPAINT':
         return (
-          <Tag color='violet' size='large' shape='circle'>
+          <Tag color='violet' size='large' shape='circle' prefixIcon={<PaintBucket size={14} />}>
             {t('局部重绘-提交')}
           </Tag>
         );
       case 'ZOOM':
         return (
-          <Tag color='teal' size='large' shape='circle'>
+          <Tag color='teal' size='large' shape='circle' prefixIcon={<Focus size={14} />}>
             {t('变焦')}
           </Tag>
         );
       case 'CUSTOM_ZOOM':
         return (
-          <Tag color='teal' size='large' shape='circle'>
+          <Tag color='teal' size='large' shape='circle' prefixIcon={<Move3D size={14} />}>
             {t('自定义变焦-提交')}
           </Tag>
         );
       case 'MODAL':
         return (
-          <Tag color='green' size='large' shape='circle'>
+          <Tag color='green' size='large' shape='circle' prefixIcon={<Monitor size={14} />}>
             {t('窗口处理')}
           </Tag>
         );
       case 'SWAP_FACE':
         return (
-          <Tag color='light-green' size='large' shape='circle'>
+          <Tag color='light-green' size='large' shape='circle' prefixIcon={<UserCheck size={14} />}>
             {t('换脸')}
           </Tag>
         );
       default:
         return (
-          <Tag color='white' size='large' shape='circle'>
+          <Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
             {t('未知')}
           </Tag>
         );
@@ -261,31 +291,31 @@ const LogsTable = () => {
     switch (code) {
       case 1:
         return (
-          <Tag color='green' size='large' shape='circle'>
+          <Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
             {t('已提交')}
           </Tag>
         );
       case 21:
         return (
-          <Tag color='lime' size='large' shape='circle'>
+          <Tag color='lime' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
             {t('等待中')}
           </Tag>
         );
       case 22:
         return (
-          <Tag color='orange' size='large' shape='circle'>
+          <Tag color='orange' size='large' shape='circle' prefixIcon={<Copy size={14} />}>
             {t('重复提交')}
           </Tag>
         );
       case 0:
         return (
-          <Tag color='yellow' size='large' shape='circle'>
+          <Tag color='yellow' size='large' shape='circle' prefixIcon={<FileX size={14} />}>
             {t('未提交')}
           </Tag>
         );
       default:
         return (
-          <Tag color='white' size='large' shape='circle'>
+          <Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
             {t('未知')}
           </Tag>
         );
@@ -296,43 +326,43 @@ const LogsTable = () => {
     switch (type) {
       case 'SUCCESS':
         return (
-          <Tag color='green' size='large' shape='circle'>
+          <Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
             {t('成功')}
           </Tag>
         );
       case 'NOT_START':
         return (
-          <Tag color='grey' size='large' shape='circle'>
+          <Tag color='grey' size='large' shape='circle' prefixIcon={<Pause size={14} />}>
             {t('未启动')}
           </Tag>
         );
       case 'SUBMITTED':
         return (
-          <Tag color='yellow' size='large' shape='circle'>
+          <Tag color='yellow' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
             {t('队列中')}
           </Tag>
         );
       case 'IN_PROGRESS':
         return (
-          <Tag color='blue' size='large' shape='circle'>
+          <Tag color='blue' size='large' shape='circle' prefixIcon={<Loader size={14} />}>
             {t('执行中')}
           </Tag>
         );
       case 'FAILURE':
         return (
-          <Tag color='red' size='large' shape='circle'>
+          <Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
             {t('失败')}
           </Tag>
         );
       case 'MODAL':
         return (
-          <Tag color='yellow' size='large' shape='circle'>
+          <Tag color='yellow' size='large' shape='circle' prefixIcon={<AlertCircle size={14} />}>
             {t('窗口等待')}
           </Tag>
         );
       default:
         return (
-          <Tag color='white' size='large' shape='circle'>
+          <Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
             {t('未知')}
           </Tag>
         );
@@ -362,7 +392,7 @@ const LogsTable = () => {
     const color = durationSec > 60 ? 'red' : 'green';
 
     return (
-      <Tag color={color} size='large' shape='circle'>
+      <Tag color={color} size='large' shape='circle' prefixIcon={<Clock size={14} />}>
         {durationSec} {t('秒')}
       </Tag>
     );
@@ -398,6 +428,7 @@ const LogsTable = () => {
               color={colors[parseInt(text) % colors.length]}
               size='large'
               shape='circle'
+              prefixIcon={<Hash size={14} />}
               onClick={() => {
                 copyText(text);
               }}
@@ -462,7 +493,7 @@ const LogsTable = () => {
                 percent={text ? parseInt(text.replace('%', '')) : 0}
                 showInfo={true}
                 aria-label='drawing progress'
-                style={{ minWidth: '200px' }}
+                style={{ minWidth: '160px' }}
               />
             }
           </div>
@@ -483,6 +514,7 @@ const LogsTable = () => {
               setModalImageUrl(text);
               setIsModalOpenurl(true);
             }}
+            className="!rounded-full"
           >
             {t('查看图片')}
           </Button>
@@ -570,7 +602,6 @@ const LogsTable = () => {
   const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
   const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
-  const [logType, setLogType] = useState(0);
   const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
   const [isModalOpenurl, setIsModalOpenurl] = useState(false);
   const [showBanner, setShowBanner] = useState(false);
@@ -578,22 +609,44 @@ const LogsTable = () => {
   // 定义模态框图片URL的状态和更新函数
   const [modalImageUrl, setModalImageUrl] = useState('');
   let now = new Date();
-  // 初始化start_timestamp为前一天
-  const [inputs, setInputs] = useState({
+
+  // Form 初始值
+  const formInitValues = {
     channel_id: '',
     mj_id: '',
-    start_timestamp: timestamp2string(now.getTime() / 1000 - 2592000),
-    end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
-  });
-  const { channel_id, mj_id, start_timestamp, end_timestamp } = inputs;
+    dateRange: [
+      timestamp2string(now.getTime() / 1000 - 2592000),
+      timestamp2string(now.getTime() / 1000 + 3600)
+    ],
+  };
+
+  // Form API 引用
+  const [formApi, setFormApi] = useState(null);
 
   const [stat, setStat] = useState({
     quota: 0,
     token: 0,
   });
 
-  const 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) => {
@@ -611,6 +664,7 @@ const LogsTable = () => {
     setLoading(true);
 
     let url = '';
+    const { channel_id, mj_id, start_timestamp, end_timestamp } = getFormValues();
     let localStartTimestamp = Date.parse(start_timestamp);
     let localEndTimestamp = Date.parse(end_timestamp);
     if (isAdminUser) {
@@ -673,7 +727,7 @@ const LogsTable = () => {
     const localPageSize = parseInt(localStorage.getItem('mj-page-size')) || ITEMS_PER_PAGE;
     setPageSize(localPageSize);
     loadLogs(0, localPageSize).then();
-  }, [logType]);
+  }, []);
 
   useEffect(() => {
     const mjNotifyEnabled = localStorage.getItem('mj_notify_enabled');
@@ -788,70 +842,93 @@ const LogsTable = () => {
               <Divider margin="12px" />
 
               {/* 搜索表单区域 */}
-              <div className="flex flex-col gap-4">
-                <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
-                  {/* 时间选择器 */}
-                  <div className="col-span-1 lg:col-span-2">
-                    <DatePicker
-                      className="w-full"
-                      value={[start_timestamp, end_timestamp]}
-                      type='dateTimeRange'
-                      onChange={(value) => {
-                        if (Array.isArray(value) && value.length === 2) {
-                          handleInputChange(value[0], 'start_timestamp');
-                          handleInputChange(value[1], 'end_timestamp');
-                        }
-                      }}
-                    />
-                  </div>
-
-                  {/* 任务 ID */}
-                  <Input
-                    prefix={<IconSearch />}
-                    placeholder={t('任务 ID')}
-                    value={mj_id}
-                    onChange={(value) => handleInputChange(value, 'mj_id')}
-                    className="!rounded-full"
-                    showClear
-                  />
-
-                  {/* 渠道 ID - 仅管理员可见 */}
-                  {isAdminUser && (
-                    <Input
+              <Form
+                initValues={formInitValues}
+                getFormApi={(api) => setFormApi(api)}
+                onSubmit={refresh}
+                allowEmpty={true}
+                autoComplete="off"
+                layout="vertical"
+                trigger="change"
+                stopValidateWithError={false}
+              >
+                <div className="flex flex-col gap-4">
+                  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
+                    {/* 时间选择器 */}
+                    <div className="col-span-1 lg:col-span-2">
+                      <Form.DatePicker
+                        field='dateRange'
+                        className="w-full"
+                        type='dateTimeRange'
+                        placeholder={[t('开始时间'), t('结束时间')]}
+                        showClear
+                        pure
+                      />
+                    </div>
+
+                    {/* 任务 ID */}
+                    <Form.Input
+                      field='mj_id'
                       prefix={<IconSearch />}
-                      placeholder={t('渠道 ID')}
-                      value={channel_id}
-                      onChange={(value) => handleInputChange(value, 'channel_id')}
+                      placeholder={t('任务 ID')}
                       className="!rounded-full"
                       showClear
+                      pure
                     />
-                  )}
-                </div>
 
-                {/* 操作按钮区域 */}
-                <div className="flex justify-between items-center pt-2">
-                  <div></div>
-                  <div className="flex gap-2">
-                    <Button
-                      type='primary'
-                      onClick={refresh}
-                      loading={loading}
-                      className="!rounded-full"
-                    >
-                      {t('查询')}
-                    </Button>
-                    <Button
-                      theme='light'
-                      type='tertiary'
-                      icon={<IconSetting />}
-                      onClick={() => setShowColumnSelector(true)}
-                      className="!rounded-full"
-                    >
-                      {t('列设置')}
-                    </Button>
+                    {/* 渠道 ID - 仅管理员可见 */}
+                    {isAdminUser && (
+                      <Form.Input
+                        field='channel_id'
+                        prefix={<IconSearch />}
+                        placeholder={t('渠道 ID')}
+                        className="!rounded-full"
+                        showClear
+                        pure
+                      />
+                    )}
+                  </div>
+
+                  {/* 操作按钮区域 */}
+                  <div className="flex justify-between items-center">
+                    <div></div>
+                    <div className="flex gap-2">
+                      <Button
+                        type='primary'
+                        htmlType='submit'
+                        loading={loading}
+                        className="!rounded-full"
+                      >
+                        {t('查询')}
+                      </Button>
+                      <Button
+                        theme='light'
+                        onClick={() => {
+                          if (formApi) {
+                            formApi.reset();
+                            // 重置后立即查询,使用setTimeout确保表单重置完成
+                            setTimeout(() => {
+                              refresh();
+                            }, 100);
+                          }
+                        }}
+                        className="!rounded-full"
+                      >
+                        {t('重置')}
+                      </Button>
+                      <Button
+                        theme='light'
+                        type='tertiary'
+                        icon={<IconSetting />}
+                        onClick={() => setShowColumnSelector(true)}
+                        className="!rounded-full"
+                      >
+                        {t('列设置')}
+                      </Button>
+                    </div>
                   </div>
                 </div>
-              </div>
+              </Form>
             </div>
           }
           shadows='always'
@@ -865,6 +942,14 @@ const LogsTable = () => {
             scroll={{ x: 'max-content' }}
             className="rounded-xl overflow-hidden"
             size="middle"
+            empty={
+              <Empty
+                image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
+                darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+                description={t('搜索无结果')}
+                style={{ padding: 30 }}
+              />
+            }
             pagination={{
               formatPageText: (page) =>
                 t('第 {{start}} - {{end}} 条,共 {{total}} 条', {

+ 14 - 1
web/src/components/table/ModelPricing.js

@@ -17,14 +17,19 @@ import {
   Tabs,
   TabPane,
   Dropdown,
+  Empty
 } from '@douyinfe/semi-ui';
+import {
+  IllustrationNoResult,
+  IllustrationNoResultDark
+} from '@douyinfe/semi-illustrations';
 import {
   IconVerify,
   IconHelpCircle,
   IconSearch,
   IconCopy,
   IconInfoCircle,
-  IconLayers,
+  IconLayers
 } from '@douyinfe/semi-icons';
 import { UserContext } from '../../context/User/index.js';
 import { AlertCircle } from 'lucide-react';
@@ -489,6 +494,14 @@ const ModelPricing = () => {
         loading={loading}
         rowSelection={rowSelection}
         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={{
           defaultPageSize: 10,
           pageSize: pageSize,

+ 113 - 36
web/src/components/table/RedemptionsTable.js

@@ -8,20 +8,33 @@ import {
   renderQuota
 } from '../../helpers';
 
+import {
+  CheckCircle,
+  XCircle,
+  Minus,
+  HelpCircle,
+  Coins
+} from 'lucide-react';
+
 import { ITEMS_PER_PAGE } from '../../constants';
 import {
   Button,
   Card,
   Divider,
   Dropdown,
-  Input,
+  Empty,
+  Form,
   Modal,
   Popover,
   Space,
   Table,
   Tag,
-  Typography,
+  Typography
 } from '@douyinfe/semi-ui';
+import {
+  IllustrationNoResult,
+  IllustrationNoResultDark
+} from '@douyinfe/semi-illustrations';
 import {
   IconPlus,
   IconCopy,
@@ -31,7 +44,7 @@ import {
   IconDelete,
   IconStop,
   IconPlay,
-  IconMore,
+  IconMore
 } from '@douyinfe/semi-icons';
 import EditRedemption from '../../pages/Redemption/EditRedemption';
 import { useTranslation } from 'react-i18next';
@@ -49,25 +62,25 @@ const RedemptionsTable = () => {
     switch (status) {
       case 1:
         return (
-          <Tag color='green' size='large' shape='circle'>
+          <Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
             {t('未使用')}
           </Tag>
         );
       case 2:
         return (
-          <Tag color='red' size='large' shape='circle'>
+          <Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
             {t('已禁用')}
           </Tag>
         );
       case 3:
         return (
-          <Tag color='grey' size='large' shape='circle'>
+          <Tag color='grey' size='large' shape='circle' prefixIcon={<Minus size={14} />}>
             {t('已使用')}
           </Tag>
         );
       default:
         return (
-          <Tag color='black' size='large' shape='circle'>
+          <Tag color='black' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
             {t('未知状态')}
           </Tag>
         );
@@ -95,7 +108,13 @@ const RedemptionsTable = () => {
       title: t('额度'),
       dataIndex: 'quota',
       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 [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
-  const [searchKeyword, setSearchKeyword] = useState('');
   const [searching, setSearching] = useState(false);
   const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE);
   const [selectedKeys, setSelectedKeys] = useState([]);
@@ -233,6 +251,22 @@ const RedemptionsTable = () => {
   });
   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 = () => {
     setShowEdit(false);
     setTimeout(() => {
@@ -340,8 +374,14 @@ const RedemptionsTable = () => {
     setLoading(false);
   };
 
-  const searchRedemptions = async (keyword, page, pageSize) => {
-    if (searchKeyword === '') {
+  const searchRedemptions = async (keyword = null, page, pageSize) => {
+    // 如果没有传递keyword参数,从表单获取值
+    if (keyword === null) {
+      const formValues = getFormValues();
+      keyword = formValues.searchKeyword;
+    }
+
+    if (keyword === '') {
       await loadRedemptions(page, pageSize);
       return;
     }
@@ -361,10 +401,6 @@ const RedemptionsTable = () => {
     setSearching(false);
   };
 
-  const handleKeywordChange = async (value) => {
-    setSearchKeyword(value.trim());
-  };
-
   const sortRedemption = (key) => {
     if (redemptions.length === 0) return;
     setLoading(true);
@@ -381,6 +417,7 @@ const RedemptionsTable = () => {
 
   const handlePageChange = (page) => {
     setActivePage(page);
+    const { searchKeyword } = getFormValues();
     if (searchKeyword === '') {
       loadRedemptions(page, pageSize).then();
     } else {
@@ -457,28 +494,59 @@ const RedemptionsTable = () => {
           </Button>
         </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="relative w-full md:w-64">
-            <Input
-              prefix={<IconSearch />}
-              placeholder={t('关键字(id或者名称)')}
-              value={searchKeyword}
-              onChange={handleKeywordChange}
-              className="!rounded-full"
-              showClear
-            />
+        <Form
+          initValues={formInitValues}
+          getFormApi={(api) => setFormApi(api)}
+          onSubmit={() => {
+            setActivePage(1);
+            searchRedemptions(null, 1, pageSize);
+          }}
+          allowEmpty={true}
+          autoComplete="off"
+          layout="horizontal"
+          trigger="change"
+          stopValidateWithError={false}
+          className="w-full md:w-auto order-1 md:order-2"
+        >
+          <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
+            <div className="relative w-full md:w-64">
+              <Form.Input
+                field="searchKeyword"
+                prefix={<IconSearch />}
+                placeholder={t('关键字(id或者名称)')}
+                className="!rounded-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="flex gap-2 w-full md:w-auto">
+              <Button
+                type="primary"
+                htmlType="submit"
+                loading={loading || searching}
+                className="!rounded-full flex-1 md:flex-initial md:w-auto"
+              >
+                {t('查询')}
+              </Button>
+              <Button
+                theme="light"
+                onClick={() => {
+                  if (formApi) {
+                    formApi.reset();
+                    // 重置后立即查询,使用setTimeout确保表单重置完成
+                    setTimeout(() => {
+                      setActivePage(1);
+                      loadRedemptions(1, pageSize);
+                    }, 100);
+                  }
+                }}
+                className="!rounded-full flex-1 md:flex-initial md:w-auto"
+              >
+                {t('重置')}
+              </Button>
+            </div>
           </div>
-          <Button
-            type="primary"
-            onClick={() => {
-              searchRedemptions(searchKeyword, 1, pageSize).then();
-            }}
-            loading={searching}
-            className="!rounded-full w-full md:w-auto"
-          >
-            {t('查询')}
-          </Button>
-        </div>
+        </Form>
       </div>
     </div>
   );
@@ -517,6 +585,7 @@ const RedemptionsTable = () => {
             onPageSizeChange: (size) => {
               setPageSize(size);
               setActivePage(1);
+              const { searchKeyword } = getFormValues();
               if (searchKeyword === '') {
                 loadRedemptions(1, size).then();
               } else {
@@ -528,6 +597,14 @@ const RedemptionsTable = () => {
           loading={loading}
           rowSelection={rowSelection}
           onRow={handleRow}
+          empty={
+            <Empty
+              image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
+              darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+              description={t('搜索无结果')}
+              style={{ padding: 30 }}
+            />
+          }
           className="rounded-xl overflow-hidden"
           size="middle"
         ></Table>

+ 159 - 87
web/src/components/table/TaskLogsTable.js

@@ -1,34 +1,51 @@
 import React, { useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
+import {
+  Music,
+  FileText,
+  HelpCircle,
+  CheckCircle,
+  Pause,
+  Clock,
+  Play,
+  XCircle,
+  Loader,
+  List,
+  Hash
+} from 'lucide-react';
 import {
   API,
   copy,
   isAdmin,
   showError,
   showSuccess,
-  timestamp2string,
+  timestamp2string
 } from '../../helpers';
 
 import {
   Button,
   Card,
   Checkbox,
-  DatePicker,
   Divider,
-  Input,
+  Empty,
+  Form,
   Layout,
   Modal,
   Progress,
   Skeleton,
   Table,
   Tag,
-  Typography,
+  Typography
 } from '@douyinfe/semi-ui';
+import {
+  IllustrationNoResult,
+  IllustrationNoResultDark
+} from '@douyinfe/semi-illustrations';
 import { ITEMS_PER_PAGE } from '../../constants';
 import {
   IconEyeOpened,
   IconSearch,
-  IconSetting,
+  IconSetting
 } from '@douyinfe/semi-icons';
 
 const { Text } = Typography;
@@ -97,7 +114,7 @@ function renderDuration(submit_time, finishTime) {
 
   // 返回带有样式的颜色标签
   return (
-    <Tag color={color} size='large'>
+    <Tag color={color} size='large' prefixIcon={<Clock size={14} />}>
       {durationSec} 秒
     </Tag>
   );
@@ -188,19 +205,19 @@ const LogsTable = () => {
     switch (type) {
       case 'MUSIC':
         return (
-          <Tag color='grey' size='large' shape='circle'>
+          <Tag color='grey' size='large' shape='circle' prefixIcon={<Music size={14} />}>
             {t('生成音乐')}
           </Tag>
         );
       case 'LYRICS':
         return (
-          <Tag color='pink' size='large' shape='circle'>
+          <Tag color='pink' size='large' shape='circle' prefixIcon={<FileText size={14} />}>
             {t('生成歌词')}
           </Tag>
         );
       default:
         return (
-          <Tag color='white' size='large' shape='circle'>
+          <Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
             {t('未知')}
           </Tag>
         );
@@ -211,13 +228,13 @@ const LogsTable = () => {
     switch (type) {
       case 'suno':
         return (
-          <Tag color='green' size='large' shape='circle'>
+          <Tag color='green' size='large' shape='circle' prefixIcon={<Music size={14} />}>
             Suno
           </Tag>
         );
       default:
         return (
-          <Tag color='white' size='large' shape='circle'>
+          <Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
             {t('未知')}
           </Tag>
         );
@@ -228,55 +245,55 @@ const LogsTable = () => {
     switch (type) {
       case 'SUCCESS':
         return (
-          <Tag color='green' size='large' shape='circle'>
+          <Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
             {t('成功')}
           </Tag>
         );
       case 'NOT_START':
         return (
-          <Tag color='grey' size='large' shape='circle'>
+          <Tag color='grey' size='large' shape='circle' prefixIcon={<Pause size={14} />}>
             {t('未启动')}
           </Tag>
         );
       case 'SUBMITTED':
         return (
-          <Tag color='yellow' size='large' shape='circle'>
+          <Tag color='yellow' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
             {t('队列中')}
           </Tag>
         );
       case 'IN_PROGRESS':
         return (
-          <Tag color='blue' size='large' shape='circle'>
+          <Tag color='blue' size='large' shape='circle' prefixIcon={<Play size={14} />}>
             {t('执行中')}
           </Tag>
         );
       case 'FAILURE':
         return (
-          <Tag color='red' size='large' shape='circle'>
+          <Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
             {t('失败')}
           </Tag>
         );
       case 'QUEUED':
         return (
-          <Tag color='orange' size='large' shape='circle'>
+          <Tag color='orange' size='large' shape='circle' prefixIcon={<List size={14} />}>
             {t('排队中')}
           </Tag>
         );
       case 'UNKNOWN':
         return (
-          <Tag color='white' size='large' shape='circle'>
+          <Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
             {t('未知')}
           </Tag>
         );
       case '':
         return (
-          <Tag color='grey' size='large' shape='circle'>
+          <Tag color='grey' size='large' shape='circle' prefixIcon={<Loader size={14} />}>
             {t('正在提交')}
           </Tag>
         );
       default:
         return (
-          <Tag color='white' size='large' shape='circle'>
+          <Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
             {t('未知')}
           </Tag>
         );
@@ -321,6 +338,7 @@ const LogsTable = () => {
               color={colors[parseInt(text) % colors.length]}
               size='large'
               shape='circle'
+              prefixIcon={<Hash size={14} />}
               onClick={() => {
                 copyText(text);
               }}
@@ -395,7 +413,7 @@ const LogsTable = () => {
                   percent={text ? parseInt(text.replace('%', '')) : 0}
                   showInfo={true}
                   aria-label='task progress'
-                  style={{ minWidth: '200px' }}
+                  style={{ minWidth: '160px' }}
                 />
               )
             }
@@ -437,21 +455,43 @@ const LogsTable = () => {
   const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
   const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
-  const [logType] = useState(0);
 
   let now = new Date();
   // 初始化start_timestamp为前一天
   let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate());
-  const [inputs, setInputs] = useState({
+
+  // Form 初始值
+  const formInitValues = {
     channel_id: '',
     task_id: '',
-    start_timestamp: timestamp2string(zeroNow.getTime() / 1000),
-    end_timestamp: '',
-  });
-  const { channel_id, task_id, start_timestamp, end_timestamp } = inputs;
+    dateRange: [
+      timestamp2string(zeroNow.getTime() / 1000),
+      timestamp2string(now.getTime() / 1000 + 3600)
+    ],
+  };
+
+  // Form API 引用
+  const [formApi, setFormApi] = useState(null);
+
+  // 获取表单值的辅助函数
+  const getFormValues = () => {
+    const formValues = formApi ? formApi.getValues() : {};
 
-  const handleInputChange = (value, name) => {
-    setInputs((inputs) => ({ ...inputs, [name]: value }));
+    // 处理时间范围
+    let start_timestamp = timestamp2string(zeroNow.getTime() / 1000);
+    let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
+
+    if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) {
+      start_timestamp = formValues.dateRange[0];
+      end_timestamp = formValues.dateRange[1];
+    }
+
+    return {
+      channel_id: formValues.channel_id || '',
+      task_id: formValues.task_id || '',
+      start_timestamp,
+      end_timestamp,
+    };
   };
 
   const setLogsFormat = (logs) => {
@@ -469,6 +509,7 @@ const LogsTable = () => {
     setLoading(true);
 
     let url = '';
+    const { channel_id, task_id, start_timestamp, end_timestamp } = getFormValues();
     let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000);
     let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000);
     if (isAdminUser) {
@@ -528,7 +569,7 @@ const LogsTable = () => {
     const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
     setPageSize(localPageSize);
     loadLogs(0, localPageSize).then();
-  }, [logType]);
+  }, []);
 
   // 列选择器模态框
   const renderColumnSelector = () => {
@@ -628,70 +669,93 @@ const LogsTable = () => {
               <Divider margin="12px" />
 
               {/* 搜索表单区域 */}
-              <div className="flex flex-col gap-4">
-                <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
-                  {/* 时间选择器 */}
-                  <div className="col-span-1 lg:col-span-2">
-                    <DatePicker
-                      className="w-full"
-                      value={[start_timestamp, end_timestamp]}
-                      type='dateTimeRange'
-                      onChange={(value) => {
-                        if (Array.isArray(value) && value.length === 2) {
-                          handleInputChange(value[0], 'start_timestamp');
-                          handleInputChange(value[1], 'end_timestamp');
-                        }
-                      }}
-                    />
-                  </div>
-
-                  {/* 任务 ID */}
-                  <Input
-                    prefix={<IconSearch />}
-                    placeholder={t('任务 ID')}
-                    value={task_id}
-                    onChange={(value) => handleInputChange(value, 'task_id')}
-                    className="!rounded-full"
-                    showClear
-                  />
-
-                  {/* 渠道 ID - 仅管理员可见 */}
-                  {isAdminUser && (
-                    <Input
+              <Form
+                initValues={formInitValues}
+                getFormApi={(api) => setFormApi(api)}
+                onSubmit={refresh}
+                allowEmpty={true}
+                autoComplete="off"
+                layout="vertical"
+                trigger="change"
+                stopValidateWithError={false}
+              >
+                <div className="flex flex-col gap-4">
+                  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
+                    {/* 时间选择器 */}
+                    <div className="col-span-1 lg:col-span-2">
+                      <Form.DatePicker
+                        field='dateRange'
+                        className="w-full"
+                        type='dateTimeRange'
+                        placeholder={[t('开始时间'), t('结束时间')]}
+                        showClear
+                        pure
+                      />
+                    </div>
+
+                    {/* 任务 ID */}
+                    <Form.Input
+                      field='task_id'
                       prefix={<IconSearch />}
-                      placeholder={t('渠道 ID')}
-                      value={channel_id}
-                      onChange={(value) => handleInputChange(value, 'channel_id')}
+                      placeholder={t('任务 ID')}
                       className="!rounded-full"
                       showClear
+                      pure
                     />
-                  )}
-                </div>
 
-                {/* 操作按钮区域 */}
-                <div className="flex justify-between items-center pt-2">
-                  <div></div>
-                  <div className="flex gap-2">
-                    <Button
-                      type='primary'
-                      onClick={refresh}
-                      loading={loading}
-                      className="!rounded-full"
-                    >
-                      {t('查询')}
-                    </Button>
-                    <Button
-                      theme='light'
-                      type='tertiary'
-                      icon={<IconSetting />}
-                      onClick={() => setShowColumnSelector(true)}
-                      className="!rounded-full"
-                    >
-                      {t('列设置')}
-                    </Button>
+                    {/* 渠道 ID - 仅管理员可见 */}
+                    {isAdminUser && (
+                      <Form.Input
+                        field='channel_id'
+                        prefix={<IconSearch />}
+                        placeholder={t('渠道 ID')}
+                        className="!rounded-full"
+                        showClear
+                        pure
+                      />
+                    )}
+                  </div>
+
+                  {/* 操作按钮区域 */}
+                  <div className="flex justify-between items-center">
+                    <div></div>
+                    <div className="flex gap-2">
+                      <Button
+                        type='primary'
+                        htmlType='submit'
+                        loading={loading}
+                        className="!rounded-full"
+                      >
+                        {t('查询')}
+                      </Button>
+                      <Button
+                        theme='light'
+                        onClick={() => {
+                          if (formApi) {
+                            formApi.reset();
+                            // 重置后立即查询,使用setTimeout确保表单重置完成
+                            setTimeout(() => {
+                              refresh();
+                            }, 100);
+                          }
+                        }}
+                        className="!rounded-full"
+                      >
+                        {t('重置')}
+                      </Button>
+                      <Button
+                        theme='light'
+                        type='tertiary'
+                        icon={<IconSetting />}
+                        onClick={() => setShowColumnSelector(true)}
+                        className="!rounded-full"
+                      >
+                        {t('列设置')}
+                      </Button>
+                    </div>
                   </div>
                 </div>
-              </div>
+              </Form>
             </div>
           }
           shadows='always'
@@ -705,6 +769,14 @@ const LogsTable = () => {
             scroll={{ x: 'max-content' }}
             className="rounded-xl overflow-hidden"
             size="middle"
+            empty={
+              <Empty
+                image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
+                darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+                description={t('搜索无结果')}
+                style={{ padding: 30 }}
+              />
+            }
             pagination={{
               formatPageText: (page) =>
                 t('第 {{start}} - {{end}} 条,共 {{total}} 条', {

+ 139 - 55
web/src/components/table/TokensTable.js

@@ -6,7 +6,8 @@ import {
   showSuccess,
   timestamp2string,
   renderGroup,
-  renderQuota
+  renderQuota,
+  getQuotaPerUnit
 } from '../../helpers';
 
 import { ITEMS_PER_PAGE } from '../../constants';
@@ -14,13 +15,29 @@ import {
   Button,
   Card,
   Dropdown,
+  Empty,
+  Form,
   Modal,
   Space,
   SplitButtonGroup,
   Table,
-  Tag,
-  Input,
+  Tag
 } from '@douyinfe/semi-ui';
+import {
+  IllustrationNoResult,
+  IllustrationNoResultDark
+} from '@douyinfe/semi-illustrations';
+
+import {
+  CheckCircle,
+  Shield,
+  XCircle,
+  Clock,
+  Gauge,
+  HelpCircle,
+  Infinity,
+  Coins
+} from 'lucide-react';
 
 import {
   IconPlus,
@@ -32,7 +49,7 @@ import {
   IconDelete,
   IconStop,
   IconPlay,
-  IconMore,
+  IconMore
 } from '@douyinfe/semi-icons';
 import EditToken from '../../pages/Token/EditToken';
 import { useTranslation } from 'react-i18next';
@@ -49,38 +66,38 @@ const TokensTable = () => {
       case 1:
         if (model_limits_enabled) {
           return (
-            <Tag color='green' size='large' shape='circle'>
+            <Tag color='green' size='large' shape='circle' prefixIcon={<Shield size={14} />}>
               {t('已启用:限制模型')}
             </Tag>
           );
         } else {
           return (
-            <Tag color='green' size='large' shape='circle'>
+            <Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
               {t('已启用')}
             </Tag>
           );
         }
       case 2:
         return (
-          <Tag color='red' size='large' shape='circle'>
+          <Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
             {t('已禁用')}
           </Tag>
         );
       case 3:
         return (
-          <Tag color='yellow' size='large' shape='circle'>
+          <Tag color='yellow' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
             {t('已过期')}
           </Tag>
         );
       case 4:
         return (
-          <Tag color='grey' size='large' shape='circle'>
+          <Tag color='grey' size='large' shape='circle' prefixIcon={<Gauge size={14} />}>
             {t('已耗尽')}
           </Tag>
         );
       default:
         return (
-          <Tag color='black' size='large' shape='circle'>
+          <Tag color='black' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
             {t('未知状态')}
           </Tag>
         );
@@ -111,21 +128,45 @@ const TokensTable = () => {
       title: t('已用额度'),
       dataIndex: 'used_quota',
       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('剩余额度'),
       dataIndex: 'remain_quota',
       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 (
           <div>
             {record.unlimited_quota ? (
-              <Tag size={'large'} color={'white'} shape='circle'>
+              <Tag size={'large'} color={'white'} shape='circle' prefixIcon={<Infinity size={14} />}>
                 {t('无限制')}
               </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))}
               </Tag>
             )}
@@ -335,14 +376,29 @@ const TokensTable = () => {
   const [tokenCount, setTokenCount] = useState(pageSize);
   const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
-  const [searchKeyword, setSearchKeyword] = useState('');
-  const [searchToken, setSearchToken] = useState('');
   const [searching, setSearching] = useState(false);
-  const [chats, setChats] = useState([]);
   const [editingToken, setEditingToken] = useState({
     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 = () => {
     setShowEdit(false);
     setTimeout(() => {
@@ -416,8 +472,6 @@ const TokensTable = () => {
     window.open(url, '_blank');
   };
 
-
-
   useEffect(() => {
     loadTokens(0)
       .then()
@@ -472,6 +526,7 @@ const TokensTable = () => {
   };
 
   const searchTokens = async () => {
+    const { searchKeyword, searchToken } = getFormValues();
     if (searchKeyword === '' && searchToken === '') {
       await loadTokens(0);
       setActivePage(1);
@@ -491,14 +546,6 @@ const TokensTable = () => {
     setSearching(false);
   };
 
-  const handleKeywordChange = async (value) => {
-    setSearchKeyword(value.trim());
-  };
-
-  const handleSearchTokenChange = async (value) => {
-    setSearchToken(value.trim());
-  };
-
   const sortToken = (key) => {
     if (tokens.length === 0) return;
     setLoading(true);
@@ -580,36 +627,65 @@ const TokensTable = () => {
           </Button>
         </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="relative w-full md:w-56">
-            <Input
-              prefix={<IconSearch />}
-              placeholder={t('搜索关键字')}
-              value={searchKeyword}
-              onChange={handleKeywordChange}
-              className="!rounded-full"
-              showClear
-            />
-          </div>
-          <div className="relative w-full md:w-56">
-            <Input
-              prefix={<IconSearch />}
-              placeholder={t('密钥')}
-              value={searchToken}
-              onChange={handleSearchTokenChange}
-              className="!rounded-full"
-              showClear
-            />
+        <Form
+          initValues={formInitValues}
+          getFormApi={(api) => setFormApi(api)}
+          onSubmit={searchTokens}
+          allowEmpty={true}
+          autoComplete="off"
+          layout="horizontal"
+          trigger="change"
+          stopValidateWithError={false}
+          className="w-full md:w-auto order-1 md:order-2"
+        >
+          <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
+            <div className="relative w-full md:w-56">
+              <Form.Input
+                field="searchKeyword"
+                prefix={<IconSearch />}
+                placeholder={t('搜索关键字')}
+                className="!rounded-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="relative w-full md:w-56">
+              <Form.Input
+                field="searchToken"
+                prefix={<IconSearch />}
+                placeholder={t('密钥')}
+                className="!rounded-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="flex gap-2 w-full md:w-auto">
+              <Button
+                type="primary"
+                htmlType="submit"
+                loading={searching}
+                className="!rounded-full flex-1 md:flex-initial md:w-auto"
+              >
+                {t('查询')}
+              </Button>
+              <Button
+                theme="light"
+                onClick={() => {
+                  if (formApi) {
+                    formApi.reset();
+                    // 重置后立即查询,使用setTimeout确保表单重置完成
+                    setTimeout(() => {
+                      searchTokens();
+                    }, 100);
+                  }
+                }}
+                className="!rounded-full flex-1 md:flex-initial md:w-auto"
+              >
+                {t('重置')}
+              </Button>
+            </div>
           </div>
-          <Button
-            type="primary"
-            onClick={searchTokens}
-            loading={searching}
-            className="!rounded-full w-full md:w-auto"
-          >
-            {t('查询')}
-          </Button>
-        </div>
+        </Form>
       </div>
     </div>
   );
@@ -654,6 +730,14 @@ const TokensTable = () => {
           loading={loading}
           rowSelection={rowSelection}
           onRow={handleRow}
+          empty={
+            <Empty
+              image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
+              darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+              description={t('搜索无结果')}
+              style={{ padding: 30 }}
+            />
+          }
           className="rounded-xl overflow-hidden"
           size="middle"
         ></Table>

+ 146 - 63
web/src/components/table/UsersTable.js

@@ -1,18 +1,37 @@
 import React, { useEffect, useState } from 'react';
 import { API, showError, showSuccess, renderGroup, renderNumber, renderQuota } from '../../helpers';
+
+import {
+  User,
+  Shield,
+  Crown,
+  HelpCircle,
+  CheckCircle,
+  XCircle,
+  Minus,
+  Coins,
+  Activity,
+  Users,
+  DollarSign,
+  UserPlus
+} from 'lucide-react';
 import {
   Button,
   Card,
   Divider,
   Dropdown,
-  Input,
+  Empty,
+  Form,
   Modal,
-  Select,
   Space,
   Table,
   Tag,
-  Typography,
+  Typography
 } from '@douyinfe/semi-ui';
+import {
+  IllustrationNoResult,
+  IllustrationNoResultDark
+} from '@douyinfe/semi-illustrations';
 import {
   IconPlus,
   IconSearch,
@@ -23,7 +42,7 @@ import {
   IconMore,
   IconUserAdd,
   IconArrowUp,
-  IconArrowDown,
+  IconArrowDown
 } from '@douyinfe/semi-icons';
 import { ITEMS_PER_PAGE } from '../../constants';
 import AddUser from '../../pages/User/AddUser';
@@ -39,25 +58,25 @@ const UsersTable = () => {
     switch (role) {
       case 1:
         return (
-          <Tag size='large' color='blue' shape='circle'>
+          <Tag size='large' color='blue' shape='circle' prefixIcon={<User size={14} />}>
             {t('普通用户')}
           </Tag>
         );
       case 10:
         return (
-          <Tag color='yellow' size='large' shape='circle'>
+          <Tag color='yellow' size='large' shape='circle' prefixIcon={<Shield size={14} />}>
             {t('管理员')}
           </Tag>
         );
       case 100:
         return (
-          <Tag color='orange' size='large' shape='circle'>
+          <Tag color='orange' size='large' shape='circle' prefixIcon={<Crown size={14} />}>
             {t('超级管理员')}
           </Tag>
         );
       default:
         return (
-          <Tag color='red' size='large' shape='circle'>
+          <Tag color='red' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
             {t('未知身份')}
           </Tag>
         );
@@ -67,16 +86,16 @@ const UsersTable = () => {
   const renderStatus = (status) => {
     switch (status) {
       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:
         return (
-          <Tag size='large' color='red' shape='circle'>
+          <Tag size='large' color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
             {t('已封禁')}
           </Tag>
         );
       default:
         return (
-          <Tag size='large' color='grey' shape='circle'>
+          <Tag size='large' color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
             {t('未知状态')}
           </Tag>
         );
@@ -106,13 +125,13 @@ const UsersTable = () => {
         return (
           <div>
             <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)}
               </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)}
               </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)}
               </Tag>
             </Space>
@@ -127,13 +146,13 @@ const UsersTable = () => {
         return (
           <div>
             <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)}
               </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)}
               </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}`}
               </Tag>
             </Space>
@@ -155,7 +174,7 @@ const UsersTable = () => {
         return (
           <div>
             {record.DeletedAt !== null ? (
-              <Tag color='red' shape='circle'>{t('已注销')}</Tag>
+              <Tag color='red' shape='circle' prefixIcon={<Minus size={14} />}>{t('已注销')}</Tag>
             ) : (
               renderStatus(text)
             )}
@@ -285,9 +304,7 @@ const UsersTable = () => {
   const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
   const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
-  const [searchKeyword, setSearchKeyword] = useState('');
   const [searching, setSearching] = useState(false);
-  const [searchGroup, setSearchGroup] = useState('');
   const [groupOptions, setGroupOptions] = useState([]);
   const [userCount, setUserCount] = useState(ITEMS_PER_PAGE);
   const [showAddUser, setShowAddUser] = useState(false);
@@ -296,6 +313,24 @@ const UsersTable = () => {
     id: undefined,
   });
 
+  // Form 初始值
+  const formInitValues = {
+    searchKeyword: '',
+    searchGroup: '',
+  };
+
+  // Form API 引用
+  const [formApi, setFormApi] = useState(null);
+
+  // 获取表单值的辅助函数
+  const getFormValues = () => {
+    const formValues = formApi ? formApi.getValues() : {};
+    return {
+      searchKeyword: formValues.searchKeyword || '',
+      searchGroup: formValues.searchGroup || '',
+    };
+  };
+
   const removeRecord = (key) => {
     let newDataSource = [...users];
     if (key != null) {
@@ -363,9 +398,16 @@ const UsersTable = () => {
   const searchUsers = async (
     startIdx,
     pageSize,
-    searchKeyword,
-    searchGroup,
+    searchKeyword = null,
+    searchGroup = null,
   ) => {
+    // 如果没有传递参数,从表单获取值
+    if (searchKeyword === null || searchGroup === null) {
+      const formValues = getFormValues();
+      searchKeyword = formValues.searchKeyword;
+      searchGroup = formValues.searchGroup;
+    }
+
     if (searchKeyword === '' && searchGroup === '') {
       // if keyword is blank, load files instead.
       await loadUsers(startIdx, pageSize);
@@ -387,12 +429,9 @@ const UsersTable = () => {
     setSearching(false);
   };
 
-  const handleKeywordChange = async (value) => {
-    setSearchKeyword(value.trim());
-  };
-
   const handlePageChange = (page) => {
     setActivePage(page);
+    const { searchKeyword, searchGroup } = getFormValues();
     if (searchKeyword === '' && searchGroup === '') {
       loadUsers(page, pageSize).then();
     } else {
@@ -413,10 +452,11 @@ const UsersTable = () => {
 
   const refresh = async () => {
     setActivePage(1);
-    if (searchKeyword === '') {
-      await loadUsers(activePage, pageSize);
+    const { searchKeyword, searchGroup } = getFormValues();
+    if (searchKeyword === '' && searchGroup === '') {
+      await loadUsers(1, pageSize);
     } else {
-      await searchUsers(activePage, pageSize, searchKeyword, searchGroup);
+      await searchUsers(1, pageSize, searchKeyword, searchGroup);
     }
   };
 
@@ -488,41 +528,76 @@ const UsersTable = () => {
           </Button>
         </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="relative w-full md:w-64">
-            <Input
-              prefix={<IconSearch />}
-              placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
-              value={searchKeyword}
-              onChange={handleKeywordChange}
-              className="!rounded-full"
-              showClear
-            />
-          </div>
-          <div className="w-full md:w-48">
-            <Select
-              placeholder={t('选择分组')}
-              optionList={groupOptions}
-              value={searchGroup}
-              onChange={(value) => {
-                setSearchGroup(value);
-                searchUsers(activePage, pageSize, searchKeyword, value);
-              }}
-              className="!rounded-full w-full"
-              showClear
-            />
+        <Form
+          initValues={formInitValues}
+          getFormApi={(api) => setFormApi(api)}
+          onSubmit={() => {
+            setActivePage(1);
+            searchUsers(1, pageSize);
+          }}
+          allowEmpty={true}
+          autoComplete="off"
+          layout="horizontal"
+          trigger="change"
+          stopValidateWithError={false}
+          className="w-full md:w-auto order-1 md:order-2"
+        >
+          <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
+            <div className="relative w-full md:w-64">
+              <Form.Input
+                field="searchKeyword"
+                prefix={<IconSearch />}
+                placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
+                className="!rounded-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="w-full md:w-48">
+              <Form.Select
+                field="searchGroup"
+                placeholder={t('选择分组')}
+                optionList={groupOptions}
+                onChange={(value) => {
+                  // 分组变化时自动搜索
+                  setTimeout(() => {
+                    setActivePage(1);
+                    searchUsers(1, pageSize);
+                  }, 100);
+                }}
+                className="!rounded-full w-full"
+                showClear
+                pure
+              />
+            </div>
+            <div className="flex gap-2 w-full md:w-auto">
+              <Button
+                type="primary"
+                htmlType="submit"
+                loading={loading || searching}
+                className="!rounded-full flex-1 md:flex-initial md:w-auto"
+              >
+                {t('查询')}
+              </Button>
+              <Button
+                theme="light"
+                onClick={() => {
+                  if (formApi) {
+                    formApi.reset();
+                    // 重置后立即查询,使用setTimeout确保表单重置完成
+                    setTimeout(() => {
+                      setActivePage(1);
+                      loadUsers(1, pageSize);
+                    }, 100);
+                  }
+                }}
+                className="!rounded-full flex-1 md:flex-initial md:w-auto"
+              >
+                {t('重置')}
+              </Button>
+            </div>
           </div>
-          <Button
-            type="primary"
-            onClick={() => {
-              searchUsers(activePage, pageSize, searchKeyword, searchGroup);
-            }}
-            loading={searching}
-            className="!rounded-full w-full md:w-auto"
-          >
-            {t('查询')}
-          </Button>
-        </div>
+        </Form>
       </div>
     </div>
   );
@@ -570,6 +645,14 @@ const UsersTable = () => {
           }}
           loading={loading}
           onRow={handleRow}
+          empty={
+            <Empty
+              image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
+              darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+              description={t('搜索无结果')}
+              style={{ padding: 30 }}
+            />
+          }
           className="rounded-xl overflow-hidden"
           size="middle"
         />

+ 155 - 65
web/src/helpers/render.js

@@ -24,6 +24,13 @@ import {
   XAI,
   Ollama,
   Doubao,
+  Suno,
+  Xinference,
+  OpenRouter,
+  Dify,
+  Coze,
+  SiliconCloud,
+  FastGPT
 } from '@lobehub/icons';
 
 import {
@@ -40,6 +47,7 @@ import {
   User,
   Settings,
   CircleUser,
+  Users
 } 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 = [
   'amber',
@@ -519,7 +609,7 @@ export function renderGroup(group) {
               showSuccess(i18next.t('已复制:') + group);
             } else {
               Modal.error({
-                title: t('无法复制到剪贴板,请手动复制'),
+                title: i18next.t('无法复制到剪贴板,请手动复制'),
                 content: group,
               });
             }
@@ -956,23 +1046,23 @@ export function renderModelPrice(
               const extraServices = [
                 webSearch && webSearchCallCount > 0
                   ? i18next.t(
-                      ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}',
-                      {
-                        count: webSearchCallCount,
-                        price: webSearchPrice,
-                        ratio: groupRatio,
-                      },
-                    )
+                    ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}',
+                    {
+                      count: webSearchCallCount,
+                      price: webSearchPrice,
+                      ratio: groupRatio,
+                    },
+                  )
                   : '',
                 fileSearch && fileSearchCallCount > 0
                   ? i18next.t(
-                      ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}',
-                      {
-                        count: fileSearchCallCount,
-                        price: fileSearchPrice,
-                        ratio: groupRatio,
-                      },
-                    )
+                    ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}',
+                    {
+                      count: fileSearchCallCount,
+                      price: fileSearchPrice,
+                      ratio: groupRatio,
+                    },
+                  )
                   : '',
               ].join('');
 
@@ -1156,10 +1246,10 @@ export function renderAudioModelPrice(
     let audioPrice =
       (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
       (audioCompletionTokens / 1000000) *
-        inputRatioPrice *
-        audioRatio *
-        audioCompletionRatio *
-        groupRatio;
+      inputRatioPrice *
+      audioRatio *
+      audioCompletionRatio *
+      groupRatio;
     let price = textPrice + audioPrice;
     return (
       <>
@@ -1215,27 +1305,27 @@ export function renderAudioModelPrice(
           <p>
             {cacheTokens > 0
               ? i18next.t(
-                  '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
-                  {
-                    nonCacheInput: inputTokens - cacheTokens,
-                    cacheInput: cacheTokens,
-                    cachePrice: inputRatioPrice * cacheRatio,
-                    price: inputRatioPrice,
-                    completion: completionTokens,
-                    compPrice: completionRatioPrice,
-                    total: textPrice.toFixed(6),
-                  },
-                )
+                '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
+                {
+                  nonCacheInput: inputTokens - cacheTokens,
+                  cacheInput: cacheTokens,
+                  cachePrice: inputRatioPrice * cacheRatio,
+                  price: inputRatioPrice,
+                  completion: completionTokens,
+                  compPrice: completionRatioPrice,
+                  total: textPrice.toFixed(6),
+                },
+              )
               : i18next.t(
-                  '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
-                  {
-                    input: inputTokens,
-                    price: inputRatioPrice,
-                    completion: completionTokens,
-                    compPrice: completionRatioPrice,
-                    total: textPrice.toFixed(6),
-                  },
-                )}
+                '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
+                {
+                  input: inputTokens,
+                  price: inputRatioPrice,
+                  completion: completionTokens,
+                  compPrice: completionRatioPrice,
+                  total: textPrice.toFixed(6),
+                },
+              )}
           </p>
           <p>
             {i18next.t(
@@ -1372,33 +1462,33 @@ export function renderClaudeModelPrice(
           <p>
             {cacheTokens > 0 || cacheCreationTokens > 0
               ? i18next.t(
-                  '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
-                  {
-                    nonCacheInput: nonCachedTokens,
-                    cacheInput: cacheTokens,
-                    cacheRatio: cacheRatio,
-                    cacheCreationInput: cacheCreationTokens,
-                    cacheCreationRatio: cacheCreationRatio,
-                    cachePrice: cacheRatioPrice,
-                    cacheCreationPrice: cacheCreationRatioPrice,
-                    price: inputRatioPrice,
-                    completion: completionTokens,
-                    compPrice: completionRatioPrice,
-                    ratio: groupRatio,
-                    total: price.toFixed(6),
-                  },
-                )
+                '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
+                {
+                  nonCacheInput: nonCachedTokens,
+                  cacheInput: cacheTokens,
+                  cacheRatio: cacheRatio,
+                  cacheCreationInput: cacheCreationTokens,
+                  cacheCreationRatio: cacheCreationRatio,
+                  cachePrice: cacheRatioPrice,
+                  cacheCreationPrice: cacheCreationRatioPrice,
+                  price: inputRatioPrice,
+                  completion: completionTokens,
+                  compPrice: completionRatioPrice,
+                  ratio: groupRatio,
+                  total: price.toFixed(6),
+                },
+              )
               : i18next.t(
-                  '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
-                  {
-                    input: inputTokens,
-                    price: inputRatioPrice,
-                    completion: completionTokens,
-                    compPrice: completionRatioPrice,
-                    ratio: groupRatio,
-                    total: price.toFixed(6),
-                  },
-                )}
+                '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
+                {
+                  input: inputTokens,
+                  price: inputRatioPrice,
+                  completion: completionTokens,
+                  compPrice: completionRatioPrice,
+                  ratio: groupRatio,
+                  total: price.toFixed(6),
+                },
+              )}
           </p>
           <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
         </article>

+ 20 - 1
web/src/i18n/locales/en.json

@@ -1567,5 +1567,24 @@
   "使用统计": "Usage Statistics",
   "资源消耗": "Resource Consumption",
   "性能指标": "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"
 }

+ 24 - 0
web/src/index.css

@@ -73,6 +73,10 @@ code {
 .semi-page-item,
 .semi-navigation-item,
 .semi-tag-closable,
+.semi-input-wrapper,
+.semi-tabs-tab-button,
+.semi-select,
+.semi-button,
 .semi-datepicker-range-input {
   border-radius: 9999px !important;
 }
@@ -322,6 +326,24 @@ code {
   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 {
   height: 100% !important;
@@ -378,6 +400,7 @@ code {
 }
 
 /* 隐藏模型设置区域的滚动条 */
+.api-info-scroll::-webkit-scrollbar,
 .model-settings-scroll::-webkit-scrollbar,
 .thinking-content-scroll::-webkit-scrollbar,
 .custom-request-textarea .semi-input::-webkit-scrollbar,
@@ -385,6 +408,7 @@ code {
   display: none;
 }
 
+.api-info-scroll,
 .model-settings-scroll,
 .thinking-content-scroll,
 .custom-request-textarea .semi-input,

+ 1 - 0
web/src/index.js

@@ -1,6 +1,7 @@
 import React from 'react';
 import ReactDOM from 'react-dom/client';
 import { BrowserRouter } from 'react-router-dom';
+import '@douyinfe/semi-ui/dist/css/semi.css';
 import { UserProvider } from './context/User';
 import 'react-toastify/dist/ReactToastify.css';
 import { StatusProvider } from './context/Status';

+ 28 - 4
web/src/pages/Channel/EditTagModal.js

@@ -194,6 +194,24 @@ const EditTagModal = (props) => {
   }, [originModelOptions, inputs.models]);
 
   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({
       ...originInputs,
       tag: tag,
@@ -201,7 +219,8 @@ const EditTagModal = (props) => {
     });
     fetchModels().then();
     fetchGroups().then();
-  }, [visible]);
+    fetchTagModels().then(); // Call the new function
+  }, [visible, tag]); // Add tag to dependency array
 
   const addCustomModels = () => {
     if (customModel.trim() === '') return;
@@ -347,6 +366,11 @@ const EditTagModal = (props) => {
             <div className="space-y-4">
               <div>
                 <Text strong className="block mb-2">{t('模型')}</Text>
+                <Banner
+                  type="info"
+                  description={t('当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。')}
+                  className="!rounded-lg mb-4"
+                />
                 <Select
                   placeholder={t('请选择该渠道所支持的模型,留空则不更改')}
                   name='models'
@@ -388,19 +412,19 @@ const EditTagModal = (props) => {
                 />
                 <Space className="mt-2">
                   <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))}
                   >
                     {t('填入模板')}
                   </Text>
                   <Text
-                    className="text-blue-500 cursor-pointer"
+                    className="!text-semi-color-primary cursor-pointer"
                     onClick={() => handleInputChange('model_mapping', JSON.stringify({}, null, 2))}
                   >
                     {t('清空重定向')}
                   </Text>
                   <Text
-                    className="text-blue-500 cursor-pointer"
+                    className="!text-semi-color-primary cursor-pointer"
                     onClick={() => handleInputChange('model_mapping', '')}
                   >
                     {t('不更改')}

File diff suppressed because it is too large
+ 460 - 342
web/src/pages/Detail/index.js


+ 53 - 61
web/src/pages/Home/index.js

@@ -4,8 +4,7 @@ import { API, showError, isMobile } from '../../helpers';
 import { StatusContext } from '../../context/Status';
 import { marked } from 'marked';
 import { useTranslation } from 'react-i18next';
-import { IconGithubLogo } from '@douyinfe/semi-icons';
-import exampleImage from '/example.png';
+import { IconGithubLogo, IconPlay, IconFile } from '@douyinfe/semi-icons';
 import { Link } from 'react-router-dom';
 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';
@@ -20,6 +19,7 @@ const Home = () => {
   const [noticeVisible, setNoticeVisible] = useState(false);
 
   const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
+  const docsLink = statusState?.status?.docs_link || '';
 
   useEffect(() => {
     const checkNoticeAndShow = async () => {
@@ -85,132 +85,123 @@ const Home = () => {
       {homePageContentLoaded && homePageContent === '' ? (
         <div className="w-full overflow-x-hidden">
           {/* 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="flex flex-col md:flex-row items-center justify-center h-full px-4 py-8 md:py-0">
-              {/* 左侧内容区 */}
-              <div className="flex-shrink-0 w-full md:w-[480px] md:mr-[60px] lg:mr-[120px] mb-8 md:mb-0">
-                <div className="flex items-center gap-2 justify-center md:justify-start">
-                  <h1 className="text-3xl md:text-4xl lg:text-5xl font-semibold text-semi-color-text-0 w-auto leading-normal md:leading-[67px]">
+          <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 items-center justify-center h-full px-4 py-12 md:py-16 lg:py-20">
+              {/* 居中内容区 */}
+              <div className="flex flex-col items-center justify-center text-center max-w-4xl mx-auto">
+                <div className="flex flex-col items-center justify-center mb-6 md:mb-8">
+                  <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'}
                   </h1>
-                  {statusState?.status?.version && (
-                    <Tag color='light-blue' size='large' shape='circle' className="ml-1">
-                      {statusState.status.version}
-                    </Tag>
-                  )}
                 </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资产')}
                 </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">
-                    <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('开始使用')}
                     </Button>
                   </Link>
-                  {isDemoSiteMode && (
+                  {isDemoSiteMode && statusState?.status?.version ? (
                     <Button
                       size="large"
-                      className="flex items-center !rounded-3xl"
+                      className="flex items-center !rounded-3xl px-6 py-2"
                       icon={<IconGithubLogo />}
                       onClick={() => window.open('https://github.com/QuantumNous/new-api', '_blank')}
                     >
-                      GitHub
+                      {statusState.status.version}
                     </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 className="mt-8 md:mt-16">
-                  <div className="flex items-center mb-3 justify-center md:justify-start">
-                    <Text type="tertiary" className="text-lg md:text-xl font-light">
+                <div className="mt-12 md:mt-16 lg:mt-20 w-full">
+                  <div className="flex items-center mb-6 md:mb-8 justify-center">
+                    <Text type="tertiary" className="text-lg md:text-xl lg:text-2xl font-light">
                       {t('支持众多的大模型供应商')}
                     </Text>
                   </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="relative w-8 md:w-10 h-8 md:h-10 flex items-center justify-center">
+                  <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="w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center">
                       <Moonshot size={40} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </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} />
                     </div>
-                    <div className="relative w-8 md:w-10 h-8 md:h-10 flex items-center justify-center">
-                      <Typography.Text className="!text-2xl font-bold">30+</Typography.Text>
+                    <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-lg sm:!text-xl md:!text-2xl lg:!text-3xl font-bold">30+</Typography.Text>
                     </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>
@@ -223,7 +214,7 @@ const Home = () => {
             />
           ) : (
             <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 }}
             ></div>
           )}
@@ -234,3 +225,4 @@ const Home = () => {
 };
 
 export default Home;
+

+ 399 - 0
web/src/pages/Setting/Dashboard/SettingsAPIInfo.js

@@ -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 - 1
web/src/pages/Setting/index.js

@@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next';
 import SystemSetting from '../../components/settings/SystemSetting.js';
 import { isRoot } from '../../helpers';
 import OtherSetting from '../../components/settings/OtherSetting';
-import PersonalSetting from '../../components/settings/PersonalSetting.js';
 import OperationSetting from '../../components/settings/OperationSetting.js';
 import RateLimitSetting from '../../components/settings/RateLimitSetting.js';
 import ModelSetting from '../../components/settings/ModelSetting.js';
+import DashboardSetting from '../../components/settings/DashboardSetting.js';
 
 const Setting = () => {
   const { t } = useTranslation();
@@ -44,6 +44,11 @@ const Setting = () => {
       content: <OtherSetting />,
       itemKey: 'other',
     });
+    panes.push({
+      tab: t('仪表盘配置'),
+      content: <DashboardSetting />,
+      itemKey: 'dashboard',
+    });
   }
   const onChangeTab = (key) => {
     setTabActiveKey(key);

+ 1 - 1
web/src/pages/Setup/index.js

@@ -133,7 +133,7 @@ const Setup = () => {
   };
 
   return (
-    <div className="min-h-screen bg-gray-50">
+    <div className="bg-gray-50">
       <Layout>
         <Layout.Content>
           <div className="flex justify-center px-4 py-8">

+ 9 - 3
web/src/pages/Token/EditToken.js

@@ -219,9 +219,15 @@ const EditToken = (props) => {
       let successCount = 0; // 记录成功创建的令牌数量
       for (let i = 0; i < tokenCount; i++) {
         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);
 

+ 216 - 194
web/src/pages/TopUp/index.js

@@ -55,6 +55,7 @@ const TopUp = () => {
   const [amountLoading, setAmountLoading] = useState(false);
   const [paymentLoading, setPaymentLoading] = useState(false);
   const [confirmLoading, setConfirmLoading] = useState(false);
+  const [isDarkMode, setIsDarkMode] = useState(false);
 
   // 邀请相关状态
   const [affLink, setAffLink] = useState('');
@@ -256,6 +257,32 @@ const TopUp = () => {
     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(() => {
     if (userState?.user?.id) {
       setUserDataLoading(false);
@@ -398,48 +425,45 @@ const TopUp = () => {
             <div className="w-full">
               <Card className="!rounded-2xl shadow-lg border-0">
                 <Card
-                  className="!rounded-2xl !border-0 !shadow-2xl overflow-hidden"
+                  className="!rounded-2xl !border-0 !shadow-lg overflow-hidden"
                   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'
                   }}
                   bodyStyle={{ padding: 0 }}
                 >
                   <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 -bottom-16 -left-16 w-48 h-48 bg-white opacity-3 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-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-slate-300 opacity-8 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 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-1 min-w-0">
                         {userDataLoading ? (
                           <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()}
                           </div>
                         )}
                       </div>
-                      <div
-                        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"
-                        style={{
-                          background: `linear-gradient(135deg, ${stringToColor(getUsername())} 0%, #f59e0b 100%)`
-                        }}
-                      >
-                        <IconCreditCard size="default" style={{ color: 'white' }} />
+                      <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">
+                        <IconCreditCard size="default" className="text-white" />
                       </div>
                     </div>
 
                     <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('当前余额')}
                       </div>
                       {userDataLoading ? (
                         <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)}
                         </div>
                       )}
@@ -448,37 +472,37 @@ const TopUp = () => {
                     <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="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('历史消耗')}
                           </div>
                           {userDataLoading ? (
                             <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)}
                             </div>
                           )}
                         </div>
                         <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('用户分组')}
                           </div>
                           {userDataLoading ? (
                             <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('默认')}
                             </div>
                           )}
                         </div>
                         <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('用户角色')}
                           </div>
                           {userDataLoading ? (
                             <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()}
                             </div>
                           )}
@@ -489,32 +513,187 @@ const TopUp = () => {
                         {userDataLoading ? (
                           <Skeleton.Title style={{ width: '50px', height: '24px' }} />
                         ) : (
-                          <div
-                            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)'
-                            }}
-                          >
+                          <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">
                             ID: {userState?.user?.id || '---'}
                           </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>
                 </Card>
 
                 <div className="p-6">
                   <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
-                    {/* 邀请信息部分 */}
-                    <div>
+                    {/* 左侧:在线充值和兑换余额 */}
+                    <div className="lg:col-span-2 space-y-8">
+                      {/* 在线充值部分 */}
+                      <div>
+                        <div className="flex items-center mb-6">
+                          <div className="w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-4">
+                            <IconPlus size="large" className="text-slate-600 dark:text-slate-300" />
+                          </div>
+                          <div>
+                            <Text className="text-xl font-semibold">{t('在线充值')}</Text>
+                            <div className="text-gray-500 text-sm">{t('支持多种支付方式')}</div>
+                          </div>
+                        </div>
+
+                        <div className="space-y-4">
+                          <div>
+                            <div className="flex justify-between mb-2">
+                              <Text strong>{t('充值数量')}</Text>
+                              {amountLoading ? (
+                                <Skeleton.Title style={{ width: '80px', height: '14px' }} />
+                              ) : (
+                                <Text type="tertiary">{t('实付金额:') + ' ' + renderAmount()}</Text>
+                              )}
+                            </div>
+                            <InputNumber
+                              disabled={!enableOnlineTopUp}
+                              placeholder={
+                                t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
+                              }
+                              value={topUpCount}
+                              min={minTopUp}
+                              max={999999999}
+                              step={1}
+                              precision={0}
+                              onChange={async (value) => {
+                                if (value && value >= 1) {
+                                  setTopUpCount(value);
+                                  await getAmount(value);
+                                }
+                              }}
+                              onBlur={(e) => {
+                                const value = parseInt(e.target.value);
+                                if (!value || value < 1) {
+                                  setTopUpCount(1);
+                                  getAmount(1);
+                                }
+                              }}
+                              size="large"
+                              className="!rounded-lg w-full"
+                              prefix={<IconCreditCard />}
+                              formatter={(value) => value ? `${value}` : ''}
+                              parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0}
+                            />
+                          </div>
+
+                          <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
+                            <Button
+                              type="primary"
+                              theme="solid"
+                              onClick={async () => {
+                                preTopUp('zfb');
+                              }}
+                              size="large"
+                              className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 h-14"
+                              disabled={!enableOnlineTopUp}
+                              loading={paymentLoading}
+                              icon={<SiAlipay size={20} />}
+                            >
+                              <span className="ml-2">{t('支付宝')}</span>
+                            </Button>
+                            <Button
+                              type="primary"
+                              theme="solid"
+                              onClick={async () => {
+                                preTopUp('wx');
+                              }}
+                              size="large"
+                              className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 h-14"
+                              disabled={!enableOnlineTopUp}
+                              loading={paymentLoading}
+                              icon={<SiWechat size={20} />}
+                            >
+                              <span className="ml-2">{t('微信')}</span>
+                            </Button>
+                          </div>
+
+                          {!enableOnlineTopUp && (
+                            <Banner
+                              fullMode={false}
+                              type="warning"
+                              icon={null}
+                              closeIcon={null}
+                              className="!rounded-lg"
+                              title={
+                                <div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>
+                                  {t('在线充值功能未开启')}
+                                </div>
+                              }
+                              description={
+                                <div>
+                                  {t('管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。')}
+                                </div>
+                              }
+                            />
+                          )}
+                        </div>
+                      </div>
+
+                      {/* 兑换余额部分 */}
+                      <div>
+                        <div className="flex items-center mb-6">
+                          <div className="w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-4">
+                            <IconGift size="large" className="text-slate-600 dark:text-slate-300" />
+                          </div>
+                          <div>
+                            <Text className="text-xl font-semibold">{t('兑换余额')}</Text>
+                            <div className="text-gray-500 text-sm">{t('使用兑换码充值余额')}</div>
+                          </div>
+                        </div>
+
+                        <div className="space-y-4">
+                          <div>
+                            <Text strong className="block mb-2">{t('兑换码')}</Text>
+                            <Input
+                              placeholder={t('请输入兑换码')}
+                              value={redemptionCode}
+                              onChange={(value) => setRedemptionCode(value)}
+                              size="large"
+                              className="!rounded-lg"
+                              prefix={<IconGift />}
+                            />
+                          </div>
+
+                          <div className="flex flex-col sm:flex-row gap-3">
+                            {topUpLink && (
+                              <Button
+                                type="primary"
+                                theme="solid"
+                                onClick={openTopUpLink}
+                                size="large"
+                                className="!rounded-lg flex-1"
+                                icon={<IconLink />}
+                              >
+                                {t('获取兑换码')}
+                              </Button>
+                            )}
+                            <Button
+                              type="warning"
+                              theme="solid"
+                              onClick={topUp}
+                              disabled={isSubmitting}
+                              loading={isSubmitting}
+                              size="large"
+                              className="!rounded-lg flex-1"
+                            >
+                              {isSubmitting ? t('兑换中...') : t('兑换')}
+                            </Button>
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+
+                    {/* 右侧:邀请信息部分 */}
+                    <div className="lg:col-span-1">
                       <div className="flex items-center justify-between mb-6">
                         <div className="flex items-center">
-                          <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 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">
@@ -524,7 +703,7 @@ const TopUp = () => {
                                 theme="solid"
                                 onClick={() => setOpenTransfer(true)}
                                 size="small"
-                                className="!rounded-lg !bg-blue-500 hover:!bg-blue-600"
+                                className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
                                 icon={<IconCreditCard />}
                               >
                                 {t('划转')}
@@ -536,7 +715,7 @@ const TopUp = () => {
                       </div>
 
                       <div className="space-y-4">
-                        <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
+                        <div className="grid grid-cols-1 gap-3">
                           <Card
                             className="!rounded-2xl text-center"
                             bodyStyle={{ padding: '16px' }}
@@ -546,7 +725,6 @@ const TopUp = () => {
                             <div className="text-gray-900 text-lg font-bold mt-1">
                               {renderQuota(userState?.user?.aff_quota)}
                             </div>
-
                           </Card>
                           <Card
                             className="!rounded-2xl text-center"
@@ -583,162 +761,6 @@ const TopUp = () => {
                         </div>
                       </div>
                     </div>
-                    <div>
-                      <div className="flex items-center mb-6">
-                        <div className="w-12 h-12 rounded-full bg-green-50 flex items-center justify-center mr-4">
-                          <IconGift size="large" className="text-green-500" />
-                        </div>
-                        <div>
-                          <Text className="text-xl font-semibold">{t('兑换余额')}</Text>
-                          <div className="text-gray-500 text-sm">{t('使用兑换码充值余额')}</div>
-                        </div>
-                      </div>
-
-                      <div className="space-y-4">
-                        <div>
-                          <Text strong className="block mb-2">{t('兑换码')}</Text>
-                          <Input
-                            placeholder={t('请输入兑换码')}
-                            value={redemptionCode}
-                            onChange={(value) => setRedemptionCode(value)}
-                            size="large"
-                            className="!rounded-lg"
-                            prefix={<IconGift />}
-                          />
-                        </div>
-
-                        <div className="flex flex-col sm:flex-row gap-3">
-                          {topUpLink && (
-                            <Button
-                              type="primary"
-                              theme="solid"
-                              onClick={openTopUpLink}
-                              size="large"
-                              className="!rounded-lg flex-1"
-                              icon={<IconLink />}
-                            >
-                              {t('获取兑换码')}
-                            </Button>
-                          )}
-                          <Button
-                            type="warning"
-                            theme="solid"
-                            onClick={topUp}
-                            disabled={isSubmitting}
-                            loading={isSubmitting}
-                            size="large"
-                            className="!rounded-lg flex-1"
-                          >
-                            {isSubmitting ? t('兑换中...') : t('兑换')}
-                          </Button>
-                        </div>
-                      </div>
-                    </div>
-
-                    <div>
-                      <div className="flex items-center mb-6">
-                        <div className="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center mr-4">
-                          <IconPlus size="large" className="text-blue-500" />
-                        </div>
-                        <div>
-                          <Text className="text-xl font-semibold">{t('在线充值')}</Text>
-                          <div className="text-gray-500 text-sm">{t('支持多种支付方式')}</div>
-                        </div>
-                      </div>
-
-                      <div className="space-y-4">
-                        <div>
-                          <div className="flex justify-between mb-2">
-                            <Text strong>{t('充值数量')}</Text>
-                            {amountLoading ? (
-                              <Skeleton.Title style={{ width: '80px', height: '14px' }} />
-                            ) : (
-                              <Text type="tertiary">{t('实付金额:') + ' ' + renderAmount()}</Text>
-                            )}
-                          </div>
-                          <InputNumber
-                            disabled={!enableOnlineTopUp}
-                            placeholder={
-                              t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
-                            }
-                            value={topUpCount}
-                            min={minTopUp}
-                            max={999999999}
-                            step={1}
-                            precision={0}
-                            onChange={async (value) => {
-                              if (value && value >= 1) {
-                                setTopUpCount(value);
-                                await getAmount(value);
-                              }
-                            }}
-                            onBlur={(e) => {
-                              const value = parseInt(e.target.value);
-                              if (!value || value < 1) {
-                                setTopUpCount(1);
-                                getAmount(1);
-                              }
-                            }}
-                            size="large"
-                            className="!rounded-lg w-full"
-                            prefix={<IconCreditCard />}
-                            formatter={(value) => value ? `${value}` : ''}
-                            parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0}
-                          />
-                        </div>
-
-                        <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
-                          <Button
-                            type="primary"
-                            theme="solid"
-                            onClick={async () => {
-                              preTopUp('zfb');
-                            }}
-                            size="large"
-                            className="!rounded-lg !bg-blue-500 hover:!bg-blue-600 h-14"
-                            disabled={!enableOnlineTopUp}
-                            loading={paymentLoading}
-                            icon={<SiAlipay size={20} />}
-                          >
-                            <span className="ml-2">{t('支付宝')}</span>
-                          </Button>
-                          <Button
-                            type="primary"
-                            theme="solid"
-                            onClick={async () => {
-                              preTopUp('wx');
-                            }}
-                            size="large"
-                            className="!rounded-lg !bg-green-500 hover:!bg-green-600 h-14"
-                            disabled={!enableOnlineTopUp}
-                            loading={paymentLoading}
-                            icon={<SiWechat size={20} />}
-                          >
-                            <span className="ml-2">{t('微信')}</span>
-                          </Button>
-                        </div>
-
-                        {!enableOnlineTopUp && (
-                          <Banner
-                            fullMode={false}
-                            type="warning"
-                            icon={null}
-                            closeIcon={null}
-                            className="!rounded-lg"
-                            title={
-                              <div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>
-                                {t('在线充值功能未开启')}
-                              </div>
-                            }
-                            description={
-                              <div>
-                                {t('管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。')}
-                              </div>
-                            }
-                          />
-                        )}
-                      </div>
-                    </div>
                   </div>
                 </div>
               </Card>

Some files were not shown because too many files changed in this diff