diff --git a/web/src/components/common/ui/SelectableButtonGroup.jsx b/web/src/components/common/ui/SelectableButtonGroup.jsx new file mode 100644 index 00000000..270cacc7 --- /dev/null +++ b/web/src/components/common/ui/SelectableButtonGroup.jsx @@ -0,0 +1,147 @@ +/* +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 } from 'react'; +import { Divider, Button, Tag, Row, Col, Collapsible } from '@douyinfe/semi-ui'; +import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons'; + +/** + * 通用可选择按钮组组件 + * + * @param {string} title 标题 + * @param {Array<{value:any,label:string,icon?:React.ReactNode,tagCount?:number}>} items 按钮项 + * @param {*} activeValue 当前激活的值 + * @param {(value:any)=>void} onChange 选择改变回调 + * @param {function} t i18n + * @param {object} style 额外样式 + * @param {boolean} collapsible 是否支持折叠,默认true + * @param {number} collapseHeight 折叠时的高度,默认200 + */ +const SelectableButtonGroup = ({ + title, + items = [], + activeValue, + onChange, + t = (v) => v, + style = {}, + collapsible = true, + collapseHeight = 200 +}) => { + const [isOpen, setIsOpen] = useState(false); + const perRow = 3; + const maxVisibleRows = Math.max(1, Math.floor(collapseHeight / 32)); // Approx row height 32 + const needCollapse = collapsible && items.length > perRow * maxVisibleRows; + + const contentRef = useRef(null); + + const maskStyle = isOpen + ? {} + : { + WebkitMaskImage: + 'linear-gradient(to bottom, black 0%, rgba(0, 0, 0, 1) 60%, rgba(0, 0, 0, 0.2) 80%, transparent 100%)', + }; + + const toggle = () => { + setIsOpen(!isOpen); + }; + + const linkStyle = { + position: 'absolute', + left: 0, + right: 0, + textAlign: 'center', + bottom: -10, + fontWeight: 400, + cursor: 'pointer', + fontSize: '12px', + color: 'var(--semi-color-text-2)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: 4, + }; + + const contentElement = ( + + {items.map((item) => { + const isActive = activeValue === item.value; + return ( + + + + ); + })} + + ); + + return ( +
+ {title && ( + + {title} + + )} + {needCollapse ? ( +
+ + {contentElement} + + {isOpen ? null : ( +
+ + {t('展开更多')} +
+ )} + {isOpen && ( +
+ + {t('收起')} +
+ )} +
+ ) : ( + contentElement + )} +
+ ); +}; + +export default SelectableButtonGroup; \ No newline at end of file diff --git a/web/src/components/layout/HeaderBar.js b/web/src/components/layout/HeaderBar.js index a2e3986c..6a158ec0 100644 --- a/web/src/components/layout/HeaderBar.js +++ b/web/src/components/layout/HeaderBar.js @@ -467,7 +467,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { }; return ( -
+
{ const { i18n } = useTranslation(); const location = useLocation(); - const shouldHideFooter = location.pathname.startsWith('/console'); + const shouldHideFooter = location.pathname.startsWith('/console') || location.pathname === '/pricing'; const shouldInnerPadding = location.pathname.includes('/console') && !location.pathname.startsWith('/console/chat') && diff --git a/web/src/components/table/model-pricing/ModelPricingFilters.jsx b/web/src/components/table/model-pricing/ModelPricingFilters.jsx deleted file mode 100644 index 57b5e7e1..00000000 --- a/web/src/components/table/model-pricing/ModelPricingFilters.jsx +++ /dev/null @@ -1,87 +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, { useMemo } from 'react'; -import { Card, Input, Button, Space, Switch, Select } from '@douyinfe/semi-ui'; -import { IconSearch, IconCopy } from '@douyinfe/semi-icons'; - -const ModelPricingFilters = ({ - selectedRowKeys, - copyText, - showWithRecharge, - setShowWithRecharge, - currency, - setCurrency, - handleChange, - handleCompositionStart, - handleCompositionEnd, - t -}) => { - const SearchAndActions = useMemo(() => ( - -
-
- } - placeholder={t('模糊搜索模型名称')} - onCompositionStart={handleCompositionStart} - onCompositionEnd={handleCompositionEnd} - onChange={handleChange} - showClear - /> -
- - - {/* 充值价格显示开关 */} - - {t('以充值价格显示')} - - {showWithRecharge && ( - - )} - -
-
- ), [selectedRowKeys, t, showWithRecharge, currency, handleCompositionStart, handleCompositionEnd, handleChange, copyText, setShowWithRecharge, setCurrency]); - - return SearchAndActions; -}; - -export default ModelPricingFilters; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/ModelPricingTabs.jsx b/web/src/components/table/model-pricing/ModelPricingTabs.jsx deleted file mode 100644 index 11a58b79..00000000 --- a/web/src/components/table/model-pricing/ModelPricingTabs.jsx +++ /dev/null @@ -1,67 +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 { Tabs, TabPane, Tag } from '@douyinfe/semi-ui'; - -const ModelPricingTabs = ({ - activeKey, - setActiveKey, - modelCategories, - categoryCounts, - availableCategories, - t -}) => { - return ( - setActiveKey(key)} - className="mt-2" - > - {Object.entries(modelCategories) - .filter(([key]) => availableCategories.includes(key)) - .map(([key, category]) => { - const modelCount = categoryCounts[key] || 0; - - return ( - - {category.icon && {category.icon}} - {category.label} - - {modelCount} - - - } - itemKey={key} - key={key} - /> - ); - })} - - ); -}; - -export default ModelPricingTabs; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/PricingContent.jsx b/web/src/components/table/model-pricing/PricingContent.jsx new file mode 100644 index 00000000..6a47df26 --- /dev/null +++ b/web/src/components/table/model-pricing/PricingContent.jsx @@ -0,0 +1,52 @@ +/* +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 PricingSearchBar from './PricingSearchBar.jsx'; +import PricingTable from './PricingTable.jsx'; + +const PricingContent = (props) => { + return ( + <> + {/* 固定的搜索和操作区域 */} +
+ +
+ + {/* 可滚动的内容区域 */} +
+ +
+ + ); +}; + +export default PricingContent; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/ModelPricingHeader.jsx b/web/src/components/table/model-pricing/PricingHeader.jsx similarity index 98% rename from web/src/components/table/model-pricing/ModelPricingHeader.jsx rename to web/src/components/table/model-pricing/PricingHeader.jsx index 40075f3a..9dc508aa 100644 --- a/web/src/components/table/model-pricing/ModelPricingHeader.jsx +++ b/web/src/components/table/model-pricing/PricingHeader.jsx @@ -22,7 +22,7 @@ import { Card } from '@douyinfe/semi-ui'; import { IconVerify, IconLayers, IconInfoCircle } from '@douyinfe/semi-icons'; import { AlertCircle } from 'lucide-react'; -const ModelPricingHeader = ({ +const PricingHeader = ({ userState, groupRatio, selectedGroup, @@ -120,4 +120,4 @@ const ModelPricingHeader = ({ ); }; -export default ModelPricingHeader; \ No newline at end of file +export default PricingHeader; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/PricingPage.jsx b/web/src/components/table/model-pricing/PricingPage.jsx new file mode 100644 index 00000000..0c360ad1 --- /dev/null +++ b/web/src/components/table/model-pricing/PricingPage.jsx @@ -0,0 +1,72 @@ +/* +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 { Layout, ImagePreview } from '@douyinfe/semi-ui'; +import PricingSidebar from './PricingSidebar.jsx'; +import PricingContent from './PricingContent.jsx'; +import { useModelPricingData } from '../../../hooks/model-pricing/useModelPricingData.js'; + +const PricingPage = () => { + const pricingData = useModelPricingData(); + const { Sider, Content } = Layout; + + // 显示倍率状态 + const [showRatio, setShowRatio] = React.useState(false); + + return ( +
+ + {/* 左侧边栏 */} + + + + + {/* 右侧内容区 */} + + + + + + {/* 倍率说明图预览 */} + pricingData.setIsModalOpenurl(visible)} + /> +
+ ); +}; + +export default PricingPage; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/PricingSearchBar.jsx b/web/src/components/table/model-pricing/PricingSearchBar.jsx new file mode 100644 index 00000000..744fd0b6 --- /dev/null +++ b/web/src/components/table/model-pricing/PricingSearchBar.jsx @@ -0,0 +1,63 @@ +/* +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, { useMemo } from 'react'; +import { Input, Button } from '@douyinfe/semi-ui'; +import { IconSearch, IconCopy } from '@douyinfe/semi-icons'; + +const PricingSearchBar = ({ + selectedRowKeys, + copyText, + handleChange, + handleCompositionStart, + handleCompositionEnd, + t +}) => { + const SearchAndActions = useMemo(() => ( +
+ {/* 搜索框 */} +
+ } + placeholder={t('模糊搜索模型名称')} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} + onChange={handleChange} + showClear + /> +
+ + {/* 操作按钮 */} + +
+ ), [selectedRowKeys, t, handleCompositionStart, handleCompositionEnd, handleChange, copyText]); + + return SearchAndActions; +}; + +export default PricingSearchBar; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/PricingSidebar.jsx b/web/src/components/table/model-pricing/PricingSidebar.jsx new file mode 100644 index 00000000..9c6389ba --- /dev/null +++ b/web/src/components/table/model-pricing/PricingSidebar.jsx @@ -0,0 +1,153 @@ +/* +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 { Divider, Button, Switch, Select, Tooltip } from '@douyinfe/semi-ui'; +import { IconHelpCircle } from '@douyinfe/semi-icons'; +import PricingCategories from './sidebar/PricingCategories.jsx'; +import PricingGroups from './sidebar/PricingGroups.jsx'; +import PricingQuotaTypes from './sidebar/PricingQuotaTypes.jsx'; + +const PricingSidebar = ({ + showWithRecharge, + setShowWithRecharge, + currency, + setCurrency, + handleChange, + setActiveKey, + showRatio, + setShowRatio, + filterGroup, + setFilterGroup, + filterQuotaType, + setFilterQuotaType, + t, + ...categoryProps +}) => { + + // 重置所有筛选条件 + const handleResetFilters = () => { + // 重置搜索 + if (handleChange) { + handleChange(''); + } + + // 重置模型分类到默认 + if (setActiveKey && categoryProps.availableCategories?.length > 0) { + setActiveKey(categoryProps.availableCategories[0]); + } + + // 重置充值价格显示 + if (setShowWithRecharge) { + setShowWithRecharge(false); + } + + // 重置货币 + if (setCurrency) { + setCurrency('USD'); + } + + // 重置显示倍率 + setShowRatio(false); + + // 重置分组筛选 + if (setFilterGroup) { + setFilterGroup('all'); + } + + // 重置计费类型筛选 + if (setFilterQuotaType) { + setFilterQuotaType('all'); + } + }; + + return ( +
+ {/* 筛选标题和重置按钮 */} +
+
+ {t('筛选')} +
+ +
+ + {/* 显示设置 */} +
+ + {t('显示设置')} + +
+
+ {t('以充值价格显示')} + +
+ {showWithRecharge && ( +
+
{t('货币单位')}
+ +
+ )} +
+
+ {t('显示倍率')} + + + +
+ +
+
+
+ + {/* 模型分类 */} + + + + + +
+ ); +}; + +export default PricingSidebar; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/ModelPricingTable.jsx b/web/src/components/table/model-pricing/PricingTable.jsx similarity index 93% rename from web/src/components/table/model-pricing/ModelPricingTable.jsx rename to web/src/components/table/model-pricing/PricingTable.jsx index 22d94f29..ae6e706c 100644 --- a/web/src/components/table/model-pricing/ModelPricingTable.jsx +++ b/web/src/components/table/model-pricing/PricingTable.jsx @@ -23,9 +23,9 @@ import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations'; -import { getModelPricingColumns } from './ModelPricingColumnDefs.js'; +import { getPricingTableColumns } from './PricingTableColumns.js'; -const ModelPricingTable = ({ +const PricingTable = ({ filteredModels, loading, rowSelection, @@ -44,10 +44,12 @@ const ModelPricingTable = ({ displayPrice, filteredValue, handleGroupClick, + showRatio, t }) => { + const columns = useMemo(() => { - return getModelPricingColumns({ + return getPricingTableColumns({ t, selectedGroup, usableGroup, @@ -61,6 +63,7 @@ const ModelPricingTable = ({ setTokenUnit, displayPrice, handleGroupClick, + showRatio, }); }, [ t, @@ -76,6 +79,7 @@ const ModelPricingTable = ({ setTokenUnit, displayPrice, handleGroupClick, + showRatio, ]); // 更新列定义中的 filteredValue @@ -121,4 +125,4 @@ const ModelPricingTable = ({ return ModelTable; }; -export default ModelPricingTable; \ No newline at end of file +export default PricingTable; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/ModelPricingColumnDefs.js b/web/src/components/table/model-pricing/PricingTableColumns.js similarity index 59% rename from web/src/components/table/model-pricing/ModelPricingColumnDefs.js rename to web/src/components/table/model-pricing/PricingTableColumns.js index bf71533c..fd234df5 100644 --- a/web/src/components/table/model-pricing/ModelPricingColumnDefs.js +++ b/web/src/components/table/model-pricing/PricingTableColumns.js @@ -76,7 +76,7 @@ function renderSupportedEndpoints(endpoints) { ); } -export const getModelPricingColumns = ({ +export const getPricingTableColumns = ({ t, selectedGroup, usableGroup, @@ -90,8 +90,9 @@ export const getModelPricingColumns = ({ setTokenUnit, displayPrice, handleGroupClick, + showRatio, }) => { - return [ + const baseColumns = [ { title: t('可用性'), dataIndex: 'available', @@ -166,96 +167,109 @@ export const getModelPricingColumns = ({ ); }, }, - { - title: () => ( -
- {t('倍率')} - - { - setModalImageUrl('/ratio.png'); - setIsModalOpenurl(true); - }} - /> - + ]; + + // 倍率列 - 只有在showRatio为true时才包含 + const ratioColumn = { + title: () => ( +
+ {t('倍率')} + + { + setModalImageUrl('/ratio.png'); + setIsModalOpenurl(true); + }} + /> + +
+ ), + dataIndex: 'model_ratio', + render: (text, record, index) => { + let content = text; + let completionRatio = parseFloat(record.completion_ratio.toFixed(3)); + content = ( +
+
+ {t('模型倍率')}:{record.quota_type === 0 ? text : t('无')} +
+
+ {t('补全倍率')}: + {record.quota_type === 0 ? completionRatio : t('无')} +
+
+ {t('分组倍率')}:{groupRatio[selectedGroup]} +
- ), - dataIndex: 'model_ratio', - render: (text, record, index) => { - let content = text; - let completionRatio = parseFloat(record.completion_ratio.toFixed(3)); + ); + return content; + }, + }; + + // 价格列 + const priceColumn = { + title: ( +
+ {t('模型价格')} + {/* 计费单位切换 */} + setTokenUnit(checked ? 'K' : 'M')} + checkedText="K" + uncheckedText="M" + /> +
+ ), + dataIndex: 'model_price', + render: (text, record, index) => { + let content = text; + if (record.quota_type === 0) { + let inputRatioPriceUSD = record.model_ratio * 2 * groupRatio[selectedGroup]; + let completionRatioPriceUSD = + record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup]; + + const unitDivisor = tokenUnit === 'K' ? 1000 : 1; + const unitLabel = tokenUnit === 'K' ? 'K' : 'M'; + + let displayInput = displayPrice(inputRatioPriceUSD); + let displayCompletion = displayPrice(completionRatioPriceUSD); + + const divisor = unitDivisor; + const numInput = parseFloat(displayInput.replace(/[^0-9.]/g, '')) / divisor; + const numCompletion = parseFloat(displayCompletion.replace(/[^0-9.]/g, '')) / divisor; + + displayInput = `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(3)}`; + displayCompletion = `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(3)}`; content = (
- {t('模型倍率')}:{record.quota_type === 0 ? text : t('无')} + {t('提示')} {displayInput} / 1{unitLabel} tokens
- {t('补全倍率')}: - {record.quota_type === 0 ? completionRatio : t('无')} -
-
- {t('分组倍率')}:{groupRatio[selectedGroup]} + {t('补全')} {displayCompletion} / 1{unitLabel} tokens
); - return content; - }, + } else { + let priceUSD = parseFloat(text) * groupRatio[selectedGroup]; + let displayVal = displayPrice(priceUSD); + content = ( +
+ {t('模型价格')}:{displayVal} +
+ ); + } + return content; }, - { - title: ( -
- {t('模型价格')} - {/* 计费单位切换 */} - setTokenUnit(checked ? 'K' : 'M')} - checkedText="K" - uncheckedText="M" - /> -
- ), - dataIndex: 'model_price', - render: (text, record, index) => { - let content = text; - if (record.quota_type === 0) { - let inputRatioPriceUSD = record.model_ratio * 2 * groupRatio[selectedGroup]; - let completionRatioPriceUSD = - record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup]; + }; - const unitDivisor = tokenUnit === 'K' ? 1000 : 1; - const unitLabel = tokenUnit === 'K' ? 'K' : 'M'; + // 根据showRatio决定是否包含倍率列 + const columns = [...baseColumns]; + if (showRatio) { + columns.push(ratioColumn); + } + columns.push(priceColumn); - let displayInput = displayPrice(inputRatioPriceUSD); - let displayCompletion = displayPrice(completionRatioPriceUSD); - - const divisor = unitDivisor; - const numInput = parseFloat(displayInput.replace(/[^0-9.]/g, '')) / divisor; - const numCompletion = parseFloat(displayCompletion.replace(/[^0-9.]/g, '')) / divisor; - - displayInput = `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(3)}`; - displayCompletion = `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(3)}`; - content = ( -
-
- {t('提示')} {displayInput} / 1{unitLabel} tokens -
-
- {t('补全')} {displayCompletion} / 1{unitLabel} tokens -
-
- ); - } else { - let priceUSD = parseFloat(text) * groupRatio[selectedGroup]; - let displayVal = displayPrice(priceUSD); - content = ( -
- {t('模型价格')}:{displayVal} -
- ); - } - return content; - }, - }, - ]; + return columns; }; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/index.jsx b/web/src/components/table/model-pricing/index.jsx index a8641ce5..d79be40c 100644 --- a/web/src/components/table/model-pricing/index.jsx +++ b/web/src/components/table/model-pricing/index.jsx @@ -17,50 +17,5 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React from 'react'; -import { Layout, Card, ImagePreview } from '@douyinfe/semi-ui'; -import ModelPricingTabs from './ModelPricingTabs.jsx'; -import ModelPricingFilters from './ModelPricingFilters.jsx'; -import ModelPricingTable from './ModelPricingTable.jsx'; -import ModelPricingHeader from './ModelPricingHeader.jsx'; -import { useModelPricingData } from '../../../hooks/model-pricing/useModelPricingData.js'; - -const ModelPricingPage = () => { - const modelPricingData = useModelPricingData(); - - return ( -
- - -
-
- {/* 主卡片容器 */} - - {/* 顶部状态卡片 */} - - - {/* 模型分类 Tabs */} -
- - - {/* 搜索和表格区域 */} - - -
- - {/* 倍率说明图预览 */} - modelPricingData.setIsModalOpenurl(visible)} - /> -
-
-
-
-
-
- ); -}; - -export default ModelPricingPage; \ No newline at end of file +// 为了向后兼容,这里重新导出新的 PricingPage 组件 +export { default } from './PricingPage.jsx'; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/sidebar/PricingCategories.jsx b/web/src/components/table/model-pricing/sidebar/PricingCategories.jsx new file mode 100644 index 00000000..65cb58c7 --- /dev/null +++ b/web/src/components/table/model-pricing/sidebar/PricingCategories.jsx @@ -0,0 +1,44 @@ +/* +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 SelectableButtonGroup from '../../../common/ui/SelectableButtonGroup.jsx'; + +const PricingCategories = ({ activeKey, setActiveKey, modelCategories, categoryCounts, availableCategories, t }) => { + const items = Object.entries(modelCategories) + .filter(([key]) => availableCategories.includes(key)) + .map(([key, category]) => ({ + value: key, + label: category.label, + icon: category.icon, + tagCount: categoryCounts[key] || 0, + })); + + return ( + + ); +}; + +export default PricingCategories; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/sidebar/PricingGroups.jsx b/web/src/components/table/model-pricing/sidebar/PricingGroups.jsx new file mode 100644 index 00000000..32643d76 --- /dev/null +++ b/web/src/components/table/model-pricing/sidebar/PricingGroups.jsx @@ -0,0 +1,58 @@ +/* +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 SelectableButtonGroup from '../../../common/ui/SelectableButtonGroup.jsx'; + +/** + * 分组筛选组件 + * @param {string} filterGroup 当前选中的分组,'all' 表示不过滤 + * @param {Function} setFilterGroup 设置选中分组 + * @param {Record} usableGroup 后端返回的可用分组对象 + * @param {Function} t i18n + */ +const PricingGroups = ({ filterGroup, setFilterGroup, usableGroup = {}, models = [], t }) => { + const groups = ['all', ...Object.keys(usableGroup)]; + + const items = groups.map((g) => { + let count = 0; + if (g === 'all') { + count = models.length; + } else { + count = models.filter(m => m.enable_groups && m.enable_groups.includes(g)).length; + } + return { + value: g, + label: g === 'all' ? t('全部分组') : g, + tagCount: count, + }; + }); + + return ( + + ); +}; + +export default PricingGroups; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/sidebar/PricingQuotaTypes.jsx b/web/src/components/table/model-pricing/sidebar/PricingQuotaTypes.jsx new file mode 100644 index 00000000..373f9f5d --- /dev/null +++ b/web/src/components/table/model-pricing/sidebar/PricingQuotaTypes.jsx @@ -0,0 +1,49 @@ +/* +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 SelectableButtonGroup from '../../../common/ui/SelectableButtonGroup.jsx'; + +/** + * 计费类型筛选组件 + * @param {string|'all'|0|1} filterQuotaType 当前值 + * @param {Function} setFilterQuotaType setter + * @param {Function} t i18n + */ +const PricingQuotaTypes = ({ filterQuotaType, setFilterQuotaType, models = [], t }) => { + const qtyCount = (type) => models.filter(m => type === 'all' ? true : m.quota_type === type).length; + + const items = [ + { value: 'all', label: t('全部类型'), tagCount: qtyCount('all') }, + { value: 0, label: t('按量计费'), tagCount: qtyCount(0) }, + { value: 1, label: t('按次计费'), tagCount: qtyCount(1) }, + ]; + + return ( + + ); +}; + +export default PricingQuotaTypes; \ No newline at end of file diff --git a/web/src/hooks/model-pricing/useModelPricingData.js b/web/src/hooks/model-pricing/useModelPricingData.js index 60445f1e..ac58d817 100644 --- a/web/src/hooks/model-pricing/useModelPricingData.js +++ b/web/src/hooks/model-pricing/useModelPricingData.js @@ -32,6 +32,10 @@ export const useModelPricingData = () => { 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 [activeKey, setActiveKey] = useState('all'); const [pageSize, setPageSize] = useState(10); const [currency, setCurrency] = useState('USD'); @@ -75,10 +79,22 @@ export const useModelPricingData = () => { const filteredModels = useMemo(() => { let result = models; + // 分类筛选 if (activeKey !== 'all') { result = result.filter(model => modelCategories[activeKey].filter(model)); } + // 分组筛选 + if (filterGroup !== 'all') { + result = result.filter(model => model.enable_groups.includes(filterGroup)); + } + + // 计费类型筛选 + if (filterQuotaType !== 'all') { + result = result.filter(model => model.quota_type === filterQuotaType); + } + + // 搜索筛选 if (filteredValue.length > 0) { const searchTerm = filteredValue[0].toLowerCase(); result = result.filter(model => @@ -87,7 +103,7 @@ export const useModelPricingData = () => { } return result; - }, [activeKey, models, filteredValue]); + }, [activeKey, models, filteredValue, filterGroup, filterQuotaType]); const rowSelection = useMemo( () => ({ @@ -184,6 +200,8 @@ export const useModelPricingData = () => { const handleGroupClick = (group) => { setSelectedGroup(group); + // 同时将分组过滤设置为该分组 + setFilterGroup(group); showInfo( t('当前查看的分组为:{{group}},倍率为:{{ratio}}', { group: group, @@ -208,6 +226,10 @@ export const useModelPricingData = () => { setIsModalOpenurl, selectedGroup, setSelectedGroup, + filterGroup, + setFilterGroup, + filterQuotaType, + setFilterQuotaType, activeKey, setActiveKey, pageSize, diff --git a/web/src/pages/Pricing/index.js b/web/src/pages/Pricing/index.js index 036e94ad..c1066203 100644 --- a/web/src/pages/Pricing/index.js +++ b/web/src/pages/Pricing/index.js @@ -21,9 +21,9 @@ import React from 'react'; import ModelPricingPage from '../../components/table/model-pricing'; const Pricing = () => ( -
+ <> -
+ ); export default Pricing;