Commit f19f1aec by t0ng7u

💄 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.
parent 7b04596b
...@@ -50,7 +50,7 @@ const CardTable = ({ ...@@ -50,7 +50,7 @@ const CardTable = ({
setShowSkeleton(true); setShowSkeleton(true);
} else { } else {
const elapsed = Date.now() - loadingStartRef.current; const elapsed = Date.now() - loadingStartRef.current;
const remaining = Math.max(0, 500 - elapsed); const remaining = Math.max(0, 1000 - elapsed);
if (remaining === 0) { if (remaining === 0) {
setShowSkeleton(false); setShowSkeleton(false);
} else { } else {
......
...@@ -65,7 +65,7 @@ const SelectableButtonGroup = ({ ...@@ -65,7 +65,7 @@ const SelectableButtonGroup = ({
setShowSkeleton(true); setShowSkeleton(true);
} else { } else {
const elapsed = Date.now() - loadingStartRef.current; const elapsed = Date.now() - loadingStartRef.current;
const remaining = Math.max(0, 500 - elapsed); const remaining = Math.max(0, 1000 - elapsed);
if (remaining === 0) { if (remaining === 0) {
setShowSkeleton(false); setShowSkeleton(false);
} else { } else {
......
...@@ -219,7 +219,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { ...@@ -219,7 +219,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
useEffect(() => { useEffect(() => {
if (statusState?.status !== undefined) { if (statusState?.status !== undefined) {
const elapsed = Date.now() - loadingStartRef.current; const elapsed = Date.now() - loadingStartRef.current;
const remaining = Math.max(0, 500 - elapsed); const remaining = Math.max(0, 1000 - elapsed);
const timer = setTimeout(() => { const timer = setTimeout(() => {
setIsLoading(false); setIsLoading(false);
}, remaining); }, remaining);
......
/*
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;
\ No newline at end of file
...@@ -29,6 +29,8 @@ const PricingDisplaySettings = ({ ...@@ -29,6 +29,8 @@ const PricingDisplaySettings = ({
setCurrency, setCurrency,
showRatio, showRatio,
setShowRatio, setShowRatio,
viewMode,
setViewMode,
loading = false, loading = false,
t t
}) => { }) => {
...@@ -50,6 +52,10 @@ const PricingDisplaySettings = ({ ...@@ -50,6 +52,10 @@ const PricingDisplaySettings = ({
</Tooltip> </Tooltip>
</span> </span>
), ),
},
{
value: 'tableView',
label: t('表格视图')
} }
]; ];
...@@ -59,10 +65,16 @@ const PricingDisplaySettings = ({ ...@@ -59,10 +65,16 @@ const PricingDisplaySettings = ({
]; ];
const handleChange = (value) => { const handleChange = (value) => {
if (value === 'recharge') { switch (value) {
setShowWithRecharge(!showWithRecharge); case 'recharge':
} else if (value === 'ratio') { setShowWithRecharge(!showWithRecharge);
setShowRatio(!showRatio); break;
case 'ratio':
setShowRatio(!showRatio);
break;
case 'tableView':
setViewMode(viewMode === 'table' ? 'card' : 'table');
break;
} }
}; };
...@@ -70,6 +82,7 @@ const PricingDisplaySettings = ({ ...@@ -70,6 +82,7 @@ const PricingDisplaySettings = ({
const activeValues = []; const activeValues = [];
if (showWithRecharge) activeValues.push('recharge'); if (showWithRecharge) activeValues.push('recharge');
if (showRatio) activeValues.push('ratio'); if (showRatio) activeValues.push('ratio');
if (viewMode === 'table') activeValues.push('tableView');
return activeValues; return activeValues;
}; };
......
...@@ -19,43 +19,19 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -19,43 +19,19 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react'; import React from 'react';
import PricingSearchBar from './PricingSearchBar'; import PricingSearchBar from './PricingSearchBar';
import PricingTable from './PricingTable'; import PricingView from './PricingView';
const PricingContent = ({ isMobile, sidebarProps, ...props }) => { const PricingContent = ({ isMobile, sidebarProps, ...props }) => {
return ( return (
<div <div className={isMobile ? "pricing-content-mobile" : "pricing-scroll-hide"}>
className={isMobile ? "" : "pricing-scroll-hide"}
style={isMobile ? {
height: '100%',
display: 'flex',
flexDirection: 'column',
overflow: 'auto'
} : {}}
>
{/* 固定的搜索和操作区域 */} {/* 固定的搜索和操作区域 */}
<div <div className="pricing-search-header">
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,
}}
>
<PricingSearchBar {...props} isMobile={isMobile} sidebarProps={sidebarProps} /> <PricingSearchBar {...props} isMobile={isMobile} sidebarProps={sidebarProps} />
</div> </div>
{/* 可滚动的内容区域 */} {/* 可滚动的内容区域 */}
<div <div className={isMobile ? "pricing-view-container-mobile" : "pricing-view-container"}>
style={{ <PricingView {...props} viewMode={sidebarProps.viewMode} />
flex: 1,
overflow: 'auto',
...(isMobile && { minHeight: 0 })
}}
>
<PricingTable {...props} />
</div> </div>
</div> </div>
); );
......
...@@ -21,56 +21,46 @@ import React from 'react'; ...@@ -21,56 +21,46 @@ import React from 'react';
import { Layout, ImagePreview } from '@douyinfe/semi-ui'; import { Layout, ImagePreview } from '@douyinfe/semi-ui';
import PricingSidebar from './PricingSidebar'; import PricingSidebar from './PricingSidebar';
import PricingContent from './PricingContent'; import PricingContent from './PricingContent';
import { useModelPricingData } from '../../../hooks/model-pricing/useModelPricingData'; import { useModelPricingData } from '../../../../hooks/model-pricing/useModelPricingData';
import { useIsMobile } from '../../../hooks/common/useIsMobile'; import { useIsMobile } from '../../../../hooks/common/useIsMobile';
const PricingPage = () => { const PricingPage = () => {
const pricingData = useModelPricingData(); const pricingData = useModelPricingData();
const { Sider, Content } = Layout; const { Sider, Content } = Layout;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
// 显示倍率状态
const [showRatio, setShowRatio] = React.useState(false); const [showRatio, setShowRatio] = React.useState(false);
const [viewMode, setViewMode] = React.useState('card');
const allProps = {
...pricingData,
showRatio,
setShowRatio,
viewMode,
setViewMode
};
return ( return (
<div className="bg-white"> <div className="bg-white">
<Layout style={{ height: 'calc(100vh - 60px)', overflow: 'hidden', marginTop: '60px' }}> <Layout className="pricing-layout">
{/* 左侧边栏 - 只在桌面端显示 */}
{!isMobile && ( {!isMobile && (
<Sider <Sider
className="pricing-scroll-hide" className="pricing-scroll-hide pricing-sidebar"
style={{ width={460}
width: 460,
height: 'calc(100vh - 60px)',
backgroundColor: 'var(--semi-color-bg-0)',
borderRight: '1px solid var(--semi-color-border)',
overflow: 'auto'
}}
> >
<PricingSidebar {...pricingData} showRatio={showRatio} setShowRatio={setShowRatio} /> <PricingSidebar {...allProps} />
</Sider> </Sider>
)} )}
{/* 右侧内容区 */}
<Content <Content
className="pricing-scroll-hide" className="pricing-scroll-hide pricing-content"
style={{
height: 'calc(100vh - 60px)',
backgroundColor: 'var(--semi-color-bg-0)',
display: 'flex',
flexDirection: 'column'
}}
> >
<PricingContent <PricingContent
{...pricingData} {...allProps}
showRatio={showRatio}
isMobile={isMobile} isMobile={isMobile}
sidebarProps={{ ...pricingData, showRatio, setShowRatio }} sidebarProps={allProps}
/> />
</Content> </Content>
</Layout> </Layout>
{/* 倍率说明图预览 */}
<ImagePreview <ImagePreview
src={pricingData.modalImageUrl} src={pricingData.modalImageUrl}
visible={pricingData.isModalOpenurl} visible={pricingData.isModalOpenurl}
......
...@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Input, Button } from '@douyinfe/semi-ui'; import { Input, Button } from '@douyinfe/semi-ui';
import { IconSearch, IconCopy, IconFilter } from '@douyinfe/semi-icons'; import { IconSearch, IconCopy, IconFilter } from '@douyinfe/semi-icons';
import PricingFilterModal from './modal/PricingFilterModal'; import PricingFilterModal from '../modal/PricingFilterModal';
const PricingSearchBar = ({ const PricingSearchBar = ({
selectedRowKeys, selectedRowKeys,
......
...@@ -19,11 +19,11 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -19,11 +19,11 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react'; import React from 'react';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import PricingCategories from './filter/PricingCategories'; import PricingCategories from '../filter/PricingCategories';
import PricingGroups from './filter/PricingGroups'; import PricingGroups from '../filter/PricingGroups';
import PricingQuotaTypes from './filter/PricingQuotaTypes'; import PricingQuotaTypes from '../filter/PricingQuotaTypes';
import PricingDisplaySettings from './filter/PricingDisplaySettings'; import PricingDisplaySettings from '../filter/PricingDisplaySettings';
import { resetPricingFilters } from '../../../helpers/utils'; import { resetPricingFilters } from '../../../../helpers/utils';
const PricingSidebar = ({ const PricingSidebar = ({
showWithRecharge, showWithRecharge,
...@@ -34,10 +34,14 @@ const PricingSidebar = ({ ...@@ -34,10 +34,14 @@ const PricingSidebar = ({
setActiveKey, setActiveKey,
showRatio, showRatio,
setShowRatio, setShowRatio,
viewMode,
setViewMode,
filterGroup, filterGroup,
setFilterGroup, setFilterGroup,
filterQuotaType, filterQuotaType,
setFilterQuotaType, setFilterQuotaType,
currentPage,
setCurrentPage,
loading, loading,
t, t,
...categoryProps ...categoryProps
...@@ -51,8 +55,10 @@ const PricingSidebar = ({ ...@@ -51,8 +55,10 @@ const PricingSidebar = ({
setShowWithRecharge, setShowWithRecharge,
setCurrency, setCurrency,
setShowRatio, setShowRatio,
setViewMode,
setFilterGroup, setFilterGroup,
setFilterQuotaType, setFilterQuotaType,
setCurrentPage,
}); });
return ( return (
...@@ -78,15 +84,36 @@ const PricingSidebar = ({ ...@@ -78,15 +84,36 @@ const PricingSidebar = ({
setCurrency={setCurrency} setCurrency={setCurrency}
showRatio={showRatio} showRatio={showRatio}
setShowRatio={setShowRatio} setShowRatio={setShowRatio}
viewMode={viewMode}
setViewMode={setViewMode}
loading={loading} loading={loading}
t={t} 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> </div>
); );
}; };
......
...@@ -17,5 +17,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,5 +17,17 @@ 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
*/ */
// 为了向后兼容,这里重新导出新的 PricingPage 组件 import React from 'react';
export { default } from './PricingPage'; import PricingTable from '../view/PricingTable';
\ No newline at end of file import PricingCardView from '../view/PricingCardView';
const PricingView = ({
viewMode = 'table',
...props
}) => {
return viewMode === 'card' ?
<PricingCardView {...props} /> :
<PricingTable {...props} />;
};
export default PricingView;
\ No newline at end of file
...@@ -40,10 +40,14 @@ const PricingFilterModal = ({ ...@@ -40,10 +40,14 @@ const PricingFilterModal = ({
setActiveKey, setActiveKey,
showRatio, showRatio,
setShowRatio, setShowRatio,
viewMode,
setViewMode,
filterGroup, filterGroup,
setFilterGroup, setFilterGroup,
filterQuotaType, filterQuotaType,
setFilterQuotaType, setFilterQuotaType,
currentPage,
setCurrentPage,
loading, loading,
...categoryProps ...categoryProps
} = sidebarProps; } = sidebarProps;
...@@ -56,14 +60,12 @@ const PricingFilterModal = ({ ...@@ -56,14 +60,12 @@ const PricingFilterModal = ({
setShowWithRecharge, setShowWithRecharge,
setCurrency, setCurrency,
setShowRatio, setShowRatio,
setViewMode,
setFilterGroup, setFilterGroup,
setFilterQuotaType, setFilterQuotaType,
setCurrentPage,
}); });
const handleConfirm = () => {
onClose();
};
const footer = ( const footer = (
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button
...@@ -76,7 +78,7 @@ const PricingFilterModal = ({ ...@@ -76,7 +78,7 @@ const PricingFilterModal = ({
<Button <Button
theme="solid" theme="solid"
type="primary" type="primary"
onClick={handleConfirm} onClick={onClose}
> >
{t('确定')} {t('确定')}
</Button> </Button>
...@@ -106,6 +108,8 @@ const PricingFilterModal = ({ ...@@ -106,6 +108,8 @@ const PricingFilterModal = ({
setCurrency={setCurrency} setCurrency={setCurrency}
showRatio={showRatio} showRatio={showRatio}
setShowRatio={setShowRatio} setShowRatio={setShowRatio}
viewMode={viewMode}
setViewMode={setViewMode}
loading={loading} loading={loading}
t={t} t={t}
/> />
......
...@@ -32,18 +32,15 @@ const PricingTable = ({ ...@@ -32,18 +32,15 @@ const PricingTable = ({
pageSize, pageSize,
setPageSize, setPageSize,
selectedGroup, selectedGroup,
usableGroup,
groupRatio, groupRatio,
copyText, copyText,
setModalImageUrl, setModalImageUrl,
setIsModalOpenurl, setIsModalOpenurl,
currency, currency,
showWithRecharge,
tokenUnit, tokenUnit,
setTokenUnit, setTokenUnit,
displayPrice, displayPrice,
filteredValue, searchValue,
handleGroupClick,
showRatio, showRatio,
compactMode = false, compactMode = false,
t t
...@@ -53,43 +50,37 @@ const PricingTable = ({ ...@@ -53,43 +50,37 @@ const PricingTable = ({
return getPricingTableColumns({ return getPricingTableColumns({
t, t,
selectedGroup, selectedGroup,
usableGroup,
groupRatio, groupRatio,
copyText, copyText,
setModalImageUrl, setModalImageUrl,
setIsModalOpenurl, setIsModalOpenurl,
currency, currency,
showWithRecharge,
tokenUnit, tokenUnit,
setTokenUnit, setTokenUnit,
displayPrice, displayPrice,
handleGroupClick,
showRatio, showRatio,
}); });
}, [ }, [
t, t,
selectedGroup, selectedGroup,
usableGroup,
groupRatio, groupRatio,
copyText, copyText,
setModalImageUrl, setModalImageUrl,
setIsModalOpenurl, setIsModalOpenurl,
currency, currency,
showWithRecharge,
tokenUnit, tokenUnit,
setTokenUnit, setTokenUnit,
displayPrice, displayPrice,
handleGroupClick,
showRatio, showRatio,
]); ]);
// 更新列定义中的 filteredValue // 更新列定义中的 searchValue
const processedColumns = useMemo(() => { const processedColumns = useMemo(() => {
const cols = columns.map(column => { const cols = columns.map(column => {
if (column.dataIndex === 'model_name') { if (column.dataIndex === 'model_name') {
return { return {
...column, ...column,
filteredValue filteredValue: searchValue ? [searchValue] : []
}; };
} }
return column; return column;
...@@ -100,7 +91,7 @@ const PricingTable = ({ ...@@ -100,7 +91,7 @@ const PricingTable = ({
return cols.map(({ fixed, ...rest }) => rest); return cols.map(({ fixed, ...rest }) => rest);
} }
return cols; return cols;
}, [columns, filteredValue, compactMode]); }, [columns, searchValue, compactMode]);
const ModelTable = useMemo(() => ( const ModelTable = useMemo(() => (
<Card className="!rounded-xl overflow-hidden" bordered={false}> <Card className="!rounded-xl overflow-hidden" bordered={false}>
......
...@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react'; import React from 'react';
import { Tag, Space, Tooltip, Switch } from '@douyinfe/semi-ui'; import { Tag, Space, Tooltip, Switch } from '@douyinfe/semi-ui';
import { IconHelpCircle } from '@douyinfe/semi-icons'; import { IconHelpCircle } from '@douyinfe/semi-icons';
import { renderModelTag, stringToColor } from '../../../helpers'; import { renderModelTag, stringToColor, calculateModelPrice } from '../../../../helpers';
function renderQuotaType(type, t) { function renderQuotaType(type, t) {
switch (type) { switch (type) {
...@@ -158,38 +158,30 @@ export const getPricingTableColumns = ({ ...@@ -158,38 +158,30 @@ export const getPricingTableColumns = ({
), ),
dataIndex: 'model_price', dataIndex: 'model_price',
render: (text, record, index) => { render: (text, record, index) => {
if (record.quota_type === 0) { const priceData = calculateModelPrice({
const inputRatioPriceUSD = record.model_ratio * 2 * groupRatio[selectedGroup]; record,
const completionRatioPriceUSD = selectedGroup,
record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup]; groupRatio,
tokenUnit,
const unitDivisor = tokenUnit === 'K' ? 1000 : 1; displayPrice,
const unitLabel = tokenUnit === 'K' ? 'K' : 'M'; currency
});
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)}`; if (priceData.isPerToken) {
const displayCompletion = `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(3)}`;
return ( return (
<div className="space-y-1"> <div className="space-y-1">
<div className="text-gray-700"> <div className="text-gray-700">
{t('提示')} {displayInput} / 1{unitLabel} tokens {t('提示')} {priceData.inputPrice} / 1{priceData.unitLabel} tokens
</div> </div>
<div className="text-gray-700"> <div className="text-gray-700">
{t('补全')} {displayCompletion} / 1{unitLabel} tokens {t('补全')} {priceData.completionPrice} / 1{priceData.unitLabel} tokens
</div> </div>
</div> </div>
); );
} else { } else {
const priceUSD = parseFloat(text) * groupRatio[selectedGroup];
const displayVal = displayPrice(priceUSD);
return ( return (
<div className="text-gray-700"> <div className="text-gray-700">
{t('模型价格')}{displayVal} {t('模型价格')}{priceData.price}
</div> </div>
); );
} }
......
...@@ -40,7 +40,7 @@ const LogsActions = ({ ...@@ -40,7 +40,7 @@ const LogsActions = ({
setShowSkeleton(true); setShowSkeleton(true);
} else { } else {
const elapsed = Date.now() - loadingStartRef.current; const elapsed = Date.now() - loadingStartRef.current;
const remaining = Math.max(0, 500 - elapsed); const remaining = Math.max(0, 1000 - elapsed);
if (remaining === 0) { if (remaining === 0) {
setShowSkeleton(false); setShowSkeleton(false);
} else { } else {
......
...@@ -569,6 +569,59 @@ export const modelSelectFilter = (input, option) => { ...@@ -569,6 +569,59 @@ export const modelSelectFilter = (input, option) => {
}; };
// ------------------------------- // -------------------------------
// 模型定价计算工具函数
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 分页配置函数
// 用于创建 CardPro 的 paginationArea 配置 // 用于创建 CardPro 的 paginationArea 配置
export const createCardProPagination = ({ export const createCardProPagination = ({
...@@ -626,8 +679,10 @@ export const resetPricingFilters = ({ ...@@ -626,8 +679,10 @@ export const resetPricingFilters = ({
setShowWithRecharge, setShowWithRecharge,
setCurrency, setCurrency,
setShowRatio, setShowRatio,
setViewMode,
setFilterGroup, setFilterGroup,
setFilterQuotaType, setFilterQuotaType,
setCurrentPage,
}) => { }) => {
// 重置搜索 // 重置搜索
if (typeof handleChange === 'function') { if (typeof handleChange === 'function') {
...@@ -658,6 +713,11 @@ export const resetPricingFilters = ({ ...@@ -658,6 +713,11 @@ export const resetPricingFilters = ({
setShowRatio(false); setShowRatio(false);
} }
// 重置视图模式
if (typeof setViewMode === 'function') {
setViewMode('card');
}
// 重置分组筛选 // 重置分组筛选
if (typeof setFilterGroup === 'function') { if (typeof setFilterGroup === 'function') {
setFilterGroup('all'); setFilterGroup('all');
...@@ -667,4 +727,9 @@ export const resetPricingFilters = ({ ...@@ -667,4 +727,9 @@ export const resetPricingFilters = ({
if (typeof setFilterQuotaType === 'function') { if (typeof setFilterQuotaType === 'function') {
setFilterQuotaType('all'); setFilterQuotaType('all');
} }
// 重置当前页面
if (typeof setCurrentPage === 'function') {
setCurrentPage(1);
}
}; };
...@@ -178,7 +178,7 @@ export const useDashboardData = (userState, userDispatch, statusState) => { ...@@ -178,7 +178,7 @@ export const useDashboardData = (userState, userDispatch, statusState) => {
} }
} finally { } finally {
const elapsed = Date.now() - startTime; const elapsed = Date.now() - startTime;
const remainingTime = Math.max(0, 500 - elapsed); const remainingTime = Math.max(0, 1000 - elapsed);
setTimeout(() => { setTimeout(() => {
setLoading(false); setLoading(false);
}, remainingTime); }, remainingTime);
......
...@@ -26,18 +26,17 @@ import { StatusContext } from '../../context/Status/index.js'; ...@@ -26,18 +26,17 @@ import { StatusContext } from '../../context/Status/index.js';
export const useModelPricingData = () => { export const useModelPricingData = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [filteredValue, setFilteredValue] = useState([]); const [searchValue, setSearchValue] = useState('');
const compositionRef = useRef({ isComposition: false }); const compositionRef = useRef({ isComposition: false });
const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [modalImageUrl, setModalImageUrl] = useState(''); const [modalImageUrl, setModalImageUrl] = useState('');
const [isModalOpenurl, setIsModalOpenurl] = useState(false); const [isModalOpenurl, setIsModalOpenurl] = useState(false);
const [selectedGroup, setSelectedGroup] = useState('default'); const [selectedGroup, setSelectedGroup] = useState('default');
// 用于 Table 的可用分组筛选,“all” 表示不过滤 const [filterGroup, setFilterGroup] = useState('all'); // 用于 Table 的可用分组筛选,“all” 表示不过滤
const [filterGroup, setFilterGroup] = useState('all'); const [filterQuotaType, setFilterQuotaType] = useState('all'); // 计费类型筛选: 'all' | 0 | 1
// 计费类型筛选: 'all' | 0 | 1
const [filterQuotaType, setFilterQuotaType] = useState('all');
const [activeKey, setActiveKey] = useState('all'); const [activeKey, setActiveKey] = useState('all');
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [currentPage, setCurrentPage] = useState(1);
const [currency, setCurrency] = useState('USD'); const [currency, setCurrency] = useState('USD');
const [showWithRecharge, setShowWithRecharge] = useState(false); const [showWithRecharge, setShowWithRecharge] = useState(false);
const [tokenUnit, setTokenUnit] = useState('M'); const [tokenUnit, setTokenUnit] = useState('M');
...@@ -95,15 +94,15 @@ export const useModelPricingData = () => { ...@@ -95,15 +94,15 @@ export const useModelPricingData = () => {
} }
// 搜索筛选 // 搜索筛选
if (filteredValue.length > 0) { if (searchValue.length > 0) {
const searchTerm = filteredValue[0].toLowerCase(); const searchTerm = searchValue.toLowerCase();
result = result.filter(model => result = result.filter(model =>
model.model_name.toLowerCase().includes(searchTerm) model.model_name.toLowerCase().includes(searchTerm)
); );
} }
return result; return result;
}, [activeKey, models, filteredValue, filterGroup, filterQuotaType]); }, [activeKey, models, searchValue, filterGroup, filterQuotaType]);
const rowSelection = useMemo( const rowSelection = useMemo(
() => ({ () => ({
...@@ -183,8 +182,8 @@ export const useModelPricingData = () => { ...@@ -183,8 +182,8 @@ export const useModelPricingData = () => {
if (compositionRef.current.isComposition) { if (compositionRef.current.isComposition) {
return; return;
} }
const newFilteredValue = value ? [value] : []; const newSearchValue = value ? value : '';
setFilteredValue(newFilteredValue); setSearchValue(newSearchValue);
}; };
const handleCompositionStart = () => { const handleCompositionStart = () => {
...@@ -194,8 +193,8 @@ export const useModelPricingData = () => { ...@@ -194,8 +193,8 @@ export const useModelPricingData = () => {
const handleCompositionEnd = (event) => { const handleCompositionEnd = (event) => {
compositionRef.current.isComposition = false; compositionRef.current.isComposition = false;
const value = event.target.value; const value = event.target.value;
const newFilteredValue = value ? [value] : []; const newSearchValue = value ? value : '';
setFilteredValue(newFilteredValue); setSearchValue(newSearchValue);
}; };
const handleGroupClick = (group) => { const handleGroupClick = (group) => {
...@@ -214,10 +213,15 @@ export const useModelPricingData = () => { ...@@ -214,10 +213,15 @@ export const useModelPricingData = () => {
refresh().then(); refresh().then();
}, []); }, []);
// 当筛选条件变化时重置到第一页
useEffect(() => {
setCurrentPage(1);
}, [activeKey, filterGroup, filterQuotaType, searchValue]);
return { return {
// 状态 // 状态
filteredValue, searchValue,
setFilteredValue, setSearchValue,
selectedRowKeys, selectedRowKeys,
setSelectedRowKeys, setSelectedRowKeys,
modalImageUrl, modalImageUrl,
...@@ -234,6 +238,8 @@ export const useModelPricingData = () => { ...@@ -234,6 +238,8 @@ export const useModelPricingData = () => {
setActiveKey, setActiveKey,
pageSize, pageSize,
setPageSize, setPageSize,
currentPage,
setCurrentPage,
currency, currency,
setCurrency, setCurrency,
showWithRecharge, showWithRecharge,
......
...@@ -617,4 +617,59 @@ html:not(.dark) .blur-ball-teal { ...@@ -617,4 +617,59 @@ html:not(.dark) .blur-ball-teal {
height: calc(100vh - 77px); height: calc(100vh - 77px);
max-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;
} }
\ No newline at end of file
...@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React from 'react'; import React from 'react';
import ModelPricingPage from '../../components/table/model-pricing'; import ModelPricingPage from '../../components/table/model-pricing/layout/PricingPage';
const Pricing = () => ( const Pricing = () => (
<> <>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment