Commit b10f28e5 by t0ng7u

🎨 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)
parent fc4d5933
...@@ -17,9 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,9 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { useMemo, useState } from 'react'; import React, { useState } from 'react';
import { Input, Button } from '@douyinfe/semi-ui';
import { IconSearch, IconCopy, IconFilter } from '@douyinfe/semi-icons';
import PricingFilterModal from '../../modal/PricingFilterModal'; import PricingFilterModal from '../../modal/PricingFilterModal';
import PricingVendorIntroWithSkeleton from './PricingVendorIntroWithSkeleton'; import PricingVendorIntroWithSkeleton from './PricingVendorIntroWithSkeleton';
...@@ -30,7 +28,6 @@ const PricingTopSection = ({ ...@@ -30,7 +28,6 @@ const PricingTopSection = ({
handleCompositionStart, handleCompositionStart,
handleCompositionEnd, handleCompositionEnd,
isMobile, isMobile,
sidebarProps,
filterVendor, filterVendor,
models, models,
filteredModels, filteredModels,
...@@ -40,69 +37,30 @@ const PricingTopSection = ({ ...@@ -40,69 +37,30 @@ const PricingTopSection = ({
}) => { }) => {
const [showFilterModal, setShowFilterModal] = useState(false); 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 ( return (
<> <>
{/* 供应商介绍区域(桌面端显示) */} {/* 供应商介绍区域(包含搜索功能) */}
{!isMobile && (
<PricingVendorIntroWithSkeleton <PricingVendorIntroWithSkeleton
loading={loading} loading={loading}
filterVendor={filterVendor} filterVendor={filterVendor}
models={filteredModels} models={filteredModels}
allModels={models} allModels={models}
t={t} t={t}
selectedRowKeys={selectedRowKeys}
copyText={copyText}
handleChange={handleChange}
handleCompositionStart={handleCompositionStart}
handleCompositionEnd={handleCompositionEnd}
isMobile={isMobile}
searchValue={searchValue}
setShowFilterModal={setShowFilterModal}
/> />
)}
{/* 搜索和操作区域 */}
{SearchAndActions}
{/* 移动端筛选Modal */} {/* 移动端筛选Modal */}
{isMobile && ( {isMobile && (
<PricingFilterModal <PricingFilterModal
visible={showFilterModal} visible={showFilterModal}
onClose={() => setShowFilterModal(false)} onClose={() => setShowFilterModal(false)}
sidebarProps={sidebarProps}
t={t} t={t}
/> />
)} )}
......
...@@ -23,48 +23,116 @@ import { Card, Skeleton } from '@douyinfe/semi-ui'; ...@@ -23,48 +23,116 @@ import { Card, Skeleton } from '@douyinfe/semi-ui';
const PricingVendorIntroSkeleton = ({ const PricingVendorIntroSkeleton = ({
isAllVendors = false 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 = ( const placeholder = (
<div className='mb-4'> <Card className="!rounded-2xl shadow-sm border-0"
<Card className="!rounded-2xl with-pastel-balls" bodyStyle={{ padding: '16px' }}> cover={
<div className="flex items-start space-x-3 md:space-x-4"> <div
{/* 供应商图标骨架 */} className="relative h-32"
<div className="flex-shrink-0 min-w-16 h-16 rounded-2xl bg-white shadow-md flex items-center justify-center px-2"> 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>
<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-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 ? ( {isAllVendors ? (
<div className="flex items-center"> <div className="flex items-center gap-2">
{Array.from({ length: 4 }).map((_, index) => ( {Array.from({ length: 4 }).map((_, index) => (
<Skeleton.Avatar rect({
key={index}
active
size="default"
style={{
width: 32, width: 32,
height: 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)
))} ))}
</div> </div>
) : ( ) : (
<Skeleton.Avatar active size="large" style={{ width: 40, height: 40, borderRadius: 8 }} /> rect({
width: 40,
height: 40,
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderRadius: 12,
border: '1px solid rgba(16, 185, 129, 0.2)'
})
)} )}
</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> </div>
<Skeleton.Paragraph
active
rows={2}
style={{ marginBottom: 0 }}
title={false}
/>
</div> </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> </div>
</Card>
{/* 操作按钮骨架 */}
{rect({
width: 80,
height: 32,
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderRadius: 8,
border: '1px solid rgba(59, 130, 246, 0.2)'
})}
</div> </div>
</Card>
); );
return ( return (
......
...@@ -27,7 +27,15 @@ const PricingVendorIntroWithSkeleton = ({ ...@@ -27,7 +27,15 @@ const PricingVendorIntroWithSkeleton = ({
filterVendor, filterVendor,
models, models,
allModels, allModels,
t t,
selectedRowKeys,
copyText,
handleChange,
handleCompositionStart,
handleCompositionEnd,
isMobile,
searchValue,
setShowFilterModal
}) => { }) => {
const showSkeleton = useMinimumLoadingTime(loading); const showSkeleton = useMinimumLoadingTime(loading);
...@@ -45,6 +53,14 @@ const PricingVendorIntroWithSkeleton = ({ ...@@ -45,6 +53,14 @@ const PricingVendorIntroWithSkeleton = ({
models={models} models={models}
allModels={allModels} allModels={allModels}
t={t} t={t}
selectedRowKeys={selectedRowKeys}
copyText={copyText}
handleChange={handleChange}
handleCompositionStart={handleCompositionStart}
handleCompositionEnd={handleCompositionEnd}
isMobile={isMobile}
searchValue={searchValue}
setShowFilterModal={setShowFilterModal}
/> />
); );
}; };
......
/*
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;
...@@ -1201,7 +1201,7 @@ ...@@ -1201,7 +1201,7 @@
"兑换码创建成功,是否下载兑换码?": "Redemption code created successfully. Do you want to download it?", "兑换码创建成功,是否下载兑换码?": "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.", "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "The redemption code will be downloaded as a text file, with the filename being the redemption code name.",
"模型价格": "Model price", "模型价格": "Model price",
"按K显示单位": "Display in K units", "按K显示单位": "Display in K",
"可用分组": "Available groups", "可用分组": "Available groups",
"您的默认分组为:{{group}},分组倍率为:{{ratio}}": "Your default group is: {{group}}, group ratio: {{ratio}}", "您的默认分组为:{{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)", "按量计费费用 = 分组倍率 × 模型倍率 × (提示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)",
......
...@@ -728,8 +728,8 @@ html.dark .with-pastel-balls::before { ...@@ -728,8 +728,8 @@ html.dark .with-pastel-balls::before {
} }
.pricing-sidebar { .pricing-sidebar {
min-width: 460px; min-width: 400px;
max-width: 460px; max-width: 400px;
height: calc(100vh - 60px); height: calc(100vh - 60px);
background-color: var(--semi-color-bg-0); background-color: var(--semi-color-bg-0);
overflow: auto; overflow: auto;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment