From 75548c449b2cb864cd9db8276df861a6f56b66d7 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sat, 26 Jul 2025 18:38:18 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20refactor:=20pricing=20filters=20for?= =?UTF-8?q?=20dynamic=20counting=20&=20cleaner=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces a unified, maintainable solution for all model-pricing filter buttons and removes redundant code. Key points • Added `usePricingFilterCounts` hook - Centralises filtering logic and returns: - `quotaTypeModels`, `endpointTypeModels`, `dynamicCategoryCounts`, `groupCountModels` - Keeps internal helpers private (removed public `modelsAfterCategory`). • Updated components to consume the new hook - `PricingSidebar.jsx` - `FilterModalContent.jsx` • Improved button UI/UX - `SelectableButtonGroup.jsx` now respects `item.disabled` and auto-disables when `tagCount === 0`. - `PricingGroups.jsx` counts models per group (after all other filters) and disables groups with zero matches. - `PricingEndpointTypes.jsx` enumerates all endpoint types, computes filtered counts, and disables entries with zero matches. • Removed obsolete / duplicate calculations and comments to keep components lean. The result is consistent, real-time tag counts across all filter groups, automatic disabling of unavailable options, and a single source of truth for filter computations, making future extensions straightforward. --- .../common/ui/SelectableButtonGroup.jsx | 4 + .../filter/PricingEndpointTypes.jsx | 22 +-- .../model-pricing/filter/PricingGroups.jsx | 4 + .../model-pricing/layout/PricingSidebar.jsx | 24 +++- .../modal/components/FilterModalContent.jsx | 31 ++++- .../model-pricing/usePricingFilterCounts.js | 131 ++++++++++++++++++ 6 files changed, 200 insertions(+), 16 deletions(-) create mode 100644 web/src/hooks/model-pricing/usePricingFilterCounts.js diff --git a/web/src/components/common/ui/SelectableButtonGroup.jsx b/web/src/components/common/ui/SelectableButtonGroup.jsx index 6792c5aa..591634a5 100644 --- a/web/src/components/common/ui/SelectableButtonGroup.jsx +++ b/web/src/components/common/ui/SelectableButtonGroup.jsx @@ -133,6 +133,7 @@ const SelectableButtonGroup = ({ const contentElement = showSkeleton ? renderSkeletonButtons() : ( {items.map((item) => { + const isDisabled = item.disabled || (typeof item.tagCount === 'number' && item.tagCount === 0); const isActive = Array.isArray(activeValue) ? activeValue.includes(item.value) : activeValue === item.value; @@ -150,10 +151,12 @@ const SelectableButtonGroup = ({ onClick={() => { /* disabled */ }} theme={isActive ? 'light' : 'outline'} type={isActive ? 'primary' : 'tertiary'} + disabled={isDisabled} icon={ onChange(item.value)} + disabled={isDisabled} style={{ pointerEvents: 'auto' }} /> } @@ -190,6 +193,7 @@ const SelectableButtonGroup = ({ theme={isActive ? 'light' : 'outline'} type={isActive ? 'primary' : 'tertiary'} icon={item.icon} + disabled={isDisabled} style={{ width: '100%' }} > {item.label} diff --git a/web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx b/web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx index c60f0ef2..c4258f67 100644 --- a/web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx +++ b/web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx @@ -28,11 +28,11 @@ import SelectableButtonGroup from '../../../common/ui/SelectableButtonGroup'; * @param {boolean} loading 是否加载中 * @param {Function} t i18n */ -const PricingEndpointTypes = ({ filterEndpointType, setFilterEndpointType, models = [], loading = false, t }) => { - // 获取所有可用的端点类型 +const PricingEndpointTypes = ({ filterEndpointType, setFilterEndpointType, models = [], allModels = [], loading = false, t }) => { + // 获取系统中所有端点类型(基于 allModels,如果未提供则退化为 models) const getAllEndpointTypes = () => { const endpointTypes = new Set(); - models.forEach(model => { + (allModels.length > 0 ? allModels : models).forEach(model => { if (model.supported_endpoint_types && Array.isArray(model.supported_endpoint_types)) { model.supported_endpoint_types.forEach(endpoint => { endpointTypes.add(endpoint); @@ -61,12 +61,16 @@ const PricingEndpointTypes = ({ filterEndpointType, setFilterEndpointType, model const availableEndpointTypes = getAllEndpointTypes(); const items = [ - { value: 'all', label: t('全部端点'), tagCount: getEndpointTypeCount('all') }, - ...availableEndpointTypes.map(endpointType => ({ - value: endpointType, - label: getEndpointTypeLabel(endpointType), - tagCount: getEndpointTypeCount(endpointType) - })) + { value: 'all', label: t('全部端点'), tagCount: getEndpointTypeCount('all'), disabled: models.length === 0 }, + ...availableEndpointTypes.map(endpointType => { + const count = getEndpointTypeCount(endpointType); + return ({ + value: endpointType, + label: getEndpointTypeLabel(endpointType), + tagCount: count, + disabled: count === 0 + }); + }) ]; return ( diff --git a/web/src/components/table/model-pricing/filter/PricingGroups.jsx b/web/src/components/table/model-pricing/filter/PricingGroups.jsx index e389bd12..432d23ab 100644 --- a/web/src/components/table/model-pricing/filter/PricingGroups.jsx +++ b/web/src/components/table/model-pricing/filter/PricingGroups.jsx @@ -34,6 +34,9 @@ const PricingGroups = ({ filterGroup, setFilterGroup, usableGroup = {}, groupRat const groups = ['all', ...Object.keys(usableGroup).filter(key => key !== '')]; const items = groups.map((g) => { + const modelCount = g === 'all' + ? models.length + : models.filter(m => m.enable_groups && m.enable_groups.includes(g)).length; let ratioDisplay = ''; if (g === 'all') { ratioDisplay = t('全部'); @@ -49,6 +52,7 @@ const PricingGroups = ({ filterGroup, setFilterGroup, usableGroup = {}, groupRat value: g, label: g === 'all' ? t('全部分组') : g, tagCount: ratioDisplay, + disabled: modelCount === 0 }; }); diff --git a/web/src/components/table/model-pricing/layout/PricingSidebar.jsx b/web/src/components/table/model-pricing/layout/PricingSidebar.jsx index a3e275c6..d6b5df79 100644 --- a/web/src/components/table/model-pricing/layout/PricingSidebar.jsx +++ b/web/src/components/table/model-pricing/layout/PricingSidebar.jsx @@ -25,6 +25,7 @@ import PricingQuotaTypes from '../filter/PricingQuotaTypes'; import PricingEndpointTypes from '../filter/PricingEndpointTypes'; import PricingDisplaySettings from '../filter/PricingDisplaySettings'; import { resetPricingFilters } from '../../../../helpers/utils'; +import { usePricingFilterCounts } from '../../../../hooks/model-pricing/usePricingFilterCounts'; const PricingSidebar = ({ showWithRecharge, @@ -52,6 +53,21 @@ const PricingSidebar = ({ ...categoryProps }) => { + const { + quotaTypeModels, + endpointTypeModels, + dynamicCategoryCounts, + groupCountModels, + } = usePricingFilterCounts({ + models: categoryProps.models, + modelCategories: categoryProps.modelCategories, + activeKey: categoryProps.activeKey, + filterGroup, + filterQuotaType, + filterEndpointType, + searchValue: categoryProps.searchValue, + }); + const handleResetFilters = () => resetPricingFilters({ handleChange, @@ -101,6 +117,7 @@ const PricingSidebar = ({ @@ -119,7 +136,7 @@ const PricingSidebar = ({ @@ -127,7 +144,8 @@ const PricingSidebar = ({ diff --git a/web/src/components/table/model-pricing/modal/components/FilterModalContent.jsx b/web/src/components/table/model-pricing/modal/components/FilterModalContent.jsx index aa9646fe..e9f3178e 100644 --- a/web/src/components/table/model-pricing/modal/components/FilterModalContent.jsx +++ b/web/src/components/table/model-pricing/modal/components/FilterModalContent.jsx @@ -23,6 +23,7 @@ import PricingCategories from '../../filter/PricingCategories'; import PricingGroups from '../../filter/PricingGroups'; import PricingQuotaTypes from '../../filter/PricingQuotaTypes'; import PricingEndpointTypes from '../../filter/PricingEndpointTypes'; +import { usePricingFilterCounts } from '../../../../../hooks/model-pricing/usePricingFilterCounts'; const FilterModalContent = ({ sidebarProps, t }) => { const { @@ -48,6 +49,21 @@ const FilterModalContent = ({ sidebarProps, t }) => { ...categoryProps } = sidebarProps; + const { + quotaTypeModels, + endpointTypeModels, + dynamicCategoryCounts, + groupCountModels, + } = usePricingFilterCounts({ + models: categoryProps.models, + modelCategories: categoryProps.modelCategories, + activeKey: categoryProps.activeKey, + filterGroup, + filterQuotaType, + filterEndpointType, + searchValue: sidebarProps.searchValue, + }); + return (
{ t={t} /> - + @@ -80,7 +102,7 @@ const FilterModalContent = ({ sidebarProps, t }) => { @@ -88,7 +110,8 @@ const FilterModalContent = ({ sidebarProps, t }) => { diff --git a/web/src/hooks/model-pricing/usePricingFilterCounts.js b/web/src/hooks/model-pricing/usePricingFilterCounts.js new file mode 100644 index 00000000..e23111f3 --- /dev/null +++ b/web/src/hooks/model-pricing/usePricingFilterCounts.js @@ -0,0 +1,131 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +/* + 统一计算模型筛选后的各种集合与动态计数,供多个组件复用 +*/ +import { useMemo } from 'react'; + +export const usePricingFilterCounts = ({ + models = [], + modelCategories = {}, + activeKey = 'all', + filterGroup = 'all', + filterQuotaType = 'all', + filterEndpointType = 'all', + searchValue = '', +}) => { + // 根据分类过滤后的模型 + const modelsAfterCategory = useMemo(() => { + if (activeKey === 'all') return models; + const category = modelCategories[activeKey]; + if (category && typeof category.filter === 'function') { + return models.filter(category.filter); + } + return models; + }, [models, activeKey, modelCategories]); + + // 根据除分类外其它过滤条件后的模型 (用于动态分类计数) + const modelsAfterOtherFilters = useMemo(() => { + let result = models; + if (filterGroup !== 'all') { + result = result.filter(m => m.enable_groups && m.enable_groups.includes(filterGroup)); + } + if (filterQuotaType !== 'all') { + result = result.filter(m => m.quota_type === filterQuotaType); + } + if (filterEndpointType !== 'all') { + result = result.filter(m => + m.supported_endpoint_types && m.supported_endpoint_types.includes(filterEndpointType) + ); + } + if (searchValue && searchValue.length > 0) { + const term = searchValue.toLowerCase(); + result = result.filter(m => m.model_name.toLowerCase().includes(term)); + } + return result; + }, [models, filterGroup, filterQuotaType, filterEndpointType, searchValue]); + + // 动态分类计数 + const dynamicCategoryCounts = useMemo(() => { + const counts = { all: modelsAfterOtherFilters.length }; + Object.entries(modelCategories).forEach(([key, category]) => { + if (key === 'all') return; + if (typeof category.filter === 'function') { + counts[key] = modelsAfterOtherFilters.filter(category.filter).length; + } else { + counts[key] = 0; + } + }); + return counts; + }, [modelsAfterOtherFilters, modelCategories]); + + // 针对计费类型按钮计数 + const quotaTypeModels = useMemo(() => { + let result = modelsAfterCategory; + if (filterGroup !== 'all') { + result = result.filter(m => m.enable_groups && m.enable_groups.includes(filterGroup)); + } + if (filterEndpointType !== 'all') { + result = result.filter(m => + m.supported_endpoint_types && m.supported_endpoint_types.includes(filterEndpointType) + ); + } + return result; + }, [modelsAfterCategory, filterGroup, filterEndpointType]); + + // 针对端点类型按钮计数 + const endpointTypeModels = useMemo(() => { + let result = modelsAfterCategory; + if (filterGroup !== 'all') { + result = result.filter(m => m.enable_groups && m.enable_groups.includes(filterGroup)); + } + if (filterQuotaType !== 'all') { + result = result.filter(m => m.quota_type === filterQuotaType); + } + return result; + }, [modelsAfterCategory, filterGroup, filterQuotaType]); + + // === 可用令牌分组计数模型(排除 group 过滤,保留其余过滤) === + const groupCountModels = useMemo(() => { + let result = modelsAfterCategory; // 已包含分类筛选 + + // 不应用 filterGroup 本身 + if (filterQuotaType !== 'all') { + result = result.filter(m => m.quota_type === filterQuotaType); + } + if (filterEndpointType !== 'all') { + result = result.filter(m => + m.supported_endpoint_types && m.supported_endpoint_types.includes(filterEndpointType) + ); + } + if (searchValue && searchValue.length > 0) { + const term = searchValue.toLowerCase(); + result = result.filter(m => m.model_name.toLowerCase().includes(term)); + } + return result; + }, [modelsAfterCategory, filterQuotaType, filterEndpointType, searchValue]); + + return { + quotaTypeModels, + endpointTypeModels, + dynamicCategoryCounts, + groupCountModels, + }; +}; \ No newline at end of file