diff --git a/app/api/materials/[id]/route.ts b/app/api/materials/[id]/route.ts index 45e0e34..764a39e 100644 --- a/app/api/materials/[id]/route.ts +++ b/app/api/materials/[id]/route.ts @@ -166,6 +166,10 @@ export async function GET( userId: true, productModel: true, reportTitle: true, + liveDate: true, + liveHost: true, + liveHostUserId: true, + livePlatform: true, categoryName: true, sceneName: true, platformName: true, @@ -239,6 +243,10 @@ export async function GET( userId: true, productModel: true, reportTitle: true, + liveDate: true, + liveHost: true, + liveHostUserId: true, + livePlatform: true, categoryName: true, sceneName: true, platformName: true, diff --git a/app/api/materials/route.ts b/app/api/materials/route.ts index d5936a1..1c5c474 100644 --- a/app/api/materials/route.ts +++ b/app/api/materials/route.ts @@ -108,6 +108,10 @@ export async function GET(request: NextRequest) { previewUrl: true, productModel: true, reportTitle: true, + liveDate: true, + liveHost: true, + liveHostUserId: true, + livePlatform: true, categoryName: true, sceneName: true, platformName: true, @@ -351,6 +355,10 @@ export async function GET(request: NextRequest) { previewUrl: true, productModel: true, reportTitle: true, + liveDate: true, + liveHost: true, + liveHostUserId: true, + livePlatform: true, categoryName: true, sceneName: true, platformName: true, diff --git a/app/api/materials/share/route.ts b/app/api/materials/share/route.ts index 695ee84..fae9b9a 100644 --- a/app/api/materials/share/route.ts +++ b/app/api/materials/share/route.ts @@ -125,6 +125,10 @@ export async function GET(request: NextRequest) { previewUrl: true, productModel: true, reportTitle: true, + liveDate: true, + liveHost: true, + liveHostUserId: true, + livePlatform: true, categoryName: true, sceneName: true, platformName: true, diff --git a/app/api/materials/update/route.ts b/app/api/materials/update/route.ts index 999abbe..b2e7e20 100644 --- a/app/api/materials/update/route.ts +++ b/app/api/materials/update/route.ts @@ -67,6 +67,18 @@ export async function PUT(request: NextRequest) { ...(formData.has('taskDetailRequesterUserId') ? { taskDetailRequesterUserId: formData.get('taskDetailRequesterUserId') as string | null } : {}), + ...(formData.has('liveDate') + ? { liveDate: formData.get('liveDate') as string | null } + : {}), + ...(formData.has('liveHost') + ? { liveHost: formData.get('liveHost') as string | null } + : {}), + ...(formData.has('liveHostUserId') + ? { liveHostUserId: formData.get('liveHostUserId') as string | null } + : {}), + ...(formData.has('livePlatform') + ? { livePlatform: formData.get('livePlatform') as string | null } + : {}), ...(formData.has('isFullSet') ? { isFullSet: formData.get('isFullSet') as string | null } : {}), diff --git a/app/api/materials/upload/route.ts b/app/api/materials/upload/route.ts index a14fcbb..4ad2f2f 100644 --- a/app/api/materials/upload/route.ts +++ b/app/api/materials/upload/route.ts @@ -61,6 +61,11 @@ import { isPlausibleSpuProductModel, normalizeSpuProductModel, } from '@/lib/product-model'; +import { + isLiveClipType, + parseLiveDate, + validateLiveClipFields, +} from '@/lib/live-clip'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -485,15 +490,36 @@ export async function POST(request: NextRequest) { const rawDingtalkApprovalNo = (formData.get('dingtalkApprovalNo') as string) || ''; const dingtalkApprovalNo = normalizeDingtalkApprovalNo(rawDingtalkApprovalNo); const rawProductModel = productModelInput || (isTaskOtherMaterial ? taskNo : ''); - const productModel = VIDEO_TYPES.has(materialType) + const isLiveClip = isLiveClipType(materialType); + // 直播切片的 productModel 存「标题」(自由文本),不做 SPU 规范化/校验 + const productModel = VIDEO_TYPES.has(materialType) && !isLiveClip ? normalizeSpuProductModel(rawProductModel) - : rawProductModel; + : rawProductModel.trim(); const shouldAutoFillGeneralCategoryScene = AUTO_GENERAL_TYPES.includes(materialType) || materialType === '标准模型'; const resolvedCategoryName = shouldAutoFillGeneralCategoryScene ? '通用' : categoryName; const resolvedSceneName = shouldAutoFillGeneralCategoryScene ? '通用' : sceneName; - if (VIDEO_TYPES.has(materialType) && !isPlausibleSpuProductModel(productModel)) { + const liveDateInput = (formData.get('liveDate') as string) || ''; + const liveHostInput = (formData.get('liveHost') as string) || ''; + const liveHostUserIdInput = (formData.get('liveHostUserId') as string) || ''; + const livePlatformInput = (formData.get('livePlatform') as string) || ''; + + if (isLiveClip) { + const liveClipErrors = validateLiveClipFields({ + title: productModel, + liveDate: liveDateInput, + livePlatform: livePlatformInput, + }); + if (liveClipErrors.length > 0) { + return NextResponse.json( + { error: liveClipErrors.map((e) => e.message).join(';') }, + { status: 400 }, + ); + } + } + + if (VIDEO_TYPES.has(materialType) && !isLiveClip && !isPlausibleSpuProductModel(productModel)) { return NextResponse.json( { error: `视频素材 SPU 不合规:${getSpuProductModelValidationError(productModel) || '请检查后重试'}`, @@ -639,7 +665,13 @@ export async function POST(request: NextRequest) { // 使用统一的验证逻辑验证必填字段 if (!productModel) { return NextResponse.json( - { error: isTaskOtherMaterial ? '任务-其他必须填写任务号' : '产品型号是必填的' }, + { + error: isTaskOtherMaterial + ? '任务-其他必须填写任务号' + : isLiveClip + ? '请填写标题' + : '产品型号是必填的', + }, { status: 400 } ); } @@ -1191,6 +1223,14 @@ export async function POST(request: NextRequest) { videoSourceUrl, // 视频类型时保存视频URL videoCoverUrl, videoProcessingStatus: videoSourceUrl ? 'pending' : null, // 视频类型时初始化为pending状态 + ...(isLiveClip + ? { + liveDate: parseLiveDate(liveDateInput) || parseLiveDate(new Date().toISOString().slice(0, 10)), + liveHost: liveHostInput.trim() || null, + liveHostUserId: liveHostUserIdInput.trim() || null, + livePlatform: livePlatformInput.trim() || null, + } + : {}), dingtalkApprovalNo, // taskNo: 不写入,上传完成后再写入 // taskStatus: 不写入,上传完成后再写入 diff --git a/app/edit/[id]/page.tsx b/app/edit/[id]/page.tsx index 1281328..5652e1d 100644 --- a/app/edit/[id]/page.tsx +++ b/app/edit/[id]/page.tsx @@ -28,6 +28,10 @@ interface Material { userId: string productModel: string reportTitle?: string | null + liveDate?: Date | string | null + liveHost?: string | null + liveHostUserId?: string | null + livePlatform?: string | null categoryName: string sceneName: string platformName?: string | null @@ -92,6 +96,10 @@ export default function EditMaterialPage({ params }: { params: { id: string } }) const [previewUrl, setPreviewUrl] = useState('') const [productModel, setProductModel] = useState('') const [reportTitle, setReportTitle] = useState('') + const [liveDate, setLiveDate] = useState('') + const [liveHost, setLiveHost] = useState('') + const [liveHostUserId, setLiveHostUserId] = useState('') + const [livePlatform, setLivePlatform] = useState('') const [categoryName, setCategoryName] = useState('') const [sceneName, setSceneName] = useState('') const [platformName, setPlatformName] = useState('') @@ -268,6 +276,10 @@ export default function EditMaterialPage({ params }: { params: { id: string } }) setPreviewUrl(data.previewUrl || '') setProductModel(data.productModel || '') setReportTitle(data.reportTitle || '') + setLiveDate(data.liveDate ? String(data.liveDate).slice(0, 10) : '') + setLiveHost(data.liveHost || '') + setLiveHostUserId(data.liveHostUserId || '') + setLivePlatform(data.livePlatform || '') setCategoryName(data.categoryName || '') setSceneName(data.sceneName || '') setPlatformName(data.platformName || '') @@ -525,6 +537,12 @@ export default function EditMaterialPage({ params }: { params: { id: string } }) const formData = new FormData() formData.append('productModel', productModel) formData.append('reportTitle', reportTitle) + if (materialType === '直播切片') { + formData.append('liveDate', liveDate) + formData.append('liveHost', liveHost) + formData.append('liveHostUserId', liveHostUserId) + formData.append('livePlatform', livePlatform) + } formData.append('categoryName', categoryName) formData.append('sceneName', sceneName) formData.append('platformName', platformName) @@ -725,7 +743,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } }) htmlFor={`edit-material-${params.id}-product-model`} className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300" > - {isCertificateReport ? 'SPU' : '产品型号 *'} + {isCertificateReport ? 'SPU' : materialType === '直播切片' ? '标题 *' : '产品型号 *'} + {materialType === '直播切片' && ( +
+
+ + setLiveDate(e.target.value)} + className="input-apple" + /> +
+
+ + setLiveHost(e.target.value)} + maxLength={50} + className="input-apple" + /> +
+
+ + +
+
+ )} + {isCertificateReport && (