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 && (