🔧 refactor(pricing-filters): extract display settings & improve mobile layout (#1365)

* **PricingDisplaySettings.jsx**
  • Extracted display settings (recharge price, currency, ratio toggle) from PricingSidebar
  • Maintains complete styling and functionality as standalone component

* **SelectableButtonGroup.jsx**
  • Added isMobile detection with conditional Col spans
  • Mobile: `span={12}` (2 buttons per row) for better touch experience
  • Desktop: preserved responsive grid `xs={24} sm={24} md={24} lg={12} xl={8}`

* **PricingSidebar.jsx**
  • Updated imports to use new PricingDisplaySettings component
  • Simplified component structure while preserving reset logic

These changes enhance code modularity and provide optimized mobile UX for filter button groups across the pricing interface.
This commit is contained in:
t0ng7u
2025-07-23 03:14:25 +08:00
parent 902aee4e6b
commit c15e753a0a
13 changed files with 239 additions and 91 deletions

View File

@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import { useIsMobile } from '../../../hooks/common/useIsMobile';
import { Divider, Button, Tag, Row, Col, Collapsible } from '@douyinfe/semi-ui'; import { Divider, Button, Tag, Row, Col, Collapsible } from '@douyinfe/semi-ui';
import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons'; import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
@@ -44,6 +45,7 @@ const SelectableButtonGroup = ({
collapseHeight = 200 collapseHeight = 200
}) => { }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const isMobile = useIsMobile();
const perRow = 3; const perRow = 3;
const maxVisibleRows = Math.max(1, Math.floor(collapseHeight / 32)); // Approx row height 32 const maxVisibleRows = Math.max(1, Math.floor(collapseHeight / 32)); // Approx row height 32
const needCollapse = collapsible && items.length > perRow * maxVisibleRows; const needCollapse = collapsible && items.length > perRow * maxVisibleRows;
@@ -82,10 +84,16 @@ const SelectableButtonGroup = ({
{items.map((item) => { {items.map((item) => {
const isActive = activeValue === item.value; const isActive = activeValue === item.value;
return ( return (
<Col xs={24} sm={24} md={24} lg={12} xl={8} key={item.value}> <Col
{...(isMobile
? { span: 12 }
: { xs: 24, sm: 24, md: 24, lg: 12, xl: 8 }
)}
key={item.value}
>
<Button <Button
onClick={() => onChange(item.value)} onClick={() => onChange(item.value)}
theme={isActive ? 'solid' : 'outline'} theme={isActive ? 'light' : 'outline'}
type={isActive ? 'primary' : 'tertiary'} type={isActive ? 'primary' : 'tertiary'}
icon={item.icon} icon={item.icon}
style={{ width: '100%' }} style={{ width: '100%' }}

View File

@@ -18,12 +18,20 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React from 'react'; import React from 'react';
import PricingSearchBar from './PricingSearchBar.jsx'; import PricingSearchBar from './PricingSearchBar';
import PricingTable from './PricingTable.jsx'; import PricingTable from './PricingTable';
const PricingContent = (props) => { const PricingContent = ({ isMobile, sidebarProps, ...props }) => {
return ( return (
<div className="pricing-scroll-hide"> <div
className={isMobile ? "" : "pricing-scroll-hide"}
style={isMobile ? {
height: '100%',
display: 'flex',
flexDirection: 'column',
overflow: 'auto'
} : {}}
>
{/* 固定的搜索和操作区域 */} {/* 固定的搜索和操作区域 */}
<div <div
style={{ style={{
@@ -36,14 +44,15 @@ const PricingContent = (props) => {
zIndex: 5, zIndex: 5,
}} }}
> >
<PricingSearchBar {...props} /> <PricingSearchBar {...props} isMobile={isMobile} sidebarProps={sidebarProps} />
</div> </div>
{/* 可滚动的内容区域 */} {/* 可滚动的内容区域 */}
<div <div
style={{ style={{
flex: 1, flex: 1,
overflow: 'auto' overflow: 'auto',
...(isMobile && { minHeight: 0 })
}} }}
> >
<PricingTable {...props} /> <PricingTable {...props} />

View File

@@ -19,13 +19,15 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react'; import React from 'react';
import { Layout, ImagePreview } from '@douyinfe/semi-ui'; import { Layout, ImagePreview } from '@douyinfe/semi-ui';
import PricingSidebar from './PricingSidebar.jsx'; import PricingSidebar from './PricingSidebar';
import PricingContent from './PricingContent.jsx'; import PricingContent from './PricingContent';
import { useModelPricingData } from '../../../hooks/model-pricing/useModelPricingData.js'; import { useModelPricingData } from '../../../hooks/model-pricing/useModelPricingData';
import { useIsMobile } from '../../../hooks/common/useIsMobile';
const PricingPage = () => { const PricingPage = () => {
const pricingData = useModelPricingData(); const pricingData = useModelPricingData();
const { Sider, Content } = Layout; const { Sider, Content } = Layout;
const isMobile = useIsMobile();
// 显示倍率状态 // 显示倍率状态
const [showRatio, setShowRatio] = React.useState(false); const [showRatio, setShowRatio] = React.useState(false);
@@ -33,19 +35,21 @@ const PricingPage = () => {
return ( return (
<div className="bg-white"> <div className="bg-white">
<Layout style={{ height: 'calc(100vh - 60px)', overflow: 'hidden', marginTop: '60px' }}> <Layout style={{ height: 'calc(100vh - 60px)', overflow: 'hidden', marginTop: '60px' }}>
{/* 左侧边栏 */} {/* 左侧边栏 - 只在桌面端显示 */}
<Sider {!isMobile && (
className="pricing-scroll-hide" <Sider
style={{ className="pricing-scroll-hide"
width: 460, style={{
height: 'calc(100vh - 60px)', width: 460,
backgroundColor: 'var(--semi-color-bg-0)', height: 'calc(100vh - 60px)',
borderRight: '1px solid var(--semi-color-border)', backgroundColor: 'var(--semi-color-bg-0)',
overflow: 'auto' borderRight: '1px solid var(--semi-color-border)',
}} overflow: 'auto'
> }}
<PricingSidebar {...pricingData} showRatio={showRatio} setShowRatio={setShowRatio} /> >
</Sider> <PricingSidebar {...pricingData} showRatio={showRatio} setShowRatio={setShowRatio} />
</Sider>
)}
{/* 右侧内容区 */} {/* 右侧内容区 */}
<Content <Content
@@ -57,7 +61,12 @@ const PricingPage = () => {
flexDirection: 'column' flexDirection: 'column'
}} }}
> >
<PricingContent {...pricingData} showRatio={showRatio} /> <PricingContent
{...pricingData}
showRatio={showRatio}
isMobile={isMobile}
sidebarProps={{ ...pricingData, showRatio, setShowRatio }}
/>
</Content> </Content>
</Layout> </Layout>

View File

@@ -17,9 +17,10 @@ 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, { useMemo } from 'react'; import React, { useMemo, useState } from 'react';
import { Input, Button } from '@douyinfe/semi-ui'; import { Input, Button } from '@douyinfe/semi-ui';
import { IconSearch, IconCopy } from '@douyinfe/semi-icons'; import { IconSearch, IconCopy, IconFilter } from '@douyinfe/semi-icons';
import PricingFilterModal from './modal/PricingFilterModal';
const PricingSearchBar = ({ const PricingSearchBar = ({
selectedRowKeys, selectedRowKeys,
@@ -27,8 +28,12 @@ const PricingSearchBar = ({
handleChange, handleChange,
handleCompositionStart, handleCompositionStart,
handleCompositionEnd, handleCompositionEnd,
isMobile,
sidebarProps,
t t
}) => { }) => {
const [showFilterModal, setShowFilterModal] = useState(false);
const SearchAndActions = useMemo(() => ( const SearchAndActions = useMemo(() => (
<div className="flex items-center gap-4 w-full"> <div className="flex items-center gap-4 w-full">
{/* 搜索框 */} {/* 搜索框 */}
@@ -45,19 +50,45 @@ const PricingSearchBar = ({
{/* 操作按钮 */} {/* 操作按钮 */}
<Button <Button
theme='light' theme='outline'
type='primary' type='primary'
icon={<IconCopy />} icon={<IconCopy />}
onClick={() => copyText(selectedRowKeys)} onClick={() => copyText(selectedRowKeys)}
disabled={selectedRowKeys.length === 0} disabled={selectedRowKeys.length === 0}
className="!bg-blue-500 hover:!bg-blue-600 text-white" className="!bg-blue-500 hover:!bg-blue-600 text-white"
> >
{t('复制选中模型')} {t('复制')}
</Button> </Button>
</div>
), [selectedRowKeys, t, handleCompositionStart, handleCompositionEnd, handleChange, copyText]);
return SearchAndActions; {/* 移动端筛选按钮 */}
{isMobile && (
<Button
theme="outline"
type='tertiary'
icon={<IconFilter />}
onClick={() => setShowFilterModal(true)}
>
{t('筛选')}
</Button>
)}
</div>
), [selectedRowKeys, t, handleCompositionStart, handleCompositionEnd, handleChange, copyText, isMobile]);
return (
<>
{SearchAndActions}
{/* 移动端筛选Modal */}
{isMobile && (
<PricingFilterModal
visible={showFilterModal}
onClose={() => setShowFilterModal(false)}
sidebarProps={sidebarProps}
t={t}
/>
)}
</>
);
}; };
export default PricingSearchBar; export default PricingSearchBar;

View File

@@ -18,11 +18,11 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React from 'react'; import React from 'react';
import { Divider, Button, Switch, Select, Tooltip } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconHelpCircle } from '@douyinfe/semi-icons'; import PricingCategories from './filter/PricingCategories';
import PricingCategories from './sidebar/PricingCategories.jsx'; import PricingGroups from './filter/PricingGroups';
import PricingGroups from './sidebar/PricingGroups.jsx'; import PricingQuotaTypes from './filter/PricingQuotaTypes';
import PricingQuotaTypes from './sidebar/PricingQuotaTypes.jsx'; import PricingDisplaySettings from './filter/PricingDisplaySettings';
const PricingSidebar = ({ const PricingSidebar = ({
showWithRecharge, showWithRecharge,
@@ -79,13 +79,13 @@ const PricingSidebar = ({
return ( return (
<div className="p-4"> <div className="p-4">
{/* 筛选标题和重置按钮 */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="text-lg font-semibold text-gray-800"> <div className="text-lg font-semibold text-gray-800">
{t('筛选')} {t('筛选')}
</div> </div>
<Button <Button
theme="outline" theme="outline"
type='tertiary'
onClick={handleResetFilters} onClick={handleResetFilters}
className="text-gray-500 hover:text-gray-700" className="text-gray-500 hover:text-gray-700"
> >
@@ -93,54 +93,16 @@ const PricingSidebar = ({
</Button> </Button>
</div> </div>
{/* 显示设置 */} <PricingDisplaySettings
<div className="mb-6"> showWithRecharge={showWithRecharge}
<Divider margin='12px' align='left'> setShowWithRecharge={setShowWithRecharge}
{t('显示设置')} currency={currency}
</Divider> setCurrency={setCurrency}
<div className="px-2"> showRatio={showRatio}
<div className="flex items-center justify-between mb-3"> setShowRatio={setShowRatio}
<span className="text-sm text-gray-700">{t('以充值价格显示')}</span> t={t}
<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} /> <PricingCategories {...categoryProps} setActiveKey={setActiveKey} t={t} />
<PricingGroups filterGroup={filterGroup} setFilterGroup={setFilterGroup} usableGroup={categoryProps.usableGroup} models={categoryProps.models} t={t} /> <PricingGroups filterGroup={filterGroup} setFilterGroup={setFilterGroup} usableGroup={categoryProps.usableGroup} models={categoryProps.models} t={t} />

View File

@@ -23,7 +23,7 @@ import {
IllustrationNoResult, IllustrationNoResult,
IllustrationNoResultDark IllustrationNoResultDark
} from '@douyinfe/semi-illustrations'; } from '@douyinfe/semi-illustrations';
import { getPricingTableColumns } from './PricingTableColumns.js'; import { getPricingTableColumns } from './PricingTableColumns';
const PricingTable = ({ const PricingTable = ({
filteredModels, filteredModels,

View File

@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React from 'react'; import React from 'react';
import SelectableButtonGroup from '../../../common/ui/SelectableButtonGroup.jsx'; import SelectableButtonGroup from '../../../common/ui/SelectableButtonGroup';
const PricingCategories = ({ activeKey, setActiveKey, modelCategories, categoryCounts, availableCategories, t }) => { const PricingCategories = ({ activeKey, setActiveKey, modelCategories, categoryCounts, availableCategories, t }) => {
const items = Object.entries(modelCategories) const items = Object.entries(modelCategories)

View File

@@ -0,0 +1,82 @@
/*
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, Switch, Select, Tooltip } from '@douyinfe/semi-ui';
import { IconHelpCircle } from '@douyinfe/semi-icons';
const PricingDisplaySettings = ({
showWithRecharge,
setShowWithRecharge,
currency,
setCurrency,
showRatio,
setShowRatio,
t
}) => {
return (
<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>
);
};
export default PricingDisplaySettings;

View File

@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React from 'react'; import React from 'react';
import SelectableButtonGroup from '../../../common/ui/SelectableButtonGroup.jsx'; import SelectableButtonGroup from '../../../common/ui/SelectableButtonGroup';
/** /**
* 分组筛选组件 * 分组筛选组件

View File

@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React from 'react'; import React from 'react';
import SelectableButtonGroup from '../../../common/ui/SelectableButtonGroup.jsx'; import SelectableButtonGroup from '../../../common/ui/SelectableButtonGroup';
/** /**
* 计费类型筛选组件 * 计费类型筛选组件

View File

@@ -18,4 +18,4 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
// 为了向后兼容,这里重新导出新的 PricingPage 组件 // 为了向后兼容,这里重新导出新的 PricingPage 组件
export { default } from './PricingPage.jsx'; export { default } from './PricingPage';

View File

@@ -0,0 +1,48 @@
/*
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 { Modal } from '@douyinfe/semi-ui';
import PricingSidebar from '../PricingSidebar';
const PricingFilterModal = ({
visible,
onClose,
sidebarProps,
t
}) => {
return (
<Modal
title={t('筛选')}
visible={visible}
onCancel={onClose}
footer={null}
style={{ width: '100%', height: '100%', margin: 0 }}
bodyStyle={{
padding: 0,
height: 'calc(100vh - 110px)',
overflow: 'auto'
}}
>
<PricingSidebar {...sidebarProps} />
</Modal>
);
};
export default PricingFilterModal;

View File

@@ -699,7 +699,6 @@
"个": "indivual", "个": "indivual",
"倍率是本站的计算方式,不同模型有着不同的倍率,并非官方价格的多少倍,请务必知晓。": "The magnification is the calculation method of this website. Different models have different magnifications, which are not multiples of the official price. Please be sure to know.", "倍率是本站的计算方式,不同模型有着不同的倍率,并非官方价格的多少倍,请务必知晓。": "The magnification is the calculation method of this website. Different models have different magnifications, which are not multiples of the official price. Please be sure to know.",
"所有各厂聊天模型请统一使用OpenAI方式请求支持OpenAI官方库<br/>Claude()Claude官方格式请求": "Please use the OpenAI method to request all chat models from each factory, and support the OpenAI official library<br/>Claude()Claude official format request", "所有各厂聊天模型请统一使用OpenAI方式请求支持OpenAI官方库<br/>Claude()Claude官方格式请求": "Please use the OpenAI method to request all chat models from each factory, and support the OpenAI official library<br/>Claude()Claude official format request",
"复制选中模型": "Copy selected model",
"分组说明": "Group description", "分组说明": "Group description",
"倍率是为了方便换算不同价格的模型": "The magnification is to facilitate the conversion of models with different prices.", "倍率是为了方便换算不同价格的模型": "The magnification is to facilitate the conversion of models with different prices.",
"点击查看倍率说明": "Click to view the magnification description", "点击查看倍率说明": "Click to view the magnification description",