diff --git a/web/src/components/table/model-pricing/layout/ModelsContent.jsx b/web/src/components/table/model-pricing/layout/ModelsContent.jsx new file mode 100644 index 0000000..6199a80 --- /dev/null +++ b/web/src/components/table/model-pricing/layout/ModelsContent.jsx @@ -0,0 +1,75 @@ +/* +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. + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { Search } from 'lucide-react'; +import PricingCardView from '../view/card/PricingCardView'; + +const TYPE_TABS = [ + { key: 'all', labelKey: '全部' }, + { key: 'text', labelKey: '文本' }, + { key: 'image', labelKey: '图像' }, + { key: 'audio', labelKey: '音频' }, + { key: 'video', labelKey: '视频' }, +]; + +const ModelsContent = ({ + filteredModels, + filterEndpointType, setFilterEndpointType, + searchValue, setSearchValue, + loading, + isMobile, + blurPricing, + t, + ...cardProps +}) => { + return ( + <> + {/* Type Tabs */} +
+ {TYPE_TABS.map((tab) => ( + + ))} +
+
+ + setSearchValue(e.target.value)} + className='w-48 bg-muted/30 border border-input rounded-lg pl-9 pr-3 py-1.5 text-sm text-foreground placeholder-muted-foreground/50 focus:outline-none focus:border-ring focus:bg-muted/50 transition-all' + /> +
+
+ + {/* Model Cards Grid */} +
+ +
+ + ); +}; + +export default ModelsContent; diff --git a/web/src/components/table/model-pricing/layout/ModelsSidebar.jsx b/web/src/components/table/model-pricing/layout/ModelsSidebar.jsx new file mode 100644 index 0000000..e308ad7 --- /dev/null +++ b/web/src/components/table/model-pricing/layout/ModelsSidebar.jsx @@ -0,0 +1,175 @@ +/* +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. + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { usePricingFilterCounts } from '../../../../hooks/model-pricing/usePricingFilterCounts'; +import { getLobeHubIcon } from '../../../../helpers'; + +const ModelsSidebar = ({ + models, + filterVendor, setFilterVendor, + filterSupplierType, setFilterSupplierType, + filterTag, setFilterTag, + filterSupplier, setFilterSupplier, + filterGroup, filterQuotaType, filterEndpointType, searchValue, + loading, t, +}) => { + const { vendorModels, tagModels, supplierTypeModels } = usePricingFilterCounts({ + models, + filterGroup, + filterQuotaType, + filterEndpointType, + filterVendor, + filterTag, + filterSupplierType, + searchValue, + }); + + // ─── 作者 (Vendors) ─── + const vendors = React.useMemo(() => { + const names = [...new Set(models.map((m) => m.vendor_name).filter(Boolean))].sort(); + return names.map((name) => { + const icon = models.find((m) => m.vendor_name === name)?.vendor_icon; + return { + name, + icon, + count: vendorModels.filter((m) => m.vendor_name === name).length, + }; + }); + }, [models, vendorModels]); + + // ─── 服务商 (Supplier Types) ─── + const supplierTypes = React.useMemo(() => { + const types = new Set(); + models.forEach((m) => { + (m.channel_list || []).forEach((ch) => { + if (ch.supplier_type) types.add(ch.supplier_type); + }); + }); + return [...types].sort(); + }, [models]); + + // ─── 输入模态 (Tags) ─── + const tags = React.useMemo(() => { + const allTags = new Set(); + models.forEach((m) => { + if (m.tags) { + String(m.tags).split(/[,;|]/).map(s => s.trim()).filter(Boolean).forEach(t => allTags.add(t.toLowerCase())); + } + }); + return [...allTags].sort(); + }, [models]); + + // ─── 供应商 (Suppliers) ─── + const suppliers = React.useMemo(() => { + const aliases = new Set(); + models.forEach((m) => { + (m.channel_list || []).forEach((ch) => { + if (ch.supplier_alias) aliases.add(ch.supplier_alias); + }); + }); + return [...aliases].sort(); + }, [models]); + + const FilterItem = ({ label, count, active, onClick }) => ( +
+ {label} + {count} +
+ ); + + return ( +
+ {/* 作者 */} +
+
{t('作者')}
+ setFilterVendor('all')} /> + {vendors.map((v) => ( + + {v.icon && ( + { e.target.style.display = 'none'; }} + /> + )} + {v.name} + + } + count={v.count} + active={filterVendor === v.name} + onClick={() => setFilterVendor(filterVendor === v.name ? 'all' : v.name)} + /> + ))} +
+ + {/* 服务商 */} +
+
{t('服务商')}
+ setFilterSupplierType('all')} /> + {supplierTypes.map((type) => ( + + (m.channel_list || []).some((ch) => ch.supplier_type === type) + ).length} + active={filterSupplierType === type} + onClick={() => setFilterSupplierType(filterSupplierType === type ? 'all' : type)} + /> + ))} +
+ + {/* 输入模态 */} +
+
{t('输入模态')}
+ setFilterTag('all')} /> + {tags.map((tag) => ( + + m.tags && String(m.tags).split(/[,;|]/).map(s => s.trim()).includes(tag) + ).length} + active={filterTag === tag} + onClick={() => setFilterTag(filterTag === tag ? 'all' : tag)} + /> + ))} +
+ + {/* 供应商 */} +
+
{t('供应商')}
+ setFilterSupplier('all')} /> + {suppliers.map((sup) => ( + + (m.channel_list || []).some((ch) => ch.supplier_alias === sup) + ).length} + active={filterSupplier === sup} + onClick={() => setFilterSupplier(filterSupplier === sup ? 'all' : sup)} + /> + ))} +
+
+ ); +}; + +export default ModelsSidebar; diff --git a/web/src/components/table/model-pricing/layout/PricingPage.jsx b/web/src/components/table/model-pricing/layout/PricingPage.jsx index 5f449df..5641e16 100644 --- a/web/src/components/table/model-pricing/layout/PricingPage.jsx +++ b/web/src/components/table/model-pricing/layout/PricingPage.jsx @@ -18,9 +18,9 @@ For commercial licensing, please contact support@quantumnous.com */ import React, { useContext, useMemo } from 'react'; -import { Layout, ImagePreview } from '@douyinfe/semi-ui'; -import PricingSidebar from './PricingSidebar'; -import PricingContent from './content/PricingContent'; +import { ImagePreview } from '@douyinfe/semi-ui'; +import ModelsSidebar from './ModelsSidebar'; +import ModelsContent from './ModelsContent'; import ModelDetailSideSheet from '../modal/ModelDetailSideSheet'; import { useModelPricingData } from '../../../../hooks/model-pricing/useModelPricingData'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; @@ -29,7 +29,6 @@ import { UserContext } from '../../../../context/User'; const PricingPage = () => { const pricingData = useModelPricingData(); - const { Sider, Content } = Layout; const isMobile = useIsMobile(); const [showRatio, setShowRatio] = React.useState(false); const [viewMode, setViewMode] = React.useState('card'); @@ -60,22 +59,22 @@ const PricingPage = () => { }; return ( -
- +
+
{!isMobile && ( - - - + )} - - + - - + +
0 时使用 grid 布局(如 gridCols=2 表示 2 列) }) => { const showSkeleton = useMinimumLoadingTime(loading); const startIndex = (currentPage - 1) * pageSize; @@ -858,8 +859,8 @@ const PricingCardView = ({ return ( <> -
-
+
0 ? '' : 'px-2 pt-2'}> +
0 ? 'grid gap-4' : 'flex flex-wrap gap-4'} style={gridCols > 0 ? { gridTemplateColumns: `repeat(${gridCols}, 1fr)` } : undefined}> {paginatedModels.map((model, index) => { const modelKey = getModelKey(model); const isSelected = selectedRowKeys.includes(modelKey); @@ -884,7 +885,7 @@ const PricingCardView = ({ return ( 0 ? '!w-full' : 'flex-1 min-w-[350px] max-w-[600px]'} !rounded-2xl transition-all duration-200 hover:shadow-lg border ${blurPricing ? '' : 'cursor-pointer'} ${isSelected ? CARD_STYLES.selected : CARD_STYLES.default}`} bodyStyle={{ height: '100%' }} onClick={() => !blurPricing && openModelDetail && openModelDetail(model) diff --git a/web/src/index.css b/web/src/index.css index 7b07468..ad6a070 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1432,6 +1432,130 @@ html.dark .with-pastel-balls::before { min-height: 0; } +/* ─── Models 新布局 ─── */ +.models-page-layout { + display: flex; + max-width: 1440px; + margin: 0 auto; + padding: 0 24px; + gap: 32px; + height: calc(100vh - 60px); + overflow: hidden; +} + +.models-sidebar { + width: 280px; + flex-shrink: 0; + overflow-y: auto; + padding: 24px 16px 24px 0; + border-right: 1px solid var(--border-color, #e5e7eb); +} + +.models-sidebar::-webkit-scrollbar { + width: 4px; +} +.models-sidebar::-webkit-scrollbar-thumb { + background: transparent; +} +.models-sidebar:hover::-webkit-scrollbar-thumb { + background: rgba(0,0,0,0.15); + border-radius: 2px; +} + +.models-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.models-tabs-bar { + display: flex; + gap: 8px; + padding: 16px 0; + flex-shrink: 0; + border-bottom: 1px solid var(--border-color, #e5e7eb); +} + +.models-tab-btn { + padding: 6px 20px; + border-radius: 20px; + border: 1px solid var(--border-color, #d1d5db); + background: transparent; + cursor: pointer; + font-size: 14px; + color: var(--text-muted, #6b7280); + transition: all 0.2s; + white-space: nowrap; +} +.models-tab-btn:hover { + border-color: var(--primary-color, #3b82f6); + color: var(--primary-color, #3b82f6); +} +.models-tab-btn.active { + background: var(--primary-color, #3b82f6); + color: white; + border-color: var(--primary-color, #3b82f6); +} + +.models-card-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + padding: 16px 0; + overflow-y: auto; + flex: 1; +} + +.models-card-grid::-webkit-scrollbar { + width: 6px; +} +.models-card-grid::-webkit-scrollbar-thumb { + background: rgba(0,0,0,0.12); + border-radius: 3px; +} + +/* 筛选面板标题 */ +.models-filter-section { + margin-bottom: 24px; +} +.models-filter-title { + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted, #6b7280); + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border-color, #e5e7eb); +} + +.models-filter-item { + display: flex; + align-items: center; + padding: 6px 10px; + border-radius: 8px; + cursor: pointer; + transition: all 0.15s; + font-size: 14px; + color: var(--text-color, #374151); + gap: 8px; +} +.models-filter-item:hover { + background: var(--hover-bg, #f3f4f6); +} +.models-filter-item.active { + background: var(--primary-light, #eff6ff); + color: var(--primary-color, #3b82f6); + font-weight: 500; +} +.models-filter-item .count { + margin-left: auto; + font-size: 12px; + color: var(--text-muted, #9ca3af); +} + /* ==================== semi-ui 缁勪欢鑷畾涔夋牱寮?==================== */ .semi-card-header, .semi-card-body {