diff --git a/web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx b/web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx index d9f22d95..c60f0ef2 100644 --- a/web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx +++ b/web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx @@ -55,15 +55,7 @@ const PricingEndpointTypes = ({ filterEndpointType, setFilterEndpointType, model // 端点类型显示名称映射 const getEndpointTypeLabel = (endpointType) => { - const labelMap = { - 'openai': 'OpenAI', - 'openai-response': 'OpenAI Response', - 'anthropic': 'Anthropic', - 'gemini': 'Gemini', - 'jina-rerank': 'Jina Rerank', - 'image-generation': t('图像生成'), - }; - return labelMap[endpointType] || endpointType; + return endpointType; }; const availableEndpointTypes = getAllEndpointTypes(); diff --git a/web/src/components/table/model-pricing/layout/PricingPage.jsx b/web/src/components/table/model-pricing/layout/PricingPage.jsx index 5db359b3..76c31e81 100644 --- a/web/src/components/table/model-pricing/layout/PricingPage.jsx +++ b/web/src/components/table/model-pricing/layout/PricingPage.jsx @@ -21,6 +21,7 @@ import React from 'react'; import { Layout, ImagePreview } from '@douyinfe/semi-ui'; import PricingSidebar from './PricingSidebar'; import PricingContent from './content/PricingContent'; +import ModelDetailSideSheet from '../modal/ModelDetailSideSheet'; import { useModelPricingData } from '../../../../hooks/model-pricing/useModelPricingData'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; @@ -66,6 +67,20 @@ const PricingPage = () => { visible={pricingData.isModalOpenurl} onVisibleChange={(visible) => pricingData.setIsModalOpenurl(visible)} /> + + ); }; diff --git a/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx b/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx new file mode 100644 index 00000000..6723e2f7 --- /dev/null +++ b/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx @@ -0,0 +1,103 @@ +/* +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 { + SideSheet, + Typography, + Button, +} from '@douyinfe/semi-ui'; +import { + IconClose, +} from '@douyinfe/semi-icons'; + +import { useIsMobile } from '../../../../hooks/common/useIsMobile'; +import ModelHeader from './components/ModelHeader'; +import ModelBasicInfo from './components/ModelBasicInfo'; +import ModelEndpoints from './components/ModelEndpoints'; +import ModelPricingTable from './components/ModelPricingTable'; + +const { Text } = Typography; + +const ModelDetailSideSheet = ({ + visible, + onClose, + modelData, + selectedGroup, + groupRatio, + currency, + tokenUnit, + displayPrice, + showRatio, + usableGroup, + t, +}) => { + const isMobile = useIsMobile(); + + return ( + } + bodyStyle={{ + padding: '0', + display: 'flex', + flexDirection: 'column', + borderBottom: '1px solid var(--semi-color-border)' + }} + visible={visible} + width={isMobile ? '100%' : 600} + closeIcon={ + - - + ); return ( @@ -107,50 +68,7 @@ const PricingFilterModal = ({ msOverflowStyle: 'none' }} > -
- - - - - - - - - -
+ ); }; diff --git a/web/src/components/table/model-pricing/modal/components/FilterModalContent.jsx b/web/src/components/table/model-pricing/modal/components/FilterModalContent.jsx new file mode 100644 index 00000000..aa9646fe --- /dev/null +++ b/web/src/components/table/model-pricing/modal/components/FilterModalContent.jsx @@ -0,0 +1,99 @@ +/* +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 PricingDisplaySettings from '../../filter/PricingDisplaySettings'; +import PricingCategories from '../../filter/PricingCategories'; +import PricingGroups from '../../filter/PricingGroups'; +import PricingQuotaTypes from '../../filter/PricingQuotaTypes'; +import PricingEndpointTypes from '../../filter/PricingEndpointTypes'; + +const FilterModalContent = ({ sidebarProps, t }) => { + const { + showWithRecharge, + setShowWithRecharge, + currency, + setCurrency, + handleChange, + setActiveKey, + showRatio, + setShowRatio, + viewMode, + setViewMode, + filterGroup, + setFilterGroup, + filterQuotaType, + setFilterQuotaType, + filterEndpointType, + setFilterEndpointType, + tokenUnit, + setTokenUnit, + loading, + ...categoryProps + } = sidebarProps; + + return ( +
+ + + + + + + + + +
+ ); +}; + +export default FilterModalContent; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/modal/components/FilterModalFooter.jsx b/web/src/components/table/model-pricing/modal/components/FilterModalFooter.jsx new file mode 100644 index 00000000..37607417 --- /dev/null +++ b/web/src/components/table/model-pricing/modal/components/FilterModalFooter.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 { Button } from '@douyinfe/semi-ui'; + +const FilterModalFooter = ({ onReset, onConfirm, t }) => { + return ( +
+ + +
+ ); +}; + +export default FilterModalFooter; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/modal/components/ModelBasicInfo.jsx b/web/src/components/table/model-pricing/modal/components/ModelBasicInfo.jsx new file mode 100644 index 00000000..662b5616 --- /dev/null +++ b/web/src/components/table/model-pricing/modal/components/ModelBasicInfo.jsx @@ -0,0 +1,55 @@ +/* +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, Avatar, Typography } from '@douyinfe/semi-ui'; +import { IconInfoCircle } from '@douyinfe/semi-icons'; + +const { Text } = Typography; + +const ModelBasicInfo = ({ modelData, t }) => { + // 获取模型描述 + const getModelDescription = () => { + if (!modelData) return t('暂无模型描述'); + // 这里可以根据模型名称返回不同的描述 + if (modelData.model_name?.includes('gpt-4o-image')) { + return t('逆向plus账号的可绘图的gpt-4o模型,由于OpenAI限制绘图数量,并非每次都能绘图成功,由于是逆向模型,只要请求成功,即使绘图失败也会扣费,请谨慎使用。推荐使用正式版的 gpt-image-1模型。'); + } + return modelData.description || t('暂无模型描述'); + }; + + return ( + +
+ + + +
+ {t('基本信息')} +
{t('模型的详细描述和基本特性')}
+
+
+
+

{getModelDescription()}

+
+
+ ); +}; + +export default ModelBasicInfo; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/modal/components/ModelEndpoints.jsx b/web/src/components/table/model-pricing/modal/components/ModelEndpoints.jsx new file mode 100644 index 00000000..31033cab --- /dev/null +++ b/web/src/components/table/model-pricing/modal/components/ModelEndpoints.jsx @@ -0,0 +1,69 @@ +/* +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, Avatar, Typography, Badge } from '@douyinfe/semi-ui'; +import { IconLink } from '@douyinfe/semi-icons'; + +const { Text } = Typography; + +const ModelEndpoints = ({ modelData, t }) => { + const renderAPIEndpoints = () => { + const endpoints = []; + + if (modelData?.supported_endpoint_types) { + modelData.supported_endpoint_types.forEach(endpoint => { + endpoints.push({ name: endpoint, type: endpoint }); + }); + } + + return endpoints.map((endpoint, index) => ( +
+ + + {endpoint.name}: + https://api.newapi.pro + /v1/chat/completions + + POST +
+ )); + }; + + return ( + +
+ + + +
+ {t('API端点')} +
{t('模型支持的接口端点信息')}
+
+
+ {renderAPIEndpoints()} +
+ ); +}; + +export default ModelEndpoints; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/modal/components/ModelHeader.jsx b/web/src/components/table/model-pricing/modal/components/ModelHeader.jsx new file mode 100644 index 00000000..23ae179c --- /dev/null +++ b/web/src/components/table/model-pricing/modal/components/ModelHeader.jsx @@ -0,0 +1,136 @@ +/* +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 { Tag, Typography, Toast, Avatar } from '@douyinfe/semi-ui'; +import { getModelCategories } from '../../../../../helpers'; + +const { Paragraph } = Typography; + +const CARD_STYLES = { + container: "w-12 h-12 rounded-2xl flex items-center justify-center relative shadow-md", + icon: "w-8 h-8 flex items-center justify-center", +}; + +const ModelHeader = ({ modelData, t }) => { + // 获取模型图标 + const getModelIcon = (modelName) => { + // 如果没有模型名称,直接返回默认头像 + if (!modelName) { + return ( +
+ + AI + +
+ ); + } + + 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 })} +
+
+ ); + } + + const avatarText = modelName?.slice(0, 2).toUpperCase() || 'AI'; + return ( +
+ + {avatarText} + +
+ ); + }; + + // 获取模型标签 + const getModelTags = () => { + const tags = [ + { text: t('文本对话'), color: 'green' }, + { text: t('图片生成'), color: 'blue' }, + { text: t('图像分析'), color: 'cyan' } + ]; + + return tags; + }; + + return ( +
+ {getModelIcon(modelData?.model_name)} +
+ Toast.success({ content: t('已复制模型名称') }) + }} + > + {modelData?.model_name || t('未知模型')} + +
+ {getModelTags().map((tag, index) => ( + + {tag.text} + + ))} +
+
+
+ ); +}; + +export default ModelHeader; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx b/web/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx new file mode 100644 index 00000000..3d8d84be --- /dev/null +++ b/web/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx @@ -0,0 +1,190 @@ +/* +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, Avatar, Typography, Table, Tag, Tooltip } from '@douyinfe/semi-ui'; +import { IconCoinMoneyStroked } from '@douyinfe/semi-icons'; +import { calculateModelPrice } from '../../../../../helpers'; + +const { Text } = Typography; + +const ModelPricingTable = ({ + modelData, + selectedGroup, + groupRatio, + currency, + tokenUnit, + displayPrice, + showRatio, + usableGroup, + t, +}) => { + // 获取分组介绍 + const getGroupDescription = (groupName) => { + const descriptions = { + 'default': t('默认分组,适用于普通用户'), + 'ssvip': t('超级VIP分组,享受最优惠价格'), + 'openai官-优质': t('OpenAI官方优质分组,最快最稳,支持o1、realtime等'), + 'origin': t('企业分组,OpenAI&Claude官方原价,不升价本分组稳定性可用性'), + 'vip': t('VIP分组,享受优惠价格'), + 'premium': t('高级分组,稳定可靠'), + 'enterprise': t('企业级分组,专业服务'), + }; + return descriptions[groupName] || t('用户分组'); + }; + + const renderGroupPriceTable = () => { + const availableGroups = Object.keys(usableGroup || {}).filter(g => g !== ''); + if (availableGroups.length === 0) { + availableGroups.push('default'); + } + + // 准备表格数据 + const tableData = availableGroups.map(group => { + const priceData = modelData ? calculateModelPrice({ + record: modelData, + selectedGroup: group, + groupRatio, + tokenUnit, + displayPrice, + currency + }) : { inputPrice: '-', outputPrice: '-', price: '-' }; + + // 获取分组倍率 + const groupRatioValue = groupRatio && groupRatio[group] ? groupRatio[group] : 1; + + return { + key: group, + group: group, + description: getGroupDescription(group), + ratio: groupRatioValue, + billingType: modelData?.quota_type === 0 ? t('按量计费') : t('按次计费'), + inputPrice: modelData?.quota_type === 0 ? priceData.inputPrice : '-', + outputPrice: modelData?.quota_type === 0 ? (priceData.completionPrice || priceData.outputPrice) : '-', + fixedPrice: modelData?.quota_type === 1 ? priceData.price : '-', + }; + }); + + // 定义表格列 + const columns = [ + { + title: t('分组'), + dataIndex: 'group', + render: (text, record) => ( + + + {text}{t('分组')} + + + ), + }, + ]; + + // 如果显示倍率,添加倍率列 + if (showRatio) { + columns.push({ + title: t('倍率'), + dataIndex: 'ratio', + render: (text) => ( + + {text}x + + ), + }); + } + + // 添加计费类型列 + columns.push({ + title: t('计费类型'), + dataIndex: 'billingType', + render: (text) => ( + + {text} + + ), + }); + + // 根据计费类型添加价格列 + if (modelData?.quota_type === 0) { + // 按量计费 + columns.push( + { + title: t('提示'), + dataIndex: 'inputPrice', + render: (text) => ( + <> +
{text}
+
/ {tokenUnit === 'K' ? '1K' : '1M'} tokens
+ + ), + }, + { + title: t('补全'), + dataIndex: 'outputPrice', + render: (text) => ( + <> +
{text}
+
/ {tokenUnit === 'K' ? '1K' : '1M'} tokens
+ + ), + } + ); + } else { + // 按次计费 + columns.push({ + title: t('价格'), + dataIndex: 'fixedPrice', + render: (text) => ( + <> +
{text}
+
/ 次
+ + ), + }); + } + + return ( + + ); + }; + + return ( + +
+ + + +
+ {t('分组价格')} +
{t('不同用户分组的价格信息')}
+
+
+ {renderGroupPriceTable()} +
+ ); +}; + +export default ModelPricingTable; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/view/card/PricingCardView.jsx b/web/src/components/table/model-pricing/view/card/PricingCardView.jsx index e107df79..9d0fbf48 100644 --- a/web/src/components/table/model-pricing/view/card/PricingCardView.jsx +++ b/web/src/components/table/model-pricing/view/card/PricingCardView.jsx @@ -54,6 +54,7 @@ const PricingCardView = ({ setSelectedRowKeys, activeKey, availableCategories, + openModelDetail, }) => { const showSkeleton = useMinimumLoadingTime(loading); @@ -138,7 +139,7 @@ const PricingCardView = ({ const renderTags = (record) => { const tags = []; - // 计费类型标签 + // 计费类型标签 const billingType = record.quota_type === 1 ? 'teal' : 'violet'; const billingText = record.quota_type === 1 ? t('按次计费') : t('按量计费'); tags.push( @@ -211,9 +212,10 @@ const PricingCardView = ({ return ( openModelDetail && openModelDetail(model)} > {/* 头部:图标 + 模型名称 + 操作按钮 */}
@@ -235,14 +237,20 @@ const PricingCardView = ({ size="small" type="tertiary" icon={} - onClick={() => copyText(model.model_name)} + onClick={(e) => { + e.stopPropagation(); + copyText(model.model_name); + }} /> {/* 选择框 */} {rowSelection && ( handleCheckboxChange(model, e.target.checked)} + onChange={(e) => { + e.stopPropagation(); + handleCheckboxChange(model, e.target.checked); + }} /> )}
@@ -265,14 +273,18 @@ const PricingCardView = ({ {/* 倍率信息(可选) */} {showRatio && ( -
+
{t('倍率信息')} { + onClick={(e) => { + e.stopPropagation(); setModalImageUrl('/ratio.png'); setIsModalOpenurl(true); }} diff --git a/web/src/components/table/model-pricing/view/table/PricingTable.jsx b/web/src/components/table/model-pricing/view/table/PricingTable.jsx index 09d9f53e..e65b63ea 100644 --- a/web/src/components/table/model-pricing/view/table/PricingTable.jsx +++ b/web/src/components/table/model-pricing/view/table/PricingTable.jsx @@ -43,6 +43,7 @@ const PricingTable = ({ searchValue, showRatio, compactMode = false, + openModelDetail, t }) => { @@ -100,6 +101,10 @@ const PricingTable = ({ rowSelection={rowSelection} className="custom-table" scroll={compactMode ? undefined : { x: 'max-content' }} + onRow={(record) => ({ + onClick: () => openModelDetail && openModelDetail(record), + style: { cursor: 'pointer' } + })} empty={ } @@ -117,7 +122,7 @@ const PricingTable = ({ }} /> - ), [filteredModels, loading, processedColumns, rowSelection, pageSize, setPageSize, t, compactMode]); + ), [filteredModels, loading, processedColumns, rowSelection, pageSize, setPageSize, openModelDetail, t, compactMode]); return ModelTable; }; diff --git a/web/src/hooks/model-pricing/useModelPricingData.js b/web/src/hooks/model-pricing/useModelPricingData.js index 3e3c4a92..98a8e566 100644 --- a/web/src/hooks/model-pricing/useModelPricingData.js +++ b/web/src/hooks/model-pricing/useModelPricingData.js @@ -32,6 +32,8 @@ export const useModelPricingData = () => { const [modalImageUrl, setModalImageUrl] = useState(''); const [isModalOpenurl, setIsModalOpenurl] = useState(false); const [selectedGroup, setSelectedGroup] = useState('default'); + const [showModelDetail, setShowModelDetail] = useState(false); + const [selectedModel, setSelectedModel] = useState(null); const [filterGroup, setFilterGroup] = useState('all'); // 用于 Table 的可用分组筛选,“all” 表示不过滤 const [filterQuotaType, setFilterQuotaType] = useState('all'); // 计费类型筛选: 'all' | 0 | 1 const [activeKey, setActiveKey] = useState('all'); @@ -219,6 +221,16 @@ export const useModelPricingData = () => { ); }; + const openModelDetail = (model) => { + setSelectedModel(model); + setShowModelDetail(true); + }; + + const closeModelDetail = () => { + setShowModelDetail(false); + setSelectedModel(null); + }; + useEffect(() => { refresh().then(); }, []); @@ -240,6 +252,10 @@ export const useModelPricingData = () => { setIsModalOpenurl, selectedGroup, setSelectedGroup, + showModelDetail, + setShowModelDetail, + selectedModel, + setSelectedModel, filterGroup, setFilterGroup, filterQuotaType, @@ -284,6 +300,8 @@ export const useModelPricingData = () => { handleCompositionStart, handleCompositionEnd, handleGroupClick, + openModelDetail, + closeModelDetail, // 引用 compositionRef,