feat: Add tag-based filtering & refactor filter counts logic

Overview:
• Introduced a new “Model Tag” filter across pricing screens
• Refactored `usePricingFilterCounts` to eliminate duplicated logic
• Improved tag handling to be case-insensitive and deduplicated
• Extended utilities to reset & persist the new filter

Details:
1. Added `filterTag` state to `useModelPricingData` and integrated it into all filtering paths.
2. Created reusable `PricingTags` component using `SelectableButtonGroup`.
3. Incorporated tag filter into `PricingSidebar` and mobile `PricingFilterModal`, including reset support.
4. Enhanced `resetPricingFilters` (helpers/utils) to restore tag filter defaults.
5. Refactored `usePricingFilterCounts.js`:
   • Centralized predicate `matchesFilters` to remove redundancy
   • Normalized tag parsing via `normalizeTags` helper
   • Memoized model subsets with concise filter calls
6. Updated lints – zero errors after refactor.

Result:
Users can now filter models by custom tags with consistent UX, and internal logic is cleaner, faster, and easier to extend.
This commit is contained in:
t0ng7u
2025-08-10 14:05:25 +08:00
parent ffa898c52d
commit 870132a5cb
9 changed files with 250 additions and 84 deletions

View File

@@ -0,0 +1,100 @@
/*
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 React from 'react';
import SelectableButtonGroup from '../../../common/ui/SelectableButtonGroup';
/**
* 模型标签筛选组件
* @param {string|'all'} filterTag 当前选中的标签
* @param {Function} setFilterTag setter
* @param {Array} models 当前过滤后模型列表(用于计数)
* @param {Array} allModels 所有模型列表(用于获取所有标签)
* @param {boolean} loading 是否加载中
* @param {Function} t i18n
*/
const PricingTags = ({ filterTag, setFilterTag, models = [], allModels = [], loading = false, t }) => {
// 提取系统所有标签
const getAllTags = React.useMemo(() => {
const tagSet = new Set();
(allModels.length > 0 ? allModels : models).forEach(model => {
if (model.tags) {
model.tags
.split(/[,;|\s]+/) // 逗号、分号、竖线或空白字符
.map(tag => tag.trim())
.filter(Boolean)
.forEach(tag => tagSet.add(tag.toLowerCase()));
}
});
return Array.from(tagSet).sort((a, b) => a.localeCompare(b));
}, [allModels, models]);
// 计算标签对应的模型数量
const getTagCount = React.useCallback((tag) => {
if (tag === 'all') return models.length;
const tagLower = tag.toLowerCase();
return models.filter(model => {
if (!model.tags) return false;
return model.tags
.toLowerCase()
.split(/[,;|\s]+/)
.map(tg => tg.trim())
.includes(tagLower);
}).length;
}, [models]);
const items = React.useMemo(() => {
const result = [
{
value: 'all',
label: t('全部标签'),
tagCount: getTagCount('all'),
disabled: models.length === 0,
}
];
getAllTags.forEach(tag => {
const count = getTagCount(tag);
result.push({
value: tag,
label: tag,
tagCount: count,
disabled: count === 0,
});
});
return result;
}, [getAllTags, getTagCount, t, models.length]);
return (
<SelectableButtonGroup
title={t('标签')}
items={items}
activeValue={filterTag}
onChange={setFilterTag}
loading={loading}
t={t}
/>
);
};
export default PricingTags;

View File

@@ -23,6 +23,7 @@ import PricingGroups from '../filter/PricingGroups';
import PricingQuotaTypes from '../filter/PricingQuotaTypes';
import PricingEndpointTypes from '../filter/PricingEndpointTypes';
import PricingVendors from '../filter/PricingVendors';
import PricingTags from '../filter/PricingTags';
import PricingDisplaySettings from '../filter/PricingDisplaySettings';
import { resetPricingFilters } from '../../../../helpers/utils';
import { usePricingFilterCounts } from '../../../../hooks/model-pricing/usePricingFilterCounts';
@@ -47,6 +48,8 @@ const PricingSidebar = ({
setFilterEndpointType,
filterVendor,
setFilterVendor,
filterTag,
setFilterTag,
currentPage,
setCurrentPage,
tokenUnit,
@@ -60,6 +63,7 @@ const PricingSidebar = ({
quotaTypeModels,
endpointTypeModels,
vendorModels,
tagModels,
groupCountModels,
} = usePricingFilterCounts({
models: categoryProps.models,
@@ -67,6 +71,7 @@ const PricingSidebar = ({
filterQuotaType,
filterEndpointType,
filterVendor,
filterTag,
searchValue: categoryProps.searchValue,
});
@@ -81,6 +86,7 @@ const PricingSidebar = ({
setFilterQuotaType,
setFilterEndpointType,
setFilterVendor,
setFilterTag,
setCurrentPage,
setTokenUnit,
});
@@ -125,6 +131,15 @@ const PricingSidebar = ({
t={t}
/>
<PricingTags
filterTag={filterTag}
setFilterTag={setFilterTag}
models={tagModels}
allModels={categoryProps.models}
loading={loading}
t={t}
/>
<PricingGroups
filterGroup={filterGroup}
setFilterGroup={handleGroupClick}

View File

@@ -50,6 +50,7 @@ const PricingTopSection = ({
onCompositionEnd={handleCompositionEnd}
onChange={handleChange}
showClear
className="!bg-transparent"
/>
</div>

View File

@@ -40,6 +40,7 @@ const PricingFilterModal = ({
setFilterQuotaType: sidebarProps.setFilterQuotaType,
setFilterEndpointType: sidebarProps.setFilterEndpointType,
setFilterVendor: sidebarProps.setFilterVendor,
setFilterTag: sidebarProps.setFilterTag,
setCurrentPage: sidebarProps.setCurrentPage,
setTokenUnit: sidebarProps.setTokenUnit,
});

View File

@@ -23,6 +23,7 @@ import PricingGroups from '../../filter/PricingGroups';
import PricingQuotaTypes from '../../filter/PricingQuotaTypes';
import PricingEndpointTypes from '../../filter/PricingEndpointTypes';
import PricingVendors from '../../filter/PricingVendors';
import PricingTags from '../../filter/PricingTags';
import { usePricingFilterCounts } from '../../../../../hooks/model-pricing/usePricingFilterCounts';
const FilterModalContent = ({ sidebarProps, t }) => {
@@ -45,6 +46,8 @@ const FilterModalContent = ({ sidebarProps, t }) => {
setFilterEndpointType,
filterVendor,
setFilterVendor,
filterTag,
setFilterTag,
tokenUnit,
setTokenUnit,
loading,
@@ -55,6 +58,7 @@ const FilterModalContent = ({ sidebarProps, t }) => {
quotaTypeModels,
endpointTypeModels,
vendorModels,
tagModels,
groupCountModels,
} = usePricingFilterCounts({
models: categoryProps.models,
@@ -62,6 +66,7 @@ const FilterModalContent = ({ sidebarProps, t }) => {
filterQuotaType,
filterEndpointType,
filterVendor,
filterTag,
searchValue: sidebarProps.searchValue,
});
@@ -91,6 +96,15 @@ const FilterModalContent = ({ sidebarProps, t }) => {
t={t}
/>
<PricingTags
filterTag={filterTag}
setFilterTag={setFilterTag}
models={tagModels}
allModels={categoryProps.models}
loading={loading}
t={t}
/>
<PricingGroups
filterGroup={filterGroup}
setFilterGroup={setFilterGroup}