refactor: Restructure model pricing components and improve UX consistency

- **Fix SideSheet double-click issue**: Remove early return for null modelData to prevent rendering blockage during async state updates
- **Component modularization**:
  - Split ModelDetailSideSheet into focused sub-components (ModelHeader, ModelBasicInfo, ModelEndpoints, ModelPricingTable)
  - Refactor PricingFilterModal with FilterModalContent and FilterModalFooter components
  - Remove unnecessary FilterSection wrapper for cleaner interface
- **Improve visual consistency**:
  - Unify avatar/icon logic between ModelHeader and PricingCardView components
  - Standardize tag colors across all pricing components (violet/teal for billing types)
  - Apply consistent dashed border styling using Semi UI theme colors
- **Enhance data accuracy**:
  - Display raw endpoint type names (e.g., "openai", "anthropic") instead of translated descriptions
  - Remove text alignment classes for better responsive layout
  - Add proper null checks to prevent runtime errors
- **Code quality improvements**:
  - Reduce component complexity by 52-74% through modularization
  - Improve maintainability with single responsibility principle
  - Add comprehensive error handling for edge cases

This refactoring improves component reusability, reduces bundle size, and provides a more consistent user experience across the model pricing interface.
This commit is contained in:
t0ng7u
2025-07-26 04:24:22 +08:00
parent 52a9cee0e1
commit 0b1a1ca064
13 changed files with 775 additions and 119 deletions

View File

@@ -0,0 +1,190 @@
/*
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 { Card, Avatar, Typography, Table, Tag, Tooltip } from '@douyinfe/semi-ui';
import { IconCoinMoneyStroked } from '@douyinfe/semi-icons';
import { calculateModelPrice } from '../../../../../helpers';
const { Text } = Typography;
const ModelPricingTable = ({
modelData,
selectedGroup,
groupRatio,
currency,
tokenUnit,
displayPrice,
showRatio,
usableGroup,
t,
}) => {
// 获取分组介绍
const getGroupDescription = (groupName) => {
const descriptions = {
'default': t('默认分组,适用于普通用户'),
'ssvip': t('超级VIP分组享受最优惠价格'),
'openai官-优质': t('OpenAI官方优质分组最快最稳支持o1、realtime等'),
'origin': t('企业分组OpenAI&Claude官方原价不升价本分组稳定性可用性'),
'vip': t('VIP分组享受优惠价格'),
'premium': t('高级分组,稳定可靠'),
'enterprise': t('企业级分组,专业服务'),
};
return descriptions[groupName] || t('用户分组');
};
const renderGroupPriceTable = () => {
const availableGroups = Object.keys(usableGroup || {}).filter(g => g !== '');
if (availableGroups.length === 0) {
availableGroups.push('default');
}
// 准备表格数据
const tableData = availableGroups.map(group => {
const priceData = modelData ? calculateModelPrice({
record: modelData,
selectedGroup: group,
groupRatio,
tokenUnit,
displayPrice,
currency
}) : { inputPrice: '-', outputPrice: '-', price: '-' };
// 获取分组倍率
const groupRatioValue = groupRatio && groupRatio[group] ? groupRatio[group] : 1;
return {
key: group,
group: group,
description: getGroupDescription(group),
ratio: groupRatioValue,
billingType: modelData?.quota_type === 0 ? t('按量计费') : t('按次计费'),
inputPrice: modelData?.quota_type === 0 ? priceData.inputPrice : '-',
outputPrice: modelData?.quota_type === 0 ? (priceData.completionPrice || priceData.outputPrice) : '-',
fixedPrice: modelData?.quota_type === 1 ? priceData.price : '-',
};
});
// 定义表格列
const columns = [
{
title: t('分组'),
dataIndex: 'group',
render: (text, record) => (
<Tooltip content={record.description} position="top">
<Tag color="white" size="small" shape="circle" className="cursor-help">
{text}{t('分组')}
</Tag>
</Tooltip>
),
},
];
// 如果显示倍率,添加倍率列
if (showRatio) {
columns.push({
title: t('倍率'),
dataIndex: 'ratio',
render: (text) => (
<Tag color="white" size="small" shape="circle">
{text}x
</Tag>
),
});
}
// 添加计费类型列
columns.push({
title: t('计费类型'),
dataIndex: 'billingType',
render: (text) => (
<Tag color={text === t('按量计费') ? 'violet' : 'teal'} size="small" shape="circle">
{text}
</Tag>
),
});
// 根据计费类型添加价格列
if (modelData?.quota_type === 0) {
// 按量计费
columns.push(
{
title: t('提示'),
dataIndex: 'inputPrice',
render: (text) => (
<>
<div className="font-semibold text-orange-600">{text}</div>
<div className="text-xs text-gray-500">/ {tokenUnit === 'K' ? '1K' : '1M'} tokens</div>
</>
),
},
{
title: t('补全'),
dataIndex: 'outputPrice',
render: (text) => (
<>
<div className="font-semibold text-orange-600">{text}</div>
<div className="text-xs text-gray-500">/ {tokenUnit === 'K' ? '1K' : '1M'} tokens</div>
</>
),
}
);
} else {
// 按次计费
columns.push({
title: t('价格'),
dataIndex: 'fixedPrice',
render: (text) => (
<>
<div className="font-semibold text-orange-600">{text}</div>
<div className="text-xs text-gray-500">/ </div>
</>
),
});
}
return (
<Table
dataSource={tableData}
columns={columns}
pagination={false}
size="small"
bordered={false}
className="!rounded-lg"
/>
);
};
return (
<Card className="!rounded-2xl shadow-sm border-0">
<div className="flex items-center mb-4">
<Avatar size="small" color="orange" className="mr-2 shadow-md">
<IconCoinMoneyStroked size={16} />
</Avatar>
<div>
<Text className="text-lg font-medium">{t('分组价格')}</Text>
<div className="text-xs text-gray-600">{t('不同用户分组的价格信息')}</div>
</div>
</div>
{renderGroupPriceTable()}
</Card>
);
};
export default ModelPricingTable;