From f246c1295936a73cb4bf142b6135ef4a8f88a44c Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sat, 23 Aug 2025 21:11:40 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20refactor(model-pricing/header):?= =?UTF-8?q?=20unify=20header=20design,=20extract=20SearchActions,=20and=20?= =?UTF-8?q?improve=20skeleton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../layout/header/PricingTopSection.jsx | 76 ++---- .../layout/header/PricingVendorIntro.jsx | 245 ++++++++++-------- .../header/PricingVendorIntroSkeleton.jsx | 140 +++++++--- .../header/PricingVendorIntroWithSkeleton.jsx | 18 +- .../layout/header/SearchActions.jsx | 79 ++++++ web/src/i18n/locales/en.json | 2 +- web/src/index.css | 4 +- 7 files changed, 362 insertions(+), 202 deletions(-) create mode 100644 web/src/components/table/model-pricing/layout/header/SearchActions.jsx diff --git a/web/src/components/table/model-pricing/layout/header/PricingTopSection.jsx b/web/src/components/table/model-pricing/layout/header/PricingTopSection.jsx index fd133980..0db0551d 100644 --- a/web/src/components/table/model-pricing/layout/header/PricingTopSection.jsx +++ b/web/src/components/table/model-pricing/layout/header/PricingTopSection.jsx @@ -17,9 +17,7 @@ along with this program. If not, see . 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(() => ( -
- {/* 搜索框 */} -
- } - placeholder={t('模糊搜索模型名称')} - value={searchValue} - onCompositionStart={handleCompositionStart} - onCompositionEnd={handleCompositionEnd} - onChange={handleChange} - showClear - /> -
- - {/* 操作按钮 */} - - - {/* 移动端筛选按钮 */} - {isMobile && ( - - )} -
- ), [selectedRowKeys, t, handleCompositionStart, handleCompositionEnd, handleChange, copyText, isMobile, searchValue]); - return ( <> - {/* 供应商介绍区域(桌面端显示) */} - {!isMobile && ( - - )} - - {/* 搜索和操作区域 */} - {SearchAndActions} + {/* 供应商介绍区域(包含搜索功能) */} + {/* 移动端筛选Modal */} {isMobile && ( setShowFilterModal(false)} - sidebarProps={sidebarProps} t={t} /> )} diff --git a/web/src/components/table/model-pricing/layout/header/PricingVendorIntro.jsx b/web/src/components/table/model-pricing/layout/header/PricingVendorIntro.jsx index 1978c7ae..e2c4664c 100644 --- a/web/src/components/table/model-pricing/layout/header/PricingVendorIntro.jsx +++ b/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 }) => ( + +
+ {/* 左侧:标题与描述 */} +
+
+

+ {title} +

+ + {t('共 {{count}} 个模型', { count })} + +
+ + {description} + +
+ + {/* 右侧:展示区 */} +
+ {rightContent} +
+
+ + } + > + {/* 搜索与操作区 */} + {renderSearchActions()} +
+ ); + // 为全部供应商创建特殊的头像组合 const renderAllVendorsAvatar = () => { // 重新排列数组,让当前偏移量的头像在第一位 @@ -115,89 +189,81 @@ const PricingVendorIntro = ({ ); } + const visible = rotatedVendors.slice(0, MAX_VISIBLE_AVATARS); + const rest = vendorInfo.length - visible.length; + return ( -
- ( - - {`+${restNumber}`} - - )} - > - {rotatedVendors.map((vendor) => ( - - {vendor.icon ? - getLobeHubIcon(vendor.icon, 20) : - (vendor.name === 'unknown' ? '?' : vendor.name.charAt(0).toUpperCase()) - } - +
+
+ {visible.map((vendor) => ( + +
+ {vendor.icon ? ( + getLobeHubIcon(vendor.icon, 18) + ) : ( + + {vendor.name === 'unknown' ? '?' : vendor.name.charAt(0).toUpperCase()} + + )} +
+
))} - + {rest > 0 && ( +
+ {`+${rest}`} +
+ )} +
); }; // 为具体供应商渲染单个图标 const renderVendorAvatar = (vendor) => ( -
+
{vendor.icon ? getLobeHubIcon(vendor.icon, 40) : - + {vendor.name === 'unknown' ? '?' : vendor.name.charAt(0).toUpperCase()} }
); + // 渲染搜索和操作区域 + const renderSearchActions = () => ( + + ); + // 如果是全部供应商 if (filterVendor === 'all') { - return ( -
- -
- {/* 全部供应商的头像组合 */} -
- {renderAllVendorsAvatar()} -
- - {/* 供应商信息 */} -
-
-

{t('全部供应商')}

- - {t('共 {{count}} 个模型', { count: currentModelCount })} - -
- - {getVendorDescription('all')} - -
-
-
-
- ); + 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 ( -
- -
- {/* 供应商图标 */} -
- {renderVendorAvatar(currentVendor)} -
- - {/* 供应商信息 */} -
-
-

{vendorDisplayName}

- - {t('共 {{count}} 个模型', { count: currentModelCount })} - -
- - {currentVendor.description || getVendorDescription(currentVendor.name)} - -
-
-
-
- ); + return renderHeaderCard({ + title: vendorDisplayName, + count: currentModelCount, + description: currentVendor.description || getVendorDescription(currentVendor.name), + rightContent: renderVendorAvatar(currentVendor), + primaryDarkerChannel: '16 185 129' + }); }; export default PricingVendorIntro; \ No newline at end of file diff --git a/web/src/components/table/model-pricing/layout/header/PricingVendorIntroSkeleton.jsx b/web/src/components/table/model-pricing/layout/header/PricingVendorIntroSkeleton.jsx index 9af84a47..3befb88e 100644 --- a/web/src/components/table/model-pricing/layout/header/PricingVendorIntroSkeleton.jsx +++ b/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) => ( +
+ ); + const placeholder = ( -
- -
- {/* 供应商图标骨架 */} -
- {isAllVendors ? ( -
- {Array.from({ length: 4 }).map((_, index) => ( - +
+ {/* 左侧:标题和描述骨架 */} +
+
+ {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)' + })} +
+
+ {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)' + })} +
+
+ + {/* 右侧:供应商图标骨架 */} +
+ {isAllVendors ? ( +
+ {Array.from({ length: 4 }).map((_, index) => ( + rect({ width: 32, height: 32, - marginRight: index < 3 ? -8 : 0, - }} - /> - ))} -
- ) : ( - - )} -
- - {/* 供应商信息骨架 */} -
-
- - + backgroundColor: 'rgba(59, 130, 246, 0.1)', + borderRadius: 9999, + border: '1px solid rgba(59, 130, 246, 0.2)' + }, index) + ))} +
+ ) : ( + rect({ + width: 40, + height: 40, + backgroundColor: 'rgba(16, 185, 129, 0.1)', + borderRadius: 12, + border: '1px solid rgba(16, 185, 129, 0.2)' + }) + )}
-
- -
+ } + > + {/* 搜索和操作区域骨架 */} +
+ {/* 搜索框骨架 */} +
+ {rect({ + width: '100%', + height: 32, + backgroundColor: 'rgba(156, 163, 175, 0.1)', + borderRadius: 8, + border: '1px solid rgba(156, 163, 175, 0.2)' + })} +
+ + {/* 操作按钮骨架 */} + {rect({ + width: 80, + height: 32, + backgroundColor: 'rgba(59, 130, 246, 0.1)', + borderRadius: 8, + border: '1px solid rgba(59, 130, 246, 0.2)' + })} +
+ ); return ( diff --git a/web/src/components/table/model-pricing/layout/header/PricingVendorIntroWithSkeleton.jsx b/web/src/components/table/model-pricing/layout/header/PricingVendorIntroWithSkeleton.jsx index dc7cba93..572a9056 100644 --- a/web/src/components/table/model-pricing/layout/header/PricingVendorIntroWithSkeleton.jsx +++ b/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} /> ); }; diff --git a/web/src/components/table/model-pricing/layout/header/SearchActions.jsx b/web/src/components/table/model-pricing/layout/header/SearchActions.jsx new file mode 100644 index 00000000..390577a1 --- /dev/null +++ b/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 . + +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 ( +
+ {/* 搜索框 */} +
+ } + placeholder={t('模糊搜索模型名称')} + value={searchValue} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} + onChange={handleChange} + showClear + /> +
+ + {/* 操作按钮 */} + + + {/* 移动端筛选按钮 */} + {isMobile && ( + + )} +
+ ); +}; + +export default SearchActions; + + diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 1a453b1f..234b47fc 100644 --- a/web/src/i18n/locales/en.json +++ b/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)", diff --git a/web/src/index.css b/web/src/index.css index c08f6f00..fff2f1d1 100644 --- a/web/src/index.css +++ b/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;