Browse Source

🎨 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 10 tháng trước cách đây
mục cha
commit
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;
 }