Просмотр исходного кода

🚀 feat: add enabled/disabled channel filtering & optimize type-based pagination (#1289)

WHAT’S NEW
• Backend
  – Introduced `parseStatusFilter` helper to normalize `status` query across handlers.
  – `GET /api/channel` & `GET /api/channel/search` now accept `status=enabled|disabled` to return only enabled or disabled channels.
  – Tag-mode branch respects both `statusFilter` and `typeFilter`; SQL paths trimmed to one query + one lightweight `GROUP BY` for `type_counts`.

• Frontend (`ChannelsTable.js`)
  – Added “Status Filter” `<Select>` (All / Enabled / Disabled) with localStorage persistence.
  – All data-loading and search requests now always append `type` (when not “all”) and `status` params, so filtering & pagination are handled entirely server-side.
  – Removed client-side post-filtering for type, preventing short pages and reducing CPU work.
  – Tabs’ type counts stay in sync via backend-provided `type_counts`.

IMPROVEMENTS
• Eliminated duplicated status-parsing logic; single source of truth eases future extension.
• Reduced redundant queries, improved consistency of counts in UI.
• Secured key leakage with `Omit("key")` unchanged; no perf regressions observed.

Closes #1289
t0ng7u 8 месяцев назад
Родитель
Сommit
bc371778b6
3 измененных файлов с 127 добавлено и 37 удалено
  1. 82 19
      controller/channel.go
  2. 43 17
      web/src/components/table/ChannelsTable.js
  3. 2 1
      web/src/i18n/locales/en.json

+ 82 - 19
controller/channel.go

@@ -40,6 +40,17 @@ type OpenAIModelsResponse struct {
 	Success bool          `json:"success"`
 }
 
+func parseStatusFilter(statusParam string) int {
+	switch strings.ToLower(statusParam) {
+	case "enabled", "1":
+		return common.ChannelStatusEnabled
+	case "disabled", "0":
+		return 0
+	default:
+		return -1
+	}
+}
+
 func GetAllChannels(c *gin.Context) {
 	p, _ := strconv.Atoi(c.Query("p"))
 	pageSize, _ := strconv.Atoi(c.Query("page_size"))
@@ -52,6 +63,9 @@ func GetAllChannels(c *gin.Context) {
 	channelData := make([]*model.Channel, 0)
 	idSort, _ := strconv.ParseBool(c.Query("id_sort"))
 	enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
+	statusParam := c.Query("status")
+	// statusFilter: -1 all, 1 enabled, 0 disabled (include auto & manual)
+	statusFilter := parseStatusFilter(statusParam)
 	// type filter
 	typeStr := c.Query("type")
 	typeFilter := -1
@@ -64,42 +78,75 @@ func GetAllChannels(c *gin.Context) {
 	var total int64
 
 	if enableTagMode {
-		// tag 分页:先分页 tag,再取各 tag 下 channels
 		tags, err := model.GetPaginatedTags((p-1)*pageSize, pageSize)
 		if err != nil {
 			c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
 			return
 		}
 		for _, tag := range tags {
-			if tag != nil && *tag != "" {
-				tagChannel, err := model.GetChannelsByTag(*tag, idSort)
-				if err == nil {
-					channelData = append(channelData, tagChannel...)
+			if tag == nil || *tag == "" {
+				continue
+			}
+			tagChannels, err := model.GetChannelsByTag(*tag, idSort)
+			if err != nil {
+				continue
+			}
+			filtered := make([]*model.Channel, 0)
+			for _, ch := range tagChannels {
+				if statusFilter == common.ChannelStatusEnabled && ch.Status != common.ChannelStatusEnabled {
+					continue
+				}
+				if statusFilter == 0 && ch.Status == common.ChannelStatusEnabled {
+					continue
 				}
+				if typeFilter >= 0 && ch.Type != typeFilter {
+					continue
+				}
+				filtered = append(filtered, ch)
 			}
+			channelData = append(channelData, filtered...)
 		}
-		// 计算 tag 总数用于分页
 		total, _ = model.CountAllTags()
-	} else if typeFilter >= 0 {
-		channels, err := model.GetChannelsByType((p-1)*pageSize, pageSize, idSort, typeFilter)
-		if err != nil {
-			c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
-			return
-		}
-		channelData = channels
-		total, _ = model.CountChannelsByType(typeFilter)
 	} else {
-		channels, err := model.GetAllChannels((p-1)*pageSize, pageSize, false, idSort)
+		baseQuery := model.DB.Model(&model.Channel{})
+		if typeFilter >= 0 {
+			baseQuery = baseQuery.Where("type = ?", typeFilter)
+		}
+		if statusFilter == common.ChannelStatusEnabled {
+			baseQuery = baseQuery.Where("status = ?", common.ChannelStatusEnabled)
+		} else if statusFilter == 0 {
+			baseQuery = baseQuery.Where("status != ?", common.ChannelStatusEnabled)
+		}
+
+		baseQuery.Count(&total)
+
+		order := "priority desc"
+		if idSort {
+			order = "id desc"
+		}
+
+		err := baseQuery.Order(order).Limit(pageSize).Offset((p-1)*pageSize).Omit("key").Find(&channelData).Error
 		if err != nil {
 			c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
 			return
 		}
-		channelData = channels
-		total, _ = model.CountAllChannels()
 	}
 
-	// calculate type counts
-	typeCounts, _ := model.CountChannelsGroupByType()
+	countQuery := model.DB.Model(&model.Channel{})
+	if statusFilter == common.ChannelStatusEnabled {
+		countQuery = countQuery.Where("status = ?", common.ChannelStatusEnabled)
+	} else if statusFilter == 0 {
+		countQuery = countQuery.Where("status != ?", common.ChannelStatusEnabled)
+	}
+	var results []struct {
+		Type  int64
+		Count int64
+	}
+	_ = countQuery.Select("type, count(*) as count").Group("type").Find(&results).Error
+	typeCounts := make(map[int64]int64)
+	for _, r := range results {
+		typeCounts[r.Type] = r.Count
+	}
 
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
@@ -199,6 +246,8 @@ func SearchChannels(c *gin.Context) {
 	keyword := c.Query("keyword")
 	group := c.Query("group")
 	modelKeyword := c.Query("model")
+	statusParam := c.Query("status")
+	statusFilter := parseStatusFilter(statusParam)
 	idSort, _ := strconv.ParseBool(c.Query("id_sort"))
 	enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
 	channelData := make([]*model.Channel, 0)
@@ -231,6 +280,20 @@ func SearchChannels(c *gin.Context) {
 		channelData = channels
 	}
 
+	if statusFilter == common.ChannelStatusEnabled || statusFilter == 0 {
+		filtered := make([]*model.Channel, 0, len(channelData))
+		for _, ch := range channelData {
+			if statusFilter == common.ChannelStatusEnabled && ch.Status != common.ChannelStatusEnabled {
+				continue
+			}
+			if statusFilter == 0 && ch.Status == common.ChannelStatusEnabled {
+				continue
+			}
+			filtered = append(filtered, ch)
+		}
+		channelData = filtered
+	}
+
 	// calculate type counts for search results
 	typeCounts := make(map[int64]int64)
 	for _, channel := range channelData {

+ 43 - 17
web/src/components/table/ChannelsTable.js

@@ -40,7 +40,8 @@ import {
   Card,
   Form,
   Tabs,
-  TabPane
+  TabPane,
+  Select,
 } from '@douyinfe/semi-ui';
 import {
   IllustrationNoResult,
@@ -189,6 +190,11 @@ const ChannelsTable = () => {
   const [visibleColumns, setVisibleColumns] = useState({});
   const [showColumnSelector, setShowColumnSelector] = useState(false);
 
+  // 状态筛选 all / enabled / disabled
+  const [statusFilter, setStatusFilter] = useState(
+    localStorage.getItem('channel-status-filter') || 'all'
+  );
+
   // Load saved column preferences from localStorage
   useEffect(() => {
     const savedColumns = localStorage.getItem('channels-table-columns');
@@ -867,12 +873,21 @@ const ChannelsTable = () => {
     setChannels(channelDates);
   };
 
-  const loadChannels = async (page, pageSize, idSort, enableTagMode, typeKey = activeTypeKey) => {
+  const loadChannels = async (
+    page,
+    pageSize,
+    idSort,
+    enableTagMode,
+    typeKey = activeTypeKey,
+    statusF,
+  ) => {
+    if (statusF === undefined) statusF = statusFilter;
     const reqId = ++requestCounter.current; // 记录当前请求序号
     setLoading(true);
-    const typeParam = (!enableTagMode && typeKey !== 'all') ? `&type=${typeKey}` : '';
+    const typeParam = (typeKey !== 'all') ? `&type=${typeKey}` : '';
+    const statusParam = statusF !== 'all' ? `&status=${statusF}` : '';
     const res = await API.get(
-      `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}`,
+      `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}${statusParam}`,
     );
     if (res === undefined || reqId !== requestCounter.current) {
       return;
@@ -1049,9 +1064,10 @@ const ChannelsTable = () => {
         return;
       }
 
-      const typeParam = (!enableTagMode && activeTypeKey !== 'all') ? `&type=${activeTypeKey}` : '';
+      const typeParam = (activeTypeKey !== 'all') ? `&type=${activeTypeKey}` : '';
+      const statusParam = statusFilter !== 'all' ? `&status=${statusFilter}` : '';
       const res = await API.get(
-        `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}`,
+        `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}${statusParam}`,
       );
       const { success, message, data } = res.data;
       if (success) {
@@ -1265,17 +1281,6 @@ const ChannelsTable = () => {
   };
 
   let pageData = channels;
-  if (activeTypeKey !== 'all') {
-    const typeVal = parseInt(activeTypeKey);
-    if (!isNaN(typeVal)) {
-      pageData = pageData.filter((ch) => {
-        if (ch.children !== undefined) {
-          return ch.children.some((c) => c.type === typeVal);
-        }
-        return ch.type === typeVal;
-      });
-    }
-  }
 
   const handlePageChange = (page) => {
     setActivePage(page);
@@ -1633,6 +1638,27 @@ const ChannelsTable = () => {
               }}
             />
           </div>
+
+          {/* 状态筛选器 */}
+          <div className="flex items-center justify-between w-full md:w-auto">
+            <Typography.Text strong className="mr-2">
+              {t('状态筛选')}
+            </Typography.Text>
+            <Select
+              value={statusFilter}
+              onChange={(v) => {
+                localStorage.setItem('channel-status-filter', v);
+                setStatusFilter(v);
+                setActivePage(1);
+                loadChannels(1, pageSize, idSort, enableTagMode, activeTypeKey, v);
+              }}
+              size="small"
+            >
+              <Select.Option value="all">{t('全部')}</Select.Option>
+              <Select.Option value="enabled">{t('已启用')}</Select.Option>
+              <Select.Option value="disabled">{t('已禁用')}</Select.Option>
+            </Select>
+          </div>
         </div>
       </div>
 

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

@@ -1732,5 +1732,6 @@
   "确认冲突项修改": "Confirm conflict item modification",
   "该模型存在固定价格与倍率计费方式冲突,请确认选择": "The model has a fixed price and ratio billing method conflict, please confirm the selection",
   "当前计费": "Current billing",
-  "修改为": "Modify to"
+  "修改为": "Modify to",
+  "状态筛选": "Status filter"
 }