| @@ -33,7 +33,8 @@ func GetAllEnableAbilityWithChannels() ([]AbilityWithChannel, error) { | |||||
| err := DB.Table("abilities"). | err := DB.Table("abilities"). | ||||
| Select("abilities.*, channels.type as channel_type"). | Select("abilities.*, channels.type as channel_type"). | ||||
| Joins("left join channels on abilities.channel_id = channels.id"). | Joins("left join channels on abilities.channel_id = channels.id"). | ||||
| Where("abilities.enabled = ?", true). | |||||
| Joins("left join models on abilities.model = models.model_name"). | |||||
| Where("abilities.enabled = ? and models.model_name is NOT null", true). | |||||
| Scan(&abilities).Error | Scan(&abilities).Error | ||||
| return abilities, err | return abilities, err | ||||
| } | } | ||||
| @@ -15,6 +15,17 @@ const ( | |||||
| NameRuleSuffix | NameRuleSuffix | ||||
| ) | ) | ||||
| const ( | |||||
| ModelTypeChat = 1 + iota | |||||
| ModelTypeImage | |||||
| ModelTypeAudio | |||||
| ModelTypeVideo | |||||
| ModelTypeEmbedding | |||||
| ModelTypeRerank | |||||
| ModelTypeVision | |||||
| ModelTypeOther | |||||
| ) | |||||
| type BoundChannel struct { | type BoundChannel struct { | ||||
| Name string `json:"name"` | Name string `json:"name"` | ||||
| Type int `json:"type"` | Type int `json:"type"` | ||||
| @@ -26,6 +37,7 @@ type Model struct { | |||||
| Description string `json:"description,omitempty" gorm:"type:text"` | Description string `json:"description,omitempty" gorm:"type:text"` | ||||
| Icon string `json:"icon,omitempty" gorm:"type:varchar(128)"` | Icon string `json:"icon,omitempty" gorm:"type:varchar(128)"` | ||||
| Tags string `json:"tags,omitempty" gorm:"type:varchar(255)"` | Tags string `json:"tags,omitempty" gorm:"type:varchar(255)"` | ||||
| Type int `json:"type" gorm:"default:0"` | |||||
| VendorID int `json:"vendor_id,omitempty" gorm:"index"` | VendorID int `json:"vendor_id,omitempty" gorm:"index"` | ||||
| Endpoints string `json:"endpoints,omitempty" gorm:"type:text"` | Endpoints string `json:"endpoints,omitempty" gorm:"type:text"` | ||||
| Status int `json:"status" gorm:"default:1"` | Status int `json:"status" gorm:"default:1"` | ||||
| @@ -28,6 +28,7 @@ type Pricing struct { | |||||
| EnableGroup []string `json:"enable_groups"` | EnableGroup []string `json:"enable_groups"` | ||||
| SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"` | SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"` | ||||
| PricingVersion string `json:"pricing_version,omitempty"` | PricingVersion string `json:"pricing_version,omitempty"` | ||||
| Type int `json:"type"` | |||||
| } | } | ||||
| type PricingVendor struct { | type PricingVendor struct { | ||||
| @@ -279,13 +280,14 @@ func updatePricing() { | |||||
| // 补充模型元数据(描述、标签、供应商、状态) | // 补充模型元数据(描述、标签、供应商、状态) | ||||
| if meta, ok := metaMap[model]; ok { | if meta, ok := metaMap[model]; ok { | ||||
| // 若模型被禁用(status!=1),则直接跳过,不返回给前端 | // 若模型被禁用(status!=1),则直接跳过,不返回给前端 | ||||
| if meta.Status != 1 { | |||||
| if meta.Status != 1 || meta.Type == 0 { | |||||
| continue | continue | ||||
| } | } | ||||
| pricing.Description = meta.Description | pricing.Description = meta.Description | ||||
| pricing.Icon = meta.Icon | pricing.Icon = meta.Icon | ||||
| pricing.Tags = meta.Tags | pricing.Tags = meta.Tags | ||||
| pricing.VendorID = meta.VendorID | pricing.VendorID = meta.VendorID | ||||
| pricing.Type = meta.Type | |||||
| } | } | ||||
| modelPrice, findPrice := ratio_setting.GetModelPrice(model, false) | modelPrice, findPrice := ratio_setting.GetModelPrice(model, false) | ||||
| if findPrice { | if findPrice { | ||||
| @@ -0,0 +1,140 @@ | |||||
| /* | |||||
| 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, { useRef } from 'react'; | |||||
| import { Button, Tag, Skeleton } from '@douyinfe/semi-ui'; | |||||
| import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons'; | |||||
| import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime'; | |||||
| /** | |||||
| * 横向筛选项行,通过左右箭头翻页 | |||||
| * @param {string} title 标题 | |||||
| * @param {Array<{value:any,label:string,icon?:React.ReactNode,tagCount?:number}>} items 选项列表 | |||||
| * @param {*} activeValue 当前选中值 | |||||
| * @param {(value:any)=>void} onChange 选择回调 | |||||
| * @param {boolean} loading 加载中 | |||||
| * @param {Function} t i18n | |||||
| */ | |||||
| const HorizontalFilterRow = ({ | |||||
| title, | |||||
| items = [], | |||||
| activeValue, | |||||
| onChange, | |||||
| loading = false, | |||||
| t = (v) => v, | |||||
| }) => { | |||||
| const scrollRef = useRef(null); | |||||
| const showSkeleton = useMinimumLoadingTime(loading); | |||||
| const handleScroll = (dir) => { | |||||
| const el = scrollRef.current; | |||||
| if (!el) return; | |||||
| const step = el.clientWidth * 0.8; | |||||
| el.scrollBy({ | |||||
| left: dir === 'left' ? -step : step, | |||||
| behavior: 'smooth', | |||||
| }); | |||||
| }; | |||||
| if (showSkeleton) { | |||||
| return ( | |||||
| <div className='mb-6'> | |||||
| <Skeleton.Title active style={{ width: 60, marginBottom: 8 }} /> | |||||
| <div className='flex gap-2'> | |||||
| {[1, 2, 3, 4, 5].map((i) => ( | |||||
| <Skeleton.Button | |||||
| key={i} | |||||
| active | |||||
| style={{ width: 80, height: 32 }} | |||||
| /> | |||||
| ))} | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <div className='mb-6 last:mb-0'> | |||||
| <div className='text-sm font-medium text-semi-color-text-1 mb-3'> | |||||
| {title} | |||||
| </div> | |||||
| <div className='flex items-center gap-2'> | |||||
| <Button | |||||
| type='tertiary' | |||||
| theme='borderless' | |||||
| icon={<IconChevronLeft />} | |||||
| onClick={() => handleScroll('left')} | |||||
| className='flex-shrink-0' | |||||
| aria-label={t('向左')} | |||||
| /> | |||||
| <div | |||||
| ref={scrollRef} | |||||
| className='flex-1 min-w-0 flex gap-2 overflow-x-auto overflow-y-hidden scroll-smooth scrollbar-hide' | |||||
| style={{ | |||||
| scrollbarWidth: 'none', | |||||
| msOverflowStyle: 'none', | |||||
| WebkitOverflowScrolling: 'touch', | |||||
| }} | |||||
| > | |||||
| {items.map((item) => { | |||||
| const isDisabled = | |||||
| item.disabled || | |||||
| (typeof item.tagCount === 'number' && item.tagCount === 0); | |||||
| const isActive = activeValue === item.value; | |||||
| return ( | |||||
| <Button | |||||
| key={item.value} | |||||
| onClick={() => !isDisabled && onChange(item.value)} | |||||
| theme={isActive ? 'solid' : 'borderless'} | |||||
| type={isActive ? 'primary' : 'tertiary'} | |||||
| disabled={isDisabled} | |||||
| className='flex-shrink-0 !rounded-full' | |||||
| > | |||||
| <span className='flex items-center gap-2'> | |||||
| {item.icon} | |||||
| <span>{item.label}</span> | |||||
| {item.tagCount !== undefined && ( | |||||
| <Tag | |||||
| color={isActive ? 'white' : 'grey'} | |||||
| shape='circle' | |||||
| size='small' | |||||
| > | |||||
| {item.tagCount} | |||||
| </Tag> | |||||
| )} | |||||
| </span> | |||||
| </Button> | |||||
| ); | |||||
| })} | |||||
| </div> | |||||
| <Button | |||||
| type='tertiary' | |||||
| theme='borderless' | |||||
| icon={<IconChevronRight />} | |||||
| onClick={() => handleScroll('right')} | |||||
| className='flex-shrink-0' | |||||
| aria-label={t('向右')} | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default HorizontalFilterRow; | |||||
| @@ -74,13 +74,14 @@ const PricingCardView = ({ | |||||
| selectedRowKeys = [], | selectedRowKeys = [], | ||||
| setSelectedRowKeys, | setSelectedRowKeys, | ||||
| openModelDetail, | openModelDetail, | ||||
| gridColsClass = 'grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3', | |||||
| showPagination = true, | |||||
| }) => { | }) => { | ||||
| const showSkeleton = useMinimumLoadingTime(loading); | const showSkeleton = useMinimumLoadingTime(loading); | ||||
| const startIndex = (currentPage - 1) * pageSize; | const startIndex = (currentPage - 1) * pageSize; | ||||
| const paginatedModels = filteredModels.slice( | |||||
| startIndex, | |||||
| startIndex + pageSize, | |||||
| ); | |||||
| const paginatedModels = showPagination | |||||
| ? filteredModels.slice(startIndex, startIndex + pageSize) | |||||
| : filteredModels; | |||||
| const getModelKey = (model) => model.key ?? model.model_name ?? model.id; | const getModelKey = (model) => model.key ?? model.model_name ?? model.id; | ||||
| const isMobile = useIsMobile(); | const isMobile = useIsMobile(); | ||||
| @@ -234,7 +235,7 @@ const PricingCardView = ({ | |||||
| return ( | return ( | ||||
| <div className='px-2 pt-2'> | <div className='px-2 pt-2'> | ||||
| <div className='grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-4'> | |||||
| <div className={`grid ${gridColsClass} gap-4`}> | |||||
| {paginatedModels.map((model, index) => { | {paginatedModels.map((model, index) => { | ||||
| const modelKey = getModelKey(model); | const modelKey = getModelKey(model); | ||||
| const isSelected = selectedRowKeys.includes(modelKey); | const isSelected = selectedRowKeys.includes(modelKey); | ||||
| @@ -357,7 +358,7 @@ const PricingCardView = ({ | |||||
| </div> | </div> | ||||
| {/* 分页 */} | {/* 分页 */} | ||||
| {filteredModels.length > 0 && ( | |||||
| {showPagination && filteredModels.length > 0 && ( | |||||
| <div className='flex justify-center mt-6 py-4 border-t pricing-pagination-divider'> | <div className='flex justify-center mt-6 py-4 border-t pricing-pagination-divider'> | ||||
| <Pagination | <Pagination | ||||
| currentPage={currentPage} | currentPage={currentPage} | ||||
| @@ -39,6 +39,7 @@ export const useModelPricingData = () => { | |||||
| const [filterEndpointType, setFilterEndpointType] = useState('all'); // 端点类型筛选: 'all' | string | const [filterEndpointType, setFilterEndpointType] = useState('all'); // 端点类型筛选: 'all' | string | ||||
| const [filterVendor, setFilterVendor] = useState('all'); // 供应商筛选: 'all' | 'unknown' | string | const [filterVendor, setFilterVendor] = useState('all'); // 供应商筛选: 'all' | 'unknown' | string | ||||
| const [filterTag, setFilterTag] = useState('all'); // 模型标签筛选: 'all' | string | const [filterTag, setFilterTag] = useState('all'); // 模型标签筛选: 'all' | string | ||||
| const [filterType, setFilterType] = useState('all'); // 模型类型筛选: 'all' | number (model_meta.ModelType) | |||||
| const [pageSize, setPageSize] = useState(20); | const [pageSize, setPageSize] = useState(20); | ||||
| const [currentPage, setCurrentPage] = useState(1); | const [currentPage, setCurrentPage] = useState(1); | ||||
| const [currency, setCurrency] = useState('USD'); | const [currency, setCurrency] = useState('USD'); | ||||
| @@ -135,6 +136,12 @@ export const useModelPricingData = () => { | |||||
| }); | }); | ||||
| } | } | ||||
| // 模型类型筛选(pricing.Type / model.Type) | |||||
| if (filterType !== 'all') { | |||||
| const typeNum = Number(filterType); | |||||
| result = result.filter((model) => model.type === typeNum); | |||||
| } | |||||
| // 搜索筛选 | // 搜索筛选 | ||||
| if (searchValue.length > 0) { | if (searchValue.length > 0) { | ||||
| const searchTerm = searchValue.toLowerCase(); | const searchTerm = searchValue.toLowerCase(); | ||||
| @@ -159,6 +166,7 @@ export const useModelPricingData = () => { | |||||
| filterEndpointType, | filterEndpointType, | ||||
| filterVendor, | filterVendor, | ||||
| filterTag, | filterTag, | ||||
| filterType, | |||||
| ]); | ]); | ||||
| const rowSelection = useMemo( | const rowSelection = useMemo( | ||||
| @@ -321,6 +329,7 @@ export const useModelPricingData = () => { | |||||
| filterEndpointType, | filterEndpointType, | ||||
| filterVendor, | filterVendor, | ||||
| filterTag, | filterTag, | ||||
| filterType, | |||||
| searchValue, | searchValue, | ||||
| ]); | ]); | ||||
| @@ -350,6 +359,8 @@ export const useModelPricingData = () => { | |||||
| setFilterVendor, | setFilterVendor, | ||||
| filterTag, | filterTag, | ||||
| setFilterTag, | setFilterTag, | ||||
| filterType, | |||||
| setFilterType, | |||||
| pageSize, | pageSize, | ||||
| setPageSize, | setPageSize, | ||||
| currentPage, | currentPage, | ||||
| @@ -37,6 +37,7 @@ export const usePricingFilterCounts = ({ | |||||
| filterEndpointType = 'all', | filterEndpointType = 'all', | ||||
| filterVendor = 'all', | filterVendor = 'all', | ||||
| filterTag = 'all', | filterTag = 'all', | ||||
| filterType = 'all', | |||||
| searchValue = '', | searchValue = '', | ||||
| }) => { | }) => { | ||||
| // 均使用同一份模型列表,避免创建新引用 | // 均使用同一份模型列表,避免创建新引用 | ||||
| @@ -84,6 +85,12 @@ export const usePricingFilterCounts = ({ | |||||
| if (!tagsArr.includes(filterTag.toLowerCase())) return false; | if (!tagsArr.includes(filterTag.toLowerCase())) return false; | ||||
| } | } | ||||
| // 模型类型(pricing.Type / model.Type) | |||||
| if (!ignore.includes('type') && filterType !== 'all') { | |||||
| const typeNum = Number(filterType); | |||||
| if (model.type !== typeNum) return false; | |||||
| } | |||||
| // 搜索 | // 搜索 | ||||
| if (!ignore.includes('search') && searchValue) { | if (!ignore.includes('search') && searchValue) { | ||||
| const term = searchValue.toLowerCase(); | const term = searchValue.toLowerCase(); | ||||
| @@ -112,6 +119,7 @@ export const usePricingFilterCounts = ({ | |||||
| filterEndpointType, | filterEndpointType, | ||||
| filterVendor, | filterVendor, | ||||
| filterTag, | filterTag, | ||||
| filterType, | |||||
| searchValue, | searchValue, | ||||
| ], | ], | ||||
| ); | ); | ||||
| @@ -124,6 +132,7 @@ export const usePricingFilterCounts = ({ | |||||
| filterQuotaType, | filterQuotaType, | ||||
| filterVendor, | filterVendor, | ||||
| filterTag, | filterTag, | ||||
| filterType, | |||||
| searchValue, | searchValue, | ||||
| ], | ], | ||||
| ); | ); | ||||
| @@ -136,6 +145,7 @@ export const usePricingFilterCounts = ({ | |||||
| filterQuotaType, | filterQuotaType, | ||||
| filterEndpointType, | filterEndpointType, | ||||
| filterTag, | filterTag, | ||||
| filterType, | |||||
| searchValue, | searchValue, | ||||
| ], | ], | ||||
| ); | ); | ||||
| @@ -148,6 +158,20 @@ export const usePricingFilterCounts = ({ | |||||
| filterQuotaType, | filterQuotaType, | ||||
| filterEndpointType, | filterEndpointType, | ||||
| filterVendor, | filterVendor, | ||||
| filterType, | |||||
| searchValue, | |||||
| ], | |||||
| ); | |||||
| const typeModels = useMemo( | |||||
| () => allModels.filter((m) => matchesFilters(m, ['type'])), | |||||
| [ | |||||
| allModels, | |||||
| filterGroup, | |||||
| filterQuotaType, | |||||
| filterEndpointType, | |||||
| filterVendor, | |||||
| filterTag, | |||||
| searchValue, | searchValue, | ||||
| ], | ], | ||||
| ); | ); | ||||
| @@ -160,6 +184,7 @@ export const usePricingFilterCounts = ({ | |||||
| filterEndpointType, | filterEndpointType, | ||||
| filterVendor, | filterVendor, | ||||
| filterTag, | filterTag, | ||||
| filterType, | |||||
| searchValue, | searchValue, | ||||
| ], | ], | ||||
| ); | ); | ||||
| @@ -170,5 +195,6 @@ export const usePricingFilterCounts = ({ | |||||
| vendorModels, | vendorModels, | ||||
| groupCountModels, | groupCountModels, | ||||
| tagModels, | tagModels, | ||||
| typeModels, | |||||
| }; | }; | ||||
| }; | }; | ||||
| @@ -0,0 +1,197 @@ | |||||
| /* | |||||
| 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, { useMemo, useState } from 'react'; | |||||
| import { ImagePreview } from '@douyinfe/semi-ui'; | |||||
| import HorizontalFilterRow from '../../components/common/ui/HorizontalFilterRow'; | |||||
| import PricingCardView from '../../components/table/model-pricing/view/card/PricingCardView'; | |||||
| import ModelDetailSideSheet from '../../components/table/model-pricing/modal/ModelDetailSideSheet'; | |||||
| import { useModelPricingData } from '../../hooks/model-pricing/useModelPricingData'; | |||||
| import { usePricingFilterCounts } from '../../hooks/model-pricing/usePricingFilterCounts'; | |||||
| import { getLobeHubIcon } from '../../helpers'; | |||||
| // 与 model_meta.go ModelType 常量一致:1=Chat, 2=Image, 3=Audio, 4=Video, 5=Embedding, 6=Rerank, 7=Vision, 8=Other | |||||
| const MODEL_TYPE_LIST = [ | |||||
| { value: 1, labelKey: '对话' }, | |||||
| { value: 2, labelKey: '图像' }, | |||||
| { value: 3, labelKey: '音频' }, | |||||
| { value: 4, labelKey: '视频' }, | |||||
| { value: 5, labelKey: '嵌入' }, | |||||
| { value: 6, labelKey: '重排序' }, | |||||
| { value: 7, labelKey: '视觉' }, | |||||
| { value: 8, labelKey: '其他' }, | |||||
| ]; | |||||
| const HomePricingFilters = ({ t }) => { | |||||
| const [showRatio, setShowRatio] = useState(false); | |||||
| const pricingData = useModelPricingData(); | |||||
| const { | |||||
| vendorModels, | |||||
| typeModels, | |||||
| } = usePricingFilterCounts({ | |||||
| models: pricingData.models, | |||||
| filterGroup: pricingData.filterGroup, | |||||
| filterQuotaType: pricingData.filterQuotaType, | |||||
| filterEndpointType: pricingData.filterEndpointType, | |||||
| filterVendor: pricingData.filterVendor, | |||||
| filterTag: pricingData.filterTag, | |||||
| filterType: pricingData.filterType, | |||||
| searchValue: pricingData.searchValue, | |||||
| }); | |||||
| const vendorItems = useMemo(() => { | |||||
| const models = pricingData.models; | |||||
| const vendorSet = new Set(); | |||||
| const vendorIcons = new Map(); | |||||
| let hasUnknown = false; | |||||
| (models || []).forEach((m) => { | |||||
| if (m.vendor_name) { | |||||
| vendorSet.add(m.vendor_name); | |||||
| if (m.vendor_icon && !vendorIcons.has(m.vendor_name)) { | |||||
| vendorIcons.set(m.vendor_name, m.vendor_icon); | |||||
| } | |||||
| } else { | |||||
| hasUnknown = true; | |||||
| } | |||||
| }); | |||||
| const vendors = Array.from(vendorSet).sort(); | |||||
| const getCount = (v) => { | |||||
| if (v === 'all') return vendorModels.length; | |||||
| if (v === 'unknown') return vendorModels.filter((m) => !m.vendor_name).length; | |||||
| return vendorModels.filter((m) => m.vendor_name === v).length; | |||||
| }; | |||||
| const items = [ | |||||
| { value: 'all', label: t('全部供应商'), tagCount: getCount('all'), disabled: (models || []).length === 0 }, | |||||
| ]; | |||||
| vendors.forEach((v) => { | |||||
| const count = getCount(v); | |||||
| items.push({ | |||||
| value: v, | |||||
| label: v, | |||||
| icon: vendorIcons.get(v) ? getLobeHubIcon(vendorIcons.get(v), 16) : null, | |||||
| tagCount: count, | |||||
| disabled: count === 0, | |||||
| }); | |||||
| }); | |||||
| if (hasUnknown) { | |||||
| items.push({ | |||||
| value: 'unknown', | |||||
| label: t('未知供应商'), | |||||
| tagCount: getCount('unknown'), | |||||
| disabled: getCount('unknown') === 0, | |||||
| }); | |||||
| } | |||||
| return items; | |||||
| }, [pricingData.models, vendorModels, t]); | |||||
| const typeItems = useMemo(() => { | |||||
| const models = pricingData.models; | |||||
| const getCount = (typeVal) => { | |||||
| if (typeVal === 'all') return typeModels.length; | |||||
| const n = Number(typeVal); | |||||
| return typeModels.filter((m) => m.type === n).length; | |||||
| }; | |||||
| const items = [ | |||||
| { value: 'all', label: t('全部类型'), tagCount: getCount('all'), disabled: (models || []).length === 0 }, | |||||
| ]; | |||||
| MODEL_TYPE_LIST.forEach(({ value, labelKey }) => { | |||||
| const count = getCount(value); | |||||
| items.push({ | |||||
| value: String(value), | |||||
| label: t(labelKey), | |||||
| tagCount: count, | |||||
| disabled: count === 0, | |||||
| }); | |||||
| }); | |||||
| return items; | |||||
| }, [pricingData.models, typeModels, t]); | |||||
| return ( | |||||
| <div className='w-full mt-8 md:mt-12'> | |||||
| <div className='rounded-xl border border-semi-color-border bg-semi-color-bg-0 p-4'> | |||||
| <HorizontalFilterRow | |||||
| title={t('供应商')} | |||||
| items={vendorItems} | |||||
| activeValue={pricingData.filterVendor} | |||||
| onChange={pricingData.setFilterVendor} | |||||
| loading={pricingData.loading} | |||||
| t={t} | |||||
| /> | |||||
| <HorizontalFilterRow | |||||
| title={t('模型类型')} | |||||
| items={typeItems} | |||||
| activeValue={pricingData.filterType} | |||||
| onChange={pricingData.setFilterType} | |||||
| loading={pricingData.loading} | |||||
| t={t} | |||||
| /> | |||||
| </div> | |||||
| <div className='mt-6'> | |||||
| <PricingCardView | |||||
| filteredModels={pricingData.filteredModels} | |||||
| loading={pricingData.loading} | |||||
| rowSelection={null} | |||||
| pageSize={pricingData.pageSize} | |||||
| setPageSize={pricingData.setPageSize} | |||||
| currentPage={pricingData.currentPage} | |||||
| setCurrentPage={pricingData.setCurrentPage} | |||||
| selectedGroup={pricingData.selectedGroup} | |||||
| groupRatio={pricingData.groupRatio} | |||||
| copyText={pricingData.copyText} | |||||
| setModalImageUrl={pricingData.setModalImageUrl} | |||||
| setIsModalOpenurl={pricingData.setIsModalOpenurl} | |||||
| currency={pricingData.currency} | |||||
| tokenUnit={pricingData.tokenUnit} | |||||
| displayPrice={pricingData.displayPrice} | |||||
| showRatio={showRatio} | |||||
| t={t} | |||||
| selectedRowKeys={[]} | |||||
| openModelDetail={pricingData.openModelDetail} | |||||
| gridColsClass='grid-cols-1 md:grid-cols-2 lg:grid-cols-3' | |||||
| showPagination={false} | |||||
| /> | |||||
| </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} | |||||
| usableGroup={pricingData.usableGroup} | |||||
| currency={pricingData.currency} | |||||
| tokenUnit={pricingData.tokenUnit} | |||||
| displayPrice={pricingData.displayPrice} | |||||
| showRatio={showRatio} | |||||
| vendorsMap={pricingData.vendorsMap} | |||||
| endpointMap={pricingData.endpointMap} | |||||
| autoGroups={pricingData.autoGroups} | |||||
| t={t} | |||||
| /> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default HomePricingFilters; | |||||
| @@ -40,6 +40,7 @@ import { | |||||
| } from '@douyinfe/semi-icons'; | } from '@douyinfe/semi-icons'; | ||||
| import { Link } from 'react-router-dom'; | import { Link } from 'react-router-dom'; | ||||
| import NoticeModal from '../../components/layout/NoticeModal'; | import NoticeModal from '../../components/layout/NoticeModal'; | ||||
| import HomePricingFilters from './HomePricingFilters'; | |||||
| import { | import { | ||||
| Moonshot, | Moonshot, | ||||
| OpenAI, | OpenAI, | ||||
| @@ -162,9 +163,9 @@ const Home = () => { | |||||
| {/* 背景模糊晕染球 */} | {/* 背景模糊晕染球 */} | ||||
| <div className='blur-ball blur-ball-indigo' /> | <div className='blur-ball blur-ball-indigo' /> | ||||
| <div className='blur-ball blur-ball-teal' /> | <div className='blur-ball blur-ball-teal' /> | ||||
| <div className='flex items-center justify-center h-full px-4 py-20 md:py-24 lg:py-32 mt-10'> | |||||
| <div className='flex flex-col items-center w-full px-4 py-20 md:py-24 lg:py-32 mt-10'> | |||||
| {/* 居中内容区 */} | {/* 居中内容区 */} | ||||
| <div className='flex flex-col items-center justify-center text-center max-w-4xl mx-auto'> | |||||
| <div className='flex flex-col items-center justify-center text-center max-w-4xl mx-auto w-full'> | |||||
| <div className='flex flex-col items-center justify-center mb-6 md:mb-8'> | <div className='flex flex-col items-center justify-center mb-6 md:mb-8'> | ||||
| <h1 | <h1 | ||||
| className={`text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold text-semi-color-text-0 leading-tight ${isChinese ? 'tracking-wide md:tracking-wider' : ''}`} | className={`text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold text-semi-color-text-0 leading-tight ${isChinese ? 'tracking-wide md:tracking-wider' : ''}`} | ||||
| @@ -252,84 +253,10 @@ const Home = () => { | |||||
| )} | )} | ||||
| </div> | </div> | ||||
| {/* 框架兼容性图标 */} | |||||
| <div className='mt-12 md:mt-16 lg:mt-20 w-full'> | |||||
| <div className='flex items-center mb-6 md:mb-8 justify-center'> | |||||
| <Text | |||||
| type='tertiary' | |||||
| className='text-lg md:text-xl lg:text-2xl font-light' | |||||
| > | |||||
| {t('支持众多的大模型供应商')} | |||||
| </Text> | |||||
| </div> | |||||
| <div className='flex flex-wrap items-center justify-center gap-3 sm:gap-4 md:gap-6 lg:gap-8 max-w-5xl mx-auto px-4'> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Moonshot size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <OpenAI size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <XAI size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Zhipu.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Volcengine.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Cohere.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Claude.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Gemini.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Suno size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Minimax.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Wenxin.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Spark.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Qingyan.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <DeepSeek.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Qwen.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Midjourney size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Grok size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <AzureAI.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Hunyuan.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Xinference.Color size={40} /> | |||||
| </div> | |||||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||||
| <Typography.Text className='!text-lg sm:!text-xl md:!text-2xl lg:!text-3xl font-bold'> | |||||
| 30+ | |||||
| </Typography.Text> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| {/* 模型广场筛选项与列表 */} | |||||
| <div className='w-full max-w-6xl mt-8 px-4'> | |||||
| <HomePricingFilters t={t} /> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -29,6 +29,11 @@ export default defineConfig({ | |||||
| resolve: { | resolve: { | ||||
| alias: { | alias: { | ||||
| '@': path.resolve(__dirname, './src'), | '@': path.resolve(__dirname, './src'), | ||||
| // 绕过 semi-ui exports 限制,dist/css/semi.css 不在 package.json exports 中 | |||||
| '@douyinfe/semi-ui/dist/css/semi.css': path.resolve( | |||||
| __dirname, | |||||
| 'node_modules/@douyinfe/semi-ui/dist/css/semi.css', | |||||
| ), | |||||
| }, | }, | ||||
| }, | }, | ||||
| plugins: [ | plugins: [ | ||||