✨ 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 PricingQuotaTypes from '../filter/PricingQuotaTypes';
|
||||||
import PricingEndpointTypes from '../filter/PricingEndpointTypes';
|
import PricingEndpointTypes from '../filter/PricingEndpointTypes';
|
||||||
import PricingVendors from '../filter/PricingVendors';
|
import PricingVendors from '../filter/PricingVendors';
|
||||||
|
import PricingTags from '../filter/PricingTags';
|
||||||
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';
|
import { usePricingFilterCounts } from '../../../../hooks/model-pricing/usePricingFilterCounts';
|
||||||
@@ -47,6 +48,8 @@ const PricingSidebar = ({
|
|||||||
setFilterEndpointType,
|
setFilterEndpointType,
|
||||||
filterVendor,
|
filterVendor,
|
||||||
setFilterVendor,
|
setFilterVendor,
|
||||||
|
filterTag,
|
||||||
|
setFilterTag,
|
||||||
currentPage,
|
currentPage,
|
||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
tokenUnit,
|
tokenUnit,
|
||||||
@@ -60,6 +63,7 @@ const PricingSidebar = ({
|
|||||||
quotaTypeModels,
|
quotaTypeModels,
|
||||||
endpointTypeModels,
|
endpointTypeModels,
|
||||||
vendorModels,
|
vendorModels,
|
||||||
|
tagModels,
|
||||||
groupCountModels,
|
groupCountModels,
|
||||||
} = usePricingFilterCounts({
|
} = usePricingFilterCounts({
|
||||||
models: categoryProps.models,
|
models: categoryProps.models,
|
||||||
@@ -67,6 +71,7 @@ const PricingSidebar = ({
|
|||||||
filterQuotaType,
|
filterQuotaType,
|
||||||
filterEndpointType,
|
filterEndpointType,
|
||||||
filterVendor,
|
filterVendor,
|
||||||
|
filterTag,
|
||||||
searchValue: categoryProps.searchValue,
|
searchValue: categoryProps.searchValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,6 +86,7 @@ const PricingSidebar = ({
|
|||||||
setFilterQuotaType,
|
setFilterQuotaType,
|
||||||
setFilterEndpointType,
|
setFilterEndpointType,
|
||||||
setFilterVendor,
|
setFilterVendor,
|
||||||
|
setFilterTag,
|
||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
setTokenUnit,
|
setTokenUnit,
|
||||||
});
|
});
|
||||||
@@ -125,6 +131,15 @@ const PricingSidebar = ({
|
|||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PricingTags
|
||||||
|
filterTag={filterTag}
|
||||||
|
setFilterTag={setFilterTag}
|
||||||
|
models={tagModels}
|
||||||
|
allModels={categoryProps.models}
|
||||||
|
loading={loading}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
|
||||||
<PricingGroups
|
<PricingGroups
|
||||||
filterGroup={filterGroup}
|
filterGroup={filterGroup}
|
||||||
setFilterGroup={handleGroupClick}
|
setFilterGroup={handleGroupClick}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ const PricingTopSection = ({
|
|||||||
onCompositionEnd={handleCompositionEnd}
|
onCompositionEnd={handleCompositionEnd}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
showClear
|
showClear
|
||||||
|
className="!bg-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const PricingFilterModal = ({
|
|||||||
setFilterQuotaType: sidebarProps.setFilterQuotaType,
|
setFilterQuotaType: sidebarProps.setFilterQuotaType,
|
||||||
setFilterEndpointType: sidebarProps.setFilterEndpointType,
|
setFilterEndpointType: sidebarProps.setFilterEndpointType,
|
||||||
setFilterVendor: sidebarProps.setFilterVendor,
|
setFilterVendor: sidebarProps.setFilterVendor,
|
||||||
|
setFilterTag: sidebarProps.setFilterTag,
|
||||||
setCurrentPage: sidebarProps.setCurrentPage,
|
setCurrentPage: sidebarProps.setCurrentPage,
|
||||||
setTokenUnit: sidebarProps.setTokenUnit,
|
setTokenUnit: sidebarProps.setTokenUnit,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ 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 PricingVendors from '../../filter/PricingVendors';
|
import PricingVendors from '../../filter/PricingVendors';
|
||||||
|
import PricingTags from '../../filter/PricingTags';
|
||||||
import { usePricingFilterCounts } from '../../../../../hooks/model-pricing/usePricingFilterCounts';
|
import { usePricingFilterCounts } from '../../../../../hooks/model-pricing/usePricingFilterCounts';
|
||||||
|
|
||||||
const FilterModalContent = ({ sidebarProps, t }) => {
|
const FilterModalContent = ({ sidebarProps, t }) => {
|
||||||
@@ -45,6 +46,8 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
|||||||
setFilterEndpointType,
|
setFilterEndpointType,
|
||||||
filterVendor,
|
filterVendor,
|
||||||
setFilterVendor,
|
setFilterVendor,
|
||||||
|
filterTag,
|
||||||
|
setFilterTag,
|
||||||
tokenUnit,
|
tokenUnit,
|
||||||
setTokenUnit,
|
setTokenUnit,
|
||||||
loading,
|
loading,
|
||||||
@@ -55,6 +58,7 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
|||||||
quotaTypeModels,
|
quotaTypeModels,
|
||||||
endpointTypeModels,
|
endpointTypeModels,
|
||||||
vendorModels,
|
vendorModels,
|
||||||
|
tagModels,
|
||||||
groupCountModels,
|
groupCountModels,
|
||||||
} = usePricingFilterCounts({
|
} = usePricingFilterCounts({
|
||||||
models: categoryProps.models,
|
models: categoryProps.models,
|
||||||
@@ -62,6 +66,7 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
|||||||
filterQuotaType,
|
filterQuotaType,
|
||||||
filterEndpointType,
|
filterEndpointType,
|
||||||
filterVendor,
|
filterVendor,
|
||||||
|
filterTag,
|
||||||
searchValue: sidebarProps.searchValue,
|
searchValue: sidebarProps.searchValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,6 +96,15 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
|||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PricingTags
|
||||||
|
filterTag={filterTag}
|
||||||
|
setFilterTag={setFilterTag}
|
||||||
|
models={tagModels}
|
||||||
|
allModels={categoryProps.models}
|
||||||
|
loading={loading}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
|
||||||
<PricingGroups
|
<PricingGroups
|
||||||
filterGroup={filterGroup}
|
filterGroup={filterGroup}
|
||||||
setFilterGroup={setFilterGroup}
|
setFilterGroup={setFilterGroup}
|
||||||
|
|||||||
@@ -699,6 +699,7 @@ const DEFAULT_PRICING_FILTERS = {
|
|||||||
filterQuotaType: 'all',
|
filterQuotaType: 'all',
|
||||||
filterEndpointType: 'all',
|
filterEndpointType: 'all',
|
||||||
filterVendor: 'all',
|
filterVendor: 'all',
|
||||||
|
filterTag: 'all',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -713,6 +714,7 @@ export const resetPricingFilters = ({
|
|||||||
setFilterQuotaType,
|
setFilterQuotaType,
|
||||||
setFilterEndpointType,
|
setFilterEndpointType,
|
||||||
setFilterVendor,
|
setFilterVendor,
|
||||||
|
setFilterTag,
|
||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
setTokenUnit,
|
setTokenUnit,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -726,5 +728,6 @@ export const resetPricingFilters = ({
|
|||||||
setFilterQuotaType?.(DEFAULT_PRICING_FILTERS.filterQuotaType);
|
setFilterQuotaType?.(DEFAULT_PRICING_FILTERS.filterQuotaType);
|
||||||
setFilterEndpointType?.(DEFAULT_PRICING_FILTERS.filterEndpointType);
|
setFilterEndpointType?.(DEFAULT_PRICING_FILTERS.filterEndpointType);
|
||||||
setFilterVendor?.(DEFAULT_PRICING_FILTERS.filterVendor);
|
setFilterVendor?.(DEFAULT_PRICING_FILTERS.filterVendor);
|
||||||
|
setFilterTag?.(DEFAULT_PRICING_FILTERS.filterTag);
|
||||||
setCurrentPage?.(DEFAULT_PRICING_FILTERS.currentPage);
|
setCurrentPage?.(DEFAULT_PRICING_FILTERS.currentPage);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const useModelPricingData = () => {
|
|||||||
const [filterQuotaType, setFilterQuotaType] = useState('all'); // 计费类型筛选: 'all' | 0 | 1
|
const [filterQuotaType, setFilterQuotaType] = useState('all'); // 计费类型筛选: 'all' | 0 | 1
|
||||||
const [filterEndpointType, setFilterEndpointType] = useState('all'); // 端点类型筛选: 'all' | string
|
const [filterEndpointType, setFilterEndpointType] = useState('all'); // 端点类型筛选: 'all' | string
|
||||||
const [filterVendor, setFilterVendor] = useState('all'); // 供应商筛选: 'all' | 'unknown' | string
|
const [filterVendor, setFilterVendor] = useState('all'); // 供应商筛选: 'all' | 'unknown' | string
|
||||||
|
const [filterTag, setFilterTag] = useState('all'); // 模型标签筛选: 'all' | string
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [currency, setCurrency] = useState('USD');
|
const [currency, setCurrency] = useState('USD');
|
||||||
@@ -88,6 +89,20 @@ export const useModelPricingData = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 标签筛选
|
||||||
|
if (filterTag !== 'all') {
|
||||||
|
const tagLower = filterTag.toLowerCase();
|
||||||
|
result = result.filter(model => {
|
||||||
|
if (!model.tags) return false;
|
||||||
|
const tagsArr = model.tags
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/[,;|\s]+/)
|
||||||
|
.map(tag => tag.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return tagsArr.includes(tagLower);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 搜索筛选
|
// 搜索筛选
|
||||||
if (searchValue.length > 0) {
|
if (searchValue.length > 0) {
|
||||||
const searchTerm = searchValue.toLowerCase();
|
const searchTerm = searchValue.toLowerCase();
|
||||||
@@ -100,7 +115,7 @@ export const useModelPricingData = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [models, searchValue, filterGroup, filterQuotaType, filterEndpointType, filterVendor]);
|
}, [models, searchValue, filterGroup, filterQuotaType, filterEndpointType, filterVendor, filterTag]);
|
||||||
|
|
||||||
const rowSelection = useMemo(
|
const rowSelection = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -245,7 +260,7 @@ export const useModelPricingData = () => {
|
|||||||
// 当筛选条件变化时重置到第一页
|
// 当筛选条件变化时重置到第一页
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}, [filterGroup, filterQuotaType, filterEndpointType, filterVendor, searchValue]);
|
}, [filterGroup, filterQuotaType, filterEndpointType, filterVendor, filterTag, searchValue]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 状态
|
// 状态
|
||||||
@@ -271,6 +286,8 @@ export const useModelPricingData = () => {
|
|||||||
setFilterEndpointType,
|
setFilterEndpointType,
|
||||||
filterVendor,
|
filterVendor,
|
||||||
setFilterVendor,
|
setFilterVendor,
|
||||||
|
filterTag,
|
||||||
|
setFilterTag,
|
||||||
pageSize,
|
pageSize,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
currentPage,
|
currentPage,
|
||||||
|
|||||||
@@ -17,115 +17,128 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
|
||||||
统一计算模型筛选后的各种集合与动态计数,供多个组件复用
|
|
||||||
*/
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
// 工具函数:将 tags 字符串转为小写去重数组
|
||||||
|
const normalizeTags = (tags = '') =>
|
||||||
|
tags
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/[,;|\s]+/)
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一计算模型筛选后的各种集合与动态计数,供多个组件复用
|
||||||
|
*/
|
||||||
export const usePricingFilterCounts = ({
|
export const usePricingFilterCounts = ({
|
||||||
models = [],
|
models = [],
|
||||||
filterGroup = 'all',
|
filterGroup = 'all',
|
||||||
filterQuotaType = 'all',
|
filterQuotaType = 'all',
|
||||||
filterEndpointType = 'all',
|
filterEndpointType = 'all',
|
||||||
filterVendor = 'all',
|
filterVendor = 'all',
|
||||||
|
filterTag = 'all',
|
||||||
searchValue = '',
|
searchValue = '',
|
||||||
}) => {
|
}) => {
|
||||||
// 所有模型(不再需要分类过滤)
|
// 均使用同一份模型列表,避免创建新引用
|
||||||
const allModels = models;
|
const allModels = models;
|
||||||
|
|
||||||
// 针对计费类型按钮计数
|
/**
|
||||||
const quotaTypeModels = useMemo(() => {
|
* 通用过滤函数
|
||||||
let result = allModels;
|
* @param {Object} model
|
||||||
if (filterGroup !== 'all') {
|
* @param {Array<string>} ignore 需要忽略的过滤条件 key
|
||||||
result = result.filter(m => m.enable_groups && m.enable_groups.includes(filterGroup));
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const matchesFilters = (model, ignore = []) => {
|
||||||
|
// 分组
|
||||||
|
if (!ignore.includes('group') && filterGroup !== 'all') {
|
||||||
|
if (!model.enable_groups || !model.enable_groups.includes(filterGroup)) return false;
|
||||||
}
|
}
|
||||||
if (filterEndpointType !== 'all') {
|
|
||||||
result = result.filter(m =>
|
// 计费类型
|
||||||
m.supported_endpoint_types && m.supported_endpoint_types.includes(filterEndpointType)
|
if (!ignore.includes('quota') && filterQuotaType !== 'all') {
|
||||||
);
|
if (model.quota_type !== filterQuotaType) return false;
|
||||||
}
|
}
|
||||||
if (filterVendor !== 'all') {
|
|
||||||
|
// 端点类型
|
||||||
|
if (!ignore.includes('endpoint') && filterEndpointType !== 'all') {
|
||||||
|
if (
|
||||||
|
!model.supported_endpoint_types ||
|
||||||
|
!model.supported_endpoint_types.includes(filterEndpointType)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 供应商
|
||||||
|
if (!ignore.includes('vendor') && filterVendor !== 'all') {
|
||||||
if (filterVendor === 'unknown') {
|
if (filterVendor === 'unknown') {
|
||||||
result = result.filter(m => !m.vendor_name);
|
if (model.vendor_name) return false;
|
||||||
} else {
|
} else if (model.vendor_name !== filterVendor) {
|
||||||
result = result.filter(m => m.vendor_name === filterVendor);
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}, [allModels, filterGroup, filterEndpointType, filterVendor]);
|
|
||||||
|
|
||||||
// 针对端点类型按钮计数
|
// 标签
|
||||||
const endpointTypeModels = useMemo(() => {
|
if (!ignore.includes('tag') && filterTag !== 'all') {
|
||||||
let result = allModels;
|
const tagsArr = normalizeTags(model.tags);
|
||||||
if (filterGroup !== 'all') {
|
if (!tagsArr.includes(filterTag.toLowerCase())) return false;
|
||||||
result = result.filter(m => m.enable_groups && m.enable_groups.includes(filterGroup));
|
|
||||||
}
|
}
|
||||||
if (filterQuotaType !== 'all') {
|
|
||||||
result = result.filter(m => m.quota_type === filterQuotaType);
|
|
||||||
}
|
|
||||||
if (filterVendor !== 'all') {
|
|
||||||
if (filterVendor === 'unknown') {
|
|
||||||
result = result.filter(m => !m.vendor_name);
|
|
||||||
} else {
|
|
||||||
result = result.filter(m => m.vendor_name === filterVendor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, [allModels, filterGroup, filterQuotaType, filterVendor]);
|
|
||||||
|
|
||||||
// === 可用令牌分组计数模型(排除 group 过滤,保留其余过滤) ===
|
// 搜索
|
||||||
const groupCountModels = useMemo(() => {
|
if (!ignore.includes('search') && searchValue) {
|
||||||
let result = allModels;
|
|
||||||
|
|
||||||
// 不应用 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 (filterVendor !== 'all') {
|
|
||||||
if (filterVendor === 'unknown') {
|
|
||||||
result = result.filter(m => !m.vendor_name);
|
|
||||||
} else {
|
|
||||||
result = result.filter(m => m.vendor_name === filterVendor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (searchValue && searchValue.length > 0) {
|
|
||||||
const term = searchValue.toLowerCase();
|
const term = searchValue.toLowerCase();
|
||||||
result = result.filter(m =>
|
const tags = model.tags ? model.tags.toLowerCase() : '';
|
||||||
m.model_name.toLowerCase().includes(term) ||
|
if (
|
||||||
(m.description && m.description.toLowerCase().includes(term)) ||
|
!(
|
||||||
(m.tags && m.tags.toLowerCase().includes(term)) ||
|
model.model_name.toLowerCase().includes(term) ||
|
||||||
(m.vendor_name && m.vendor_name.toLowerCase().includes(term))
|
(model.description && model.description.toLowerCase().includes(term)) ||
|
||||||
);
|
tags.includes(term) ||
|
||||||
|
(model.vendor_name && model.vendor_name.toLowerCase().includes(term))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}, [allModels, filterQuotaType, filterEndpointType, filterVendor, searchValue]);
|
|
||||||
|
|
||||||
// 针对供应商按钮计数
|
return true;
|
||||||
const vendorModels = useMemo(() => {
|
};
|
||||||
let result = allModels;
|
|
||||||
if (filterGroup !== 'all') {
|
// 生成不同视图所需的模型集合
|
||||||
result = result.filter(m => m.enable_groups && m.enable_groups.includes(filterGroup));
|
const quotaTypeModels = useMemo(
|
||||||
}
|
() => allModels.filter((m) => matchesFilters(m, ['quota'])),
|
||||||
if (filterQuotaType !== 'all') {
|
[allModels, filterGroup, filterEndpointType, filterVendor, filterTag]
|
||||||
result = result.filter(m => m.quota_type === filterQuotaType);
|
);
|
||||||
}
|
|
||||||
if (filterEndpointType !== 'all') {
|
const endpointTypeModels = useMemo(
|
||||||
result = result.filter(m =>
|
() => allModels.filter((m) => matchesFilters(m, ['endpoint'])),
|
||||||
m.supported_endpoint_types && m.supported_endpoint_types.includes(filterEndpointType)
|
[allModels, filterGroup, filterQuotaType, filterVendor, filterTag]
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return result;
|
const vendorModels = useMemo(
|
||||||
}, [allModels, filterGroup, filterQuotaType, filterEndpointType]);
|
() => allModels.filter((m) => matchesFilters(m, ['vendor'])),
|
||||||
|
[allModels, filterGroup, filterQuotaType, filterEndpointType, filterTag]
|
||||||
|
);
|
||||||
|
|
||||||
|
const tagModels = useMemo(
|
||||||
|
() => allModels.filter((m) => matchesFilters(m, ['tag'])),
|
||||||
|
[allModels, filterGroup, filterQuotaType, filterEndpointType, filterVendor]
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupCountModels = useMemo(
|
||||||
|
() => allModels.filter((m) => matchesFilters(m, ['group'])),
|
||||||
|
[
|
||||||
|
allModels,
|
||||||
|
filterQuotaType,
|
||||||
|
filterEndpointType,
|
||||||
|
filterVendor,
|
||||||
|
filterTag,
|
||||||
|
searchValue,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
quotaTypeModels,
|
quotaTypeModels,
|
||||||
endpointTypeModels,
|
endpointTypeModels,
|
||||||
vendorModels,
|
vendorModels,
|
||||||
groupCountModels,
|
groupCountModels,
|
||||||
|
tagModels,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1876,6 +1876,7 @@
|
|||||||
"全部分组": "All groups",
|
"全部分组": "All groups",
|
||||||
"全部类型": "All types",
|
"全部类型": "All types",
|
||||||
"全部端点": "All endpoints",
|
"全部端点": "All endpoints",
|
||||||
|
"全部标签": "All tags",
|
||||||
"显示倍率": "Show ratio",
|
"显示倍率": "Show ratio",
|
||||||
"表格视图": "Table view",
|
"表格视图": "Table view",
|
||||||
"模型的详细描述和基本特性": "Detailed description and basic characteristics of the model",
|
"模型的详细描述和基本特性": "Detailed description and basic characteristics of the model",
|
||||||
@@ -1914,5 +1915,6 @@
|
|||||||
"精确名称匹配": "Exact name matching",
|
"精确名称匹配": "Exact name matching",
|
||||||
"前缀名称匹配": "Prefix name matching",
|
"前缀名称匹配": "Prefix name matching",
|
||||||
"后缀名称匹配": "Suffix name matching",
|
"后缀名称匹配": "Suffix name matching",
|
||||||
"包含名称匹配": "Contains name matching"
|
"包含名称匹配": "Contains name matching",
|
||||||
|
"展开更多": "Expand more"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user