Преглед изворни кода

✨ feat(ui): enhance pricing table & filters with responsive button-group, fixed column, scroll tweaks (#1365)

• SelectableButtonGroup
  • Added optional collapsible support with gradient mask & toggle
  • Dynamic tagCount badge support for groups / quota types
  • Switched to responsive Row/Col (`xs 24`, `sm 24`, `lg 12`, `xl 8`) for fluid layout
  • Shows expand button only when item count exceeds visible rows

• Sidebar filters
  • PricingGroups & PricingQuotaTypes now pass tag counts to button-group
  • Counts derived from current models & quota_type

• PricingTableColumns
  • Moved “Availability” column to far right; fixed via `fixed: 'right'`
  • Re-ordered columns and preserved ratio / price logic

• PricingTable
  • Added `compactMode` prop; strips fixed columns and sets `scroll={compactMode ? undefined : { x: 'max-content' }}`
  • Processes columns to remove `fixed` in compact mode

• PricingPage & index.css
  • Added `.pricing-scroll-hide` utility to hide Y-axis scrollbar for `Sider` & `Content`

• Responsive / style refinements
  • Sidebar width adjusted to 460px
  • Scrollbars hidden uniformly across pricing modules

These changes complete the model-pricing UI refactor, ensuring clean scrolling, responsive filters, and fixed availability column for better usability.
t0ng7u пре 7 месеци
родитељ
комит
b964f755ec

+ 1 - 1
web/src/components/common/ui/SelectableButtonGroup.jsx

@@ -82,7 +82,7 @@ const SelectableButtonGroup = ({
       {items.map((item) => {
       {items.map((item) => {
         const isActive = activeValue === item.value;
         const isActive = activeValue === item.value;
         return (
         return (
-          <Col span={8} key={item.value}>
+          <Col 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 ? 'solid' : 'outline'}

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

@@ -23,7 +23,7 @@ import PricingTable from './PricingTable.jsx';
 
 
 const PricingContent = (props) => {
 const PricingContent = (props) => {
   return (
   return (
-    <>
+    <div className="pricing-scroll-hide">
       {/* 固定的搜索和操作区域 */}
       {/* 固定的搜索和操作区域 */}
       <div
       <div
         style={{
         style={{
@@ -45,7 +45,7 @@ const PricingContent = (props) => {
       >
       >
         <PricingTable {...props} />
         <PricingTable {...props} />
       </div>
       </div>
-    </>
+    </div>
   );
   );
 };
 };
 
 

+ 2 - 0
web/src/components/table/model-pricing/PricingPage.jsx

@@ -35,6 +35,7 @@ const PricingPage = () => {
       <Layout style={{ height: 'calc(100vh - 60px)', overflow: 'hidden', marginTop: '60px' }}>
       <Layout style={{ height: 'calc(100vh - 60px)', overflow: 'hidden', marginTop: '60px' }}>
         {/* 左侧边栏 */}
         {/* 左侧边栏 */}
         <Sider
         <Sider
+          className="pricing-scroll-hide"
           style={{
           style={{
             width: 460,
             width: 460,
             height: 'calc(100vh - 60px)',
             height: 'calc(100vh - 60px)',
@@ -48,6 +49,7 @@ const PricingPage = () => {
 
 
         {/* 右侧内容区 */}
         {/* 右侧内容区 */}
         <Content
         <Content
+          className="pricing-scroll-hide"
           style={{
           style={{
             height: 'calc(100vh - 60px)',
             height: 'calc(100vh - 60px)',
             backgroundColor: 'var(--semi-color-bg-0)',
             backgroundColor: 'var(--semi-color-bg-0)',

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

@@ -45,6 +45,7 @@ const PricingTable = ({
   filteredValue,
   filteredValue,
   handleGroupClick,
   handleGroupClick,
   showRatio,
   showRatio,
+  compactMode = false,
   t
   t
 }) => {
 }) => {
 
 
@@ -83,8 +84,8 @@ const PricingTable = ({
   ]);
   ]);
 
 
   // 更新列定义中的 filteredValue
   // 更新列定义中的 filteredValue
-  const tableColumns = useMemo(() => {
-    return columns.map(column => {
+  const processedColumns = useMemo(() => {
+    const cols = columns.map(column => {
       if (column.dataIndex === 'model_name') {
       if (column.dataIndex === 'model_name') {
         return {
         return {
           ...column,
           ...column,
@@ -93,16 +94,23 @@ const PricingTable = ({
       }
       }
       return column;
       return column;
     });
     });
-  }, [columns, filteredValue]);
+
+    // Remove fixed property when in compact mode (mobile view)
+    if (compactMode) {
+      return cols.map(({ fixed, ...rest }) => rest);
+    }
+    return cols;
+  }, [columns, filteredValue, compactMode]);
 
 
   const ModelTable = useMemo(() => (
   const ModelTable = useMemo(() => (
     <Card className="!rounded-xl overflow-hidden" bordered={false}>
     <Card className="!rounded-xl overflow-hidden" bordered={false}>
       <Table
       <Table
-        columns={tableColumns}
+        columns={processedColumns}
         dataSource={filteredModels}
         dataSource={filteredModels}
         loading={loading}
         loading={loading}
         rowSelection={rowSelection}
         rowSelection={rowSelection}
         className="custom-table"
         className="custom-table"
+        scroll={compactMode ? undefined : { x: 'max-content' }}
         empty={
         empty={
           <Empty
           <Empty
             image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
             image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
@@ -120,7 +128,7 @@ const PricingTable = ({
         }}
         }}
       />
       />
     </Card>
     </Card>
-  ), [filteredModels, loading, tableColumns, rowSelection, pageSize, setPageSize, t]);
+  ), [filteredModels, loading, processedColumns, rowSelection, pageSize, setPageSize, t, compactMode]);
 
 
   return ModelTable;
   return ModelTable;
 };
 };

+ 77 - 75
web/src/components/table/model-pricing/PricingTableColumns.js

@@ -92,84 +92,88 @@ export const getPricingTableColumns = ({
   handleGroupClick,
   handleGroupClick,
   showRatio,
   showRatio,
 }) => {
 }) => {
-  const baseColumns = [
-    {
-      title: t('可用性'),
-      dataIndex: 'available',
-      render: (text, record, index) => {
-        return renderAvailable(record.enable_groups.includes(selectedGroup), t);
-      },
-      sorter: (a, b) => {
-        const aAvailable = a.enable_groups.includes(selectedGroup);
-        const bAvailable = b.enable_groups.includes(selectedGroup);
-        return Number(aAvailable) - Number(bAvailable);
-      },
-      defaultSortOrder: 'descend',
-    },
-    {
-      title: t('可用端点类型'),
-      dataIndex: 'supported_endpoint_types',
-      render: (text, record, index) => {
-        return renderSupportedEndpoints(text);
-      },
+  const endpointColumn = {
+    title: t('可用端点类型'),
+    dataIndex: 'supported_endpoint_types',
+    render: (text, record, index) => {
+      return renderSupportedEndpoints(text);
     },
     },
-    {
-      title: t('模型名称'),
-      dataIndex: 'model_name',
-      render: (text, record, index) => {
-        return renderModelTag(text, {
-          onClick: () => {
-            copyText(text);
-          }
-        });
-      },
-      onFilter: (value, record) =>
-        record.model_name.toLowerCase().includes(value.toLowerCase()),
+  };
+
+  const modelNameColumn = {
+    title: t('模型名称'),
+    dataIndex: 'model_name',
+    render: (text, record, index) => {
+      return renderModelTag(text, {
+        onClick: () => {
+          copyText(text);
+        }
+      });
     },
     },
-    {
-      title: t('计费类型'),
-      dataIndex: 'quota_type',
-      render: (text, record, index) => {
-        return renderQuotaType(parseInt(text), t);
-      },
-      sorter: (a, b) => a.quota_type - b.quota_type,
+    onFilter: (value, record) =>
+      record.model_name.toLowerCase().includes(value.toLowerCase()),
+  };
+
+  const quotaColumn = {
+    title: t('计费类型'),
+    dataIndex: 'quota_type',
+    render: (text, record, index) => {
+      return renderQuotaType(parseInt(text), t);
     },
     },
-    {
-      title: t('可用分组'),
-      dataIndex: 'enable_groups',
-      render: (text, record, index) => {
-        return (
-          <Space wrap>
-            {text.map((group) => {
-              if (usableGroup[group]) {
-                if (group === selectedGroup) {
-                  return (
-                    <Tag key={group} color='blue' shape='circle' prefixIcon={<IconVerify />}>
-                      {group}
-                    </Tag>
-                  );
-                } else {
-                  return (
-                    <Tag
-                      key={group}
-                      color='blue'
-                      shape='circle'
-                      onClick={() => handleGroupClick(group)}
-                      className="cursor-pointer hover:opacity-80 transition-opacity"
-                    >
-                      {group}
-                    </Tag>
-                  );
-                }
+    sorter: (a, b) => a.quota_type - b.quota_type,
+  };
+
+  const enableGroupColumn = {
+    title: t('可用分组'),
+    dataIndex: 'enable_groups',
+    render: (text, record, index) => {
+      return (
+        <Space wrap>
+          {text.map((group) => {
+            if (usableGroup[group]) {
+              if (group === selectedGroup) {
+                return (
+                  <Tag key={group} color='blue' shape='circle' prefixIcon={<IconVerify />}>
+                    {group}
+                  </Tag>
+                );
+              } else {
+                return (
+                  <Tag
+                    key={group}
+                    color='blue'
+                    shape='circle'
+                    onClick={() => handleGroupClick(group)}
+                    className="cursor-pointer hover:opacity-80 transition-opacity"
+                  >
+                    {group}
+                  </Tag>
+                );
               }
               }
-            })}
-          </Space>
-        );
-      },
+            }
+          })}
+        </Space>
+      );
     },
     },
-  ];
+  };
+
+  const baseColumns = [endpointColumn, modelNameColumn, quotaColumn, enableGroupColumn];
+
+  const availabilityColumn = {
+    title: t('可用性'),
+    dataIndex: 'available',
+    fixed: 'right',
+    render: (text, record, index) => {
+      return renderAvailable(record.enable_groups.includes(selectedGroup), t);
+    },
+    sorter: (a, b) => {
+      const aAvailable = a.enable_groups.includes(selectedGroup);
+      const bAvailable = b.enable_groups.includes(selectedGroup);
+      return Number(aAvailable) - Number(bAvailable);
+    },
+    defaultSortOrder: 'descend',
+  };
 
 
-  // 倍率列 - 只有在showRatio为true时才包含
   const ratioColumn = {
   const ratioColumn = {
     title: () => (
     title: () => (
       <div className="flex items-center space-x-1">
       <div className="flex items-center space-x-1">
@@ -207,7 +211,6 @@ export const getPricingTableColumns = ({
     },
     },
   };
   };
 
 
-  // 价格列
   const priceColumn = {
   const priceColumn = {
     title: (
     title: (
       <div className="flex items-center space-x-2">
       <div className="flex items-center space-x-2">
@@ -264,12 +267,11 @@ export const getPricingTableColumns = ({
     },
     },
   };
   };
 
 
-  // 根据showRatio决定是否包含倍率列
   const columns = [...baseColumns];
   const columns = [...baseColumns];
   if (showRatio) {
   if (showRatio) {
     columns.push(ratioColumn);
     columns.push(ratioColumn);
   }
   }
   columns.push(priceColumn);
   columns.push(priceColumn);
-
+  columns.push(availabilityColumn);
   return columns;
   return columns;
 }; 
 }; 

+ 3 - 1
web/src/index.css

@@ -391,7 +391,8 @@ code {
   background: transparent;
   background: transparent;
 }
 }
 
 
-/* 隐藏卡片内容区域的滚动条 */
+/* 隐藏内容区域滚动条 */
+.pricing-scroll-hide,
 .model-test-scroll,
 .model-test-scroll,
 .card-content-scroll,
 .card-content-scroll,
 .model-settings-scroll,
 .model-settings-scroll,
@@ -403,6 +404,7 @@ code {
   scrollbar-width: none;
   scrollbar-width: none;
 }
 }
 
 
+.pricing-scroll-hide::-webkit-scrollbar,
 .model-test-scroll::-webkit-scrollbar,
 .model-test-scroll::-webkit-scrollbar,
 .card-content-scroll::-webkit-scrollbar,
 .card-content-scroll::-webkit-scrollbar,
 .model-settings-scroll::-webkit-scrollbar,
 .model-settings-scroll::-webkit-scrollbar,