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 = () => ( <>