feat: add live clip batch upload and date platform filters
Allow multi-file live clip uploads with shared date/host/platform and per-clip titles, filter materials by live platform and date range, and fix the migration to alter the mapped materials table.
This commit is contained in:
parent
be43f1a6a5
commit
bea82d67b0
@ -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<FacetItem[]>([])
|
||||
const [scenes, setScenes] = useState<FacetItem[]>([])
|
||||
@ -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}
|
||||
|
||||
@ -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<FacetItem[]>([])
|
||||
const [scenes, setScenes] = useState<FacetItem[]>([])
|
||||
@ -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}
|
||||
|
||||
@ -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灵感') && (
|
||||
<>
|
||||
<input
|
||||
ref={batchFileInputRef}
|
||||
@ -2671,6 +2691,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
className={`${outlineButtonClass} mr-2`}
|
||||
>
|
||||
{materialType === '音乐' && '选择音乐文件(可多选)'}
|
||||
{materialType === '直播切片' && '选择视频文件(可多选,最多20个)'}
|
||||
{isBatchVideoMaterialType(materialType) && '选择视频文件(可多选,最多8个)'}
|
||||
{materialType === '3D场景' && '选择3D文件或压缩包(可多选,最多5个)'}
|
||||
{materialType === '品牌VI灵感' && '选择PDF文件(可多选,最多50个)'}
|
||||
|
||||
@ -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) => <button key={item.id} onClick={() => setSelectedPlatform(item.name === selectedPlatform ? '' : item.name)} className={`${chipBase} ${selectedPlatform === item.name ? chipActive : chipInactive}`}>{item.name}<span className={`ml-1.5 ${countClass}`}>{countText(item.count || 0)}</span></button>)}
|
||||
</div>
|
||||
</div>}
|
||||
{setSelectedLivePlatform && (
|
||||
<div className="mb-6">
|
||||
<h2 className="mb-3 text-[13px] text-[#6b7075]">直播平台</h2>
|
||||
<div className="flex flex-wrap gap-x-5 gap-y-2">
|
||||
{['', '抖音', '快手', '视频号', '淘宝直播', 'B站'].map((name) => (
|
||||
<button
|
||||
key={name || 'all'}
|
||||
type="button"
|
||||
onClick={() => setSelectedLivePlatform(name)}
|
||||
className={`${chipBase} ${selectedLivePlatform === name ? chipActive : chipInactive}`}
|
||||
>
|
||||
{name || '全部'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(setLiveDateFrom || setLiveDateTo) && (
|
||||
<div className="mb-6">
|
||||
<h2 className="mb-3 text-[13px] text-[#6b7075]">直播日期</h2>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<input
|
||||
type="date"
|
||||
aria-label="直播日期起"
|
||||
value={liveDateFrom}
|
||||
onChange={(e) => setLiveDateFrom?.(e.target.value)}
|
||||
className={`${chipBase} cursor-text`}
|
||||
/>
|
||||
<span className="text-[12px] text-[#86898e]">至</span>
|
||||
<input
|
||||
type="date"
|
||||
aria-label="直播日期止"
|
||||
value={liveDateTo}
|
||||
onChange={(e) => setLiveDateTo?.(e.target.value)}
|
||||
className={`${chipBase} cursor-text`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(rankedProductCategories.length > 0 || facetsLoading) && <div className="mb-6">
|
||||
<h2 className="mb-3 text-[13px] text-[#6b7075]">产品类别</h2>
|
||||
<div className="flex flex-wrap gap-x-5 gap-y-2">
|
||||
|
||||
@ -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,8 +30,11 @@ 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}`,
|
||||
|
||||
@ -307,6 +307,10 @@ export async function completeBatchUpload(uploadId: string, metadata: Record<str
|
||||
formData.append('sceneName', metadata.sceneName || '通用')
|
||||
formData.append('platformName', metadata.platformName || '')
|
||||
formData.append('productCategoryName', metadata.productCategoryName || '')
|
||||
if (metadata.liveDate) formData.append('liveDate', String(metadata.liveDate))
|
||||
if (metadata.liveHost) formData.append('liveHost', String(metadata.liveHost))
|
||||
if (metadata.liveHostUserId) formData.append('liveHostUserId', String(metadata.liveHostUserId))
|
||||
if (metadata.livePlatform) formData.append('livePlatform', String(metadata.livePlatform))
|
||||
const materialType = metadata.materialType || '3D场景'
|
||||
formData.append('materialType', materialType)
|
||||
|
||||
|
||||
@ -86,6 +86,12 @@ export interface UseHomeStateReturn {
|
||||
setSelectedMaterialType: React.Dispatch<React.SetStateAction<string>>
|
||||
selectedPlatform: string
|
||||
setSelectedPlatform: React.Dispatch<React.SetStateAction<string>>
|
||||
selectedLivePlatform: string
|
||||
setSelectedLivePlatform: React.Dispatch<React.SetStateAction<string>>
|
||||
liveDateFrom: string
|
||||
setLiveDateFrom: React.Dispatch<React.SetStateAction<string>>
|
||||
liveDateTo: string
|
||||
setLiveDateTo: React.Dispatch<React.SetStateAction<string>>
|
||||
selectedProductCategory: string
|
||||
setSelectedProductCategory: React.Dispatch<React.SetStateAction<string>>
|
||||
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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user