tokenFactory/web/src/components/home/HomeModelList.jsx

452 lines
16 KiB
JavaScript

/*
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, { useContext, useMemo } from 'react';
import {
Input,
ImagePreview,
Button,
Collapsible,
Select,
} from '@douyinfe/semi-ui';
import { useIsMobile } from '../../hooks/common/useIsMobile';
import { IconSearch } from '@douyinfe/semi-icons';
import PricingVendors from '../table/model-pricing/filter/PricingVendors';
import PricingProviderType from '../table/model-pricing/filter/PricingProviderType';
import PricingQuotaTypes from '../table/model-pricing/filter/PricingQuotaTypes';
import PricingTags from '../table/model-pricing/filter/PricingTags';
import PricingEndpointTypes from '../table/model-pricing/filter/PricingEndpointTypes';
import PricingCardView from '../table/model-pricing/view/card/PricingCardView';
import ModelDetailSideSheet from '../table/model-pricing/modal/ModelDetailSideSheet';
import { useModelPricingData } from '../../hooks/model-pricing/useModelPricingData';
import { usePricingFilterCounts } from '../../hooks/model-pricing/usePricingFilterCounts';
import { StatusContext } from '../../context/Status';
import { UserContext } from '../../context/User';
const HomeModelList = () => {
const isMobile = useIsMobile();
const pricingData = useModelPricingData({ defaultSortKey: 'hot' });
const [statusState] = useContext(StatusContext);
const [userState] = useContext(UserContext);
const headerNavHomeConfig = useMemo(() => {
try {
const config = statusState?.status?.HeaderNavModules;
if (!config) return null;
const modules = JSON.parse(config);
const home = modules?.home;
if (typeof home === 'object' && home !== null) {
return home;
}
} catch {}
return null;
}, [statusState?.status?.HeaderNavModules]);
const blurPricing = useMemo(() => {
if (userState?.user) return false;
return !!headerNavHomeConfig?.blurPricing;
}, [headerNavHomeConfig, userState?.user]);
const showCostPrice = useMemo(
() => !!headerNavHomeConfig?.showCostPrice,
[headerNavHomeConfig],
);
const {
quotaTypeModels,
endpointTypeModels,
vendorModels,
tagModels,
supplierTypeModels,
} = usePricingFilterCounts({
models: pricingData.models,
filterGroup: pricingData.filterGroup,
filterQuotaType: pricingData.filterQuotaType,
filterEndpointType: pricingData.filterEndpointType,
filterVendor: pricingData.filterVendor,
filterTag: pricingData.filterTag,
filterSupplierType: pricingData.filterSupplierType,
searchValue: pricingData.searchValue,
});
React.useEffect(() => {
pricingData.setPageSize(40);
}, []);
const handleResetFilters = () => {
pricingData.setSearchValue('');
pricingData.setFilterVendor('all');
pricingData.setFilterQuotaType('all');
pricingData.setFilterTag('all');
pricingData.setFilterEndpointType('all');
pricingData.setFilterSupplierType?.('all');
pricingData.setFilterSupplier && pricingData.setFilterSupplier('all');
pricingData.setSortKey && pricingData.setSortKey('hot');
pricingData.setCurrentPage(1);
};
const handleFilterVendorChange = (vendor) => {
pricingData.setFilterVendor(vendor);
if (vendor === 'all') {
pricingData.setSortKey?.('hot');
} else {
pricingData.setSortKey?.('discount');
}
};
const sortSelectValue =
!pricingData.sortKey || pricingData.sortKey === 'default'
? 'hot'
: pricingData.sortKey;
const sortOptions = [
// { value: 'default', label: pricingData.t('默认') },
{ value: 'hot', label: pricingData.t('热门') },
{ value: 'price', label: pricingData.t('价格') },
{ value: 'discount', label: pricingData.t('折扣率') },
{ value: 'supplier_grade', label: pricingData.t('供应商等级') },
{ value: 'latency', label: pricingData.t('时延') },
];
return (
<div className='w-full home-model-list'>
<style>{`
.home-model-card-wrapper .grid {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
gap: 0.75rem !important;
}
@media (max-width: 767px) {
.home-model-card-wrapper .grid {
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
}
}
@media (min-width: 768px) {
.home-model-card-wrapper .grid {
grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
}
}
@media (min-width: 1280px) {
.home-model-card-wrapper .grid {
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
}
}
.home-model-layout {
display: flex;
gap: 1.5rem;
width: 100%;
}
@media (max-width: 767px) {
.home-model-layout {
flex-direction: column;
gap: 1rem;
}
}
.home-model-sidebar {
position: sticky;
top: 60px;
align-self: flex-start;
width: 280px;
min-width: 280px;
max-width: 280px;
max-height: calc(100vh - 60px);
flex-shrink: 0;
display: flex;
flex-direction: column;
}
@media (max-width: 767px) {
.home-model-sidebar {
position: static;
width: 100%;
min-width: 100%;
max-width: 100%;
max-height: none;
}
}
.home-sidebar-header {
position: sticky;
top: 0;
z-index: 11;
padding: 1rem 1rem 0.5rem 1rem;
}
.home-sidebar-filters {
flex: 1;
overflow-y: auto;
scrollbar-width: none;
padding: 0 1rem 1rem 1rem;
}
.home-sidebar-filters::-webkit-scrollbar {
display: none;
}
@media (max-width: 767px) {
.home-sidebar-header {
padding: 0 0 1rem 0;
margin-bottom: 0.5rem;
}
.home-sidebar-filters {
padding: 0;
overflow-y: visible;
}
}
.home-model-content {
flex: 1;
min-width: 0;
}
.home-search-wrapper {
display: block;
}
.home-search-wrapper-mobile {
display: block;
}
@media (min-width: 768px) {
.home-search-wrapper-mobile {
display: none !important;
}
}
@media (max-width: 767px) {
.home-search-wrapper {
display: none !important;
}
.home-model-layout {
display: flex;
flex-direction: column;
}
.home-search-wrapper-mobile {
order: 1;
width: 100%;
margin-bottom: 1rem;
}
.home-model-sidebar {
order: 2;
padding-left: 0;
padding-right: 0;
}
.home-model-content {
order: 3;
padding-left: 0 !important;
padding-right: 0 !important;
}
}
`}</style>
<div className='home-model-layout'>
{/* 移动端搜索框 */}
<div className='home-search-wrapper-mobile'>
<div className='flex flex-col gap-2 w-full'>
<Input
prefix={<IconSearch />}
placeholder={pricingData.t('模糊搜索模型名称')}
value={pricingData.searchValue}
onCompositionStart={pricingData.handleCompositionStart}
onCompositionEnd={pricingData.handleCompositionEnd}
onChange={pricingData.handleChange}
showClear
size='large'
/>
<Select
size='large'
style={{ width: '100%' }}
value={sortSelectValue}
onChange={(v) =>
pricingData.setSortKey && pricingData.setSortKey(v)
}
optionList={sortOptions}
prefix={pricingData.t('排序')}
/>
</div>
</div>
<div className='home-model-sidebar'>
<div className='home-sidebar-header'>
<div className='flex items-center justify-between mb-4'>
<div className='text-lg font-semibold text-gray-800'>
{pricingData.t('筛选')}
</div>
<Button
theme='outline'
type='tertiary'
onClick={handleResetFilters}
className='text-gray-500 hover:text-gray-700'
>
{pricingData.t('重置')}
</Button>
</div>
</div>
<div className='home-sidebar-filters'>
<PricingVendors
filterVendor={pricingData.filterVendor}
setFilterVendor={handleFilterVendorChange}
models={vendorModels}
allModels={pricingData.models}
loading={pricingData.loading}
t={pricingData.t}
/>
{/* <PricingQuotaTypes
filterQuotaType={pricingData.filterQuotaType}
setFilterQuotaType={pricingData.setFilterQuotaType}
models={quotaTypeModels}
loading={pricingData.loading}
t={pricingData.t}
/> */}
<PricingTags
filterTag={pricingData.filterTag}
setFilterTag={pricingData.setFilterTag}
models={tagModels}
allModels={pricingData.models}
loading={pricingData.loading}
t={pricingData.t}
/>
<PricingProviderType
filterProviderType={pricingData.filterSupplierType}
setFilterProviderType={pricingData.setFilterSupplierType}
models={pricingData.models}
countModels={supplierTypeModels}
loading={pricingData.loading}
t={pricingData.t}
/>
{/* <PricingEndpointTypes
filterEndpointType={pricingData.filterEndpointType}
setFilterEndpointType={pricingData.setFilterEndpointType}
models={endpointTypeModels}
allModels={pricingData.models}
loading={pricingData.loading}
t={pricingData.t}
/> */}
</div>
</div>
<div className='home-model-content px-4'>
<div
className={`home-search-wrapper ${isMobile ? 'w-full mb-4' : 'w-full sticky top-[75px] my-4 rounded-xl'}`}
style={{ zIndex: 100, backgroundColor: 'transparent' }}
>
<div className='flex items-center gap-2 w-full'>
<Input
prefix={<IconSearch />}
placeholder={pricingData.t('模糊搜索模型名称')}
value={pricingData.searchValue}
onCompositionStart={pricingData.handleCompositionStart}
onCompositionEnd={pricingData.handleCompositionEnd}
onChange={pricingData.handleChange}
showClear
size='large'
className='flex-1'
style={{
backgroundColor: 'var(--semi-color-bg-1)',
opacity: 1,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}
/>
<Select
size='large'
style={{
width: 180,
backgroundColor: 'var(--semi-color-bg-1)',
opacity: 1,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}
value={sortSelectValue}
onChange={(v) =>
pricingData.setSortKey && pricingData.setSortKey(v)
}
optionList={sortOptions}
prefix={pricingData.t('排序')}
/>
</div>
</div>
<div className='home-model-card-wrapper'>
<PricingCardView
filteredModels={pricingData.filteredModels}
loading={pricingData.loading}
pageSize={pricingData.pageSize}
setPageSize={pricingData.setPageSize}
currentPage={pricingData.currentPage}
setCurrentPage={pricingData.setCurrentPage}
selectedGroup={pricingData.selectedGroup}
groupRatio={pricingData.groupRatio}
groupModelPrice={pricingData.groupModelPrice}
groupModelRatio={pricingData.groupModelRatio}
currency={pricingData.currency}
siteDisplayType={pricingData.siteDisplayType}
tokenUnit={pricingData.tokenUnit}
displayPrice={pricingData.displayPrice}
channelVideoRatio={pricingData.channelVideoRatio}
channelVideoCompletionRatio={
pricingData.channelVideoCompletionRatio
}
channelVideoPrice={pricingData.channelVideoPrice}
t={pricingData.t}
openModelDetail={pricingData.openModelDetail}
showSizeChanger={false}
blurPricing={blurPricing}
searchValue={pricingData.searchValue}
/>
</div>
</div>
</div>
<ImagePreview
src={pricingData.modalImageUrl}
visible={pricingData.isModalOpenurl}
onVisibleChange={(visible) => pricingData.setIsModalOpenurl(visible)}
/>
<ModelDetailSideSheet
visible={pricingData.showModelDetail}
onClose={pricingData.closeModelDetail}
modelData={pricingData.selectedModel}
groupRatio={pricingData.groupRatio}
groupModelPrice={pricingData.groupModelPrice}
groupModelRatio={pricingData.groupModelRatio}
usableGroup={pricingData.usableGroup}
currency={pricingData.currency}
siteDisplayType={pricingData.siteDisplayType}
tokenUnit={pricingData.tokenUnit}
displayPrice={pricingData.displayPrice}
showRatio={false}
vendorsMap={pricingData.vendorsMap}
endpointMap={pricingData.endpointMap}
autoGroups={pricingData.autoGroups}
t={pricingData.t}
selectedGroup={pricingData.selectedGroup}
blurPricing={blurPricing}
showCostPrice={showCostPrice}
channelModelRatioMap={pricingData.channelModelRatio}
channelModelPriceMap={pricingData.channelModelPrice}
channelCompletionRatioMap={pricingData.channelCompletionRatio}
channelCacheRatioMap={pricingData.channelCacheRatio}
channelCreateCacheRatioMap={pricingData.channelCreateCacheRatio}
channelImageRatioMap={pricingData.channelImageRatio}
channelImagePriceMap={pricingData.channelImagePrice}
channelAudioRatioMap={pricingData.channelAudioRatio}
channelAudioCompletionRatioMap={pricingData.channelAudioCompletionRatio}
channelVideoRatioMap={pricingData.channelVideoRatio}
channelVideoCompletionRatioMap={pricingData.channelVideoCompletionRatio}
channelVideoPriceMap={pricingData.channelVideoPrice}
/>
</div>
);
};
export default HomeModelList;