diff --git a/app/assets/material-library/page.tsx b/app/assets/material-library/page.tsx index 92fcc73..1295ede 100644 --- a/app/assets/material-library/page.tsx +++ b/app/assets/material-library/page.tsx @@ -107,7 +107,10 @@ export default function MaterialLibraryPage() { const [selectedScene, setSelectedScene] = useState('') const [selectedMaterialType, setSelectedMaterialType] = useState('') // 素材类型筛选,默认展示全部 // const [selectedMaterialType, setSelectedMaterialType] = useState('3D场景') // 素材类型筛选,默认展示3D场景(已注释,改为默认展示全部) - const [selectedPlatform, setSelectedPlatform] = useState('') // 平台分类筛选 + const [selectedPlatform, setSelectedPlatform] = useState('') + const [selectedLivePlatform, setSelectedLivePlatform] = useState('') + const [liveDateFrom, setLiveDateFrom] = useState('') + const [liveDateTo, setLiveDateTo] = useState('') // 平台分类筛选 const [selectedProductCategory, setSelectedProductCategory] = useState('') // 品类筛选 const [categories, setCategories] = useState([]) const [scenes, setScenes] = useState([]) @@ -231,6 +234,9 @@ export default function MaterialLibraryPage() { if (selectedScene) params.append('scene', selectedScene) if (selectedMaterialType) params.append('materialType', selectedMaterialType) if (selectedPlatform) params.append('platform', selectedPlatform) + if (selectedLivePlatform) params.append('livePlatform', selectedLivePlatform) + if (liveDateFrom) params.append('liveDateFrom', liveDateFrom) + if (liveDateTo) params.append('liveDateTo', liveDateTo) if (selectedProductCategory) params.append('productCategory', selectedProductCategory) if (selectedMonth) params.append('month', selectedMonth) if (includeSort) { @@ -315,6 +321,9 @@ export default function MaterialLibraryPage() { if (selectedCategory) params.set('category', selectedCategory) if (selectedScene) params.set('scene', selectedScene) if (selectedPlatform) params.set('platform', selectedPlatform) + if (selectedLivePlatform) params.set('livePlatform', selectedLivePlatform) + if (liveDateFrom) params.set('liveDateFrom', liveDateFrom) + if (liveDateTo) params.set('liveDateTo', liveDateTo) if (selectedProductCategory) params.set('productCategory', selectedProductCategory) if (selectedMonth) params.set('month', selectedMonth) if (sortBy && sortBy !== 'createdAt') params.set('sortBy', sortBy) @@ -1386,6 +1395,12 @@ export default function MaterialLibraryPage() { setSelectedScene={setSelectedScene} selectedPlatform={selectedPlatform} setSelectedPlatform={setSelectedPlatform} + selectedLivePlatform={selectedLivePlatform} + setSelectedLivePlatform={setSelectedLivePlatform} + liveDateFrom={liveDateFrom} + setLiveDateFrom={setLiveDateFrom} + liveDateTo={liveDateTo} + setLiveDateTo={setLiveDateTo} selectedProductCategory={selectedProductCategory} setSelectedProductCategory={setSelectedProductCategory} selectedMonth={selectedMonth} diff --git a/app/materials/page.tsx b/app/materials/page.tsx index 5a7e051..3b49f9a 100644 --- a/app/materials/page.tsx +++ b/app/materials/page.tsx @@ -120,6 +120,9 @@ export default function Home() { const [selectedMaterialType, setSelectedMaterialType] = useState('') // 素材类型筛选,默认展示全部 // const [selectedMaterialType, setSelectedMaterialType] = useState('3D场景') // 素材类型筛选,默认展示3D场景(已注释,改为默认展示全部) const [selectedPlatform, setSelectedPlatform] = useState('') // 平台分类筛选 + const [selectedLivePlatform, setSelectedLivePlatform] = useState('') + const [liveDateFrom, setLiveDateFrom] = useState('') + const [liveDateTo, setLiveDateTo] = useState('') const [selectedProductCategory, setSelectedProductCategory] = useState('') // 品类筛选 const [categories, setCategories] = useState([]) const [scenes, setScenes] = useState([]) @@ -205,6 +208,9 @@ export default function Home() { if (selectedScene) params.append('scene', selectedScene) if (selectedMaterialType) params.append('materialType', selectedMaterialType) if (selectedPlatform) params.append('platform', selectedPlatform) + if (selectedLivePlatform) params.append('livePlatform', selectedLivePlatform) + if (liveDateFrom) params.append('liveDateFrom', liveDateFrom) + if (liveDateTo) params.append('liveDateTo', liveDateTo) if (selectedProductCategory) params.append('productCategory', selectedProductCategory) if (selectedMonth) params.append('month', selectedMonth) if (includeSort) { @@ -287,6 +293,9 @@ export default function Home() { if (selectedCategory) params.set('category', selectedCategory) if (selectedScene) params.set('scene', selectedScene) if (selectedPlatform) params.set('platform', selectedPlatform) + if (selectedLivePlatform) params.set('livePlatform', selectedLivePlatform) + if (liveDateFrom) params.set('liveDateFrom', liveDateFrom) + if (liveDateTo) params.set('liveDateTo', liveDateTo) if (selectedProductCategory) params.set('productCategory', selectedProductCategory) if (selectedMonth) params.set('month', selectedMonth) if (sortBy && sortBy !== 'createdAt') params.set('sortBy', sortBy) @@ -1417,6 +1426,12 @@ export default function Home() { setSelectedScene={setSelectedScene} selectedPlatform={selectedPlatform} setSelectedPlatform={setSelectedPlatform} + selectedLivePlatform={selectedLivePlatform} + setSelectedLivePlatform={setSelectedLivePlatform} + liveDateFrom={liveDateFrom} + setLiveDateFrom={setLiveDateFrom} + liveDateTo={liveDateTo} + setLiveDateTo={setLiveDateTo} selectedProductCategory={selectedProductCategory} setSelectedProductCategory={setSelectedProductCategory} selectedMonth={selectedMonth} diff --git a/components/UploadModal.tsx b/components/UploadModal.tsx index e521d29..3bc5ee3 100644 --- a/components/UploadModal.tsx +++ b/components/UploadModal.tsx @@ -1044,6 +1044,23 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) { const handleBatchFilesUpload = async () => { if (batchFiles.length === 0) return + if (materialType === '直播切片') { + const missingTitle = batchFiles.some((item) => !item.productModel.trim()) + if (missingTitle) { + setError('请为每个切片填写标题') + return + } + const liveErr: string[] = [] + if (!liveDate.trim()) liveErr.push('请选择直播日期') + if (livePlatform.trim() && !['抖音', '快手', '视频号', '淘宝直播', 'B站'].includes(livePlatform.trim())) { + liveErr.push('请选择有效的直播平台') + } + if (liveErr.length > 0) { + setError(liveErr.join(';')) + return + } + } + setUploading(true) setIsProcessing(false) setUploadProgress(0) @@ -1115,6 +1132,9 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) { : item.sceneName, platformName: item.platformName || '', productCategoryName: item.productCategoryName || '', + ...(materialType === '直播切片' + ? { liveDate, liveHost, liveHostUserId, livePlatform } + : {}), } // 添加渲染器(3D场景) @@ -2653,7 +2673,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) { disabled={interactionLocked} /> {/* 批量上传文件选择 */} - {!isTaskOtherMaterial && (materialType === '音乐' || isBatchVideoMaterialType(materialType) || materialType === '3D场景' || materialType === '品牌VI灵感') && ( + {!isTaskOtherMaterial && (materialType === '音乐' || materialType === '直播切片' || isBatchVideoMaterialType(materialType) || materialType === '3D场景' || materialType === '品牌VI灵感') && ( <> {materialType === '音乐' && '选择音乐文件(可多选)'} - {isBatchVideoMaterialType(materialType) && '选择视频文件(可多选,最多8个)'} + {materialType === '直播切片' && '选择视频文件(可多选,最多20个)'} +{isBatchVideoMaterialType(materialType) && '选择视频文件(可多选,最多8个)'} {materialType === '3D场景' && '选择3D文件或压缩包(可多选,最多5个)'} {materialType === '品牌VI灵感' && '选择PDF文件(可多选,最多50个)'} diff --git a/components/home/FilterPanel.tsx b/components/home/FilterPanel.tsx index bfaf53c..a632d39 100644 --- a/components/home/FilterPanel.tsx +++ b/components/home/FilterPanel.tsx @@ -32,6 +32,12 @@ interface FilterPanelProps { setSelectedScene: (value: string) => void selectedPlatform: string setSelectedPlatform: (value: string) => void + selectedLivePlatform?: string + setSelectedLivePlatform?: (value: string) => void + liveDateFrom?: string + setLiveDateFrom?: (value: string) => void + liveDateTo?: string + setLiveDateTo?: (value: string) => void selectedProductCategory: string setSelectedProductCategory: (value: string) => void selectedMonth: string @@ -61,6 +67,9 @@ function FilterPanel(props: FilterPanelProps) { selectedCategory, setSelectedCategory, selectedScene, setSelectedScene, selectedPlatform, setSelectedPlatform, + selectedLivePlatform = '', setSelectedLivePlatform, + liveDateFrom = '', setLiveDateFrom, + liveDateTo = '', setLiveDateTo, selectedProductCategory, setSelectedProductCategory, selectedMonth, setSelectedMonth, sortBy, setSortBy, sortOrder, setSortOrder, @@ -328,6 +337,45 @@ function FilterPanel(props: FilterPanelProps) { {rankedPlatforms.map((item) => )} } + {setSelectedLivePlatform && ( +
+

直播平台

+
+ {['', '抖音', '快手', '视频号', '淘宝直播', 'B站'].map((name) => ( + + ))} +
+
+ )} + {(setLiveDateFrom || setLiveDateTo) && ( +
+

直播日期

+
+ setLiveDateFrom?.(e.target.value)} + className={`${chipBase} cursor-text`} + /> + 至 + setLiveDateTo?.(e.target.value)} + className={`${chipBase} cursor-text`} + /> +
+
+ )} {(rankedProductCategories.length > 0 || facetsLoading) &&

产品类别

diff --git a/components/upload/file-handlers.ts b/components/upload/file-handlers.ts index 4b520ab..1539f2f 100644 --- a/components/upload/file-handlers.ts +++ b/components/upload/file-handlers.ts @@ -4,12 +4,14 @@ import { BATCH_LIMITS } from './upload-constants' import { isValidMainFileType, extractSpuFromFileName } from './upload-utils' import { requiresRenderer, shouldAutoFillGeneral } from '@/lib/material-validation' import { isBatchVideoMaterialType } from '@/lib/video-material-types' +import { isLiveClipType } from '@/lib/live-clip' /** * 获取批量上传限制 */ export function getBatchLimit(materialType: string): number { if (materialType === '音乐') return BATCH_LIMITS.music + if (isLiveClipType(materialType)) return 20 if (isBatchVideoMaterialType(materialType)) return BATCH_LIMITS.video if (materialType === '3D场景') return BATCH_LIMITS['3d'] if (materialType === '品牌VI灵感') return BATCH_LIMITS.brandVI @@ -28,9 +30,12 @@ export function createBatchFileItems( productCategoryName: string, renderer: string ): BatchFileItem[] { + const isLiveClip = isLiveClipType(materialType) return files.map((file, index) => { - const extractedSpu = extractSpuFromFileName(file.name) - + const extractedSpu = isLiveClip + ? file.name.replace(/\.[^.]+$/, '') + : extractSpuFromFileName(file.name) + return { id: `${Date.now()}-${index}`, file, diff --git a/components/upload/special-batch-uploaders.ts b/components/upload/special-batch-uploaders.ts index 20fafaf..4067fe7 100644 --- a/components/upload/special-batch-uploaders.ts +++ b/components/upload/special-batch-uploaders.ts @@ -307,6 +307,10 @@ export async function completeBatchUpload(uploadId: string, metadata: Record> selectedPlatform: string setSelectedPlatform: React.Dispatch> + selectedLivePlatform: string + setSelectedLivePlatform: React.Dispatch> + liveDateFrom: string + setLiveDateFrom: React.Dispatch> + liveDateTo: string + setLiveDateTo: React.Dispatch> selectedProductCategory: string setSelectedProductCategory: React.Dispatch> selectedMonth: string @@ -149,6 +155,9 @@ export function useHomeState(): UseHomeStateReturn { const [selectedScene, setSelectedScene] = useState('') const [selectedMaterialType, setSelectedMaterialType] = useState('') const [selectedPlatform, setSelectedPlatform] = useState('') + const [selectedLivePlatform, setSelectedLivePlatform] = useState('') + const [liveDateFrom, setLiveDateFrom] = useState('') + const [liveDateTo, setLiveDateTo] = useState('') const [selectedProductCategory, setSelectedProductCategory] = useState('') const [selectedMonth, setSelectedMonth] = useState('') @@ -234,6 +243,9 @@ export function useHomeState(): UseHomeStateReturn { if (selectedCategory) params.set('category', selectedCategory) if (selectedScene) params.set('scene', selectedScene) if (selectedPlatform) params.set('platform', selectedPlatform) + if (selectedLivePlatform) params.set('livePlatform', selectedLivePlatform) + if (liveDateFrom) params.set('liveDateFrom', liveDateFrom) + if (liveDateTo) params.set('liveDateTo', liveDateTo) if (selectedProductCategory) params.set('productCategory', selectedProductCategory) if (selectedMonth) params.set('month', selectedMonth) if (sortBy && sortBy !== 'createdAt') params.set('sortBy', sortBy) @@ -241,7 +253,7 @@ export function useHomeState(): UseHomeStateReturn { const newUrl = params.toString() ? `${window.location.pathname}?${params.toString()}` : window.location.pathname window.history.replaceState({}, '', newUrl) - }, [searchKeyword, selectedMaterialType, selectedCategory, selectedScene, selectedPlatform, selectedProductCategory, selectedMonth, sortBy, sortOrder]) + }, [searchKeyword, selectedMaterialType, selectedCategory, selectedScene, selectedPlatform, selectedLivePlatform, liveDateFrom, liveDateTo, selectedProductCategory, selectedMonth, sortBy, sortOrder]) return { // 素材数据 @@ -263,6 +275,12 @@ export function useHomeState(): UseHomeStateReturn { setSelectedMaterialType, selectedPlatform, setSelectedPlatform, + selectedLivePlatform, + setSelectedLivePlatform, + liveDateFrom, + setLiveDateFrom, + liveDateTo, + setLiveDateTo, selectedProductCategory, setSelectedProductCategory, selectedMonth, diff --git a/lib/material-query.ts b/lib/material-query.ts index 9465e46..548aef0 100644 --- a/lib/material-query.ts +++ b/lib/material-query.ts @@ -400,6 +400,10 @@ export async function buildMaterialWhereFromSearchParams( const materialType = searchParams.get('materialType') || '' const platform = searchParams.get('platform') || '' const productCategory = searchParams.get('productCategory') || '' + const livePlatform = searchParams.get('livePlatform') || '' + const liveDateFrom = searchParams.get('liveDateFrom') || '' + const liveDateTo = searchParams.get('liveDateTo') || '' + const liveHost = searchParams.get('liveHost') || '' const month = searchParams.get('month') || '' if (context.searchOrConditions) { @@ -415,6 +419,21 @@ export async function buildMaterialWhereFromSearchParams( if (platform && options.exclude !== 'platform') { where.platformName = platform } + if (livePlatform) { + where.livePlatform = livePlatform + } + if (liveHost) { + where.liveHost = { contains: liveHost, mode: 'insensitive' } + } + if (liveDateFrom || liveDateTo) { + where.liveDate = {} + if (liveDateFrom) { + where.liveDate.gte = new Date(`${liveDateFrom}T00:00:00.000Z`) + } + if (liveDateTo) { + where.liveDate.lte = new Date(`${liveDateTo}T23:59:59.999Z`) + } + } if (productCategory && options.exclude !== 'productCategory') { where.productCategoryName = productCategory } diff --git a/prisma/migrations/20260922_add_live_clip_fields/migration.sql b/prisma/migrations/20260922_add_live_clip_fields/migration.sql index a374e01..19af9bc 100644 --- a/prisma/migrations/20260922_add_live_clip_fields/migration.sql +++ b/prisma/migrations/20260922_add_live_clip_fields/migration.sql @@ -1,6 +1,6 @@ -- 直播切片:直播日期 / 主播 / 可选关联用户 / 直播平台 -ALTER TABLE "Material" ADD COLUMN IF NOT EXISTS "live_date" TIMESTAMP(3); -ALTER TABLE "Material" ADD COLUMN IF NOT EXISTS "live_host" TEXT; -ALTER TABLE "Material" ADD COLUMN IF NOT EXISTS "live_host_user_id" TEXT; -ALTER TABLE "Material" ADD COLUMN IF NOT EXISTS "live_platform" TEXT; -CREATE INDEX IF NOT EXISTS "Material_live_date_idx" ON "Material"("live_date"); +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "live_date" TIMESTAMP(3); +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "live_host" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "live_host_user_id" TEXT; +ALTER TABLE "materials" ADD COLUMN IF NOT EXISTS "live_platform" TEXT; +CREATE INDEX IF NOT EXISTS "materials_live_date_idx" ON "materials"("live_date");