✨ 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:
100
web/src/components/table/model-pricing/filter/PricingTags.jsx
Normal file
100
web/src/components/table/model-pricing/filter/PricingTags.jsx
Normal 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;
|
||||
@@ -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}
|
||||
|
||||
@@ -50,6 +50,7 @@ const PricingTopSection = ({
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
onChange={handleChange}
|
||||
showClear
|
||||
className="!bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ const PricingFilterModal = ({
|
||||
setFilterQuotaType: sidebarProps.setFilterQuotaType,
|
||||
setFilterEndpointType: sidebarProps.setFilterEndpointType,
|
||||
setFilterVendor: sidebarProps.setFilterVendor,
|
||||
setFilterTag: sidebarProps.setFilterTag,
|
||||
setCurrentPage: sidebarProps.setCurrentPage,
|
||||
setTokenUnit: sidebarProps.setTokenUnit,
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user