🎨 feat(model-pricing): refactor layout and component structure (#1365)
* Re-architected model-pricing page into modular components: * PricingPage / PricingSidebar / PricingContent * Removed obsolete `ModelPricing*` components and column defs * Introduced reusable `SelectableButtonGroup` in `common/ui` * Supports Row/Col grid (3 per row) * Optional collapsible mode with gradient mask & toggle * Rebuilt filter panels with the new button-group: * Model categories, token groups, and quota types * Added dynamic `tagCount` badges to display item totals * Extended `useModelPricingData` hook * Added `filterGroup` and `filterQuotaType` state and logic * Updated PricingTable columns & sidebar reset logic to respect new states * Ensured backward compatibility via re-export in `index.jsx` * Polished styling, icons and i18n keys
This commit is contained in:
147
web/src/components/common/ui/SelectableButtonGroup.jsx
Normal file
147
web/src/components/common/ui/SelectableButtonGroup.jsx
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 = (
|
||||||
|
<Row gutter={[8, 8]} style={{ lineHeight: '32px', ...style }} ref={contentRef}>
|
||||||
|
{items.map((item) => {
|
||||||
|
const isActive = activeValue === item.value;
|
||||||
|
return (
|
||||||
|
<Col span={8} key={item.value}>
|
||||||
|
<Button
|
||||||
|
onClick={() => onChange(item.value)}
|
||||||
|
theme={isActive ? 'solid' : 'outline'}
|
||||||
|
type={isActive ? 'primary' : 'tertiary'}
|
||||||
|
icon={item.icon}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: item.tagCount !== undefined ? 4 : 0 }}>{item.label}</span>
|
||||||
|
{item.tagCount !== undefined && (
|
||||||
|
<Tag
|
||||||
|
color='white'
|
||||||
|
shape="circle"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{item.tagCount}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-8">
|
||||||
|
{title && (
|
||||||
|
<Divider margin="12px" align="left">
|
||||||
|
{title}
|
||||||
|
</Divider>
|
||||||
|
)}
|
||||||
|
{needCollapse ? (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<Collapsible isOpen={isOpen} collapseHeight={collapseHeight} style={{ ...maskStyle }}>
|
||||||
|
{contentElement}
|
||||||
|
</Collapsible>
|
||||||
|
{isOpen ? null : (
|
||||||
|
<div onClick={toggle} style={{ ...linkStyle }}>
|
||||||
|
<IconChevronDown size="small" />
|
||||||
|
<span>{t('展开更多')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isOpen && (
|
||||||
|
<div onClick={toggle} style={{
|
||||||
|
...linkStyle,
|
||||||
|
position: 'static',
|
||||||
|
marginTop: 8,
|
||||||
|
bottom: 'auto'
|
||||||
|
}}>
|
||||||
|
<IconChevronUp size="small" />
|
||||||
|
<span>{t('收起')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
contentElement
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectableButtonGroup;
|
||||||
@@ -467,7 +467,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg">
|
<header className="text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg" style={{ borderBottom: '1px solid var(--semi-color-border)' }}>
|
||||||
<NoticeModal
|
<NoticeModal
|
||||||
visible={noticeVisible}
|
visible={noticeVisible}
|
||||||
onClose={handleNoticeClose}
|
onClose={handleNoticeClose}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const PageLayout = () => {
|
|||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const shouldHideFooter = location.pathname.startsWith('/console');
|
const shouldHideFooter = location.pathname.startsWith('/console') || location.pathname === '/pricing';
|
||||||
|
|
||||||
const shouldInnerPadding = location.pathname.includes('/console') &&
|
const shouldInnerPadding = location.pathname.includes('/console') &&
|
||||||
!location.pathname.startsWith('/console/chat') &&
|
!location.pathname.startsWith('/console/chat') &&
|
||||||
|
|||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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(() => (
|
|
||||||
<Card className="!rounded-xl mb-6" bordered={false}>
|
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
|
||||||
<div className="flex-1 min-w-[200px]">
|
|
||||||
<Input
|
|
||||||
prefix={<IconSearch />}
|
|
||||||
placeholder={t('模糊搜索模型名称')}
|
|
||||||
onCompositionStart={handleCompositionStart}
|
|
||||||
onCompositionEnd={handleCompositionEnd}
|
|
||||||
onChange={handleChange}
|
|
||||||
showClear
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
theme='light'
|
|
||||||
type='primary'
|
|
||||||
icon={<IconCopy />}
|
|
||||||
onClick={() => copyText(selectedRowKeys)}
|
|
||||||
disabled={selectedRowKeys.length === 0}
|
|
||||||
className="!bg-blue-500 hover:!bg-blue-600 text-white"
|
|
||||||
>
|
|
||||||
{t('复制选中模型')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* 充值价格显示开关 */}
|
|
||||||
<Space align="center">
|
|
||||||
<span>{t('以充值价格显示')}</span>
|
|
||||||
<Switch
|
|
||||||
checked={showWithRecharge}
|
|
||||||
onChange={setShowWithRecharge}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
{showWithRecharge && (
|
|
||||||
<Select
|
|
||||||
value={currency}
|
|
||||||
onChange={setCurrency}
|
|
||||||
size="small"
|
|
||||||
style={{ width: 100 }}
|
|
||||||
>
|
|
||||||
<Select.Option value="USD">USD ($)</Select.Option>
|
|
||||||
<Select.Option value="CNY">CNY (¥)</Select.Option>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
), [selectedRowKeys, t, showWithRecharge, currency, handleCompositionStart, handleCompositionEnd, handleChange, copyText, setShowWithRecharge, setCurrency]);
|
|
||||||
|
|
||||||
return SearchAndActions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModelPricingFilters;
|
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<Tabs
|
|
||||||
activeKey={activeKey}
|
|
||||||
type="card"
|
|
||||||
collapsible
|
|
||||||
onChange={key => setActiveKey(key)}
|
|
||||||
className="mt-2"
|
|
||||||
>
|
|
||||||
{Object.entries(modelCategories)
|
|
||||||
.filter(([key]) => availableCategories.includes(key))
|
|
||||||
.map(([key, category]) => {
|
|
||||||
const modelCount = categoryCounts[key] || 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TabPane
|
|
||||||
tab={
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
{category.icon && <span className="w-4 h-4">{category.icon}</span>}
|
|
||||||
{category.label}
|
|
||||||
<Tag
|
|
||||||
color={activeKey === key ? 'red' : 'grey'}
|
|
||||||
shape='circle'
|
|
||||||
>
|
|
||||||
{modelCount}
|
|
||||||
</Tag>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
itemKey={key}
|
|
||||||
key={key}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModelPricingTabs;
|
|
||||||
52
web/src/components/table/model-pricing/PricingContent.jsx
Normal file
52
web/src/components/table/model-pricing/PricingContent.jsx
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
{/* 固定的搜索和操作区域 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '16px 24px',
|
||||||
|
borderBottom: '1px solid var(--semi-color-border)',
|
||||||
|
backgroundColor: 'var(--semi-color-bg-0)',
|
||||||
|
flexShrink: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PricingSearchBar {...props} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 可滚动的内容区域 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PricingTable {...props} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PricingContent;
|
||||||
@@ -22,7 +22,7 @@ import { Card } from '@douyinfe/semi-ui';
|
|||||||
import { IconVerify, IconLayers, IconInfoCircle } from '@douyinfe/semi-icons';
|
import { IconVerify, IconLayers, IconInfoCircle } from '@douyinfe/semi-icons';
|
||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
const ModelPricingHeader = ({
|
const PricingHeader = ({
|
||||||
userState,
|
userState,
|
||||||
groupRatio,
|
groupRatio,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
@@ -120,4 +120,4 @@ const ModelPricingHeader = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ModelPricingHeader;
|
export default PricingHeader;
|
||||||
72
web/src/components/table/model-pricing/PricingPage.jsx
Normal file
72
web/src/components/table/model-pricing/PricingPage.jsx
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="bg-white">
|
||||||
|
<Layout style={{ height: 'calc(100vh - 60px)', overflow: 'hidden', marginTop: '60px' }}>
|
||||||
|
{/* 左侧边栏 */}
|
||||||
|
<Sider
|
||||||
|
style={{
|
||||||
|
width: 460,
|
||||||
|
height: 'calc(100vh - 60px)',
|
||||||
|
backgroundColor: 'var(--semi-color-bg-0)',
|
||||||
|
borderRight: '1px solid var(--semi-color-border)',
|
||||||
|
overflow: 'auto'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PricingSidebar {...pricingData} showRatio={showRatio} setShowRatio={setShowRatio} />
|
||||||
|
</Sider>
|
||||||
|
|
||||||
|
{/* 右侧内容区 */}
|
||||||
|
<Content
|
||||||
|
style={{
|
||||||
|
height: 'calc(100vh - 60px)',
|
||||||
|
backgroundColor: 'var(--semi-color-bg-0)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PricingContent {...pricingData} showRatio={showRatio} />
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
{/* 倍率说明图预览 */}
|
||||||
|
<ImagePreview
|
||||||
|
src={pricingData.modalImageUrl}
|
||||||
|
visible={pricingData.isModalOpenurl}
|
||||||
|
onVisibleChange={(visible) => pricingData.setIsModalOpenurl(visible)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PricingPage;
|
||||||
63
web/src/components/table/model-pricing/PricingSearchBar.jsx
Normal file
63
web/src/components/table/model-pricing/PricingSearchBar.jsx
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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(() => (
|
||||||
|
<div className="flex items-center gap-4 w-full">
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
prefix={<IconSearch />}
|
||||||
|
placeholder={t('模糊搜索模型名称')}
|
||||||
|
onCompositionStart={handleCompositionStart}
|
||||||
|
onCompositionEnd={handleCompositionEnd}
|
||||||
|
onChange={handleChange}
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<Button
|
||||||
|
theme='light'
|
||||||
|
type='primary'
|
||||||
|
icon={<IconCopy />}
|
||||||
|
onClick={() => copyText(selectedRowKeys)}
|
||||||
|
disabled={selectedRowKeys.length === 0}
|
||||||
|
className="!bg-blue-500 hover:!bg-blue-600 text-white"
|
||||||
|
>
|
||||||
|
{t('复制选中模型')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
), [selectedRowKeys, t, handleCompositionStart, handleCompositionEnd, handleChange, copyText]);
|
||||||
|
|
||||||
|
return SearchAndActions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PricingSearchBar;
|
||||||
153
web/src/components/table/model-pricing/PricingSidebar.jsx
Normal file
153
web/src/components/table/model-pricing/PricingSidebar.jsx
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="p-4">
|
||||||
|
{/* 筛选标题和重置按钮 */}
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="text-lg font-semibold text-gray-800">
|
||||||
|
{t('筛选')}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
onClick={handleResetFilters}
|
||||||
|
className="text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
{t('重置')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 显示设置 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<Divider margin='12px' align='left'>
|
||||||
|
{t('显示设置')}
|
||||||
|
</Divider>
|
||||||
|
<div className="px-2">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className="text-sm text-gray-700">{t('以充值价格显示')}</span>
|
||||||
|
<Switch
|
||||||
|
checked={showWithRecharge}
|
||||||
|
onChange={setShowWithRecharge}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{showWithRecharge && (
|
||||||
|
<div className="mt-2 mb-3">
|
||||||
|
<div className="text-xs text-gray-500 mb-1">{t('货币单位')}</div>
|
||||||
|
<Select
|
||||||
|
value={currency}
|
||||||
|
onChange={setCurrency}
|
||||||
|
size="small"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Select.Option value="USD">USD ($)</Select.Option>
|
||||||
|
<Select.Option value="CNY">CNY (¥)</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-sm text-gray-700">{t('显示倍率')}</span>
|
||||||
|
<Tooltip content={t('倍率是用于系统计算不同模型的最终价格用的,如果您不理解倍率,请忽略')}>
|
||||||
|
<IconHelpCircle
|
||||||
|
size="small"
|
||||||
|
style={{ color: 'var(--semi-color-text-2)', cursor: 'help' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={showRatio}
|
||||||
|
onChange={setShowRatio}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 模型分类 */}
|
||||||
|
<PricingCategories {...categoryProps} setActiveKey={setActiveKey} t={t} />
|
||||||
|
|
||||||
|
<PricingGroups filterGroup={filterGroup} setFilterGroup={setFilterGroup} usableGroup={categoryProps.usableGroup} models={categoryProps.models} t={t} />
|
||||||
|
|
||||||
|
<PricingQuotaTypes filterQuotaType={filterQuotaType} setFilterQuotaType={setFilterQuotaType} models={categoryProps.models} t={t} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PricingSidebar;
|
||||||
@@ -23,9 +23,9 @@ import {
|
|||||||
IllustrationNoResult,
|
IllustrationNoResult,
|
||||||
IllustrationNoResultDark
|
IllustrationNoResultDark
|
||||||
} from '@douyinfe/semi-illustrations';
|
} from '@douyinfe/semi-illustrations';
|
||||||
import { getModelPricingColumns } from './ModelPricingColumnDefs.js';
|
import { getPricingTableColumns } from './PricingTableColumns.js';
|
||||||
|
|
||||||
const ModelPricingTable = ({
|
const PricingTable = ({
|
||||||
filteredModels,
|
filteredModels,
|
||||||
loading,
|
loading,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
@@ -44,10 +44,12 @@ const ModelPricingTable = ({
|
|||||||
displayPrice,
|
displayPrice,
|
||||||
filteredValue,
|
filteredValue,
|
||||||
handleGroupClick,
|
handleGroupClick,
|
||||||
|
showRatio,
|
||||||
t
|
t
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
return getModelPricingColumns({
|
return getPricingTableColumns({
|
||||||
t,
|
t,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
usableGroup,
|
usableGroup,
|
||||||
@@ -61,6 +63,7 @@ const ModelPricingTable = ({
|
|||||||
setTokenUnit,
|
setTokenUnit,
|
||||||
displayPrice,
|
displayPrice,
|
||||||
handleGroupClick,
|
handleGroupClick,
|
||||||
|
showRatio,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
t,
|
t,
|
||||||
@@ -76,6 +79,7 @@ const ModelPricingTable = ({
|
|||||||
setTokenUnit,
|
setTokenUnit,
|
||||||
displayPrice,
|
displayPrice,
|
||||||
handleGroupClick,
|
handleGroupClick,
|
||||||
|
showRatio,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 更新列定义中的 filteredValue
|
// 更新列定义中的 filteredValue
|
||||||
@@ -121,4 +125,4 @@ const ModelPricingTable = ({
|
|||||||
return ModelTable;
|
return ModelTable;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ModelPricingTable;
|
export default PricingTable;
|
||||||
@@ -76,7 +76,7 @@ function renderSupportedEndpoints(endpoints) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getModelPricingColumns = ({
|
export const getPricingTableColumns = ({
|
||||||
t,
|
t,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
usableGroup,
|
usableGroup,
|
||||||
@@ -90,8 +90,9 @@ export const getModelPricingColumns = ({
|
|||||||
setTokenUnit,
|
setTokenUnit,
|
||||||
displayPrice,
|
displayPrice,
|
||||||
handleGroupClick,
|
handleGroupClick,
|
||||||
|
showRatio,
|
||||||
}) => {
|
}) => {
|
||||||
return [
|
const baseColumns = [
|
||||||
{
|
{
|
||||||
title: t('可用性'),
|
title: t('可用性'),
|
||||||
dataIndex: 'available',
|
dataIndex: 'available',
|
||||||
@@ -166,96 +167,109 @@ export const getModelPricingColumns = ({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
title: () => (
|
|
||||||
<div className="flex items-center space-x-1">
|
// 倍率列 - 只有在showRatio为true时才包含
|
||||||
<span>{t('倍率')}</span>
|
const ratioColumn = {
|
||||||
<Tooltip content={t('倍率是为了方便换算不同价格的模型')}>
|
title: () => (
|
||||||
<IconHelpCircle
|
<div className="flex items-center space-x-1">
|
||||||
className="text-blue-500 cursor-pointer"
|
<span>{t('倍率')}</span>
|
||||||
onClick={() => {
|
<Tooltip content={t('倍率是为了方便换算不同价格的模型')}>
|
||||||
setModalImageUrl('/ratio.png');
|
<IconHelpCircle
|
||||||
setIsModalOpenurl(true);
|
className="text-blue-500 cursor-pointer"
|
||||||
}}
|
onClick={() => {
|
||||||
/>
|
setModalImageUrl('/ratio.png');
|
||||||
</Tooltip>
|
setIsModalOpenurl(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
dataIndex: 'model_ratio',
|
||||||
|
render: (text, record, index) => {
|
||||||
|
let content = text;
|
||||||
|
let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
|
||||||
|
content = (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-gray-700">
|
||||||
|
{t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-700">
|
||||||
|
{t('补全倍率')}:
|
||||||
|
{record.quota_type === 0 ? completionRatio : t('无')}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-700">
|
||||||
|
{t('分组倍率')}:{groupRatio[selectedGroup]}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
);
|
||||||
dataIndex: 'model_ratio',
|
return content;
|
||||||
render: (text, record, index) => {
|
},
|
||||||
let content = text;
|
};
|
||||||
let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
|
|
||||||
|
// 价格列
|
||||||
|
const priceColumn = {
|
||||||
|
title: (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span>{t('模型价格')}</span>
|
||||||
|
{/* 计费单位切换 */}
|
||||||
|
<Switch
|
||||||
|
checked={tokenUnit === 'K'}
|
||||||
|
onChange={(checked) => setTokenUnit(checked ? 'K' : 'M')}
|
||||||
|
checkedText="K"
|
||||||
|
uncheckedText="M"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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 = (
|
content = (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="text-gray-700">
|
<div className="text-gray-700">
|
||||||
{t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}
|
{t('提示')} {displayInput} / 1{unitLabel} tokens
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-700">
|
<div className="text-gray-700">
|
||||||
{t('补全倍率')}:
|
{t('补全')} {displayCompletion} / 1{unitLabel} tokens
|
||||||
{record.quota_type === 0 ? completionRatio : t('无')}
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-700">
|
|
||||||
{t('分组倍率')}:{groupRatio[selectedGroup]}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return content;
|
} else {
|
||||||
},
|
let priceUSD = parseFloat(text) * groupRatio[selectedGroup];
|
||||||
|
let displayVal = displayPrice(priceUSD);
|
||||||
|
content = (
|
||||||
|
<div className="text-gray-700">
|
||||||
|
{t('模型价格')}:{displayVal}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
},
|
},
|
||||||
{
|
};
|
||||||
title: (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span>{t('模型价格')}</span>
|
|
||||||
{/* 计费单位切换 */}
|
|
||||||
<Switch
|
|
||||||
checked={tokenUnit === 'K'}
|
|
||||||
onChange={(checked) => setTokenUnit(checked ? 'K' : 'M')}
|
|
||||||
checkedText="K"
|
|
||||||
uncheckedText="M"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
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;
|
// 根据showRatio决定是否包含倍率列
|
||||||
const unitLabel = tokenUnit === 'K' ? 'K' : 'M';
|
const columns = [...baseColumns];
|
||||||
|
if (showRatio) {
|
||||||
|
columns.push(ratioColumn);
|
||||||
|
}
|
||||||
|
columns.push(priceColumn);
|
||||||
|
|
||||||
let displayInput = displayPrice(inputRatioPriceUSD);
|
return columns;
|
||||||
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 = (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-gray-700">
|
|
||||||
{t('提示')} {displayInput} / 1{unitLabel} tokens
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-700">
|
|
||||||
{t('补全')} {displayCompletion} / 1{unitLabel} tokens
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let priceUSD = parseFloat(text) * groupRatio[selectedGroup];
|
|
||||||
let displayVal = displayPrice(priceUSD);
|
|
||||||
content = (
|
|
||||||
<div className="text-gray-700">
|
|
||||||
{t('模型价格')}:{displayVal}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return content;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
@@ -17,50 +17,5 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
// 为了向后兼容,这里重新导出新的 PricingPage 组件
|
||||||
import { Layout, Card, ImagePreview } from '@douyinfe/semi-ui';
|
export { default } from './PricingPage.jsx';
|
||||||
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 (
|
|
||||||
<div className="bg-gray-50">
|
|
||||||
<Layout>
|
|
||||||
<Layout.Content>
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="w-full">
|
|
||||||
{/* 主卡片容器 */}
|
|
||||||
<Card bordered={false} className="!rounded-2xl shadow-lg border-0">
|
|
||||||
{/* 顶部状态卡片 */}
|
|
||||||
<ModelPricingHeader {...modelPricingData} />
|
|
||||||
|
|
||||||
{/* 模型分类 Tabs */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<ModelPricingTabs {...modelPricingData} />
|
|
||||||
|
|
||||||
{/* 搜索和表格区域 */}
|
|
||||||
<ModelPricingFilters {...modelPricingData} />
|
|
||||||
<ModelPricingTable {...modelPricingData} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 倍率说明图预览 */}
|
|
||||||
<ImagePreview
|
|
||||||
src={modelPricingData.modalImageUrl}
|
|
||||||
visible={modelPricingData.isModalOpenurl}
|
|
||||||
onVisibleChange={(visible) => modelPricingData.setIsModalOpenurl(visible)}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout.Content>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModelPricingPage;
|
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<SelectableButtonGroup
|
||||||
|
title={t('模型分类')}
|
||||||
|
items={items}
|
||||||
|
activeValue={activeKey}
|
||||||
|
onChange={setActiveKey}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PricingCategories;
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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<string, any>} 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 (
|
||||||
|
<SelectableButtonGroup
|
||||||
|
title={t('可用令牌分组')}
|
||||||
|
items={items}
|
||||||
|
activeValue={filterGroup}
|
||||||
|
onChange={setFilterGroup}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PricingGroups;
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<SelectableButtonGroup
|
||||||
|
title={t('计费类型')}
|
||||||
|
items={items}
|
||||||
|
activeValue={filterQuotaType}
|
||||||
|
onChange={setFilterQuotaType}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PricingQuotaTypes;
|
||||||
@@ -32,6 +32,10 @@ export const useModelPricingData = () => {
|
|||||||
const [modalImageUrl, setModalImageUrl] = useState('');
|
const [modalImageUrl, setModalImageUrl] = useState('');
|
||||||
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
||||||
const [selectedGroup, setSelectedGroup] = useState('default');
|
const [selectedGroup, setSelectedGroup] = useState('default');
|
||||||
|
// 用于 Table 的可用分组筛选,“all” 表示不过滤
|
||||||
|
const [filterGroup, setFilterGroup] = useState('all');
|
||||||
|
// 计费类型筛选: 'all' | 0 | 1
|
||||||
|
const [filterQuotaType, setFilterQuotaType] = useState('all');
|
||||||
const [activeKey, setActiveKey] = useState('all');
|
const [activeKey, setActiveKey] = useState('all');
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [currency, setCurrency] = useState('USD');
|
const [currency, setCurrency] = useState('USD');
|
||||||
@@ -75,10 +79,22 @@ export const useModelPricingData = () => {
|
|||||||
const filteredModels = useMemo(() => {
|
const filteredModels = useMemo(() => {
|
||||||
let result = models;
|
let result = models;
|
||||||
|
|
||||||
|
// 分类筛选
|
||||||
if (activeKey !== 'all') {
|
if (activeKey !== 'all') {
|
||||||
result = result.filter(model => modelCategories[activeKey].filter(model));
|
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) {
|
if (filteredValue.length > 0) {
|
||||||
const searchTerm = filteredValue[0].toLowerCase();
|
const searchTerm = filteredValue[0].toLowerCase();
|
||||||
result = result.filter(model =>
|
result = result.filter(model =>
|
||||||
@@ -87,7 +103,7 @@ export const useModelPricingData = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [activeKey, models, filteredValue]);
|
}, [activeKey, models, filteredValue, filterGroup, filterQuotaType]);
|
||||||
|
|
||||||
const rowSelection = useMemo(
|
const rowSelection = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -184,6 +200,8 @@ export const useModelPricingData = () => {
|
|||||||
|
|
||||||
const handleGroupClick = (group) => {
|
const handleGroupClick = (group) => {
|
||||||
setSelectedGroup(group);
|
setSelectedGroup(group);
|
||||||
|
// 同时将分组过滤设置为该分组
|
||||||
|
setFilterGroup(group);
|
||||||
showInfo(
|
showInfo(
|
||||||
t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
|
t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
|
||||||
group: group,
|
group: group,
|
||||||
@@ -208,6 +226,10 @@ export const useModelPricingData = () => {
|
|||||||
setIsModalOpenurl,
|
setIsModalOpenurl,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
setSelectedGroup,
|
setSelectedGroup,
|
||||||
|
filterGroup,
|
||||||
|
setFilterGroup,
|
||||||
|
filterQuotaType,
|
||||||
|
setFilterQuotaType,
|
||||||
activeKey,
|
activeKey,
|
||||||
setActiveKey,
|
setActiveKey,
|
||||||
pageSize,
|
pageSize,
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import React from 'react';
|
|||||||
import ModelPricingPage from '../../components/table/model-pricing';
|
import ModelPricingPage from '../../components/table/model-pricing';
|
||||||
|
|
||||||
const Pricing = () => (
|
const Pricing = () => (
|
||||||
<div className="mt-[60px] px-2">
|
<>
|
||||||
<ModelPricingPage />
|
<ModelPricingPage />
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Pricing;
|
export default Pricing;
|
||||||
|
|||||||
Reference in New Issue
Block a user