diff --git a/controller/channel.go b/controller/channel.go index 1cfb7906..acaf2977 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -52,6 +52,14 @@ func GetAllChannels(c *gin.Context) { channelData := make([]*model.Channel, 0) idSort, _ := strconv.ParseBool(c.Query("id_sort")) enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode")) + // type filter + typeStr := c.Query("type") + typeFilter := -1 + if typeStr != "" { + if t, err := strconv.Atoi(typeStr); err == nil { + typeFilter = t + } + } var total int64 @@ -72,6 +80,14 @@ func GetAllChannels(c *gin.Context) { } // 计算 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) if err != nil { @@ -82,14 +98,18 @@ func GetAllChannels(c *gin.Context) { total, _ = model.CountAllChannels() } + // calculate type counts + typeCounts, _ := model.CountChannelsGroupByType() + c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": gin.H{ - "items": channelData, - "total": total, - "page": p, - "page_size": pageSize, + "success": true, + "message": "", + "data": gin.H{ + "items": channelData, + "total": total, + "page": p, + "page_size": pageSize, + "type_counts": typeCounts, }, }) return diff --git a/model/channel.go b/model/channel.go index b5503eee..6cbd8adc 100644 --- a/model/channel.go +++ b/model/channel.go @@ -597,3 +597,39 @@ func CountAllTags() (int64, error) { err := DB.Model(&Channel{}).Where("tag is not null AND tag != ''").Distinct("tag").Count(&total).Error return total, err } + +// Get channels of specified type with pagination +func GetChannelsByType(startIdx int, num int, idSort bool, channelType int) ([]*Channel, error) { + var channels []*Channel + order := "priority desc" + if idSort { + order = "id desc" + } + err := DB.Where("type = ?", channelType).Order(order).Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error + return channels, err +} + +// Count channels of specific type +func CountChannelsByType(channelType int) (int64, error) { + var count int64 + err := DB.Model(&Channel{}).Where("type = ?", channelType).Count(&count).Error + return count, err +} + +// Return map[type]count for all channels +func CountChannelsGroupByType() (map[int64]int64, error) { + type result struct { + Type int64 `gorm:"column:type"` + Count int64 `gorm:"column:count"` + } + var results []result + err := DB.Model(&Channel{}).Select("type, count(*) as count").Group("type").Find(&results).Error + if err != nil { + return nil, err + } + counts := make(map[int64]int64) + for _, r := range results { + counts[r.Type] = r.Count + } + return counts, nil +} diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js index f5a78490..a18920ab 100644 --- a/web/src/components/table/ChannelsTable.js +++ b/web/src/components/table/ChannelsTable.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo, useRef } from 'react'; import { API, showError, @@ -16,11 +16,6 @@ import { XCircle, AlertCircle, HelpCircle, - TestTube, - Zap, - Timer, - Clock, - AlertTriangle, Coins, Tags } from 'lucide-react'; @@ -43,7 +38,9 @@ import { Typography, Checkbox, Card, - Form + Form, + Tabs, + TabPane } from '@douyinfe/semi-ui'; import { IllustrationNoResult, @@ -141,31 +138,31 @@ const ChannelsTable = () => { time = time.toFixed(2) + t(' 秒'); if (responseTime === 0) { return ( - }> + {t('未测试')} ); } else if (responseTime <= 1000) { return ( - }> + {time} ); } else if (responseTime <= 3000) { return ( - }> + {time} ); } else if (responseTime <= 5000) { return ( - }> + {time} ); } else { return ( - }> + {time} ); @@ -682,11 +679,10 @@ const ChannelsTable = () => { const [isBatchTesting, setIsBatchTesting] = useState(false); const [testQueue, setTestQueue] = useState([]); const [isProcessingQueue, setIsProcessingQueue] = useState(false); - - // Form API 引用 + const [activeTypeKey, setActiveTypeKey] = useState('all'); + const [typeCounts, setTypeCounts] = useState({}); + const requestCounter = useRef(0); const [formApi, setFormApi] = useState(null); - - // Form 初始值 const formInitValues = { searchKeyword: '', searchGroup: '', @@ -868,17 +864,23 @@ const ChannelsTable = () => { setChannels(channelDates); }; - const loadChannels = async (page, pageSize, idSort, enableTagMode) => { + const loadChannels = async (page, pageSize, idSort, enableTagMode, typeKey = activeTypeKey) => { + const reqId = ++requestCounter.current; // 记录当前请求序号 setLoading(true); + const typeParam = typeKey === 'all' ? '' : `&type=${typeKey}`; const res = await API.get( - `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}`, + `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}`, ); - if (res === undefined) { + if (res === undefined || reqId !== requestCounter.current) { return; } const { success, message, data } = res.data; if (success) { - const { items, total } = data; + const { items, total, type_counts } = data; + if (type_counts) { + const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0); + setTypeCounts({ ...type_counts, all: sumAll }); + } setChannelFormat(items, enableTagMode); setChannelCount(total); } else { @@ -1044,12 +1046,16 @@ const ChannelsTable = () => { return; } + const typeParam = activeTypeKey === 'all' ? '' : `&type=${activeTypeKey}`; const res = await API.get( - `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}`, + `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}`, ); const { success, message, data } = res.data; if (success) { - setChannelFormat(data, enableTagMode); + const { items = [], type_counts = {} } = data; + const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0); + setTypeCounts({ ...type_counts, all: sumAll }); + setChannelFormat(items, enableTagMode); setActivePage(1); } else { showError(message); @@ -1179,7 +1185,92 @@ const ChannelsTable = () => { } }; + const channelTypeCounts = useMemo(() => { + if (Object.keys(typeCounts).length > 0) return typeCounts; + // fallback 本地计算 + const counts = { all: channels.length }; + channels.forEach((channel) => { + const collect = (ch) => { + const type = ch.type; + counts[type] = (counts[type] || 0) + 1; + }; + if (channel.children !== undefined) { + channel.children.forEach(collect); + } else { + collect(channel); + } + }); + return counts; + }, [typeCounts, channels]); + + const availableTypeKeys = useMemo(() => { + const keys = ['all']; + Object.entries(channelTypeCounts).forEach(([k, v]) => { + if (k !== 'all' && v > 0) keys.push(String(k)); + }); + return keys; + }, [channelTypeCounts]); + + const renderTypeTabs = () => { + return ( + { + setActiveTypeKey(key); + setActivePage(1); + loadChannels(1, pageSize, idSort, enableTagMode, key); + }} + className="mb-4" + > + + {t('全部')} + + {channelTypeCounts['all'] || 0} + + + } + /> + + {CHANNEL_OPTIONS.filter((opt) => availableTypeKeys.includes(String(opt.value))).map((option) => { + const key = String(option.value); + const count = channelTypeCounts[option.value] || 0; + return ( + + {getChannelIcon(option.value)} + {option.label} + + {count} + + + } + /> + ); + })} + + ); + }; + 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); @@ -1371,6 +1462,7 @@ const ChannelsTable = () => { const renderHeader = () => (
+ {renderTypeTabs()}