From 53be79a00e4792ce41ed878fe9c05c1c2da9fd93 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Thu, 24 Jul 2025 03:19:32 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20style(pricing):=20enhance=20card?= =?UTF-8?q?=20view=20UI=20and=20skeleton=20loading=20experience=20(#1365)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- web/src/components/common/ui/CardTable.js | 2 +- .../common/ui/SelectableButtonGroup.jsx | 2 +- web/src/components/layout/HeaderBar.js | 2 +- .../table/model-pricing/PricingHeader.jsx | 123 ----- .../filter/PricingDisplaySettings.jsx | 21 +- .../{ => layout}/PricingContent.jsx | 34 +- .../{ => layout}/PricingPage.jsx | 46 +- .../{ => layout}/PricingSearchBar.jsx | 2 +- .../{ => layout}/PricingSidebar.jsx | 43 +- .../{index.jsx => layout/PricingView.jsx} | 16 +- .../modal/PricingFilterModal.jsx | 14 +- .../model-pricing/view/PricingCardView.jsx | 444 ++++++++++++++++++ .../model-pricing/{ => view}/PricingTable.jsx | 17 +- .../{ => view}/PricingTableColumns.js | 34 +- .../table/usage-logs/UsageLogsActions.jsx | 2 +- web/src/helpers/utils.js | 65 +++ web/src/hooks/dashboard/useDashboardData.js | 2 +- .../model-pricing/useModelPricingData.js | 34 +- web/src/index.css | 55 +++ web/src/pages/Pricing/index.js | 2 +- 20 files changed, 706 insertions(+), 254 deletions(-) delete mode 100644 web/src/components/table/model-pricing/PricingHeader.jsx rename web/src/components/table/model-pricing/{ => layout}/PricingContent.jsx (61%) rename web/src/components/table/model-pricing/{ => layout}/PricingPage.jsx (57%) rename web/src/components/table/model-pricing/{ => layout}/PricingSearchBar.jsx (97%) rename web/src/components/table/model-pricing/{ => layout}/PricingSidebar.jsx (66%) rename web/src/components/table/model-pricing/{index.jsx => layout/PricingView.jsx} (69%) create mode 100644 web/src/components/table/model-pricing/view/PricingCardView.jsx rename web/src/components/table/model-pricing/{ => view}/PricingTable.jsx (91%) rename web/src/components/table/model-pricing/{ => view}/PricingTableColumns.js (79%) diff --git a/web/src/components/common/ui/CardTable.js b/web/src/components/common/ui/CardTable.js index 75b6df00..bb80046d 100644 --- a/web/src/components/common/ui/CardTable.js +++ b/web/src/components/common/ui/CardTable.js @@ -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 { diff --git a/web/src/components/common/ui/SelectableButtonGroup.jsx b/web/src/components/common/ui/SelectableButtonGroup.jsx index dd7fd8ab..c3fe28ff 100644 --- a/web/src/components/common/ui/SelectableButtonGroup.jsx +++ b/web/src/components/common/ui/SelectableButtonGroup.jsx @@ -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 { diff --git a/web/src/components/layout/HeaderBar.js b/web/src/components/layout/HeaderBar.js index 6a158ec0..a935da12 100644 --- a/web/src/components/layout/HeaderBar.js +++ b/web/src/components/layout/HeaderBar.js @@ -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); diff --git a/web/src/components/table/model-pricing/PricingHeader.jsx b/web/src/components/table/model-pricing/PricingHeader.jsx deleted file mode 100644 index 9dc508aa..00000000 --- a/web/src/components/table/model-pricing/PricingHeader.jsx +++ /dev/null @@ -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 . - -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 ( - -
-
-
-
- -
-
-
- {t('模型定价')} -
-
- {userState.user ? ( -
- - - {t('当前分组')}: {userState.user.group},{t('倍率')}: {groupRatio[userState.user.group]} - -
- ) : ( -
- - - {t('未登录,使用默认分组倍率:')}{groupRatio['default']} - -
- )} -
-
-
- -
-
-
{t('分组倍率')}
-
{groupRatio[selectedGroup] || '1.0'}x
-
-
-
{t('可用模型')}
-
- {models.filter(m => m.enable_groups.includes(selectedGroup)).length} -
-
-
-
{t('计费类型')}
-
2
-
-
-
- - {/* 计费说明 */} -
-
-
- - - {t('按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')} - -
-
-
- -
-
-
- ); -}; - -export default PricingHeader; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx b/web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx index 9d4d8312..321450a3 100644 --- a/web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx +++ b/web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx @@ -29,6 +29,8 @@ const PricingDisplaySettings = ({ setCurrency, showRatio, setShowRatio, + viewMode, + setViewMode, loading = false, t }) => { @@ -50,6 +52,10 @@ const PricingDisplaySettings = ({ ), + }, + { + 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; }; diff --git a/web/src/components/table/model-pricing/PricingContent.jsx b/web/src/components/table/model-pricing/layout/PricingContent.jsx similarity index 61% rename from web/src/components/table/model-pricing/PricingContent.jsx rename to web/src/components/table/model-pricing/layout/PricingContent.jsx index de6344c8..edb97514 100644 --- a/web/src/components/table/model-pricing/PricingContent.jsx +++ b/web/src/components/table/model-pricing/layout/PricingContent.jsx @@ -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 ( -
+
{/* 固定的搜索和操作区域 */} -
+
{/* 可滚动的内容区域 */} -
- +
+
); diff --git a/web/src/components/table/model-pricing/PricingPage.jsx b/web/src/components/table/model-pricing/layout/PricingPage.jsx similarity index 57% rename from web/src/components/table/model-pricing/PricingPage.jsx rename to web/src/components/table/model-pricing/layout/PricingPage.jsx index eb76944f..0f150122 100644 --- a/web/src/components/table/model-pricing/PricingPage.jsx +++ b/web/src/components/table/model-pricing/layout/PricingPage.jsx @@ -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 (
- - {/* 左侧边栏 - 只在桌面端显示 */} + {!isMobile && ( - + )} - {/* 右侧内容区 */} - - {/* 倍率说明图预览 */} - + - + - +
); }; diff --git a/web/src/components/table/model-pricing/index.jsx b/web/src/components/table/model-pricing/layout/PricingView.jsx similarity index 69% rename from web/src/components/table/model-pricing/index.jsx rename to web/src/components/table/model-pricing/layout/PricingView.jsx index 948285f0..16e9db99 100644 --- a/web/src/components/table/model-pricing/index.jsx +++ b/web/src/components/table/model-pricing/layout/PricingView.jsx @@ -17,5 +17,17 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -// 为了向后兼容,这里重新导出新的 PricingPage 组件 -export { default } from './PricingPage'; \ No newline at end of file +import React from 'react'; +import PricingTable from '../view/PricingTable'; +import PricingCardView from '../view/PricingCardView'; + +const PricingView = ({ + viewMode = 'table', + ...props +}) => { + return viewMode === 'card' ? + : + ; +}; + +export default PricingView; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/modal/PricingFilterModal.jsx b/web/src/components/table/model-pricing/modal/PricingFilterModal.jsx index 84edb454..3d0601b8 100644 --- a/web/src/components/table/model-pricing/modal/PricingFilterModal.jsx +++ b/web/src/components/table/model-pricing/modal/PricingFilterModal.jsx @@ -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 = (
@@ -106,6 +108,8 @@ const PricingFilterModal = ({ setCurrency={setCurrency} showRatio={showRatio} setShowRatio={setShowRatio} + viewMode={viewMode} + setViewMode={setViewMode} loading={loading} t={t} /> diff --git a/web/src/components/table/model-pricing/view/PricingCardView.jsx b/web/src/components/table/model-pricing/view/PricingCardView.jsx new file mode 100644 index 00000000..1d743412 --- /dev/null +++ b/web/src/components/table/model-pricing/view/PricingCardView.jsx @@ -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 . + +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 = ( +
+
+ {Array.from({ length: skeletonCount }).map((_, index) => ( + + {/* 头部:图标 + 模型名称 + 操作按钮 */} +
+
+ {/* 模型图标骨架 */} +
+ +
+ {/* 模型名称骨架 */} +
+ +
+
+ +
+ {/* 操作按钮骨架 */} + + {rowSelection && ( + + )} +
+
+ + {/* 价格信息骨架 */} +
+ +
+ + {/* 模型描述骨架 */} +
+ +
+ + {/* 标签区域骨架 */} +
+ {Array.from({ length: 3 + (index % 2) }).map((_, tagIndex) => ( + + ))} +
+ + {/* 倍率信息骨架(可选) */} + {showRatio && ( +
+
+ + +
+
+ {Array.from({ length: 3 }).map((_, ratioIndex) => ( + + ))} +
+
+ )} +
+ ))} +
+ + {/* 分页骨架 */} +
+ +
+
+ ); + + return ( + + ); + }; + + // 获取模型图标 + 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 ( +
+
+ {React.cloneElement(icon, { size: 32 })} +
+
+ ); + } + + // 默认图标(如果没有匹配到任何分类) + return ( +
+ {/* 默认的螺旋图案 */} + + + +
+ ); + }; + + // 获取模型描述 + 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( + + {t('按次计费')} + + ); + } else { + tags.push( + + {t('按量计费')} + + ); + } + + // 热度标签(示例) + if (record.model_name.includes('gpt-3.5-turbo') || record.model_name.includes('gpt-4')) { + tags.push( + + {t('热')} + + ); + } + + // 端点类型标签 + if (record.supported_endpoint_types && record.supported_endpoint_types.length > 0) { + record.supported_endpoint_types.slice(0, 2).forEach((endpoint, index) => { + tags.push( + + {endpoint} + + ); + }); + } + + // 上下文长度标签(示例) + if (record.model_name.includes('16k')) { + tags.push(16K); + } else if (record.model_name.includes('32k')) { + tags.push(32K); + } else { + tags.push(4K); + } + + return tags; + }; + + // 显示骨架屏 + if (showSkeleton) { + return renderSkeletonCards(); + } + + if (!filteredModels || filteredModels.length === 0) { + return ( +
+ } + darkModeImage={} + description={t('搜索无结果')} + /> +
+ ); + } + + return ( +
+
+ {paginatedModels.map((model, index) => { + const isSelected = rowSelection?.selectedRowKeys?.includes(model.id); + + return ( + + {/* 头部:图标 + 模型名称 + 操作按钮 */} +
+
+ {getModelIcon(model.model_name)} +
+

+ {model.model_name} +

+
+
+ +
+ {/* 复制按钮 */} +
+
+ + {/* 价格信息 */} +
+
+ {renderPriceInfo(model)} +
+
+ + {/* 模型描述 */} +
+

+ {getModelDescription(model.model_name)} +

+
+ + {/* 标签区域 */} +
+ {renderTags(model)} +
+ + {/* 倍率信息(可选) */} + {showRatio && ( +
+
+ {t('倍率信息')} + + { + setModalImageUrl('/ratio.png'); + setIsModalOpenurl(true); + }} + /> + +
+
+
+ {t('模型')}: {model.quota_type === 0 ? model.model_ratio : t('无')} +
+
+ {t('补全')}: {model.quota_type === 0 ? parseFloat(model.completion_ratio.toFixed(3)) : t('无')} +
+
+ {t('分组')}: {groupRatio[selectedGroup]} +
+
+
+ )} +
+ ); + })} +
+ + {/* 分页 */} + {filteredModels.length > 0 && ( +
+ setCurrentPage(page)} + onPageSizeChange={(size) => { + setPageSize(size); + setCurrentPage(1); + }} + /> +
+ )} +
+ ); +}; + +export default PricingCardView; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/PricingTable.jsx b/web/src/components/table/model-pricing/view/PricingTable.jsx similarity index 91% rename from web/src/components/table/model-pricing/PricingTable.jsx rename to web/src/components/table/model-pricing/view/PricingTable.jsx index 4fb2a8e8..26c7edbb 100644 --- a/web/src/components/table/model-pricing/PricingTable.jsx +++ b/web/src/components/table/model-pricing/view/PricingTable.jsx @@ -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(() => ( diff --git a/web/src/components/table/model-pricing/PricingTableColumns.js b/web/src/components/table/model-pricing/view/PricingTableColumns.js similarity index 79% rename from web/src/components/table/model-pricing/PricingTableColumns.js rename to web/src/components/table/model-pricing/view/PricingTableColumns.js index f0c9783d..54b3889c 100644 --- a/web/src/components/table/model-pricing/PricingTableColumns.js +++ b/web/src/components/table/model-pricing/view/PricingTableColumns.js @@ -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 (
- {t('提示')} {displayInput} / 1{unitLabel} tokens + {t('提示')} {priceData.inputPrice} / 1{priceData.unitLabel} tokens
- {t('补全')} {displayCompletion} / 1{unitLabel} tokens + {t('补全')} {priceData.completionPrice} / 1{priceData.unitLabel} tokens
); } else { - const priceUSD = parseFloat(text) * groupRatio[selectedGroup]; - const displayVal = displayPrice(priceUSD); return (
- {t('模型价格')}:{displayVal} + {t('模型价格')}:{priceData.price}
); } diff --git a/web/src/components/table/usage-logs/UsageLogsActions.jsx b/web/src/components/table/usage-logs/UsageLogsActions.jsx index 72db01e4..c14ffcbf 100644 --- a/web/src/components/table/usage-logs/UsageLogsActions.jsx +++ b/web/src/components/table/usage-logs/UsageLogsActions.jsx @@ -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 { diff --git a/web/src/helpers/utils.js b/web/src/helpers/utils.js index 265be6c2..22b4fbc6 100644 --- a/web/src/helpers/utils.js +++ b/web/src/helpers/utils.js @@ -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); + } }; diff --git a/web/src/hooks/dashboard/useDashboardData.js b/web/src/hooks/dashboard/useDashboardData.js index 4eaeca77..255f48d3 100644 --- a/web/src/hooks/dashboard/useDashboardData.js +++ b/web/src/hooks/dashboard/useDashboardData.js @@ -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); diff --git a/web/src/hooks/model-pricing/useModelPricingData.js b/web/src/hooks/model-pricing/useModelPricingData.js index ac58d817..c32ddf84 100644 --- a/web/src/hooks/model-pricing/useModelPricingData.js +++ b/web/src/hooks/model-pricing/useModelPricingData.js @@ -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, diff --git a/web/src/index.css b/web/src/index.css index afbb7862..b624d749 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -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; } \ No newline at end of file diff --git a/web/src/pages/Pricing/index.js b/web/src/pages/Pricing/index.js index c1066203..e37167d8 100644 --- a/web/src/pages/Pricing/index.js +++ b/web/src/pages/Pricing/index.js @@ -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 = () => ( <>