💄 style(pricing): enhance card view UI and skeleton loading experience (#1365)
- Increase skeleton card count from 6 to 10 for better visual coverage - Extend minimum skeleton display duration from 500ms to 1000ms for smoother UX - Add circle shape to all pricing tags for consistent rounded design - Apply circle styling to billing type, popularity, endpoint, and context tags This commit improves the visual consistency and user experience of the pricing card view by standardizing tag appearance and optimizing skeleton loading timing.
This commit is contained in:
@@ -50,7 +50,7 @@ const CardTable = ({
|
||||
setShowSkeleton(true);
|
||||
} else {
|
||||
const elapsed = Date.now() - loadingStartRef.current;
|
||||
const remaining = Math.max(0, 500 - elapsed);
|
||||
const remaining = Math.max(0, 1000 - elapsed);
|
||||
if (remaining === 0) {
|
||||
setShowSkeleton(false);
|
||||
} else {
|
||||
|
||||
@@ -65,7 +65,7 @@ const SelectableButtonGroup = ({
|
||||
setShowSkeleton(true);
|
||||
} else {
|
||||
const elapsed = Date.now() - loadingStartRef.current;
|
||||
const remaining = Math.max(0, 500 - elapsed);
|
||||
const remaining = Math.max(0, 1000 - elapsed);
|
||||
if (remaining === 0) {
|
||||
setShowSkeleton(false);
|
||||
} else {
|
||||
|
||||
@@ -219,7 +219,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
||||
useEffect(() => {
|
||||
if (statusState?.status !== undefined) {
|
||||
const elapsed = Date.now() - loadingStartRef.current;
|
||||
const remaining = Math.max(0, 500 - elapsed);
|
||||
const remaining = Math.max(0, 1000 - elapsed);
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, remaining);
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
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 } from '@douyinfe/semi-ui';
|
||||
import { IconVerify, IconLayers, IconInfoCircle } from '@douyinfe/semi-icons';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
|
||||
const PricingHeader = ({
|
||||
userState,
|
||||
groupRatio,
|
||||
selectedGroup,
|
||||
models,
|
||||
t
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
className="!rounded-2xl !border-0 !shadow-md overflow-hidden mb-6"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 25%, #a855f7 50%, #c084fc 75%, #d8b4fe 100%)',
|
||||
position: 'relative'
|
||||
}}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<div className="relative p-6 sm:p-8" style={{ color: 'white' }}>
|
||||
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4 lg:gap-6">
|
||||
<div className="flex items-start">
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-white/10 flex items-center justify-center mr-3 sm:mr-4">
|
||||
<IconLayers size="extra-large" className="text-white" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-base sm:text-lg font-semibold mb-1 sm:mb-2">
|
||||
{t('模型定价')}
|
||||
</div>
|
||||
<div className="text-sm text-white/80">
|
||||
{userState.user ? (
|
||||
<div className="flex items-center">
|
||||
<IconVerify className="mr-1.5 flex-shrink-0" size="small" />
|
||||
<span className="truncate">
|
||||
{t('当前分组')}: {userState.user.group},{t('倍率')}: {groupRatio[userState.user.group]}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<AlertCircle size={14} className="mr-1.5 flex-shrink-0" />
|
||||
<span className="truncate">
|
||||
{t('未登录,使用默认分组倍率:')}{groupRatio['default']}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 sm:gap-3 mt-2 lg:mt-0">
|
||||
<div
|
||||
className="text-center px-2 py-2 sm:px-3 sm:py-2.5 bg-white/10 rounded-lg backdrop-blur-sm hover:bg-white/20 transition-colors duration-200"
|
||||
style={{ backdropFilter: 'blur(10px)' }}
|
||||
>
|
||||
<div className="text-xs text-white/70 mb-0.5">{t('分组倍率')}</div>
|
||||
<div className="text-sm sm:text-base font-semibold">{groupRatio[selectedGroup] || '1.0'}x</div>
|
||||
</div>
|
||||
<div
|
||||
className="text-center px-2 py-2 sm:px-3 sm:py-2.5 bg-white/10 rounded-lg backdrop-blur-sm hover:bg-white/20 transition-colors duration-200"
|
||||
style={{ backdropFilter: 'blur(10px)' }}
|
||||
>
|
||||
<div className="text-xs text-white/70 mb-0.5">{t('可用模型')}</div>
|
||||
<div className="text-sm sm:text-base font-semibold">
|
||||
{models.filter(m => m.enable_groups.includes(selectedGroup)).length}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="text-center px-2 py-2 sm:px-3 sm:py-2.5 bg-white/10 rounded-lg backdrop-blur-sm hover:bg-white/20 transition-colors duration-200"
|
||||
style={{ backdropFilter: 'blur(10px)' }}
|
||||
>
|
||||
<div className="text-xs text-white/70 mb-0.5">{t('计费类型')}</div>
|
||||
<div className="text-sm sm:text-base font-semibold">2</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 计费说明 */}
|
||||
<div className="mt-4 sm:mt-5">
|
||||
<div className="flex items-start">
|
||||
<div
|
||||
className="w-full flex items-start space-x-2 px-3 py-2 sm:px-4 sm:py-2.5 rounded-lg text-xs sm:text-sm"
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
color: 'white',
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}
|
||||
>
|
||||
<IconInfoCircle className="flex-shrink-0 mt-0.5" size="small" />
|
||||
<span>
|
||||
{t('按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-yellow-400 via-orange-400 to-red-400" style={{ opacity: 0.6 }}></div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingHeader;
|
||||
@@ -29,6 +29,8 @@ const PricingDisplaySettings = ({
|
||||
setCurrency,
|
||||
showRatio,
|
||||
setShowRatio,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
loading = false,
|
||||
t
|
||||
}) => {
|
||||
@@ -50,6 +52,10 @@ const PricingDisplaySettings = ({
|
||||
</Tooltip>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'tableView',
|
||||
label: t('表格视图')
|
||||
}
|
||||
];
|
||||
|
||||
@@ -59,10 +65,16 @@ const PricingDisplaySettings = ({
|
||||
];
|
||||
|
||||
const handleChange = (value) => {
|
||||
if (value === 'recharge') {
|
||||
setShowWithRecharge(!showWithRecharge);
|
||||
} else if (value === 'ratio') {
|
||||
setShowRatio(!showRatio);
|
||||
switch (value) {
|
||||
case 'recharge':
|
||||
setShowWithRecharge(!showWithRecharge);
|
||||
break;
|
||||
case 'ratio':
|
||||
setShowRatio(!showRatio);
|
||||
break;
|
||||
case 'tableView':
|
||||
setViewMode(viewMode === 'table' ? 'card' : 'table');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,6 +82,7 @@ const PricingDisplaySettings = ({
|
||||
const activeValues = [];
|
||||
if (showWithRecharge) activeValues.push('recharge');
|
||||
if (showRatio) activeValues.push('ratio');
|
||||
if (viewMode === 'table') activeValues.push('tableView');
|
||||
return activeValues;
|
||||
};
|
||||
|
||||
|
||||
@@ -19,43 +19,19 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
|
||||
import React from 'react';
|
||||
import PricingSearchBar from './PricingSearchBar';
|
||||
import PricingTable from './PricingTable';
|
||||
import PricingView from './PricingView';
|
||||
|
||||
const PricingContent = ({ isMobile, sidebarProps, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
className={isMobile ? "" : "pricing-scroll-hide"}
|
||||
style={isMobile ? {
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto'
|
||||
} : {}}
|
||||
>
|
||||
<div className={isMobile ? "pricing-content-mobile" : "pricing-scroll-hide"}>
|
||||
{/* 固定的搜索和操作区域 */}
|
||||
<div
|
||||
style={{
|
||||
padding: '16px 24px',
|
||||
borderBottom: '1px solid var(--semi-color-border)',
|
||||
backgroundColor: 'var(--semi-color-bg-0)',
|
||||
flexShrink: 0,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 5,
|
||||
}}
|
||||
>
|
||||
<div className="pricing-search-header">
|
||||
<PricingSearchBar {...props} isMobile={isMobile} sidebarProps={sidebarProps} />
|
||||
</div>
|
||||
|
||||
{/* 可滚动的内容区域 */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
...(isMobile && { minHeight: 0 })
|
||||
}}
|
||||
>
|
||||
<PricingTable {...props} />
|
||||
<div className={isMobile ? "pricing-view-container-mobile" : "pricing-view-container"}>
|
||||
<PricingView {...props} viewMode={sidebarProps.viewMode} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -21,56 +21,46 @@ import React from 'react';
|
||||
import { Layout, ImagePreview } from '@douyinfe/semi-ui';
|
||||
import PricingSidebar from './PricingSidebar';
|
||||
import PricingContent from './PricingContent';
|
||||
import { useModelPricingData } from '../../../hooks/model-pricing/useModelPricingData';
|
||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||
import { useModelPricingData } from '../../../../hooks/model-pricing/useModelPricingData';
|
||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||
|
||||
const PricingPage = () => {
|
||||
const pricingData = useModelPricingData();
|
||||
const { Sider, Content } = Layout;
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// 显示倍率状态
|
||||
const [showRatio, setShowRatio] = React.useState(false);
|
||||
const [viewMode, setViewMode] = React.useState('card');
|
||||
const allProps = {
|
||||
...pricingData,
|
||||
showRatio,
|
||||
setShowRatio,
|
||||
viewMode,
|
||||
setViewMode
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white">
|
||||
<Layout style={{ height: 'calc(100vh - 60px)', overflow: 'hidden', marginTop: '60px' }}>
|
||||
{/* 左侧边栏 - 只在桌面端显示 */}
|
||||
<Layout className="pricing-layout">
|
||||
{!isMobile && (
|
||||
<Sider
|
||||
className="pricing-scroll-hide"
|
||||
style={{
|
||||
width: 460,
|
||||
height: 'calc(100vh - 60px)',
|
||||
backgroundColor: 'var(--semi-color-bg-0)',
|
||||
borderRight: '1px solid var(--semi-color-border)',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
className="pricing-scroll-hide pricing-sidebar"
|
||||
width={460}
|
||||
>
|
||||
<PricingSidebar {...pricingData} showRatio={showRatio} setShowRatio={setShowRatio} />
|
||||
<PricingSidebar {...allProps} />
|
||||
</Sider>
|
||||
)}
|
||||
|
||||
{/* 右侧内容区 */}
|
||||
<Content
|
||||
className="pricing-scroll-hide"
|
||||
style={{
|
||||
height: 'calc(100vh - 60px)',
|
||||
backgroundColor: 'var(--semi-color-bg-0)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
className="pricing-scroll-hide pricing-content"
|
||||
>
|
||||
<PricingContent
|
||||
{...pricingData}
|
||||
showRatio={showRatio}
|
||||
<PricingContent
|
||||
{...allProps}
|
||||
isMobile={isMobile}
|
||||
sidebarProps={{ ...pricingData, showRatio, setShowRatio }}
|
||||
sidebarProps={allProps}
|
||||
/>
|
||||
</Content>
|
||||
</Layout>
|
||||
|
||||
{/* 倍率说明图预览 */}
|
||||
<ImagePreview
|
||||
src={pricingData.modalImageUrl}
|
||||
visible={pricingData.isModalOpenurl}
|
||||
@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Input, Button } from '@douyinfe/semi-ui';
|
||||
import { IconSearch, IconCopy, IconFilter } from '@douyinfe/semi-icons';
|
||||
import PricingFilterModal from './modal/PricingFilterModal';
|
||||
import PricingFilterModal from '../modal/PricingFilterModal';
|
||||
|
||||
const PricingSearchBar = ({
|
||||
selectedRowKeys,
|
||||
@@ -19,11 +19,11 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import PricingCategories from './filter/PricingCategories';
|
||||
import PricingGroups from './filter/PricingGroups';
|
||||
import PricingQuotaTypes from './filter/PricingQuotaTypes';
|
||||
import PricingDisplaySettings from './filter/PricingDisplaySettings';
|
||||
import { resetPricingFilters } from '../../../helpers/utils';
|
||||
import PricingCategories from '../filter/PricingCategories';
|
||||
import PricingGroups from '../filter/PricingGroups';
|
||||
import PricingQuotaTypes from '../filter/PricingQuotaTypes';
|
||||
import PricingDisplaySettings from '../filter/PricingDisplaySettings';
|
||||
import { resetPricingFilters } from '../../../../helpers/utils';
|
||||
|
||||
const PricingSidebar = ({
|
||||
showWithRecharge,
|
||||
@@ -34,10 +34,14 @@ const PricingSidebar = ({
|
||||
setActiveKey,
|
||||
showRatio,
|
||||
setShowRatio,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
filterGroup,
|
||||
setFilterGroup,
|
||||
filterQuotaType,
|
||||
setFilterQuotaType,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
loading,
|
||||
t,
|
||||
...categoryProps
|
||||
@@ -51,8 +55,10 @@ const PricingSidebar = ({
|
||||
setShowWithRecharge,
|
||||
setCurrency,
|
||||
setShowRatio,
|
||||
setViewMode,
|
||||
setFilterGroup,
|
||||
setFilterQuotaType,
|
||||
setCurrentPage,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -78,15 +84,36 @@ const PricingSidebar = ({
|
||||
setCurrency={setCurrency}
|
||||
showRatio={showRatio}
|
||||
setShowRatio={setShowRatio}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
loading={loading}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<PricingCategories {...categoryProps} setActiveKey={setActiveKey} loading={loading} t={t} />
|
||||
<PricingCategories
|
||||
{...categoryProps}
|
||||
setActiveKey={setActiveKey}
|
||||
loading={loading}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<PricingGroups filterGroup={filterGroup} setFilterGroup={setFilterGroup} usableGroup={categoryProps.usableGroup} groupRatio={categoryProps.groupRatio} models={categoryProps.models} loading={loading} t={t} />
|
||||
<PricingGroups
|
||||
filterGroup={filterGroup}
|
||||
setFilterGroup={setFilterGroup}
|
||||
usableGroup={categoryProps.usableGroup}
|
||||
groupRatio={categoryProps.groupRatio}
|
||||
models={categoryProps.models}
|
||||
loading={loading}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<PricingQuotaTypes filterQuotaType={filterQuotaType} setFilterQuotaType={setFilterQuotaType} models={categoryProps.models} loading={loading} t={t} />
|
||||
<PricingQuotaTypes
|
||||
filterQuotaType={filterQuotaType}
|
||||
setFilterQuotaType={setFilterQuotaType}
|
||||
models={categoryProps.models}
|
||||
loading={loading}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -17,5 +17,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
// 为了向后兼容,这里重新导出新的 PricingPage 组件
|
||||
export { default } from './PricingPage';
|
||||
import React from 'react';
|
||||
import PricingTable from '../view/PricingTable';
|
||||
import PricingCardView from '../view/PricingCardView';
|
||||
|
||||
const PricingView = ({
|
||||
viewMode = 'table',
|
||||
...props
|
||||
}) => {
|
||||
return viewMode === 'card' ?
|
||||
<PricingCardView {...props} /> :
|
||||
<PricingTable {...props} />;
|
||||
};
|
||||
|
||||
export default PricingView;
|
||||
@@ -40,10 +40,14 @@ const PricingFilterModal = ({
|
||||
setActiveKey,
|
||||
showRatio,
|
||||
setShowRatio,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
filterGroup,
|
||||
setFilterGroup,
|
||||
filterQuotaType,
|
||||
setFilterQuotaType,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
loading,
|
||||
...categoryProps
|
||||
} = sidebarProps;
|
||||
@@ -56,14 +60,12 @@ const PricingFilterModal = ({
|
||||
setShowWithRecharge,
|
||||
setCurrency,
|
||||
setShowRatio,
|
||||
setViewMode,
|
||||
setFilterGroup,
|
||||
setFilterQuotaType,
|
||||
setCurrentPage,
|
||||
});
|
||||
|
||||
const handleConfirm = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const footer = (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
@@ -76,7 +78,7 @@ const PricingFilterModal = ({
|
||||
<Button
|
||||
theme="solid"
|
||||
type="primary"
|
||||
onClick={handleConfirm}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('确定')}
|
||||
</Button>
|
||||
@@ -106,6 +108,8 @@ const PricingFilterModal = ({
|
||||
setCurrency={setCurrency}
|
||||
showRatio={showRatio}
|
||||
setShowRatio={setShowRatio}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
loading={loading}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
444
web/src/components/table/model-pricing/view/PricingCardView.jsx
Normal file
444
web/src/components/table/model-pricing/view/PricingCardView.jsx
Normal file
@@ -0,0 +1,444 @@
|
||||
/*
|
||||
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, { useState, useRef, useEffect } from 'react';
|
||||
import { Card, Tag, Tooltip, Checkbox, Empty, Pagination, Button, Skeleton } from '@douyinfe/semi-ui';
|
||||
import { IconHelpCircle, IconCopy } from '@douyinfe/semi-icons';
|
||||
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
|
||||
import { stringToColor, getModelCategories, calculateModelPrice, formatPriceInfo } from '../../../../helpers';
|
||||
|
||||
const PricingCardView = ({
|
||||
filteredModels,
|
||||
loading,
|
||||
rowSelection,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
selectedGroup,
|
||||
groupRatio,
|
||||
copyText,
|
||||
setModalImageUrl,
|
||||
setIsModalOpenurl,
|
||||
currency,
|
||||
tokenUnit,
|
||||
setTokenUnit,
|
||||
displayPrice,
|
||||
showRatio,
|
||||
t
|
||||
}) => {
|
||||
const [showSkeleton, setShowSkeleton] = useState(loading);
|
||||
const [skeletonCount] = useState(10);
|
||||
const loadingStartRef = useRef(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
loadingStartRef.current = Date.now();
|
||||
setShowSkeleton(true);
|
||||
} else {
|
||||
const elapsed = Date.now() - loadingStartRef.current;
|
||||
const remaining = Math.max(0, 1000 - elapsed);
|
||||
if (remaining === 0) {
|
||||
setShowSkeleton(false);
|
||||
} else {
|
||||
const timer = setTimeout(() => setShowSkeleton(false), remaining);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
// 计算当前页面要显示的数据
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const paginatedModels = filteredModels.slice(startIndex, endIndex);
|
||||
|
||||
// 渲染骨架屏卡片
|
||||
const renderSkeletonCards = () => {
|
||||
const placeholder = (
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: skeletonCount }).map((_, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="!rounded-2xl border border-gray-200"
|
||||
bodyStyle={{ padding: '24px' }}
|
||||
>
|
||||
{/* 头部:图标 + 模型名称 + 操作按钮 */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
||||
{/* 模型图标骨架 */}
|
||||
<div className="w-12 h-12 rounded-2xl flex items-center justify-center relative shadow-sm">
|
||||
<Skeleton.Avatar
|
||||
size="large"
|
||||
style={{ width: 48, height: 48, borderRadius: 16 }}
|
||||
/>
|
||||
</div>
|
||||
{/* 模型名称骨架 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<Skeleton.Title
|
||||
style={{
|
||||
width: `${120 + (index % 3) * 30}px`,
|
||||
height: 20,
|
||||
marginBottom: 0
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 ml-3">
|
||||
{/* 操作按钮骨架 */}
|
||||
<Skeleton.Button size="small" style={{ width: 32, height: 32 }} />
|
||||
{rowSelection && (
|
||||
<Skeleton.Button size="small" style={{ width: 16, height: 16 }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 价格信息骨架 */}
|
||||
<div className="mb-3">
|
||||
<Skeleton.Title
|
||||
style={{
|
||||
width: `${180 + (index % 4) * 20}px`,
|
||||
height: 16,
|
||||
marginBottom: 0
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 模型描述骨架 */}
|
||||
<div className="mb-4">
|
||||
<Skeleton.Paragraph
|
||||
rows={2}
|
||||
style={{ marginBottom: 0 }}
|
||||
title={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 标签区域骨架 */}
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{Array.from({ length: 3 + (index % 2) }).map((_, tagIndex) => (
|
||||
<Skeleton.Button
|
||||
key={tagIndex}
|
||||
size="small"
|
||||
style={{
|
||||
width: `${40 + (tagIndex % 3) * 15}px`,
|
||||
height: 24,
|
||||
borderRadius: 12
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 倍率信息骨架(可选) */}
|
||||
{showRatio && (
|
||||
<div className="mt-4 pt-3 border-t border-gray-100">
|
||||
<div className="flex items-center space-x-1 mb-2">
|
||||
<Skeleton.Title
|
||||
style={{ width: 60, height: 12, marginBottom: 0 }}
|
||||
/>
|
||||
<Skeleton.Button size="small" style={{ width: 14, height: 14 }} />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{Array.from({ length: 3 }).map((_, ratioIndex) => (
|
||||
<Skeleton.Title
|
||||
key={ratioIndex}
|
||||
style={{ width: '100%', height: 12, marginBottom: 0 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 分页骨架 */}
|
||||
<div className="flex justify-center mt-6 pt-4 border-t pricing-pagination-divider">
|
||||
<Skeleton.Button style={{ width: 300, height: 32 }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Skeleton loading={true} active placeholder={placeholder}></Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
// 获取模型图标
|
||||
const getModelIcon = (modelName) => {
|
||||
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) {
|
||||
return (
|
||||
<div className="w-12 h-12 rounded-2xl flex items-center justify-center relative shadow-sm">
|
||||
<div className="w-8 h-8 flex items-center justify-center">
|
||||
{React.cloneElement(icon, { size: 32 })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 默认图标(如果没有匹配到任何分类)
|
||||
return (
|
||||
<div className="w-12 h-12 rounded-2xl flex items-center justify-center relative shadow-sm">
|
||||
{/* 默认的螺旋图案 */}
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" className="text-gray-600">
|
||||
<path
|
||||
d="M12 2C17.5 2 22 6.5 22 12S17.5 22 12 22 2 17.5 2 12 6.5 2 12 2M12 4C7.6 4 4 7.6 4 12S7.6 20 12 20 20 16.4 20 12 16.4 4 12 4M12 6C15.3 6 18 8.7 18 12S15.3 18 12 18 6 15.3 6 12 8.7 6 12 6M12 8C10.9 8 10 8.9 10 10S10.9 12 12 12 14 11.1 14 10 13.1 8 12 8Z"
|
||||
fill="currentColor"
|
||||
opacity="0.6"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 获取模型描述
|
||||
const getModelDescription = (modelName) => {
|
||||
// 根据模型名称返回描述,这里可以扩展
|
||||
if (modelName.includes('gpt-3.5-turbo')) {
|
||||
return t('该模型目前指向gpt-35-turbo-0125模型,综合能力强,过去使用最广泛的文本模型。');
|
||||
}
|
||||
if (modelName.includes('gpt-4')) {
|
||||
return t('更强大的GPT-4模型,具有更好的推理能力和更准确的输出。');
|
||||
}
|
||||
if (modelName.includes('claude')) {
|
||||
return t('Anthropic开发的Claude模型,以安全性和有用性著称。');
|
||||
}
|
||||
return t('高性能AI模型,适用于各种文本生成和理解任务。');
|
||||
};
|
||||
|
||||
// 渲染价格信息
|
||||
const renderPriceInfo = (record) => {
|
||||
const priceData = calculateModelPrice({
|
||||
record,
|
||||
selectedGroup,
|
||||
groupRatio,
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
currency,
|
||||
precision: 4
|
||||
});
|
||||
return formatPriceInfo(priceData, t);
|
||||
};
|
||||
|
||||
// 渲染标签
|
||||
const renderTags = (record) => {
|
||||
const tags = [];
|
||||
|
||||
// 计费类型标签
|
||||
if (record.quota_type === 1) {
|
||||
tags.push(
|
||||
<Tag shape='circle' key="billing" color='teal' size='small'>
|
||||
{t('按次计费')}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
tags.push(
|
||||
<Tag shape='circle' key="billing" color='violet' size='small'>
|
||||
{t('按量计费')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
// 热度标签(示例)
|
||||
if (record.model_name.includes('gpt-3.5-turbo') || record.model_name.includes('gpt-4')) {
|
||||
tags.push(
|
||||
<Tag shape='circle' key="hot" color='red' size='small'>
|
||||
{t('热')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
// 端点类型标签
|
||||
if (record.supported_endpoint_types && record.supported_endpoint_types.length > 0) {
|
||||
record.supported_endpoint_types.slice(0, 2).forEach((endpoint, index) => {
|
||||
tags.push(
|
||||
<Tag shape='circle' key={`endpoint-${index}`} color={stringToColor(endpoint)} size='small'>
|
||||
{endpoint}
|
||||
</Tag>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 上下文长度标签(示例)
|
||||
if (record.model_name.includes('16k')) {
|
||||
tags.push(<Tag shape='circle' key="context" color='blue' size='small'>16K</Tag>);
|
||||
} else if (record.model_name.includes('32k')) {
|
||||
tags.push(<Tag shape='circle' key="context" color='blue' size='small'>32K</Tag>);
|
||||
} else {
|
||||
tags.push(<Tag shape='circle' key="context" color='blue' size='small'>4K</Tag>);
|
||||
}
|
||||
|
||||
return tags;
|
||||
};
|
||||
|
||||
// 显示骨架屏
|
||||
if (showSkeleton) {
|
||||
return renderSkeletonCards();
|
||||
}
|
||||
|
||||
if (!filteredModels || filteredModels.length === 0) {
|
||||
return (
|
||||
<div className="flex justify-center items-center py-20">
|
||||
<Empty
|
||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('搜索无结果')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{paginatedModels.map((model, index) => {
|
||||
const isSelected = rowSelection?.selectedRowKeys?.includes(model.id);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={model.id || index}
|
||||
className={`!rounded-2xl transition-all duration-200 hover:shadow-lg border ${isSelected
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
bodyStyle={{ padding: '24px' }}
|
||||
>
|
||||
{/* 头部:图标 + 模型名称 + 操作按钮 */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
||||
{getModelIcon(model.model_name)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-xl font-bold text-gray-900 truncate">
|
||||
{model.model_name}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 ml-3">
|
||||
{/* 复制按钮 */}
|
||||
<Button
|
||||
size="small"
|
||||
type="tertiary"
|
||||
icon={<IconCopy />}
|
||||
onClick={() => copyText(model.model_name)}
|
||||
/>
|
||||
|
||||
{/* 选择框 */}
|
||||
{rowSelection && (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
rowSelection.onSelect(model, true);
|
||||
} else {
|
||||
rowSelection.onSelect(model, false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 价格信息 */}
|
||||
<div className="mb-3">
|
||||
<div className="text-gray-700 text-base font-medium">
|
||||
{renderPriceInfo(model)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模型描述 */}
|
||||
<div className="mb-4">
|
||||
<p className="text-gray-500 text-sm leading-relaxed">
|
||||
{getModelDescription(model.model_name)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 标签区域 */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{renderTags(model)}
|
||||
</div>
|
||||
|
||||
{/* 倍率信息(可选) */}
|
||||
{showRatio && (
|
||||
<div className="mt-4 pt-3 border-t border-gray-100">
|
||||
<div className="flex items-center space-x-1 mb-2">
|
||||
<span className="text-xs font-medium text-gray-700">{t('倍率信息')}</span>
|
||||
<Tooltip content={t('倍率是为了方便换算不同价格的模型')}>
|
||||
<IconHelpCircle
|
||||
className="text-blue-500 cursor-pointer"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setModalImageUrl('/ratio.png');
|
||||
setIsModalOpenurl(true);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs text-gray-600">
|
||||
<div>
|
||||
{t('模型')}: {model.quota_type === 0 ? model.model_ratio : t('无')}
|
||||
</div>
|
||||
<div>
|
||||
{t('补全')}: {model.quota_type === 0 ? parseFloat(model.completion_ratio.toFixed(3)) : t('无')}
|
||||
</div>
|
||||
<div>
|
||||
{t('分组')}: {groupRatio[selectedGroup]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{filteredModels.length > 0 && (
|
||||
<div className="flex justify-center mt-6 pt-4 border-t pricing-pagination-divider">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
total={filteredModels.length}
|
||||
showSizeChanger={true}
|
||||
pageSizeOptions={[10, 20, 50, 100]}
|
||||
onPageChange={(page) => setCurrentPage(page)}
|
||||
onPageSizeChange={(size) => {
|
||||
setPageSize(size);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingCardView;
|
||||
@@ -32,18 +32,15 @@ const PricingTable = ({
|
||||
pageSize,
|
||||
setPageSize,
|
||||
selectedGroup,
|
||||
usableGroup,
|
||||
groupRatio,
|
||||
copyText,
|
||||
setModalImageUrl,
|
||||
setIsModalOpenurl,
|
||||
currency,
|
||||
showWithRecharge,
|
||||
tokenUnit,
|
||||
setTokenUnit,
|
||||
displayPrice,
|
||||
filteredValue,
|
||||
handleGroupClick,
|
||||
searchValue,
|
||||
showRatio,
|
||||
compactMode = false,
|
||||
t
|
||||
@@ -53,43 +50,37 @@ const PricingTable = ({
|
||||
return getPricingTableColumns({
|
||||
t,
|
||||
selectedGroup,
|
||||
usableGroup,
|
||||
groupRatio,
|
||||
copyText,
|
||||
setModalImageUrl,
|
||||
setIsModalOpenurl,
|
||||
currency,
|
||||
showWithRecharge,
|
||||
tokenUnit,
|
||||
setTokenUnit,
|
||||
displayPrice,
|
||||
handleGroupClick,
|
||||
showRatio,
|
||||
});
|
||||
}, [
|
||||
t,
|
||||
selectedGroup,
|
||||
usableGroup,
|
||||
groupRatio,
|
||||
copyText,
|
||||
setModalImageUrl,
|
||||
setIsModalOpenurl,
|
||||
currency,
|
||||
showWithRecharge,
|
||||
tokenUnit,
|
||||
setTokenUnit,
|
||||
displayPrice,
|
||||
handleGroupClick,
|
||||
showRatio,
|
||||
]);
|
||||
|
||||
// 更新列定义中的 filteredValue
|
||||
// 更新列定义中的 searchValue
|
||||
const processedColumns = useMemo(() => {
|
||||
const cols = columns.map(column => {
|
||||
if (column.dataIndex === 'model_name') {
|
||||
return {
|
||||
...column,
|
||||
filteredValue
|
||||
filteredValue: searchValue ? [searchValue] : []
|
||||
};
|
||||
}
|
||||
return column;
|
||||
@@ -100,7 +91,7 @@ const PricingTable = ({
|
||||
return cols.map(({ fixed, ...rest }) => rest);
|
||||
}
|
||||
return cols;
|
||||
}, [columns, filteredValue, compactMode]);
|
||||
}, [columns, searchValue, compactMode]);
|
||||
|
||||
const ModelTable = useMemo(() => (
|
||||
<Card className="!rounded-xl overflow-hidden" bordered={false}>
|
||||
@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import React from 'react';
|
||||
import { Tag, Space, Tooltip, Switch } from '@douyinfe/semi-ui';
|
||||
import { IconHelpCircle } from '@douyinfe/semi-icons';
|
||||
import { renderModelTag, stringToColor } from '../../../helpers';
|
||||
import { renderModelTag, stringToColor, calculateModelPrice } from '../../../../helpers';
|
||||
|
||||
function renderQuotaType(type, t) {
|
||||
switch (type) {
|
||||
@@ -158,38 +158,30 @@ export const getPricingTableColumns = ({
|
||||
),
|
||||
dataIndex: 'model_price',
|
||||
render: (text, record, index) => {
|
||||
if (record.quota_type === 0) {
|
||||
const inputRatioPriceUSD = record.model_ratio * 2 * groupRatio[selectedGroup];
|
||||
const completionRatioPriceUSD =
|
||||
record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup];
|
||||
const priceData = calculateModelPrice({
|
||||
record,
|
||||
selectedGroup,
|
||||
groupRatio,
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
currency
|
||||
});
|
||||
|
||||
const unitDivisor = tokenUnit === 'K' ? 1000 : 1;
|
||||
const unitLabel = tokenUnit === 'K' ? 'K' : 'M';
|
||||
|
||||
const rawDisplayInput = displayPrice(inputRatioPriceUSD);
|
||||
const rawDisplayCompletion = displayPrice(completionRatioPriceUSD);
|
||||
|
||||
const numInput = parseFloat(rawDisplayInput.replace(/[^0-9.]/g, '')) / unitDivisor;
|
||||
const numCompletion = parseFloat(rawDisplayCompletion.replace(/[^0-9.]/g, '')) / unitDivisor;
|
||||
|
||||
const displayInput = `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(3)}`;
|
||||
const displayCompletion = `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(3)}`;
|
||||
if (priceData.isPerToken) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-700">
|
||||
{t('提示')} {displayInput} / 1{unitLabel} tokens
|
||||
{t('提示')} {priceData.inputPrice} / 1{priceData.unitLabel} tokens
|
||||
</div>
|
||||
<div className="text-gray-700">
|
||||
{t('补全')} {displayCompletion} / 1{unitLabel} tokens
|
||||
{t('补全')} {priceData.completionPrice} / 1{priceData.unitLabel} tokens
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const priceUSD = parseFloat(text) * groupRatio[selectedGroup];
|
||||
const displayVal = displayPrice(priceUSD);
|
||||
return (
|
||||
<div className="text-gray-700">
|
||||
{t('模型价格')}:{displayVal}
|
||||
{t('模型价格')}:{priceData.price}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -40,7 +40,7 @@ const LogsActions = ({
|
||||
setShowSkeleton(true);
|
||||
} else {
|
||||
const elapsed = Date.now() - loadingStartRef.current;
|
||||
const remaining = Math.max(0, 500 - elapsed);
|
||||
const remaining = Math.max(0, 1000 - elapsed);
|
||||
if (remaining === 0) {
|
||||
setShowSkeleton(false);
|
||||
} else {
|
||||
|
||||
@@ -568,6 +568,59 @@ export const modelSelectFilter = (input, option) => {
|
||||
return val.includes(input.trim().toLowerCase());
|
||||
};
|
||||
|
||||
// -------------------------------
|
||||
// 模型定价计算工具函数
|
||||
export const calculateModelPrice = ({
|
||||
record,
|
||||
selectedGroup,
|
||||
groupRatio,
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
currency,
|
||||
precision = 3
|
||||
}) => {
|
||||
if (record.quota_type === 0) {
|
||||
// 按量计费
|
||||
const inputRatioPriceUSD = record.model_ratio * 2 * groupRatio[selectedGroup];
|
||||
const completionRatioPriceUSD =
|
||||
record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup];
|
||||
|
||||
const unitDivisor = tokenUnit === 'K' ? 1000 : 1;
|
||||
const unitLabel = tokenUnit === 'K' ? 'K' : 'M';
|
||||
|
||||
const rawDisplayInput = displayPrice(inputRatioPriceUSD);
|
||||
const rawDisplayCompletion = displayPrice(completionRatioPriceUSD);
|
||||
|
||||
const numInput = parseFloat(rawDisplayInput.replace(/[^0-9.]/g, '')) / unitDivisor;
|
||||
const numCompletion = parseFloat(rawDisplayCompletion.replace(/[^0-9.]/g, '')) / unitDivisor;
|
||||
|
||||
return {
|
||||
inputPrice: `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(precision)}`,
|
||||
completionPrice: `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(precision)}`,
|
||||
unitLabel,
|
||||
isPerToken: true
|
||||
};
|
||||
} else {
|
||||
// 按次计费
|
||||
const priceUSD = parseFloat(record.model_price) * groupRatio[selectedGroup];
|
||||
const displayVal = displayPrice(priceUSD);
|
||||
|
||||
return {
|
||||
price: displayVal,
|
||||
isPerToken: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化价格信息为字符串(用于卡片视图)
|
||||
export const formatPriceInfo = (priceData, t) => {
|
||||
if (priceData.isPerToken) {
|
||||
return `${t('输入')} ${priceData.inputPrice}/${priceData.unitLabel} ${t('输出')} ${priceData.completionPrice}/${priceData.unitLabel}`;
|
||||
} else {
|
||||
return `${t('模型价格')} ${priceData.price}`;
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------------------
|
||||
// CardPro 分页配置函数
|
||||
// 用于创建 CardPro 的 paginationArea 配置
|
||||
@@ -626,8 +679,10 @@ export const resetPricingFilters = ({
|
||||
setShowWithRecharge,
|
||||
setCurrency,
|
||||
setShowRatio,
|
||||
setViewMode,
|
||||
setFilterGroup,
|
||||
setFilterQuotaType,
|
||||
setCurrentPage,
|
||||
}) => {
|
||||
// 重置搜索
|
||||
if (typeof handleChange === 'function') {
|
||||
@@ -658,6 +713,11 @@ export const resetPricingFilters = ({
|
||||
setShowRatio(false);
|
||||
}
|
||||
|
||||
// 重置视图模式
|
||||
if (typeof setViewMode === 'function') {
|
||||
setViewMode('card');
|
||||
}
|
||||
|
||||
// 重置分组筛选
|
||||
if (typeof setFilterGroup === 'function') {
|
||||
setFilterGroup('all');
|
||||
@@ -667,4 +727,9 @@ export const resetPricingFilters = ({
|
||||
if (typeof setFilterQuotaType === 'function') {
|
||||
setFilterQuotaType('all');
|
||||
}
|
||||
|
||||
// 重置当前页面
|
||||
if (typeof setCurrentPage === 'function') {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -178,7 +178,7 @@ export const useDashboardData = (userState, userDispatch, statusState) => {
|
||||
}
|
||||
} finally {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const remainingTime = Math.max(0, 500 - elapsed);
|
||||
const remainingTime = Math.max(0, 1000 - elapsed);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, remainingTime);
|
||||
|
||||
@@ -26,18 +26,17 @@ import { StatusContext } from '../../context/Status/index.js';
|
||||
|
||||
export const useModelPricingData = () => {
|
||||
const { t } = useTranslation();
|
||||
const [filteredValue, setFilteredValue] = useState([]);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const compositionRef = useRef({ isComposition: false });
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||
const [modalImageUrl, setModalImageUrl] = useState('');
|
||||
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
||||
const [selectedGroup, setSelectedGroup] = useState('default');
|
||||
// 用于 Table 的可用分组筛选,“all” 表示不过滤
|
||||
const [filterGroup, setFilterGroup] = useState('all');
|
||||
// 计费类型筛选: 'all' | 0 | 1
|
||||
const [filterQuotaType, setFilterQuotaType] = useState('all');
|
||||
const [filterGroup, setFilterGroup] = useState('all'); // 用于 Table 的可用分组筛选,“all” 表示不过滤
|
||||
const [filterQuotaType, setFilterQuotaType] = useState('all'); // 计费类型筛选: 'all' | 0 | 1
|
||||
const [activeKey, setActiveKey] = useState('all');
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [currency, setCurrency] = useState('USD');
|
||||
const [showWithRecharge, setShowWithRecharge] = useState(false);
|
||||
const [tokenUnit, setTokenUnit] = useState('M');
|
||||
@@ -95,15 +94,15 @@ export const useModelPricingData = () => {
|
||||
}
|
||||
|
||||
// 搜索筛选
|
||||
if (filteredValue.length > 0) {
|
||||
const searchTerm = filteredValue[0].toLowerCase();
|
||||
if (searchValue.length > 0) {
|
||||
const searchTerm = searchValue.toLowerCase();
|
||||
result = result.filter(model =>
|
||||
model.model_name.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [activeKey, models, filteredValue, filterGroup, filterQuotaType]);
|
||||
}, [activeKey, models, searchValue, filterGroup, filterQuotaType]);
|
||||
|
||||
const rowSelection = useMemo(
|
||||
() => ({
|
||||
@@ -183,8 +182,8 @@ export const useModelPricingData = () => {
|
||||
if (compositionRef.current.isComposition) {
|
||||
return;
|
||||
}
|
||||
const newFilteredValue = value ? [value] : [];
|
||||
setFilteredValue(newFilteredValue);
|
||||
const newSearchValue = value ? value : '';
|
||||
setSearchValue(newSearchValue);
|
||||
};
|
||||
|
||||
const handleCompositionStart = () => {
|
||||
@@ -194,8 +193,8 @@ export const useModelPricingData = () => {
|
||||
const handleCompositionEnd = (event) => {
|
||||
compositionRef.current.isComposition = false;
|
||||
const value = event.target.value;
|
||||
const newFilteredValue = value ? [value] : [];
|
||||
setFilteredValue(newFilteredValue);
|
||||
const newSearchValue = value ? value : '';
|
||||
setSearchValue(newSearchValue);
|
||||
};
|
||||
|
||||
const handleGroupClick = (group) => {
|
||||
@@ -214,10 +213,15 @@ export const useModelPricingData = () => {
|
||||
refresh().then();
|
||||
}, []);
|
||||
|
||||
// 当筛选条件变化时重置到第一页
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [activeKey, filterGroup, filterQuotaType, searchValue]);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
filteredValue,
|
||||
setFilteredValue,
|
||||
searchValue,
|
||||
setSearchValue,
|
||||
selectedRowKeys,
|
||||
setSelectedRowKeys,
|
||||
modalImageUrl,
|
||||
@@ -234,6 +238,8 @@ export const useModelPricingData = () => {
|
||||
setActiveKey,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
currency,
|
||||
setCurrency,
|
||||
showWithRecharge,
|
||||
|
||||
@@ -617,4 +617,59 @@ html:not(.dark) .blur-ball-teal {
|
||||
height: calc(100vh - 77px);
|
||||
max-height: calc(100vh - 77px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 模型定价页面布局 ==================== */
|
||||
.pricing-layout {
|
||||
height: calc(100vh - 60px);
|
||||
overflow: hidden;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.pricing-sidebar {
|
||||
min-width: 460px;
|
||||
max-width: 460px;
|
||||
height: calc(100vh - 60px);
|
||||
background-color: var(--semi-color-bg-0);
|
||||
border-right: 1px solid var(--semi-color-border);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pricing-content {
|
||||
height: calc(100vh - 60px);
|
||||
background-color: var(--semi-color-bg-0);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pricing-pagination-divider {
|
||||
border-color: var(--semi-color-border);
|
||||
}
|
||||
|
||||
.pricing-content-mobile {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pricing-search-header {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--semi-color-border);
|
||||
background-color: var(--semi-color-bg-0);
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.pricing-view-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pricing-view-container-mobile {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ModelPricingPage from '../../components/table/model-pricing';
|
||||
import ModelPricingPage from '../../components/table/model-pricing/layout/PricingPage';
|
||||
|
||||
const Pricing = () => (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user