Browse Source

🔧 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.
t0ng7u 10 tháng trước cách đây
mục cha
commit
c15e753a0a

+ 10 - 2
web/src/components/common/ui/SelectableButtonGroup.jsx

@@ -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%' }}

+ 15 - 6
web/src/components/table/model-pricing/PricingContent.jsx

@@ -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} />

+ 26 - 17
web/src/components/table/model-pricing/PricingPage.jsx

@@ -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>
 

+ 37 - 6
web/src/components/table/model-pricing/PricingSearchBar.jsx

@@ -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>
+
+      {/* 移动端筛选按钮 */}
+      {isMobile && (
+        <Button
+          theme="outline"
+          type='tertiary'
+          icon={<IconFilter />}
+          onClick={() => setShowFilterModal(true)}
+        >
+          {t('筛选')}
+        </Button>
+      )}
     </div>
-  ), [selectedRowKeys, t, handleCompositionStart, handleCompositionEnd, handleChange, copyText]);
+  ), [selectedRowKeys, t, handleCompositionStart, handleCompositionEnd, handleChange, copyText, isMobile]);
 
-  return SearchAndActions;
+  return (
+    <>
+      {SearchAndActions}
+
+      {/* 移动端筛选Modal */}
+      {isMobile && (
+        <PricingFilterModal
+          visible={showFilterModal}
+          onClose={() => setShowFilterModal(false)}
+          sidebarProps={sidebarProps}
+          t={t}
+        />
+      )}
+    </>
+  );
 };
 
 export default PricingSearchBar; 

+ 15 - 53
web/src/components/table/model-pricing/PricingSidebar.jsx

@@ -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} />

+ 1 - 1
web/src/components/table/model-pricing/PricingTable.jsx

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

+ 1 - 1
web/src/components/table/model-pricing/sidebar/PricingCategories.jsx → web/src/components/table/model-pricing/filter/PricingCategories.jsx

@@ -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)

+ 82 - 0
web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx

@@ -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; 

+ 1 - 1
web/src/components/table/model-pricing/sidebar/PricingGroups.jsx → web/src/components/table/model-pricing/filter/PricingGroups.jsx

@@ -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';
 
 /**
  * 分组筛选组件

+ 1 - 1
web/src/components/table/model-pricing/sidebar/PricingQuotaTypes.jsx → web/src/components/table/model-pricing/filter/PricingQuotaTypes.jsx

@@ -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';
 
 /**
  * 计费类型筛选组件

+ 1 - 1
web/src/components/table/model-pricing/index.jsx

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

+ 48 - 0
web/src/components/table/model-pricing/modal/PricingFilterModal.jsx

@@ -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; 

+ 0 - 1
web/src/i18n/locales/en.json

@@ -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",