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

🎨 feat(ui): enhance pricing components with improved icons and responsive design

- Replace copy button icon from semi-ui IconCopy to lucide-react Copy in PricingCardView
- Add conditional tooltip functionality to SelectableButtonGroup that only shows when text overflows
- Implement responsive table column behavior in PricingTableColumns with mobile-aware fixed positioning
- Use DOM-based overflow detection (scrollWidth vs clientWidth) for better performance
- Apply useIsMobile hook to conditionally set fixed: 'right' only on desktop devices

These changes improve user experience across different screen sizes and provide more consistent iconography throughout the pricing interface.
t0ng7u 6 месяцев назад
Родитель
Сommit
86964bb426

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

@@ -38,7 +38,7 @@ const ScrollableContainer = forwardRef(({
   children,
   maxHeight = '24rem',
   className = '',
-  contentClassName = 'p-2',
+  contentClassName = '',
   fadeIndicatorClassName = '',
   checkInterval = 100,
   scrollThreshold = 5,

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

@@ -17,8 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact support@quantumnous.com
 */
 
-import React, { useState } from 'react';
-import { useIsMobile } from '../../../hooks/common/useIsMobile';
+import React, { useState, useRef, useEffect } from 'react';
 import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime';
 import { useContainerWidth } from '../../../hooks/common/useContainerWidth';
 import { Divider, Button, Tag, Row, Col, Collapsible, Checkbox, Skeleton, Tooltip } from '@douyinfe/semi-ui';
@@ -51,10 +50,34 @@ const SelectableButtonGroup = ({
   loading = false
 }) => {
   const [isOpen, setIsOpen] = useState(false);
-  const [skeletonCount] = useState(6);
-  const isMobile = useIsMobile();
+  const [skeletonCount] = useState(12);
   const [containerRef, containerWidth] = useContainerWidth();
 
+  const ConditionalTooltipText = ({ text }) => {
+    const textRef = useRef(null);
+    const [isOverflowing, setIsOverflowing] = useState(false);
+
+    useEffect(() => {
+      const el = textRef.current;
+      if (!el) return;
+      setIsOverflowing(el.scrollWidth > el.clientWidth);
+    }, [text, containerWidth]);
+
+    const textElement = (
+      <span ref={textRef} className="sbg-ellipsis">
+        {text}
+      </span>
+    );
+
+    return isOverflowing ? (
+      <Tooltip content={text}>
+        {textElement}
+      </Tooltip>
+    ) : (
+      textElement
+    );
+  };
+
   // 基于容器宽度计算响应式列数和标签显示策略
   const getResponsiveConfig = () => {
     if (containerWidth <= 280) return { columns: 1, showTags: true };   // 极窄:1列+标签
@@ -176,9 +199,7 @@ const SelectableButtonGroup = ({
               >
                 <div className="sbg-content">
                   {item.icon && (<span className="sbg-icon">{item.icon}</span>)}
-                  <Tooltip content={item.label}>
-                    <span className="sbg-ellipsis">{item.label}</span>
-                  </Tooltip>
+                  <ConditionalTooltipText text={item.label} />
                   {item.tagCount !== undefined && shouldShowTags && (
                     <Tag className="sbg-tag" color='white' shape="circle" size="small">{item.tagCount}</Tag>
                   )}
@@ -203,9 +224,7 @@ const SelectableButtonGroup = ({
             >
               <div className="sbg-content">
                 {item.icon && (<span className="sbg-icon">{item.icon}</span>)}
-                <Tooltip content={item.label}>
-                  <span className="sbg-ellipsis">{item.label}</span>
-                </Tooltip>
+                <ConditionalTooltipText text={item.label} />
                 {item.tagCount !== undefined && shouldShowTags && (
                   <Tag className="sbg-tag" color='white' shape="circle" size="small">{item.tagCount}</Tag>
                 )}

+ 1 - 16
web/src/components/table/model-pricing/layout/PricingSidebar.jsx

@@ -24,7 +24,7 @@ import PricingQuotaTypes from '../filter/PricingQuotaTypes';
 import PricingEndpointTypes from '../filter/PricingEndpointTypes';
 import PricingVendors from '../filter/PricingVendors';
 import PricingTags from '../filter/PricingTags';
-import PricingDisplaySettings from '../filter/PricingDisplaySettings';
+
 import { resetPricingFilters } from '../../../../helpers/utils';
 import { usePricingFilterCounts } from '../../../../hooks/model-pricing/usePricingFilterCounts';
 
@@ -107,21 +107,6 @@ const PricingSidebar = ({
         </Button>
       </div>
 
-      <PricingDisplaySettings
-        showWithRecharge={showWithRecharge}
-        setShowWithRecharge={setShowWithRecharge}
-        currency={currency}
-        setCurrency={setCurrency}
-        showRatio={showRatio}
-        setShowRatio={setShowRatio}
-        viewMode={viewMode}
-        setViewMode={setViewMode}
-        tokenUnit={tokenUnit}
-        setTokenUnit={setTokenUnit}
-        loading={loading}
-        t={t}
-      />
-
       <PricingVendors
         filterVendor={filterVendor}
         setFilterVendor={setFilterVendor}

+ 15 - 1
web/src/components/table/model-pricing/layout/content/PricingContent.jsx

@@ -26,7 +26,21 @@ const PricingContent = ({ isMobile, sidebarProps, ...props }) => {
     <div className={isMobile ? "pricing-content-mobile" : "pricing-scroll-hide"}>
       {/* 固定的顶部区域(分类介绍 + 搜索和操作) */}
       <div className="pricing-search-header">
-        <PricingTopSection {...props} isMobile={isMobile} sidebarProps={sidebarProps} />
+        <PricingTopSection
+          {...props}
+          isMobile={isMobile}
+          sidebarProps={sidebarProps}
+          showWithRecharge={sidebarProps.showWithRecharge}
+          setShowWithRecharge={sidebarProps.setShowWithRecharge}
+          currency={sidebarProps.currency}
+          setCurrency={sidebarProps.setCurrency}
+          showRatio={sidebarProps.showRatio}
+          setShowRatio={sidebarProps.setShowRatio}
+          viewMode={sidebarProps.viewMode}
+          setViewMode={sidebarProps.setViewMode}
+          tokenUnit={sidebarProps.tokenUnit}
+          setTokenUnit={sidebarProps.setTokenUnit}
+        />
       </div>
 
       {/* 可滚动的内容区域 */}

+ 30 - 0
web/src/components/table/model-pricing/layout/header/PricingTopSection.jsx

@@ -35,6 +35,16 @@ const PricingTopSection = memo(({
   filteredModels,
   loading,
   searchValue,
+  showWithRecharge,
+  setShowWithRecharge,
+  currency,
+  setCurrency,
+  showRatio,
+  setShowRatio,
+  viewMode,
+  setViewMode,
+  tokenUnit,
+  setTokenUnit,
   t
 }) => {
   const [showFilterModal, setShowFilterModal] = useState(false);
@@ -53,6 +63,16 @@ const PricingTopSection = memo(({
               isMobile={isMobile}
               searchValue={searchValue}
               setShowFilterModal={setShowFilterModal}
+              showWithRecharge={showWithRecharge}
+              setShowWithRecharge={setShowWithRecharge}
+              currency={currency}
+              setCurrency={setCurrency}
+              showRatio={showRatio}
+              setShowRatio={setShowRatio}
+              viewMode={viewMode}
+              setViewMode={setViewMode}
+              tokenUnit={tokenUnit}
+              setTokenUnit={setTokenUnit}
               t={t}
             />
           </div>
@@ -78,6 +98,16 @@ const PricingTopSection = memo(({
           isMobile={isMobile}
           searchValue={searchValue}
           setShowFilterModal={setShowFilterModal}
+          showWithRecharge={showWithRecharge}
+          setShowWithRecharge={setShowWithRecharge}
+          currency={currency}
+          setCurrency={setCurrency}
+          showRatio={showRatio}
+          setShowRatio={setShowRatio}
+          viewMode={viewMode}
+          setViewMode={setViewMode}
+          tokenUnit={tokenUnit}
+          setTokenUnit={setTokenUnit}
         />
       )}
     </>

+ 22 - 2
web/src/components/table/model-pricing/layout/header/PricingVendorIntro.jsx

@@ -126,7 +126,17 @@ const PricingVendorIntro = memo(({
   handleCompositionEnd,
   isMobile = false,
   searchValue = '',
-  setShowFilterModal
+  setShowFilterModal,
+  showWithRecharge,
+  setShowWithRecharge,
+  currency,
+  setCurrency,
+  showRatio,
+  setShowRatio,
+  viewMode,
+  setViewMode,
+  tokenUnit,
+  setTokenUnit
 }) => {
   const [currentOffset, setCurrentOffset] = useState(0);
   const [descModalVisible, setDescModalVisible] = useState(false);
@@ -239,9 +249,19 @@ const PricingVendorIntro = memo(({
       isMobile={isMobile}
       searchValue={searchValue}
       setShowFilterModal={setShowFilterModal}
+      showWithRecharge={showWithRecharge}
+      setShowWithRecharge={setShowWithRecharge}
+      currency={currency}
+      setCurrency={setCurrency}
+      showRatio={showRatio}
+      setShowRatio={setShowRatio}
+      viewMode={viewMode}
+      setViewMode={setViewMode}
+      tokenUnit={tokenUnit}
+      setTokenUnit={setTokenUnit}
       t={t}
     />
-  ), [selectedRowKeys, copyText, handleChange, handleCompositionStart, handleCompositionEnd, isMobile, searchValue, setShowFilterModal, t]);
+  ), [selectedRowKeys, copyText, handleChange, handleCompositionStart, handleCompositionEnd, isMobile, searchValue, setShowFilterModal, showWithRecharge, setShowWithRecharge, currency, setCurrency, showRatio, setShowRatio, viewMode, setViewMode, tokenUnit, setTokenUnit, t]);
 
   const renderHeaderCard = useCallback(({ title, count, description, rightContent, primaryDarkerChannel }) => (
     <Card className="!rounded-2xl shadow-sm border-0"

+ 77 - 2
web/src/components/table/model-pricing/layout/header/SearchActions.jsx

@@ -18,8 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React, { memo, useCallback } from 'react';
-import { Input, Button } from '@douyinfe/semi-ui';
-import { IconSearch, IconCopy, IconFilter } from '@douyinfe/semi-icons';
+import { Input, Button, Switch, Select, Divider, Tooltip } from '@douyinfe/semi-ui';
+import { IconSearch, IconCopy, IconFilter, IconHelpCircle } from '@douyinfe/semi-icons';
 
 const SearchActions = memo(({
   selectedRowKeys = [],
@@ -30,6 +30,16 @@ const SearchActions = memo(({
   isMobile = false,
   searchValue = '',
   setShowFilterModal,
+  showWithRecharge,
+  setShowWithRecharge,
+  currency,
+  setCurrency,
+  showRatio,
+  setShowRatio,
+  viewMode,
+  setViewMode,
+  tokenUnit,
+  setTokenUnit,
   t
 }) => {
   const handleCopyClick = useCallback(() => {
@@ -42,6 +52,14 @@ const SearchActions = memo(({
     setShowFilterModal?.(true);
   }, [setShowFilterModal]);
 
+  const handleViewModeToggle = useCallback(() => {
+    setViewMode?.(viewMode === 'table' ? 'card' : 'table');
+  }, [viewMode, setViewMode]);
+
+  const handleTokenUnitToggle = useCallback(() => {
+    setTokenUnit?.(tokenUnit === 'K' ? 'M' : 'K');
+  }, [tokenUnit, setTokenUnit]);
+
   return (
     <div className="flex items-center gap-2 w-full">
       <div className="flex-1">
@@ -67,6 +85,63 @@ const SearchActions = memo(({
         {t('复制')}
       </Button>
 
+      {!isMobile && (
+        <>
+          <Divider layout="vertical" margin="8px" />
+
+          {/* 充值价格显示开关 */}
+          <div className="flex items-center gap-2">
+            <span className="text-sm text-gray-600">{t('充值价格显示')}</span>
+            <Switch
+              checked={showWithRecharge}
+              onChange={setShowWithRecharge}
+            />
+          </div>
+
+          {/* 货币单位选择 */}
+          {showWithRecharge && (
+            <Select
+              value={currency}
+              onChange={setCurrency}
+              optionList={[
+                { value: 'USD', label: 'USD' },
+                { value: 'CNY', label: 'CNY' }
+              ]}
+            />
+          )}
+
+          {/* 显示倍率开关 */}
+          <div className="flex items-center gap-2">
+            <span className="text-sm text-gray-600">{t('倍率')}</span>
+            <Tooltip content={t('倍率是用于系统计算不同模型的最终价格用的,如果您不理解倍率,请忽略')}>
+              <IconHelpCircle size="small" style={{ color: 'var(--semi-color-text-2)', cursor: 'help' }} />
+            </Tooltip>
+            <Switch
+              checked={showRatio}
+              onChange={setShowRatio}
+            />
+          </div>
+
+          {/* 视图模式切换按钮 */}
+          <Button
+            theme={viewMode === 'table' ? 'solid' : 'outline'}
+            type={viewMode === 'table' ? 'primary' : 'tertiary'}
+            onClick={handleViewModeToggle}
+          >
+            {t('表格视图')}
+          </Button>
+
+          {/* Token单位切换按钮 */}
+          <Button
+            theme={tokenUnit === 'K' ? 'solid' : 'outline'}
+            type={tokenUnit === 'K' ? 'primary' : 'tertiary'}
+            onClick={handleTokenUnitToggle}
+          >
+            {tokenUnit}
+          </Button>
+        </>
+      )}
+
       {isMobile && (
         <Button
           theme="outline"

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

@@ -71,7 +71,7 @@ const FilterModalContent = ({ sidebarProps, t }) => {
   });
 
   return (
-    <div className="p-2">
+    <>
       <PricingDisplaySettings
         showWithRecharge={showWithRecharge}
         setShowWithRecharge={setShowWithRecharge}
@@ -131,7 +131,7 @@ const FilterModalContent = ({ sidebarProps, t }) => {
         loading={loading}
         t={t}
       />
-    </div>
+    </>
   );
 };
 

+ 1 - 1
web/src/components/table/model-pricing/view/card/PricingCardSkeleton.jsx

@@ -92,7 +92,7 @@ const PricingCardSkeleton = ({
                   size="small"
                   style={{
                     width: 64,
-                    height: 20,
+                    height: 18,
                     borderRadius: 10
                   }}
                 />

+ 3 - 2
web/src/components/table/model-pricing/view/card/PricingCardView.jsx

@@ -19,7 +19,8 @@ For commercial licensing, please contact support@quantumnous.com
 
 import React from 'react';
 import { Card, Tag, Tooltip, Checkbox, Empty, Pagination, Button, Avatar } from '@douyinfe/semi-ui';
-import { IconHelpCircle, IconCopy } from '@douyinfe/semi-icons';
+import { IconHelpCircle } from '@douyinfe/semi-icons';
+import { Copy } from 'lucide-react';
 import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
 import { stringToColor, calculateModelPrice, formatPriceInfo, getLobeHubIcon } from '../../../../../helpers';
 import PricingCardSkeleton from './PricingCardSkeleton';
@@ -245,7 +246,7 @@ const PricingCardView = ({
                       size="small"
                       theme="outline"
                       type="tertiary"
-                      icon={<IconCopy />}
+                      icon={<Copy size={12} />}
                       onClick={(e) => {
                         e.stopPropagation();
                         copyText(model.model_name);

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

@@ -38,7 +38,6 @@ const PricingTable = ({
   setIsModalOpenurl,
   currency,
   tokenUnit,
-  setTokenUnit,
   displayPrice,
   searchValue,
   showRatio,
@@ -99,7 +98,6 @@ const PricingTable = ({
         dataSource={filteredModels}
         loading={loading}
         rowSelection={rowSelection}
-        className="custom-table"
         scroll={compactMode ? undefined : { x: 'max-content' }}
         onRow={(record) => ({
           onClick: () => openModelDetail && openModelDetail(record),
@@ -114,7 +112,7 @@ const PricingTable = ({
           />
         }
         pagination={{
-          defaultPageSize: 100,
+          defaultPageSize: 20,
           pageSize: pageSize,
           showSizeChanger: true,
           pageSizeOptions: [10, 20, 50, 100],

+ 5 - 4
web/src/components/table/model-pricing/view/table/PricingTableColumns.jsx

@@ -22,6 +22,7 @@ import { Tag, Space, Tooltip } from '@douyinfe/semi-ui';
 import { IconHelpCircle } from '@douyinfe/semi-icons';
 import { renderModelTag, stringToColor, calculateModelPrice, getLobeHubIcon } from '../../../../../helpers';
 import { renderLimitedItems, renderDescription } from '../../../../common/ui/RenderUtils';
+import { useIsMobile } from '../../../../../hooks/common/useIsMobile';
 
 function renderQuotaType(type, t) {
   switch (type) {
@@ -98,7 +99,7 @@ export const getPricingTableColumns = ({
   displayPrice,
   showRatio,
 }) => {
-
+  const isMobile = useIsMobile();
   const priceDataCache = new WeakMap();
 
   const getPriceData = (record) => {
@@ -207,7 +208,7 @@ export const getPricingTableColumns = ({
   const priceColumn = {
     title: t('模型价格'),
     dataIndex: 'model_price',
-    fixed: 'right',
+    ...(isMobile ? {} : { fixed: 'right' }),
     render: (text, record, index) => {
       const priceData = getPriceData(record);
 
@@ -215,10 +216,10 @@ export const getPricingTableColumns = ({
         return (
           <div className="space-y-1">
             <div className="text-gray-700">
-              {t('提示')} {priceData.inputPrice} / 1{priceData.unitLabel} tokens
+              {t('输入')} {priceData.inputPrice} / 1{priceData.unitLabel} tokens
             </div>
             <div className="text-gray-700">
-              {t('补全')} {priceData.completionPrice} / 1{priceData.unitLabel} tokens
+              {t('输出')} {priceData.completionPrice} / 1{priceData.unitLabel} tokens
             </div>
           </div>
         );

+ 1 - 1
web/src/hooks/model-pricing/useModelPricingData.jsx

@@ -39,7 +39,7 @@ export const useModelPricingData = () => {
   const [filterEndpointType, setFilterEndpointType] = useState('all'); // 端点类型筛选: 'all' | string
   const [filterVendor, setFilterVendor] = useState('all'); // 供应商筛选: 'all' | 'unknown' | string
   const [filterTag, setFilterTag] = useState('all'); // 模型标签筛选: 'all' | string
-  const [pageSize, setPageSize] = useState(100);
+  const [pageSize, setPageSize] = useState(20);
   const [currentPage, setCurrentPage] = useState(1);
   const [currency, setCurrency] = useState('USD');
   const [showWithRecharge, setShowWithRecharge] = useState(false);

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

@@ -293,7 +293,8 @@
   "账号绑定": "Account Binding",
   "绑定微信账号": "Bind WeChat Account",
   "微信扫码关注公众号": "Scan the QR code with WeChat to follow the official account",
-  "输入": "Enter",
+  "输入": "Input",
+  "输出": "Output",
   "验证码": "Verification Code",
   "获取验证码": "Get Verification Code",
   "三分钟内有效": "Valid for three minutes",

+ 17 - 16
web/src/index.css

@@ -52,22 +52,6 @@ code {
 }
 
 /* ==================== 导航和侧边栏样式 ==================== */
-.semi-radio,
-.semi-tagInput,
-.semi-input-textarea-wrapper,
-.semi-navigation-sub-title,
-.semi-chat-inputBox-sendButton,
-.semi-page-item,
-.semi-navigation-item,
-.semi-tag-closable,
-.semi-input-wrapper,
-.semi-tabs-tab-button,
-.semi-select,
-.semi-button,
-.semi-datepicker-range-input {
-  border-radius: 10px !important;
-}
-
 .semi-navigation-item {
   margin-bottom: 4px !important;
   padding: 4px 12px !important;
@@ -778,4 +762,21 @@ html.dark .with-pastel-balls::before {
 .semi-card-header,
 .semi-card-body {
   padding: 10px !important;
+}
+
+/* ==================== 自定义圆角样式 ==================== */
+.semi-radio,
+.semi-tagInput,
+.semi-input-textarea-wrapper,
+.semi-navigation-sub-title,
+.semi-chat-inputBox-sendButton,
+.semi-page-item,
+.semi-navigation-item,
+.semi-tag-closable,
+.semi-input-wrapper,
+.semi-tabs-tab-button,
+.semi-select,
+.semi-button,
+.semi-datepicker-range-input {
+  border-radius: 10px !important;
 }