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 = () => (