✨ refactor: pricing filters for dynamic counting & cleaner logic
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.
This commit is contained in:
@@ -133,6 +133,7 @@ const SelectableButtonGroup = ({
|
|||||||
const contentElement = showSkeleton ? renderSkeletonButtons() : (
|
const contentElement = showSkeleton ? renderSkeletonButtons() : (
|
||||||
<Row gutter={[8, 8]} style={{ lineHeight: '32px', ...style }} ref={contentRef}>
|
<Row gutter={[8, 8]} style={{ lineHeight: '32px', ...style }} ref={contentRef}>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
|
const isDisabled = item.disabled || (typeof item.tagCount === 'number' && item.tagCount === 0);
|
||||||
const isActive = Array.isArray(activeValue)
|
const isActive = Array.isArray(activeValue)
|
||||||
? activeValue.includes(item.value)
|
? activeValue.includes(item.value)
|
||||||
: activeValue === item.value;
|
: activeValue === item.value;
|
||||||
@@ -150,10 +151,12 @@ const SelectableButtonGroup = ({
|
|||||||
onClick={() => { /* disabled */ }}
|
onClick={() => { /* disabled */ }}
|
||||||
theme={isActive ? 'light' : 'outline'}
|
theme={isActive ? 'light' : 'outline'}
|
||||||
type={isActive ? 'primary' : 'tertiary'}
|
type={isActive ? 'primary' : 'tertiary'}
|
||||||
|
disabled={isDisabled}
|
||||||
icon={
|
icon={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isActive}
|
checked={isActive}
|
||||||
onChange={() => onChange(item.value)}
|
onChange={() => onChange(item.value)}
|
||||||
|
disabled={isDisabled}
|
||||||
style={{ pointerEvents: 'auto' }}
|
style={{ pointerEvents: 'auto' }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -190,6 +193,7 @@ const SelectableButtonGroup = ({
|
|||||||
theme={isActive ? 'light' : 'outline'}
|
theme={isActive ? 'light' : 'outline'}
|
||||||
type={isActive ? 'primary' : 'tertiary'}
|
type={isActive ? 'primary' : 'tertiary'}
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
|
disabled={isDisabled}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
>
|
>
|
||||||
<span style={{ marginRight: item.tagCount !== undefined ? 4 : 0 }}>{item.label}</span>
|
<span style={{ marginRight: item.tagCount !== undefined ? 4 : 0 }}>{item.label}</span>
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ import SelectableButtonGroup from '../../../common/ui/SelectableButtonGroup';
|
|||||||
* @param {boolean} loading 是否加载中
|
* @param {boolean} loading 是否加载中
|
||||||
* @param {Function} t i18n
|
* @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 getAllEndpointTypes = () => {
|
||||||
const endpointTypes = new Set();
|
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)) {
|
if (model.supported_endpoint_types && Array.isArray(model.supported_endpoint_types)) {
|
||||||
model.supported_endpoint_types.forEach(endpoint => {
|
model.supported_endpoint_types.forEach(endpoint => {
|
||||||
endpointTypes.add(endpoint);
|
endpointTypes.add(endpoint);
|
||||||
@@ -61,12 +61,16 @@ const PricingEndpointTypes = ({ filterEndpointType, setFilterEndpointType, model
|
|||||||
const availableEndpointTypes = getAllEndpointTypes();
|
const availableEndpointTypes = getAllEndpointTypes();
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{ value: 'all', label: t('全部端点'), tagCount: getEndpointTypeCount('all') },
|
{ value: 'all', label: t('全部端点'), tagCount: getEndpointTypeCount('all'), disabled: models.length === 0 },
|
||||||
...availableEndpointTypes.map(endpointType => ({
|
...availableEndpointTypes.map(endpointType => {
|
||||||
value: endpointType,
|
const count = getEndpointTypeCount(endpointType);
|
||||||
label: getEndpointTypeLabel(endpointType),
|
return ({
|
||||||
tagCount: getEndpointTypeCount(endpointType)
|
value: endpointType,
|
||||||
}))
|
label: getEndpointTypeLabel(endpointType),
|
||||||
|
tagCount: count,
|
||||||
|
disabled: count === 0
|
||||||
|
});
|
||||||
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ const PricingGroups = ({ filterGroup, setFilterGroup, usableGroup = {}, groupRat
|
|||||||
const groups = ['all', ...Object.keys(usableGroup).filter(key => key !== '')];
|
const groups = ['all', ...Object.keys(usableGroup).filter(key => key !== '')];
|
||||||
|
|
||||||
const items = groups.map((g) => {
|
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 = '';
|
let ratioDisplay = '';
|
||||||
if (g === 'all') {
|
if (g === 'all') {
|
||||||
ratioDisplay = t('全部');
|
ratioDisplay = t('全部');
|
||||||
@@ -49,6 +52,7 @@ const PricingGroups = ({ filterGroup, setFilterGroup, usableGroup = {}, groupRat
|
|||||||
value: g,
|
value: g,
|
||||||
label: g === 'all' ? t('全部分组') : g,
|
label: g === 'all' ? t('全部分组') : g,
|
||||||
tagCount: ratioDisplay,
|
tagCount: ratioDisplay,
|
||||||
|
disabled: modelCount === 0
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import PricingQuotaTypes from '../filter/PricingQuotaTypes';
|
|||||||
import PricingEndpointTypes from '../filter/PricingEndpointTypes';
|
import PricingEndpointTypes from '../filter/PricingEndpointTypes';
|
||||||
import PricingDisplaySettings from '../filter/PricingDisplaySettings';
|
import PricingDisplaySettings from '../filter/PricingDisplaySettings';
|
||||||
import { resetPricingFilters } from '../../../../helpers/utils';
|
import { resetPricingFilters } from '../../../../helpers/utils';
|
||||||
|
import { usePricingFilterCounts } from '../../../../hooks/model-pricing/usePricingFilterCounts';
|
||||||
|
|
||||||
const PricingSidebar = ({
|
const PricingSidebar = ({
|
||||||
showWithRecharge,
|
showWithRecharge,
|
||||||
@@ -52,6 +53,21 @@ const PricingSidebar = ({
|
|||||||
...categoryProps
|
...categoryProps
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
|
const {
|
||||||
|
quotaTypeModels,
|
||||||
|
endpointTypeModels,
|
||||||
|
dynamicCategoryCounts,
|
||||||
|
groupCountModels,
|
||||||
|
} = usePricingFilterCounts({
|
||||||
|
models: categoryProps.models,
|
||||||
|
modelCategories: categoryProps.modelCategories,
|
||||||
|
activeKey: categoryProps.activeKey,
|
||||||
|
filterGroup,
|
||||||
|
filterQuotaType,
|
||||||
|
filterEndpointType,
|
||||||
|
searchValue: categoryProps.searchValue,
|
||||||
|
});
|
||||||
|
|
||||||
const handleResetFilters = () =>
|
const handleResetFilters = () =>
|
||||||
resetPricingFilters({
|
resetPricingFilters({
|
||||||
handleChange,
|
handleChange,
|
||||||
@@ -101,6 +117,7 @@ const PricingSidebar = ({
|
|||||||
|
|
||||||
<PricingCategories
|
<PricingCategories
|
||||||
{...categoryProps}
|
{...categoryProps}
|
||||||
|
categoryCounts={dynamicCategoryCounts}
|
||||||
setActiveKey={setActiveKey}
|
setActiveKey={setActiveKey}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
t={t}
|
t={t}
|
||||||
@@ -111,7 +128,7 @@ const PricingSidebar = ({
|
|||||||
setFilterGroup={setFilterGroup}
|
setFilterGroup={setFilterGroup}
|
||||||
usableGroup={categoryProps.usableGroup}
|
usableGroup={categoryProps.usableGroup}
|
||||||
groupRatio={categoryProps.groupRatio}
|
groupRatio={categoryProps.groupRatio}
|
||||||
models={categoryProps.models}
|
models={groupCountModels}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
@@ -119,7 +136,7 @@ const PricingSidebar = ({
|
|||||||
<PricingQuotaTypes
|
<PricingQuotaTypes
|
||||||
filterQuotaType={filterQuotaType}
|
filterQuotaType={filterQuotaType}
|
||||||
setFilterQuotaType={setFilterQuotaType}
|
setFilterQuotaType={setFilterQuotaType}
|
||||||
models={categoryProps.models}
|
models={quotaTypeModels}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
@@ -127,7 +144,8 @@ const PricingSidebar = ({
|
|||||||
<PricingEndpointTypes
|
<PricingEndpointTypes
|
||||||
filterEndpointType={filterEndpointType}
|
filterEndpointType={filterEndpointType}
|
||||||
setFilterEndpointType={setFilterEndpointType}
|
setFilterEndpointType={setFilterEndpointType}
|
||||||
models={categoryProps.models}
|
models={endpointTypeModels}
|
||||||
|
allModels={categoryProps.models}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import PricingCategories from '../../filter/PricingCategories';
|
|||||||
import PricingGroups from '../../filter/PricingGroups';
|
import PricingGroups from '../../filter/PricingGroups';
|
||||||
import PricingQuotaTypes from '../../filter/PricingQuotaTypes';
|
import PricingQuotaTypes from '../../filter/PricingQuotaTypes';
|
||||||
import PricingEndpointTypes from '../../filter/PricingEndpointTypes';
|
import PricingEndpointTypes from '../../filter/PricingEndpointTypes';
|
||||||
|
import { usePricingFilterCounts } from '../../../../../hooks/model-pricing/usePricingFilterCounts';
|
||||||
|
|
||||||
const FilterModalContent = ({ sidebarProps, t }) => {
|
const FilterModalContent = ({ sidebarProps, t }) => {
|
||||||
const {
|
const {
|
||||||
@@ -48,6 +49,21 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
|||||||
...categoryProps
|
...categoryProps
|
||||||
} = sidebarProps;
|
} = sidebarProps;
|
||||||
|
|
||||||
|
const {
|
||||||
|
quotaTypeModels,
|
||||||
|
endpointTypeModels,
|
||||||
|
dynamicCategoryCounts,
|
||||||
|
groupCountModels,
|
||||||
|
} = usePricingFilterCounts({
|
||||||
|
models: categoryProps.models,
|
||||||
|
modelCategories: categoryProps.modelCategories,
|
||||||
|
activeKey: categoryProps.activeKey,
|
||||||
|
filterGroup,
|
||||||
|
filterQuotaType,
|
||||||
|
filterEndpointType,
|
||||||
|
searchValue: sidebarProps.searchValue,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<PricingDisplaySettings
|
<PricingDisplaySettings
|
||||||
@@ -65,14 +81,20 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
|||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PricingCategories {...categoryProps} setActiveKey={setActiveKey} loading={loading} t={t} />
|
<PricingCategories
|
||||||
|
{...categoryProps}
|
||||||
|
categoryCounts={dynamicCategoryCounts}
|
||||||
|
setActiveKey={setActiveKey}
|
||||||
|
loading={loading}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
|
||||||
<PricingGroups
|
<PricingGroups
|
||||||
filterGroup={filterGroup}
|
filterGroup={filterGroup}
|
||||||
setFilterGroup={setFilterGroup}
|
setFilterGroup={setFilterGroup}
|
||||||
usableGroup={categoryProps.usableGroup}
|
usableGroup={categoryProps.usableGroup}
|
||||||
groupRatio={categoryProps.groupRatio}
|
groupRatio={categoryProps.groupRatio}
|
||||||
models={categoryProps.models}
|
models={groupCountModels}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
@@ -80,7 +102,7 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
|||||||
<PricingQuotaTypes
|
<PricingQuotaTypes
|
||||||
filterQuotaType={filterQuotaType}
|
filterQuotaType={filterQuotaType}
|
||||||
setFilterQuotaType={setFilterQuotaType}
|
setFilterQuotaType={setFilterQuotaType}
|
||||||
models={categoryProps.models}
|
models={quotaTypeModels}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
@@ -88,7 +110,8 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
|||||||
<PricingEndpointTypes
|
<PricingEndpointTypes
|
||||||
filterEndpointType={filterEndpointType}
|
filterEndpointType={filterEndpointType}
|
||||||
setFilterEndpointType={setFilterEndpointType}
|
setFilterEndpointType={setFilterEndpointType}
|
||||||
models={categoryProps.models}
|
models={endpointTypeModels}
|
||||||
|
allModels={categoryProps.models}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|||||||
131
web/src/hooks/model-pricing/usePricingFilterCounts.js
Normal file
131
web/src/hooks/model-pricing/usePricingFilterCounts.js
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user