🏗️ refactor: Replace model categories with vendor-based filtering and optimize data structure

- **Backend Changes:**
  - Refactor pricing API to return separate vendors array with ID-based model references
  - Remove redundant vendor_name/vendor_icon fields from pricing records, use vendor_id only
  - Add vendor_description to pricing response for frontend display
  - Maintain 1-minute cache protection for pricing endpoint security

- **Frontend Data Flow:**
  - Update useModelPricingData hook to build vendorsMap from API response
  - Enhance model records with vendor info during data processing
  - Pass vendorsMap through component hierarchy for consistent vendor data access

- **UI Component Replacements:**
  - Replace PricingCategories with PricingVendors component for vendor-based filtering
  - Replace PricingCategoryIntro with PricingVendorIntro in header section
  - Remove all model category related components and logic

- **Header Improvements:**
  - Implement vendor intro with real backend data (name, icon, description)
  - Add text collapsible feature (2-line limit with expand/collapse functionality)
  - Support carousel animation for "All Vendors" view with vendor icon rotation

- **Model Detail Modal Enhancements:**
  - Update ModelHeader to use real vendor icons via getLobeHubIcon()
  - Move tags from header to ModelBasicInfo content area to avoid SideSheet title width constraints
  - Display only custom tags from backend with stringToColor() for consistent styling
  - Use Space component with wrap property for proper tag layout

- **Table View Optimizations:**
  - Integrate RenderUtils for description and tags columns
  - Implement renderLimitedItems for tags (max 3 visible, +x popover for overflow)
  - Use renderDescription for text truncation with tooltip support

- **Filter Logic Updates:**
  - Vendor filter shows disabled options instead of hiding when no models match
  - Include "Unknown Vendor" category for models without vendor information
  - Remove all hardcoded vendor descriptions, use real backend data

- **Code Quality:**
  - Fix import paths after component relocation
  - Remove unused model category utilities and hardcoded mappings
  - Ensure consistent vendor data usage across all pricing views
  - Maintain backward compatibility with existing pricing calculation logic

This refactor provides a more scalable vendor-based architecture while eliminating
data redundancy and improving user experience with real-time backend data integration.
This commit is contained in:
t0ng7u
2025-08-04 21:36:31 +08:00
parent fc69f4f757
commit 0e9c3cde7c
24 changed files with 780 additions and 576 deletions

View File

@@ -46,6 +46,7 @@ const ModelDetailSideSheet = ({
displayPrice,
showRatio,
usableGroup,
vendorsMap,
t,
}) => {
const isMobile = useIsMobile();
@@ -53,7 +54,7 @@ const ModelDetailSideSheet = ({
return (
<SideSheet
placement="right"
title={<ModelHeader modelData={modelData} t={t} />}
title={<ModelHeader modelData={modelData} vendorsMap={vendorsMap} t={t} />}
bodyStyle={{
padding: '0',
display: 'flex',
@@ -80,7 +81,7 @@ const ModelDetailSideSheet = ({
)}
{modelData && (
<>
<ModelBasicInfo modelData={modelData} t={t} />
<ModelBasicInfo modelData={modelData} vendorsMap={vendorsMap} t={t} />
<ModelEndpoints modelData={modelData} t={t} />
<ModelPricingTable
modelData={modelData}

View File

@@ -32,8 +32,6 @@ const PricingFilterModal = ({
const handleResetFilters = () =>
resetPricingFilters({
handleChange: sidebarProps.handleChange,
setActiveKey: sidebarProps.setActiveKey,
availableCategories: sidebarProps.availableCategories,
setShowWithRecharge: sidebarProps.setShowWithRecharge,
setCurrency: sidebarProps.setCurrency,
setShowRatio: sidebarProps.setShowRatio,
@@ -41,6 +39,7 @@ const PricingFilterModal = ({
setFilterGroup: sidebarProps.setFilterGroup,
setFilterQuotaType: sidebarProps.setFilterQuotaType,
setFilterEndpointType: sidebarProps.setFilterEndpointType,
setFilterVendor: sidebarProps.setFilterVendor,
setCurrentPage: sidebarProps.setCurrentPage,
setTokenUnit: sidebarProps.setTokenUnit,
});

View File

@@ -19,10 +19,10 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react';
import PricingDisplaySettings from '../../filter/PricingDisplaySettings';
import PricingCategories from '../../filter/PricingCategories';
import PricingGroups from '../../filter/PricingGroups';
import PricingQuotaTypes from '../../filter/PricingQuotaTypes';
import PricingEndpointTypes from '../../filter/PricingEndpointTypes';
import PricingVendors from '../../filter/PricingVendors';
import { usePricingFilterCounts } from '../../../../../hooks/model-pricing/usePricingFilterCounts';
const FilterModalContent = ({ sidebarProps, t }) => {
@@ -43,6 +43,8 @@ const FilterModalContent = ({ sidebarProps, t }) => {
setFilterQuotaType,
filterEndpointType,
setFilterEndpointType,
filterVendor,
setFilterVendor,
tokenUnit,
setTokenUnit,
loading,
@@ -52,15 +54,14 @@ const FilterModalContent = ({ sidebarProps, t }) => {
const {
quotaTypeModels,
endpointTypeModels,
dynamicCategoryCounts,
vendorModels,
groupCountModels,
} = usePricingFilterCounts({
models: categoryProps.models,
modelCategories: categoryProps.modelCategories,
activeKey: categoryProps.activeKey,
filterGroup,
filterQuotaType,
filterEndpointType,
filterVendor,
searchValue: sidebarProps.searchValue,
});
@@ -81,10 +82,11 @@ const FilterModalContent = ({ sidebarProps, t }) => {
t={t}
/>
<PricingCategories
{...categoryProps}
categoryCounts={dynamicCategoryCounts}
setActiveKey={setActiveKey}
<PricingVendors
filterVendor={filterVendor}
setFilterVendor={setFilterVendor}
models={vendorModels}
allModels={categoryProps.models}
loading={loading}
t={t}
/>

View File

@@ -18,20 +18,43 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { Card, Avatar, Typography } from '@douyinfe/semi-ui';
import { Card, Avatar, Typography, Tag, Space } from '@douyinfe/semi-ui';
import { IconInfoCircle } from '@douyinfe/semi-icons';
import { stringToColor } from '../../../../../helpers';
const { Text } = Typography;
const ModelBasicInfo = ({ modelData, t }) => {
// 获取模型描述
const ModelBasicInfo = ({ modelData, vendorsMap = {}, t }) => {
// 获取模型描述(使用后端真实数据)
const getModelDescription = () => {
if (!modelData) return t('暂无模型描述');
// 这里可以根据模型名称返回不同的描述
if (modelData.model_name?.includes('gpt-4o-image')) {
return t('逆向plus账号的可绘图的gpt-4o模型由于OpenAI限制绘图数量并非每次都能绘图成功由于是逆向模型只要请求成功即使绘图失败也会扣费请谨慎使用。推荐使用正式版的 gpt-image-1模型。');
// 优先使用后端提供的描述
if (modelData.description) {
return modelData.description;
}
return modelData.description || t('暂无模型描述');
// 如果没有描述但有供应商描述,显示供应商信息
if (modelData.vendor_description) {
return t('供应商信息:') + modelData.vendor_description;
}
return t('暂无模型描述');
};
// 获取模型标签
const getModelTags = () => {
const tags = [];
if (modelData?.tags) {
const customTags = modelData.tags.split(',').filter(tag => tag.trim());
customTags.forEach(tag => {
const tagText = tag.trim();
tags.push({ text: tagText, color: stringToColor(tagText) });
});
}
return tags;
};
return (
@@ -46,7 +69,24 @@ const ModelBasicInfo = ({ modelData, t }) => {
</div>
</div>
<div className="text-gray-600">
<p>{getModelDescription()}</p>
<p className="mb-4">{getModelDescription()}</p>
{getModelTags().length > 0 && (
<div>
<Text className="text-sm font-medium text-gray-700 mb-2 block">{t('模型标签')}</Text>
<Space wrap>
{getModelTags().map((tag, index) => (
<Tag
key={index}
color={tag.color}
shape="circle"
size="small"
>
{tag.text}
</Tag>
))}
</Space>
</div>
)}
</div>
</Card>
);

View File

@@ -18,8 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { Tag, Typography, Toast, Avatar } from '@douyinfe/semi-ui';
import { getModelCategories } from '../../../../../helpers';
import { Typography, Toast, Avatar } from '@douyinfe/semi-ui';
import { getLobeHubIcon } from '../../../../../helpers';
const { Paragraph } = Typography;
@@ -28,52 +28,22 @@ const CARD_STYLES = {
icon: "w-8 h-8 flex items-center justify-center",
};
const ModelHeader = ({ modelData, t }) => {
// 获取模型图标
const getModelIcon = (modelName) => {
// 如果没有模型名称,直接返回默认头像
if (!modelName) {
return (
<div className={CARD_STYLES.container}>
<Avatar
size="large"
style={{
width: 48,
height: 48,
borderRadius: 16,
fontSize: 16,
fontWeight: 'bold'
}}
>
AI
</Avatar>
</div>
);
}
const categories = getModelCategories(t);
let icon = null;
// 遍历分类,找到匹配的模型图标
for (const [key, category] of Object.entries(categories)) {
if (key !== 'all' && category.filter({ model_name: modelName })) {
icon = category.icon;
break;
}
}
// 如果找到了匹配的图标,返回包装后的图标
if (icon) {
const ModelHeader = ({ modelData, vendorsMap = {}, t }) => {
// 获取模型图标(使用供应商图标)
const getModelIcon = () => {
// 优先使用供应商图标
if (modelData?.vendor_icon) {
return (
<div className={CARD_STYLES.container}>
<div className={CARD_STYLES.icon}>
{React.cloneElement(icon, { size: 32 })}
{getLobeHubIcon(modelData.vendor_icon, 32)}
</div>
</div>
);
}
const avatarText = modelName?.slice(0, 2).toUpperCase() || 'AI';
// 如果没有供应商图标,使用模型名称的前两个字符
const avatarText = modelData?.model_name?.slice(0, 2).toUpperCase() || 'AI';
return (
<div className={CARD_STYLES.container}>
<Avatar
@@ -92,23 +62,12 @@ const ModelHeader = ({ modelData, t }) => {
);
};
// 获取模型标签
const getModelTags = () => {
const tags = [
{ text: t('文本对话'), color: 'green' },
{ text: t('图片生成'), color: 'blue' },
{ text: t('图像分析'), color: 'cyan' }
];
return tags;
};
return (
<div className="flex items-center">
{getModelIcon(modelData?.model_name)}
{getModelIcon()}
<div className="ml-3 font-normal">
<Paragraph
className="!mb-1 !text-lg !font-medium"
className="!mb-0 !text-lg !font-medium"
copyable={{
content: modelData?.model_name || '',
onCopy: () => Toast.success({ content: t('已复制模型名称') })
@@ -116,18 +75,6 @@ const ModelHeader = ({ modelData, t }) => {
>
<span className="truncate max-w-60 font-bold">{modelData?.model_name || t('未知模型')}</span>
</Paragraph>
<div className="inline-flex gap-2 mt-1">
{getModelTags().map((tag, index) => (
<Tag
key={index}
color={tag.color}
shape="circle"
size="small"
>
{tag.text}
</Tag>
))}
</div>
</div>
</div>
);