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/>.
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}
/>
)}
......
......@@ -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 (
......
......@@ -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}
/>
);
};
......
/*
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 @@
"兑换码创建成功,是否下载兑换码?": "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)",
......
......@@ -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;
......
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