Просмотр исходного кода

🎨 refactor(model-pricing/header): unify header design, extract SearchActions, and improve skeleton

- Extract SearchActions.jsx and replace inline renderSearchActions in PricingVendorIntro.jsx for reuse
- Refactor PricingVendorIntro.jsx:
  - Introduce renderHeaderCard(), tagStyle, getCoverStyle(), and MAX_VISIBLE_AVATARS constant
  - Standardize vendor header cover (gradient + background image) and tag contrast
  - Use border instead of ring for vendor badges; unify visuals and remove Tailwind ring dependency
  - Rotate vendors every 2s only when filterVendor === 'all' and vendor count > 3
  - Remove unused imports; keep prop surface minimal; pass setShowFilterModal downward only
- Refactor PricingVendorIntroSkeleton.jsx:
  - Add getCoverStyle() and rect() helpers; rebuild skeleton to match final UI
  - Replace invalid Skeleton.Input usage; add missing keys; unify colors/borders/radius
- Update PricingTopSection.jsx:
  - Manage filter modal locally; drop redundant prop passing
- Update PricingVendorIntroWithSkeleton.jsx:
  - Align prop interface; forward only required props and keep useMinimumLoadingTime
- Add: web/src/components/table/model-pricing/layout/header/SearchActions.jsx
- Lint: all files pass; no dark:* classes present in this scope

Files touched:
- web/src/components/table/model-pricing/layout/header/PricingTopSection.jsx
- web/src/components/table/model-pricing/layout/header/PricingVendorIntro.jsx
- web/src/components/table/model-pricing/layout/header/PricingVendorIntroWithSkeleton.jsx
- web/src/components/table/model-pricing/layout/header/PricingVendorIntroSkeleton.jsx
- web/src/components/table/model-pricing/layout/header/SearchActions.jsx (new)
t0ng7u 6 месяцев назад
Родитель
Сommit
f246c12959

+ 17 - 59
web/src/components/table/model-pricing/layout/header/PricingTopSection.jsx

@@ -17,9 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact support@quantumnous.com
 */
 
-import React, { useMemo, useState } from 'react';
-import { Input, Button } from '@douyinfe/semi-ui';
-import { IconSearch, IconCopy, IconFilter } from '@douyinfe/semi-icons';
+import React, { useState } from 'react';
 import PricingFilterModal from '../../modal/PricingFilterModal';
 import PricingVendorIntroWithSkeleton from './PricingVendorIntroWithSkeleton';
 
@@ -30,7 +28,6 @@ const PricingTopSection = ({
   handleCompositionStart,
   handleCompositionEnd,
   isMobile,
-  sidebarProps,
   filterVendor,
   models,
   filteredModels,
@@ -40,69 +37,30 @@ const PricingTopSection = ({
 }) => {
   const [showFilterModal, setShowFilterModal] = useState(false);
 
-  const SearchAndActions = useMemo(() => (
-    <div className="flex items-center gap-4 w-full">
-      {/* 搜索框 */}
-      <div className="flex-1">
-        <Input
-          prefix={<IconSearch />}
-          placeholder={t('模糊搜索模型名称')}
-          value={searchValue}
-          onCompositionStart={handleCompositionStart}
-          onCompositionEnd={handleCompositionEnd}
-          onChange={handleChange}
-          showClear
-        />
-      </div>
-
-      {/* 操作按钮 */}
-      <Button
-        theme='outline'
-        type='primary'
-        icon={<IconCopy />}
-        onClick={() => copyText(selectedRowKeys)}
-        disabled={selectedRowKeys.length === 0}
-        className="!bg-blue-500 hover:!bg-blue-600 text-white"
-      >
-        {t('复制')}
-      </Button>
-
-      {/* 移动端筛选按钮 */}
-      {isMobile && (
-        <Button
-          theme="outline"
-          type='tertiary'
-          icon={<IconFilter />}
-          onClick={() => setShowFilterModal(true)}
-        >
-          {t('筛选')}
-        </Button>
-      )}
-    </div>
-  ), [selectedRowKeys, t, handleCompositionStart, handleCompositionEnd, handleChange, copyText, isMobile, searchValue]);
-
   return (
     <>
-      {/* 供应商介绍区域(桌面端显示) */}
-      {!isMobile && (
-        <PricingVendorIntroWithSkeleton
-          loading={loading}
-          filterVendor={filterVendor}
-          models={filteredModels}
-          allModels={models}
-          t={t}
-        />
-      )}
-
-      {/* 搜索和操作区域 */}
-      {SearchAndActions}
+      {/* 供应商介绍区域(包含搜索功能) */}
+      <PricingVendorIntroWithSkeleton
+        loading={loading}
+        filterVendor={filterVendor}
+        models={filteredModels}
+        allModels={models}
+        t={t}
+        selectedRowKeys={selectedRowKeys}
+        copyText={copyText}
+        handleChange={handleChange}
+        handleCompositionStart={handleCompositionStart}
+        handleCompositionEnd={handleCompositionEnd}
+        isMobile={isMobile}
+        searchValue={searchValue}
+        setShowFilterModal={setShowFilterModal}
+      />
 
       {/* 移动端筛选Modal */}
       {isMobile && (
         <PricingFilterModal
           visible={showFilterModal}
           onClose={() => setShowFilterModal(false)}
-          sidebarProps={sidebarProps}
           t={t}
         />
       )}

+ 141 - 102
web/src/components/table/model-pricing/layout/header/PricingVendorIntro.jsx

@@ -18,8 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React, { useState, useEffect, useMemo } from 'react';
-import { Card, Tag, Avatar, AvatarGroup, Typography } from '@douyinfe/semi-ui';
+import { Card, Tag, Avatar, Typography, Tooltip } from '@douyinfe/semi-ui';
 import { getLobeHubIcon } from '../../../../../helpers';
+import SearchActions from './SearchActions';
 
 const { Paragraph } = Typography;
 
@@ -27,8 +28,17 @@ const PricingVendorIntro = ({
   filterVendor,
   models = [],
   allModels = [],
-  t
+  t,
+  selectedRowKeys = [],
+  copyText,
+  handleChange,
+  handleCompositionStart,
+  handleCompositionEnd,
+  isMobile = false,
+  searchValue = '',
+  setShowFilterModal
 }) => {
+  const MAX_VISIBLE_AVATARS = 8;
   // 轮播动效状态(只对全部供应商生效)
   const [currentOffset, setCurrentOffset] = useState(0);
 
@@ -65,7 +75,7 @@ const PricingVendorIntro = ({
     }
 
     return vendorList;
-  }, [allModels, models]);
+  }, [allModels, models, t]);
 
   // 计算当前过滤器的模型数量
   const currentModelCount = models.length;
@@ -79,7 +89,7 @@ const PricingVendorIntro = ({
 
     const interval = setInterval(() => {
       setCurrentOffset(prev => (prev + 1) % vendorInfo.length);
-    }, 2000); // 每2秒切换一次
+    }, 2000);
 
     return () => clearInterval(interval);
   }, [filterVendor, vendorInfo.length]);
@@ -96,6 +106,70 @@ const PricingVendorIntro = ({
     return vendor?.description || t('该供应商提供多种AI模型,适用于不同的应用场景。');
   };
 
+  // 统一的 Tag 样式
+  const tagStyle = {
+    backgroundColor: 'rgba(255,255,255,0.95)',
+    color: '#1f2937',
+    border: '1px solid rgba(255,255,255,0.8)',
+    fontWeight: '500'
+  };
+
+  // 生成封面背景样式
+  const getCoverStyle = (primaryDarkerChannel) => ({
+    '--palette-primary-darkerChannel': primaryDarkerChannel,
+    backgroundImage: `linear-gradient(0deg, rgba(var(--palette-primary-darkerChannel) / 80%), rgba(var(--palette-primary-darkerChannel) / 80%)), url('/cover-4.webp')`,
+    backgroundSize: 'cover',
+    backgroundPosition: 'center',
+    backgroundRepeat: 'no-repeat'
+  });
+
+  // 抽象的头部卡片渲染(用于全部供应商与具体供应商)
+  const renderHeaderCard = ({ title, count, description, rightContent, primaryDarkerChannel }) => (
+    <Card className="!rounded-2xl shadow-sm border-0"
+      cover={
+        <div
+          className="relative h-32"
+          style={getCoverStyle(primaryDarkerChannel)}
+        >
+          <div className="relative z-10 h-full flex items-center justify-between p-4">
+            {/* 左侧:标题与描述 */}
+            <div className="flex-1 min-w-0 mr-4">
+              <div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 mb-2">
+                <h2 className="text-lg sm:text-xl font-bold truncate" style={{ color: 'white' }}>
+                  {title}
+                </h2>
+                <Tag style={tagStyle} shape="circle" size="small" className="self-start sm:self-center">
+                  {t('共 {{count}} 个模型', { count })}
+                </Tag>
+              </div>
+              <Paragraph
+                className="text-xs sm:text-sm leading-relaxed !mb-0"
+                style={{ color: 'rgba(255,255,255,0.9)' }}
+                ellipsis={{
+                  rows: 2,
+                  expandable: true,
+                  collapsible: true,
+                  collapseText: t('收起'),
+                  expandText: t('展开')
+                }}
+              >
+                {description}
+              </Paragraph>
+            </div>
+
+            {/* 右侧:展示区 */}
+            <div className="flex-shrink-0">
+              {rightContent}
+            </div>
+          </div>
+        </div>
+      }
+    >
+      {/* 搜索与操作区 */}
+      {renderSearchActions()}
+    </Card>
+  );
+
   // 为全部供应商创建特殊的头像组合
   const renderAllVendorsAvatar = () => {
     // 重新排列数组,让当前偏移量的头像在第一位
@@ -115,89 +189,81 @@ const PricingVendorIntro = ({
       );
     }
 
+    const visible = rotatedVendors.slice(0, MAX_VISIBLE_AVATARS);
+    const rest = vendorInfo.length - visible.length;
+
     return (
-      <div className="min-w-16 h-16 rounded-2xl bg-white shadow-md flex items-center justify-center px-2">
-        <AvatarGroup
-          maxCount={4}
-          size="default"
-          overlapFrom='end'
-          key={currentOffset}
-          renderMore={(restNumber) => (
-            <Avatar
-              size="default"
-              style={{ backgroundColor: 'transparent', color: 'var(--semi-color-text-0)' }}
-              alt={`${restNumber} more vendors`}
+      <div className="min-w-16 h-16 rounded-2xl bg-white/90 shadow-md backdrop-blur-sm flex items-center justify-center px-2">
+        <div className="flex items-center gap-2">
+          {visible.map((vendor) => (
+            <Tooltip key={vendor.name} content={vendor.name === 'unknown' ? t('未知供应商') : vendor.name} position="top">
+              <div
+                className="w-8 h-8 rounded-full flex items-center justify-center border"
+                style={{
+                  background: 'linear-gradient(180deg, rgba(59,130,246,0.08), rgba(59,130,246,0.02))',
+                  boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
+                  borderColor: 'rgba(59, 130, 246, 0.25)'
+                }}
+              >
+                {vendor.icon ? (
+                  getLobeHubIcon(vendor.icon, 18)
+                ) : (
+                  <Avatar size="small" style={{ backgroundColor: 'rgba(59, 130, 246, 0.08)' }}>
+                    {vendor.name === 'unknown' ? '?' : vendor.name.charAt(0).toUpperCase()}
+                  </Avatar>
+                )}
+              </div>
+            </Tooltip>
+          ))}
+          {rest > 0 && (
+            <div
+              className="w-8 h-8 rounded-full bg-blue-50 text-blue-600 text-xs font-medium flex items-center justify-center"
+              title={`+${rest}`}
             >
-              {`+${restNumber}`}
-            </Avatar>
+              {`+${rest}`}
+            </div>
           )}
-        >
-          {rotatedVendors.map((vendor) => (
-            <Avatar
-              key={vendor.name}
-              size="default"
-              color="transparent"
-              alt={vendor.name === 'unknown' ? t('未知供应商') : vendor.name}
-            >
-              {vendor.icon ?
-                getLobeHubIcon(vendor.icon, 20) :
-                (vendor.name === 'unknown' ? '?' : vendor.name.charAt(0).toUpperCase())
-              }
-            </Avatar>
-          ))}
-        </AvatarGroup>
+        </div>
       </div>
     );
   };
 
   // 为具体供应商渲染单个图标
   const renderVendorAvatar = (vendor) => (
-    <div className="w-16 h-16 rounded-2xl bg-white shadow-md flex items-center justify-center">
+    <div className="w-16 h-16 rounded-2xl bg-white/90 shadow-md backdrop-blur-sm flex items-center justify-center">
       {vendor.icon ?
         getLobeHubIcon(vendor.icon, 40) :
-        <Avatar size="large" color="transparent">
+        <Avatar size="large" style={{ backgroundColor: 'rgba(16, 185, 129, 0.1)' }}>
           {vendor.name === 'unknown' ? '?' : vendor.name.charAt(0).toUpperCase()}
         </Avatar>
       }
     </div>
   );
 
+  // 渲染搜索和操作区域
+  const renderSearchActions = () => (
+    <SearchActions
+      selectedRowKeys={selectedRowKeys}
+      copyText={copyText}
+      handleChange={handleChange}
+      handleCompositionStart={handleCompositionStart}
+      handleCompositionEnd={handleCompositionEnd}
+      isMobile={isMobile}
+      searchValue={searchValue}
+      setShowFilterModal={setShowFilterModal}
+      t={t}
+    />
+  );
+
   // 如果是全部供应商
   if (filterVendor === 'all') {
-    return (
-      <div className='mb-4'>
-        <Card className="!rounded-2xl with-pastel-balls" bodyStyle={{ padding: '16px' }}>
-          <div className="flex items-start space-x-3 md:space-x-4">
-            {/* 全部供应商的头像组合 */}
-            <div className="flex-shrink-0">
-              {renderAllVendorsAvatar()}
-            </div>
-
-            {/* 供应商信息 */}
-            <div className="flex-1 min-w-0">
-              <div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 mb-2">
-                <h2 className="text-lg sm:text-xl font-bold text-gray-900 truncate">{t('全部供应商')}</h2>
-                <Tag color="white" shape="circle" size="small" className="self-start sm:self-center">
-                  {t('共 {{count}} 个模型', { count: currentModelCount })}
-                </Tag>
-              </div>
-              <Paragraph
-                className="text-xs sm:text-sm text-gray-600 leading-relaxed !mb-0"
-                ellipsis={{
-                  rows: 2,
-                  expandable: true,
-                  collapsible: true,
-                  collapseText: t('收起'),
-                  expandText: t('展开')
-                }}
-              >
-                {getVendorDescription('all')}
-              </Paragraph>
-            </div>
-          </div>
-        </Card>
-      </div>
-    );
+    return renderHeaderCard({
+      title: t('全部供应商'),
+      count: currentModelCount,
+      description: getVendorDescription('all'),
+      rightContent: renderAllVendorsAvatar(),
+      primaryDarkerChannel: '37 99 235'
+    });
   }
 
   // 具体供应商
@@ -208,40 +274,13 @@ const PricingVendorIntro = ({
 
   const vendorDisplayName = currentVendor.name === 'unknown' ? t('未知供应商') : currentVendor.name;
 
-  return (
-    <div className='mb-4'>
-      <Card className="!rounded-2xl with-pastel-balls" bodyStyle={{ padding: '16px' }}>
-        <div className="flex items-start space-x-3 md:space-x-4">
-          {/* 供应商图标 */}
-          <div className="flex-shrink-0">
-            {renderVendorAvatar(currentVendor)}
-          </div>
-
-          {/* 供应商信息 */}
-          <div className="flex-1 min-w-0">
-            <div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 mb-2">
-              <h2 className="text-lg sm:text-xl font-bold text-gray-900 truncate">{vendorDisplayName}</h2>
-              <Tag color="white" shape="circle" size="small" className="self-start sm:self-center">
-                {t('共 {{count}} 个模型', { count: currentModelCount })}
-              </Tag>
-            </div>
-            <Paragraph
-              className="text-xs sm:text-sm text-gray-600 leading-relaxed !mb-0"
-              ellipsis={{
-                rows: 2,
-                expandable: true,
-                collapsible: true,
-                collapseText: t('收起'),
-                expandText: t('展开')
-              }}
-            >
-              {currentVendor.description || getVendorDescription(currentVendor.name)}
-            </Paragraph>
-          </div>
-        </div>
-      </Card>
-    </div>
-  );
+  return renderHeaderCard({
+    title: vendorDisplayName,
+    count: currentModelCount,
+    description: currentVendor.description || getVendorDescription(currentVendor.name),
+    rightContent: renderVendorAvatar(currentVendor),
+    primaryDarkerChannel: '16 185 129'
+  });
 };
 
 export default PricingVendorIntro;

+ 104 - 36
web/src/components/table/model-pricing/layout/header/PricingVendorIntroSkeleton.jsx

@@ -23,48 +23,116 @@ import { Card, Skeleton } from '@douyinfe/semi-ui';
 const PricingVendorIntroSkeleton = ({
   isAllVendors = false
 }) => {
+  // 统一的封面样式函数
+  const getCoverStyle = (primaryDarkerChannel) => ({
+    '--palette-primary-darkerChannel': primaryDarkerChannel,
+    backgroundImage: `linear-gradient(0deg, rgba(var(--palette-primary-darkerChannel) / 80%), rgba(var(--palette-primary-darkerChannel) / 80%)), url('/cover-4.webp')`,
+    backgroundSize: 'cover',
+    backgroundPosition: 'center',
+    backgroundRepeat: 'no-repeat'
+  });
+
+  // 快速生成骨架矩形
+  const rect = (style = {}, key) => (
+    <div key={key} className="animate-pulse" style={style} />
+  );
+
   const placeholder = (
-    <div className='mb-4'>
-      <Card className="!rounded-2xl with-pastel-balls" bodyStyle={{ padding: '16px' }}>
-        <div className="flex items-start space-x-3 md:space-x-4">
-          {/* 供应商图标骨架 */}
-          <div className="flex-shrink-0 min-w-16 h-16 rounded-2xl bg-white shadow-md flex items-center justify-center px-2">
-            {isAllVendors ? (
-              <div className="flex items-center">
-                {Array.from({ length: 4 }).map((_, index) => (
-                  <Skeleton.Avatar
-                    key={index}
-                    active
-                    size="default"
-                    style={{
-                      width: 32,
-                      height: 32,
-                      marginRight: index < 3 ? -8 : 0,
-                    }}
-                  />
-                ))}
+    <Card className="!rounded-2xl shadow-sm border-0"
+      cover={
+        <div
+          className="relative h-32"
+          style={getCoverStyle(isAllVendors ? '37 99 235' : '16 185 129')}
+        >
+          <div className="relative z-10 h-full flex items-center justify-between p-4">
+            {/* 左侧:标题和描述骨架 */}
+            <div className="flex-1 min-w-0 mr-4">
+              <div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 mb-2">
+                {rect({
+                  width: isAllVendors ? 120 : 100,
+                  height: 24,
+                  backgroundColor: 'rgba(255, 255, 255, 0.25)',
+                  borderRadius: 8,
+                  backdropFilter: 'blur(4px)'
+                })}
+                {rect({
+                  width: 80,
+                  height: 20,
+                  backgroundColor: 'rgba(255, 255, 255, 0.2)',
+                  borderRadius: 9999,
+                  backdropFilter: 'blur(4px)',
+                  border: '1px solid rgba(255,255,255,0.3)'
+                })}
               </div>
-            ) : (
-              <Skeleton.Avatar active size="large" style={{ width: 40, height: 40, borderRadius: 8 }} />
-            )}
-          </div>
+              <div className="space-y-2">
+                {rect({
+                  width: '100%',
+                  height: 14,
+                  backgroundColor: 'rgba(255, 255, 255, 0.2)',
+                  borderRadius: 4,
+                  backdropFilter: 'blur(4px)'
+                })}
+                {rect({
+                  width: '75%',
+                  height: 14,
+                  backgroundColor: 'rgba(255, 255, 255, 0.15)',
+                  borderRadius: 4,
+                  backdropFilter: 'blur(4px)'
+                })}
+              </div>
+            </div>
 
-          {/* 供应商信息骨架 */}
-          <div className="flex-1 min-w-0">
-            <div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 mb-2">
-              <Skeleton.Title active style={{ width: 120, height: 24, marginBottom: 0 }} />
-              <Skeleton.Button active size="small" style={{ width: 80, height: 20, borderRadius: 12 }} />
+            {/* 右侧:供应商图标骨架 */}
+            <div className="flex-shrink-0 min-w-16 h-16 rounded-2xl bg-white/90 shadow-md backdrop-blur-sm flex items-center justify-center px-2">
+              {isAllVendors ? (
+                <div className="flex items-center gap-2">
+                  {Array.from({ length: 4 }).map((_, index) => (
+                    rect({
+                      width: 32,
+                      height: 32,
+                      backgroundColor: 'rgba(59, 130, 246, 0.1)',
+                      borderRadius: 9999,
+                      border: '1px solid rgba(59, 130, 246, 0.2)'
+                    }, index)
+                  ))}
+                </div>
+              ) : (
+                rect({
+                  width: 40,
+                  height: 40,
+                  backgroundColor: 'rgba(16, 185, 129, 0.1)',
+                  borderRadius: 12,
+                  border: '1px solid rgba(16, 185, 129, 0.2)'
+                })
+              )}
             </div>
-            <Skeleton.Paragraph
-              active
-              rows={2}
-              style={{ marginBottom: 0 }}
-              title={false}
-            />
           </div>
         </div>
-      </Card>
-    </div>
+      }
+    >
+      {/* 搜索和操作区域骨架 */}
+      <div className="flex items-center gap-4 w-full">
+        {/* 搜索框骨架 */}
+        <div className="flex-1">
+          {rect({
+            width: '100%',
+            height: 32,
+            backgroundColor: 'rgba(156, 163, 175, 0.1)',
+            borderRadius: 8,
+            border: '1px solid rgba(156, 163, 175, 0.2)'
+          })}
+        </div>
+
+        {/* 操作按钮骨架 */}
+        {rect({
+          width: 80,
+          height: 32,
+          backgroundColor: 'rgba(59, 130, 246, 0.1)',
+          borderRadius: 8,
+          border: '1px solid rgba(59, 130, 246, 0.2)'
+        })}
+      </div>
+    </Card>
   );
 
   return (

+ 17 - 1
web/src/components/table/model-pricing/layout/header/PricingVendorIntroWithSkeleton.jsx

@@ -27,7 +27,15 @@ const PricingVendorIntroWithSkeleton = ({
   filterVendor,
   models,
   allModels,
-  t
+  t,
+  selectedRowKeys,
+  copyText,
+  handleChange,
+  handleCompositionStart,
+  handleCompositionEnd,
+  isMobile,
+  searchValue,
+  setShowFilterModal
 }) => {
   const showSkeleton = useMinimumLoadingTime(loading);
 
@@ -45,6 +53,14 @@ const PricingVendorIntroWithSkeleton = ({
       models={models}
       allModels={allModels}
       t={t}
+      selectedRowKeys={selectedRowKeys}
+      copyText={copyText}
+      handleChange={handleChange}
+      handleCompositionStart={handleCompositionStart}
+      handleCompositionEnd={handleCompositionEnd}
+      isMobile={isMobile}
+      searchValue={searchValue}
+      setShowFilterModal={setShowFilterModal}
     />
   );
 };

+ 79 - 0
web/src/components/table/model-pricing/layout/header/SearchActions.jsx

@@ -0,0 +1,79 @@
+/*
+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 { Input, Button } from '@douyinfe/semi-ui';
+import { IconSearch, IconCopy, IconFilter } from '@douyinfe/semi-icons';
+
+const SearchActions = ({
+  selectedRowKeys = [],
+  copyText,
+  handleChange,
+  handleCompositionStart,
+  handleCompositionEnd,
+  isMobile = false,
+  searchValue = '',
+  setShowFilterModal,
+  t
+}) => {
+  return (
+    <div className="flex items-center gap-4 w-full">
+      {/* 搜索框 */}
+      <div className="flex-1">
+        <Input
+          prefix={<IconSearch />}
+          placeholder={t('模糊搜索模型名称')}
+          value={searchValue}
+          onCompositionStart={handleCompositionStart}
+          onCompositionEnd={handleCompositionEnd}
+          onChange={handleChange}
+          showClear
+        />
+      </div>
+
+      {/* 操作按钮 */}
+      <Button
+        theme='outline'
+        type='primary'
+        icon={<IconCopy />}
+        onClick={() => copyText?.(selectedRowKeys)}
+        disabled={selectedRowKeys.length === 0}
+        className="!bg-blue-500 hover:!bg-blue-600 text-white"
+      >
+        {t('复制')}
+      </Button>
+
+      {/* 移动端筛选按钮 */}
+      {isMobile && (
+        <Button
+          theme="outline"
+          type='tertiary'
+          icon={<IconFilter />}
+          onClick={() => setShowFilterModal?.(true)}
+        >
+          {t('筛选')}
+        </Button>
+      )}
+    </div>
+  );
+};
+
+export default SearchActions;
+
+

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

@@ -1201,7 +1201,7 @@
   "兑换码创建成功,是否下载兑换码?": "Redemption code created successfully. Do you want to download it?",
   "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "The redemption code will be downloaded as a text file, with the filename being the redemption code name.",
   "模型价格": "Model price",
-  "按K显示单位": "Display in K units",
+  "按K显示单位": "Display in K",
   "可用分组": "Available groups",
   "您的默认分组为:{{group}},分组倍率为:{{ratio}}": "Your default group is: {{group}}, group ratio: {{ratio}}",
   "按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)": "The cost of pay-as-you-go = Group ratio × Model ratio × (Prompt token number + Completion token number × Completion ratio) / 500000 (Unit: USD)",

+ 2 - 2
web/src/index.css

@@ -728,8 +728,8 @@ html.dark .with-pastel-balls::before {
 }
 
 .pricing-sidebar {
-  min-width: 460px;
-  max-width: 460px;
+  min-width: 400px;
+  max-width: 400px;
   height: calc(100vh - 60px);
   background-color: var(--semi-color-bg-0);
   overflow: auto;