🔧 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 { useIsMobile } from '../../../hooks/common/useIsMobile';
import { Divider, Button, Tag, Row, Col, Collapsible } from '@douyinfe/semi-ui';
import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
@@ -44,6 +45,7 @@ const SelectableButtonGroup = ({
collapseHeight = 200
}) => {
const [isOpen, setIsOpen] = useState(false);
const isMobile = useIsMobile();
const perRow = 3;
const maxVisibleRows = Math.max(1, Math.floor(collapseHeight / 32)); // Approx row height 32
const needCollapse = collapsible && items.length > perRow * maxVisibleRows;
@@ -82,10 +84,16 @@ const SelectableButtonGroup = ({
{items.map((item) => {
const isActive = activeValue === item.value;
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
onClick={() => onChange(item.value)}
theme={isActive ? 'solid' : 'outline'}
theme={isActive ? 'light' : 'outline'}
type={isActive ? 'primary' : 'tertiary'}
icon={item.icon}
style={{ width: '100%' }}

View File

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

View File

@@ -19,13 +19,15 @@ 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';
import PricingSidebar from './PricingSidebar';
import PricingContent from './PricingContent';
import { useModelPricingData } from '../../../hooks/model-pricing/useModelPricingData';
import { useIsMobile } from '../../../hooks/common/useIsMobile';
const PricingPage = () => {
const pricingData = useModelPricingData();
const { Sider, Content } = Layout;
const isMobile = useIsMobile();
// 显示倍率状态
const [showRatio, setShowRatio] = React.useState(false);
@@ -33,19 +35,21 @@ const PricingPage = () => {
return (
<div className="bg-white">
<Layout style={{ height: 'calc(100vh - 60px)', overflow: 'hidden', marginTop: '60px' }}>
{/* 左侧边栏 */}
<Sider
className="pricing-scroll-hide"
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>
{/* 左侧边栏 - 只在桌面端显示 */}
{!isMobile && (
<Sider
className="pricing-scroll-hide"
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
@@ -57,7 +61,12 @@ const PricingPage = () => {
flexDirection: 'column'
}}
>
<PricingContent {...pricingData} showRatio={showRatio} />
<PricingContent
{...pricingData}
showRatio={showRatio}
isMobile={isMobile}
sidebarProps={{ ...pricingData, showRatio, setShowRatio }}
/>
</Content>
</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
*/
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
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 = ({
selectedRowKeys,
@@ -27,8 +28,12 @@ const PricingSearchBar = ({
handleChange,
handleCompositionStart,
handleCompositionEnd,
isMobile,
sidebarProps,
t
}) => {
const [showFilterModal, setShowFilterModal] = useState(false);
const SearchAndActions = useMemo(() => (
<div className="flex items-center gap-4 w-full">
{/* 搜索框 */}
@@ -45,19 +50,45 @@ const PricingSearchBar = ({
{/* 操作按钮 */}
<Button
theme='light'
theme='outline'
type='primary'
icon={<IconCopy />}
onClick={() => copyText(selectedRowKeys)}
disabled={selectedRowKeys.length === 0}
className="!bg-blue-500 hover:!bg-blue-600 text-white"
>
{t('复制选中模型')}
{t('复制')}
</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;

View File

@@ -18,11 +18,11 @@ 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';
import { Button } from '@douyinfe/semi-ui';
import PricingCategories from './filter/PricingCategories';
import PricingGroups from './filter/PricingGroups';
import PricingQuotaTypes from './filter/PricingQuotaTypes';
import PricingDisplaySettings from './filter/PricingDisplaySettings';
const PricingSidebar = ({
showWithRecharge,
@@ -79,13 +79,13 @@ const PricingSidebar = ({
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"
type='tertiary'
onClick={handleResetFilters}
className="text-gray-500 hover:text-gray-700"
>
@@ -93,54 +93,16 @@ const PricingSidebar = ({
</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>
<PricingDisplaySettings
showWithRecharge={showWithRecharge}
setShowWithRecharge={setShowWithRecharge}
currency={currency}
setCurrency={setCurrency}
showRatio={showRatio}
setShowRatio={setShowRatio}
t={t}
/>
{/* 模型分类 */}
<PricingCategories {...categoryProps} setActiveKey={setActiveKey} t={t} />
<PricingGroups filterGroup={filterGroup} setFilterGroup={setFilterGroup} usableGroup={categoryProps.usableGroup} models={categoryProps.models} t={t} />

View File

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

View File

@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/
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 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 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 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 组件
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",
"倍率是本站的计算方式,不同模型有着不同的倍率,并非官方价格的多少倍,请务必知晓。": "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",
"复制选中模型": "Copy selected model",
"分组说明": "Group description",
"倍率是为了方便换算不同价格的模型": "The magnification is to facilitate the conversion of models with different prices.",
"点击查看倍率说明": "Click to view the magnification description",