feat: 为任务素材增加钉钉审批编号
This commit is contained in:
parent
b19a5fff58
commit
362a468d67
@ -184,6 +184,7 @@ export async function GET(
|
|||||||
videoDuration: true,
|
videoDuration: true,
|
||||||
videoProcessingStatus: true,
|
videoProcessingStatus: true,
|
||||||
taskNo: true,
|
taskNo: true,
|
||||||
|
dingtalkApprovalNo: true,
|
||||||
taskStatus: true,
|
taskStatus: true,
|
||||||
taskFetchedAt: true,
|
taskFetchedAt: true,
|
||||||
isFullSet: true,
|
isFullSet: true,
|
||||||
@ -255,6 +256,7 @@ export async function GET(
|
|||||||
videoDuration: true,
|
videoDuration: true,
|
||||||
videoProcessingStatus: true,
|
videoProcessingStatus: true,
|
||||||
taskNo: true,
|
taskNo: true,
|
||||||
|
dingtalkApprovalNo: true,
|
||||||
taskStatus: true,
|
taskStatus: true,
|
||||||
taskFetchedAt: true,
|
taskFetchedAt: true,
|
||||||
isFullSet: true,
|
isFullSet: true,
|
||||||
@ -415,6 +417,7 @@ export async function GET(
|
|||||||
videoDuration: materialWithUser.videoDuration || null,
|
videoDuration: materialWithUser.videoDuration || null,
|
||||||
videoProcessingStatus: materialWithUser.videoProcessingStatus || null,
|
videoProcessingStatus: materialWithUser.videoProcessingStatus || null,
|
||||||
taskNo: materialWithUser.taskNo || null,
|
taskNo: materialWithUser.taskNo || null,
|
||||||
|
dingtalkApprovalNo: materialWithUser.dingtalkApprovalNo || null,
|
||||||
taskStatus: materialWithUser.taskStatus || null,
|
taskStatus: materialWithUser.taskStatus || null,
|
||||||
taskFetchedAt: materialWithUser.taskFetchedAt || null,
|
taskFetchedAt: materialWithUser.taskFetchedAt || null,
|
||||||
isFullSet: deriveTaskDetailIsFullSet(
|
isFullSet: deriveTaskDetailIsFullSet(
|
||||||
@ -507,6 +510,9 @@ export async function PUT(
|
|||||||
authorName: String(formData.get('authorName') || ''),
|
authorName: String(formData.get('authorName') || ''),
|
||||||
userId: formData.get('userId') as string | null,
|
userId: formData.get('userId') as string | null,
|
||||||
taskNo: formData.get('taskNo') as string | null,
|
taskNo: formData.get('taskNo') as string | null,
|
||||||
|
...(formData.has('dingtalkApprovalNo')
|
||||||
|
? { dingtalkApprovalNo: formData.get('dingtalkApprovalNo') as string | null }
|
||||||
|
: {}),
|
||||||
taskDetailSourceType: formData.get('taskDetailSourceType') as string | null,
|
taskDetailSourceType: formData.get('taskDetailSourceType') as string | null,
|
||||||
taskDetailRequesterUserId: formData.get('taskDetailRequesterUserId') as string | null,
|
taskDetailRequesterUserId: formData.get('taskDetailRequesterUserId') as string | null,
|
||||||
isFullSet: formData.get('isFullSet') as string | null,
|
isFullSet: formData.get('isFullSet') as string | null,
|
||||||
@ -545,6 +551,7 @@ export async function PUT(
|
|||||||
renderer: result.material.renderer,
|
renderer: result.material.renderer,
|
||||||
authorName: result.material.authorName,
|
authorName: result.material.authorName,
|
||||||
taskNo: result.material.taskNo,
|
taskNo: result.material.taskNo,
|
||||||
|
dingtalkApprovalNo: (result.material as any).dingtalkApprovalNo || null,
|
||||||
taskDetailSourceType: (result.material as any).taskDetailSourceType || null,
|
taskDetailSourceType: (result.material as any).taskDetailSourceType || null,
|
||||||
taskDetailRequesterUserId: (result.material as any).taskDetailRequesterUserId || null,
|
taskDetailRequesterUserId: (result.material as any).taskDetailRequesterUserId || null,
|
||||||
taskDetailRequesterName: (result.material as any).taskDetailRequesterName || null,
|
taskDetailRequesterName: (result.material as any).taskDetailRequesterName || null,
|
||||||
|
|||||||
@ -32,6 +32,9 @@ export async function PUT(request: NextRequest) {
|
|||||||
authorName: String(formData.get('authorName') || ''),
|
authorName: String(formData.get('authorName') || ''),
|
||||||
userId: formData.get('userId') as string | null,
|
userId: formData.get('userId') as string | null,
|
||||||
taskNo: formData.get('taskNo') as string | null,
|
taskNo: formData.get('taskNo') as string | null,
|
||||||
|
...(formData.has('dingtalkApprovalNo')
|
||||||
|
? { dingtalkApprovalNo: formData.get('dingtalkApprovalNo') as string | null }
|
||||||
|
: {}),
|
||||||
taskDetailSourceType: formData.get('taskDetailSourceType') as string | null,
|
taskDetailSourceType: formData.get('taskDetailSourceType') as string | null,
|
||||||
taskDetailRequesterUserId: formData.get('taskDetailRequesterUserId') as string | null,
|
taskDetailRequesterUserId: formData.get('taskDetailRequesterUserId') as string | null,
|
||||||
isFullSet: formData.get('isFullSet') as string | null,
|
isFullSet: formData.get('isFullSet') as string | null,
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { materialId: matId, assets, taskNo } = body;
|
const { materialId: matId, assets, taskNo, dingtalkApprovalNo } = body;
|
||||||
|
|
||||||
materialId = matId;
|
materialId = matId;
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ export async function POST(request: NextRequest) {
|
|||||||
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
||||||
|
|
||||||
// 步骤4.5: 仅在素材成功完成后写入任务号
|
// 步骤4.5: 仅在素材成功完成后写入任务号
|
||||||
await assignTaskNoOnComplete(materialId!, taskNo);
|
await assignTaskNoOnComplete(materialId!, taskNo, dingtalkApprovalNo);
|
||||||
|
|
||||||
// 步骤5: 清理临时文件
|
// 步骤5: 清理临时文件
|
||||||
await cleanupTempFiles(tempFiles, uploadIds);
|
await cleanupTempFiles(tempFiles, uploadIds);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
|
|||||||
import { authOptions } from '@/lib/auth';
|
import { authOptions } from '@/lib/auth';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||||
|
|
||||||
// POST - 初始化3D原图素材记录
|
// POST - 初始化3D原图素材记录
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { taskNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
const { taskNo, dingtalkApprovalNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||||
|
|
||||||
// 验证必填字段
|
// 验证必填字段
|
||||||
if (!productModel || !categoryName || !sceneName) {
|
if (!productModel || !categoryName || !sceneName) {
|
||||||
@ -54,6 +55,11 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
return NextResponse.json({ error: dingtalkApprovalNoError }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: (session.user as any).id },
|
where: { id: (session.user as any).id },
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { materialId: matId, assets, taskNo } = body;
|
const { materialId: matId, assets, taskNo, dingtalkApprovalNo } = body;
|
||||||
|
|
||||||
materialId = matId;
|
materialId = matId;
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ export async function POST(request: NextRequest) {
|
|||||||
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
||||||
|
|
||||||
// 步骤4.5: 仅在素材成功完成后写入任务号
|
// 步骤4.5: 仅在素材成功完成后写入任务号
|
||||||
await assignTaskNoOnComplete(materialId!, taskNo);
|
await assignTaskNoOnComplete(materialId!, taskNo, dingtalkApprovalNo);
|
||||||
|
|
||||||
// 步骤5: 清理临时文件
|
// 步骤5: 清理临时文件
|
||||||
await cleanupTempFiles(tempFiles, uploadIds);
|
await cleanupTempFiles(tempFiles, uploadIds);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
|
|||||||
import { authOptions } from '@/lib/auth';
|
import { authOptions } from '@/lib/auth';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||||
|
|
||||||
// POST - 初始化AI原图素材记录
|
// POST - 初始化AI原图素材记录
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { taskNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
const { taskNo, dingtalkApprovalNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||||
|
|
||||||
// 验证必填字段
|
// 验证必填字段
|
||||||
if (!productModel || !categoryName || !sceneName) {
|
if (!productModel || !categoryName || !sceneName) {
|
||||||
@ -58,6 +59,11 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
return NextResponse.json({ error: dingtalkApprovalNoError }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: (session.user as any).id },
|
where: { id: (session.user as any).id },
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export const maxDuration = 10800
|
|||||||
interface CompleteRequestBody {
|
interface CompleteRequestBody {
|
||||||
materialId: string
|
materialId: string
|
||||||
taskNo?: string
|
taskNo?: string
|
||||||
|
dingtalkApprovalNo?: string
|
||||||
noBarcodes?: boolean
|
noBarcodes?: boolean
|
||||||
assets: unknown
|
assets: unknown
|
||||||
}
|
}
|
||||||
@ -88,6 +89,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const result = await completeProductPackageUpload({
|
const result = await completeProductPackageUpload({
|
||||||
materialId: body.materialId,
|
materialId: body.materialId,
|
||||||
taskNo: body.taskNo,
|
taskNo: body.taskNo,
|
||||||
|
dingtalkApprovalNo: body.dingtalkApprovalNo,
|
||||||
assets,
|
assets,
|
||||||
operatorUserId: (session.user as any).id,
|
operatorUserId: (session.user as any).id,
|
||||||
options: {
|
options: {
|
||||||
@ -100,6 +102,7 @@ export async function POST(request: NextRequest) {
|
|||||||
data: {
|
data: {
|
||||||
materialId: result.materialId,
|
materialId: result.materialId,
|
||||||
taskNo: result.taskNo,
|
taskNo: result.taskNo,
|
||||||
|
dingtalkApprovalNo: result.dingtalkApprovalNo,
|
||||||
isFullSet: result.isFullSet,
|
isFullSet: result.isFullSet,
|
||||||
message: '上传完成',
|
message: '上传完成',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { authOptions } from '@/lib/auth';
|
|||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { validateTaskNo, isTaskNoExists, purgeDeletedMaterialsByTaskNo } from '@/lib/product-package';
|
import { validateTaskNo, isTaskNoExists, purgeDeletedMaterialsByTaskNo } from '@/lib/product-package';
|
||||||
import { normalizeTaskDetailSourceType } from '@/lib/task-detail-source-type';
|
import { normalizeTaskDetailSourceType } from '@/lib/task-detail-source-type';
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@ -24,6 +25,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const {
|
const {
|
||||||
taskNo,
|
taskNo,
|
||||||
|
dingtalkApprovalNo,
|
||||||
productModel,
|
productModel,
|
||||||
categoryName,
|
categoryName,
|
||||||
sceneName,
|
sceneName,
|
||||||
@ -33,6 +35,14 @@ export async function POST(request: NextRequest) {
|
|||||||
const rawTaskDetailSourceType = body?.taskDetailSourceType;
|
const rawTaskDetailSourceType = body?.taskDetailSourceType;
|
||||||
const taskDetailSourceType = normalizeTaskDetailSourceType(rawTaskDetailSourceType);
|
const taskDetailSourceType = normalizeTaskDetailSourceType(rawTaskDetailSourceType);
|
||||||
const taskDetailRequesterUserId = String(body?.taskDetailRequesterUserId || '').trim();
|
const taskDetailRequesterUserId = String(body?.taskDetailRequesterUserId || '').trim();
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||||
|
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: dingtalkApprovalNoError },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (rawTaskDetailSourceType != null && String(rawTaskDetailSourceType).trim() && !taskDetailSourceType) {
|
if (rawTaskDetailSourceType != null && String(rawTaskDetailSourceType).trim() && !taskDetailSourceType) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { materialId: matId, assets, taskNo } = body;
|
const { materialId: matId, assets, taskNo, dingtalkApprovalNo } = body;
|
||||||
|
|
||||||
materialId = matId;
|
materialId = matId;
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ export async function POST(request: NextRequest) {
|
|||||||
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
||||||
|
|
||||||
// 步骤4.5: 仅在素材成功完成后写入任务号
|
// 步骤4.5: 仅在素材成功完成后写入任务号
|
||||||
await assignTaskNoOnComplete(materialId!, taskNo);
|
await assignTaskNoOnComplete(materialId!, taskNo, dingtalkApprovalNo);
|
||||||
|
|
||||||
// 步骤5: 清理临时文件
|
// 步骤5: 清理临时文件
|
||||||
await cleanupTempFiles(tempFiles, uploadIds);
|
await cleanupTempFiles(tempFiles, uploadIds);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
|
|||||||
import { authOptions } from '@/lib/auth';
|
import { authOptions } from '@/lib/auth';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||||
|
|
||||||
// POST - 初始化实拍原图素材记录
|
// POST - 初始化实拍原图素材记录
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { taskNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
const { taskNo, dingtalkApprovalNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||||
|
|
||||||
// 验证必填字段
|
// 验证必填字段
|
||||||
if (!productModel || !categoryName || !sceneName) {
|
if (!productModel || !categoryName || !sceneName) {
|
||||||
@ -58,6 +59,11 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
return NextResponse.json({ error: dingtalkApprovalNoError }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: (session.user as any).id },
|
where: { id: (session.user as any).id },
|
||||||
|
|||||||
@ -28,6 +28,10 @@ import {
|
|||||||
resolveOwnedMergedUpload,
|
resolveOwnedMergedUpload,
|
||||||
type OwnedMergedUpload,
|
type OwnedMergedUpload,
|
||||||
} from '@/lib/upload-sessions';
|
} from '@/lib/upload-sessions';
|
||||||
|
import {
|
||||||
|
getDingtalkApprovalNoError,
|
||||||
|
normalizeDingtalkApprovalNo,
|
||||||
|
} from '@/lib/dingtalk-approval-no';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
@ -234,6 +238,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isTaskOtherMaterial = materialType === '任务-其他';
|
const isTaskOtherMaterial = materialType === '任务-其他';
|
||||||
|
const isTaskMaterial = materialType.startsWith('任务-');
|
||||||
const rendererParam = formData.get('renderer') as string | null;
|
const rendererParam = formData.get('renderer') as string | null;
|
||||||
// 统一处理renderer:3D场景必须有renderer,其他类型可选
|
// 统一处理renderer:3D场景必须有renderer,其他类型可选
|
||||||
const renderer = requiresRenderer(materialType)
|
const renderer = requiresRenderer(materialType)
|
||||||
@ -241,6 +246,10 @@ export async function POST(request: NextRequest) {
|
|||||||
: (rendererParam || '');
|
: (rendererParam || '');
|
||||||
const rawTaskNo = (formData.get('taskNo') as string) || '';
|
const rawTaskNo = (formData.get('taskNo') as string) || '';
|
||||||
const taskNo = isTaskOtherMaterial ? rawTaskNo.trim().toUpperCase() : rawTaskNo;
|
const taskNo = isTaskOtherMaterial ? rawTaskNo.trim().toUpperCase() : rawTaskNo;
|
||||||
|
const rawDingtalkApprovalNo = (formData.get('dingtalkApprovalNo') as string) || '';
|
||||||
|
const dingtalkApprovalNo = isTaskMaterial
|
||||||
|
? normalizeDingtalkApprovalNo(rawDingtalkApprovalNo)
|
||||||
|
: null;
|
||||||
const productModel = productModelInput || (isTaskOtherMaterial ? taskNo : '');
|
const productModel = productModelInput || (isTaskOtherMaterial ? taskNo : '');
|
||||||
const shouldAutoFillGeneralCategoryScene =
|
const shouldAutoFillGeneralCategoryScene =
|
||||||
AUTO_GENERAL_TYPES.includes(materialType) || materialType === '标准模型';
|
AUTO_GENERAL_TYPES.includes(materialType) || materialType === '标准模型';
|
||||||
@ -298,6 +307,21 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isTaskMaterial && rawDingtalkApprovalNo.trim()) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '钉钉审批编号仅适用于“任务-”素材' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: dingtalkApprovalNoError },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 任务号验证(可选,如果提供则验证格式和唯一性)
|
// 任务号验证(可选,如果提供则验证格式和唯一性)
|
||||||
if (taskNo && taskNo.trim()) {
|
if (taskNo && taskNo.trim()) {
|
||||||
const { validateTaskNo, isTaskNoExists, purgeDeletedMaterialsByTaskNo } = await import('@/lib/product-package');
|
const { validateTaskNo, isTaskNoExists, purgeDeletedMaterialsByTaskNo } = await import('@/lib/product-package');
|
||||||
@ -831,31 +855,10 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果提供了taskNo,上传完成后写入taskNo(所有素材类型统一处理)
|
// 文件成功落盘后再写入任务关联字段,避免半成品占用任务号。
|
||||||
if (taskNo && taskNo.trim()) {
|
if (isTaskMaterial || taskNo.trim()) {
|
||||||
try {
|
const { assignTaskNoOnComplete } = await import('@/lib/product-package');
|
||||||
// 双重验证:再次检查taskNo唯一性(防止并发问题)
|
await assignTaskNoOnComplete(material.id, taskNo, dingtalkApprovalNo);
|
||||||
const { validateTaskNo, isTaskNoExists } = await import('@/lib/product-package');
|
|
||||||
if (validateTaskNo(taskNo)) {
|
|
||||||
const exists = await isTaskNoExists(taskNo);
|
|
||||||
if (!exists) {
|
|
||||||
// 写入taskNo和taskStatus
|
|
||||||
await prisma.material.update({
|
|
||||||
where: { id: material.id },
|
|
||||||
data: {
|
|
||||||
taskNo: taskNo,
|
|
||||||
taskStatus: 'completed',
|
|
||||||
} as any,
|
|
||||||
});
|
|
||||||
console.log('[Upload] TaskNo written to database:', taskNo);
|
|
||||||
} else {
|
|
||||||
console.warn('[Upload] TaskNo already exists, skipping write:', taskNo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (taskNoError) {
|
|
||||||
console.error('[Upload] Failed to write taskNo:', taskNoError);
|
|
||||||
// taskNo写入失败不影响上传流程,只记录错误
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录上传日志
|
// 记录上传日志
|
||||||
@ -884,6 +887,10 @@ export async function POST(request: NextRequest) {
|
|||||||
oldValue: null,
|
oldValue: null,
|
||||||
newValue: taskNo || null,
|
newValue: taskNo || null,
|
||||||
},
|
},
|
||||||
|
dingtalkApprovalNo: {
|
||||||
|
oldValue: null,
|
||||||
|
newValue: dingtalkApprovalNo,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1010,16 +1017,15 @@ export async function POST(request: NextRequest) {
|
|||||||
console.log(`[Upload] Cleaned up ${cleanupResult.deletedCount} files, ${cleanupResult.failedCount} failed`);
|
console.log(`[Upload] Cleaned up ${cleanupResult.deletedCount} files, ${cleanupResult.failedCount} failed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果素材记录已创建但没有taskNo,删除记录(避免残留)
|
// 只回滚尚未完成的素材;任务号本身是可选字段,不能据此判断上传是否完成。
|
||||||
if (materialId) {
|
if (materialId) {
|
||||||
try {
|
try {
|
||||||
const material = await prisma.material.findUnique({
|
const material = await prisma.material.findUnique({
|
||||||
where: { id: materialId },
|
where: { id: materialId },
|
||||||
select: { taskNo: true },
|
select: { taskStatus: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果没有taskNo,说明上传未完成,删除记录
|
if (material && material.taskStatus !== 'completed') {
|
||||||
if (material && !material.taskNo) {
|
|
||||||
// 先删除关联记录
|
// 先删除关联记录
|
||||||
const materialType = (error as any)?.materialType || '未知';
|
const materialType = (error as any)?.materialType || '未知';
|
||||||
await deleteMaterialRelatedRecords(materialId, materialType);
|
await deleteMaterialRelatedRecords(materialId, materialType);
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { materialId: matId, assets, taskNo } = body;
|
const { materialId: matId, assets, taskNo, dingtalkApprovalNo } = body;
|
||||||
|
|
||||||
materialId = matId;
|
materialId = matId;
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ export async function POST(request: NextRequest) {
|
|||||||
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
||||||
|
|
||||||
// 步骤4.5: 仅在素材成功完成后写入任务号
|
// 步骤4.5: 仅在素材成功完成后写入任务号
|
||||||
await assignTaskNoOnComplete(materialId!, taskNo);
|
await assignTaskNoOnComplete(materialId!, taskNo, dingtalkApprovalNo);
|
||||||
|
|
||||||
// 步骤5: 清理临时文件
|
// 步骤5: 清理临时文件
|
||||||
await cleanupTempFiles(tempFiles, uploadIds);
|
await cleanupTempFiles(tempFiles, uploadIds);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
|
|||||||
import { authOptions } from '@/lib/auth';
|
import { authOptions } from '@/lib/auth';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||||
|
|
||||||
// POST - 初始化任务-页面素材记录
|
// POST - 初始化任务-页面素材记录
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { taskNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
const { taskNo, dingtalkApprovalNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||||
|
|
||||||
// 验证必填字段
|
// 验证必填字段
|
||||||
if (!productModel || !categoryName || !sceneName) {
|
if (!productModel || !categoryName || !sceneName) {
|
||||||
@ -58,6 +59,11 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
return NextResponse.json({ error: dingtalkApprovalNoError }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: (session.user as any).id },
|
where: { id: (session.user as any).id },
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { materialId: matId, assets, taskNo } = body;
|
const { materialId: matId, assets, taskNo, dingtalkApprovalNo } = body;
|
||||||
|
|
||||||
materialId = matId;
|
materialId = matId;
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ export async function POST(request: NextRequest) {
|
|||||||
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
||||||
|
|
||||||
// 步骤4.5: 仅在素材成功完成后写入任务号
|
// 步骤4.5: 仅在素材成功完成后写入任务号
|
||||||
await assignTaskNoOnComplete(materialId!, taskNo);
|
await assignTaskNoOnComplete(materialId!, taskNo, dingtalkApprovalNo);
|
||||||
|
|
||||||
// 步骤5: 清理临时文件
|
// 步骤5: 清理临时文件
|
||||||
await cleanupTempFiles(tempFiles, uploadIds);
|
await cleanupTempFiles(tempFiles, uploadIds);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
|
|||||||
import { authOptions } from '@/lib/auth';
|
import { authOptions } from '@/lib/auth';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||||
|
|
||||||
// POST - 初始化视频截图素材记录
|
// POST - 初始化视频截图素材记录
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { taskNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
const { taskNo, dingtalkApprovalNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||||
|
|
||||||
// 验证必填字段
|
// 验证必填字段
|
||||||
if (!productModel || !categoryName || !sceneName) {
|
if (!productModel || !categoryName || !sceneName) {
|
||||||
@ -58,6 +59,11 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
return NextResponse.json({ error: dingtalkApprovalNoError }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: (session.user as any).id },
|
where: { id: (session.user as any).id },
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useSession } from 'next-auth/react'
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import Header from '@/components/Header'
|
import Header from '@/components/Header'
|
||||||
import SearchableUserSelect from '@/components/SearchableUserSelect'
|
import SearchableUserSelect from '@/components/SearchableUserSelect'
|
||||||
|
import TaskReferenceFields from '@/components/upload/TaskReferenceFields'
|
||||||
import {
|
import {
|
||||||
canManuallyReplacePreview,
|
canManuallyReplacePreview,
|
||||||
getSelectableMaterialTypeOptions,
|
getSelectableMaterialTypeOptions,
|
||||||
@ -16,6 +17,7 @@ import {
|
|||||||
requiresProductCategoryName,
|
requiresProductCategoryName,
|
||||||
requiresRenderer,
|
requiresRenderer,
|
||||||
} from '@/lib/material-validation'
|
} from '@/lib/material-validation'
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no'
|
||||||
|
|
||||||
interface Material {
|
interface Material {
|
||||||
id: string
|
id: string
|
||||||
@ -30,6 +32,7 @@ interface Material {
|
|||||||
authorName: string
|
authorName: string
|
||||||
previewUrl?: string | null
|
previewUrl?: string | null
|
||||||
taskNo?: string | null
|
taskNo?: string | null
|
||||||
|
dingtalkApprovalNo?: string | null
|
||||||
taskStatus?: string | null
|
taskStatus?: string | null
|
||||||
taskFetchedAt?: string | null
|
taskFetchedAt?: string | null
|
||||||
isFullSet?: boolean
|
isFullSet?: boolean
|
||||||
@ -58,6 +61,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
|
|||||||
const [authorName, setAuthorName] = useState('')
|
const [authorName, setAuthorName] = useState('')
|
||||||
const [userId, setUserId] = useState('')
|
const [userId, setUserId] = useState('')
|
||||||
const [taskNo, setTaskNo] = useState('')
|
const [taskNo, setTaskNo] = useState('')
|
||||||
|
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('')
|
||||||
const [taskStatus, setTaskStatus] = useState('')
|
const [taskStatus, setTaskStatus] = useState('')
|
||||||
const [taskFetchedAt, setTaskFetchedAt] = useState('')
|
const [taskFetchedAt, setTaskFetchedAt] = useState('')
|
||||||
const [isFullSet, setIsFullSet] = useState(false)
|
const [isFullSet, setIsFullSet] = useState(false)
|
||||||
@ -136,6 +140,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
|
|||||||
setAuthorName(data.authorName || '')
|
setAuthorName(data.authorName || '')
|
||||||
setUserId(data.userId || '')
|
setUserId(data.userId || '')
|
||||||
setTaskNo(data.taskNo || '')
|
setTaskNo(data.taskNo || '')
|
||||||
|
setDingtalkApprovalNo(data.dingtalkApprovalNo || '')
|
||||||
setTaskStatus(data.taskStatus || '')
|
setTaskStatus(data.taskStatus || '')
|
||||||
setTaskFetchedAt(data.taskFetchedAt || '')
|
setTaskFetchedAt(data.taskFetchedAt || '')
|
||||||
setIsFullSet(Boolean(data.isFullSet))
|
setIsFullSet(Boolean(data.isFullSet))
|
||||||
@ -236,6 +241,14 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showTaskNo) {
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo)
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
setError(dingtalkApprovalNoError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isFetchedTask) {
|
if (isFetchedTask) {
|
||||||
const confirmed = window.confirm('这条任务素材已经被外部系统获取过,继续保存可能影响对方使用,确认继续吗?')
|
const confirmed = window.confirm('这条任务素材已经被外部系统获取过,继续保存可能影响对方使用,确认继续吗?')
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
@ -257,6 +270,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
|
|||||||
formData.append('authorName', authorName)
|
formData.append('authorName', authorName)
|
||||||
formData.append('userId', userId)
|
formData.append('userId', userId)
|
||||||
formData.append('taskNo', showTaskNo ? taskNo : '')
|
formData.append('taskNo', showTaskNo ? taskNo : '')
|
||||||
|
formData.append('dingtalkApprovalNo', showTaskNo ? dingtalkApprovalNo : '')
|
||||||
formData.append(
|
formData.append(
|
||||||
'taskDetailRequesterUserId',
|
'taskDetailRequesterUserId',
|
||||||
materialType === '任务-详情' ? taskDetailRequesterUserId : '',
|
materialType === '任务-详情' ? taskDetailRequesterUserId : '',
|
||||||
@ -478,18 +492,14 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{showTaskNo && (
|
{showTaskNo && (
|
||||||
<div>
|
<TaskReferenceFields
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
idPrefix={`edit-material-page-${params.id}`}
|
||||||
任务号
|
taskNo={taskNo}
|
||||||
</label>
|
onTaskNoChange={setTaskNo}
|
||||||
<input
|
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||||
type="text"
|
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||||
value={taskNo}
|
disabled={submitting}
|
||||||
onChange={(event) => setTaskNo(event.target.value)}
|
/>
|
||||||
className="input-apple"
|
|
||||||
placeholder="TASK + 12-18位数字,可带多级 -01"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{materialType === '任务-详情' && (
|
{materialType === '任务-详情' && (
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import {
|
|||||||
} from '@/lib/material-type-options'
|
} from '@/lib/material-type-options'
|
||||||
import { isCertificateReportMaterialType } from '@/lib/certificate-report'
|
import { isCertificateReportMaterialType } from '@/lib/certificate-report'
|
||||||
import { TASK_DETAIL_SOURCE_TYPE_OPTIONS } from '@/lib/task-detail-source-type'
|
import { TASK_DETAIL_SOURCE_TYPE_OPTIONS } from '@/lib/task-detail-source-type'
|
||||||
|
import TaskReferenceFields from '@/components/upload/TaskReferenceFields'
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no'
|
||||||
import {
|
import {
|
||||||
requiresCategoryAndScene,
|
requiresCategoryAndScene,
|
||||||
requiresProductCategoryName,
|
requiresProductCategoryName,
|
||||||
@ -31,6 +33,7 @@ interface EditMaterialModalProps {
|
|||||||
authorName: string
|
authorName: string
|
||||||
userId: string
|
userId: string
|
||||||
taskNo?: string | null
|
taskNo?: string | null
|
||||||
|
dingtalkApprovalNo?: string | null
|
||||||
taskDetailSourceType?: string | null
|
taskDetailSourceType?: string | null
|
||||||
taskStatus?: string | null
|
taskStatus?: string | null
|
||||||
taskFetchedAt?: string | null
|
taskFetchedAt?: string | null
|
||||||
@ -56,6 +59,7 @@ export default function EditMaterialModal({ material, onClose, onSuccess }: Edit
|
|||||||
const [authorName, setAuthorName] = useState(material.authorName)
|
const [authorName, setAuthorName] = useState(material.authorName)
|
||||||
const [userId, setUserId] = useState(material.userId)
|
const [userId, setUserId] = useState(material.userId)
|
||||||
const [taskNo, setTaskNo] = useState(material.taskNo || '')
|
const [taskNo, setTaskNo] = useState(material.taskNo || '')
|
||||||
|
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState(material.dingtalkApprovalNo || '')
|
||||||
const [taskDetailSourceType, setTaskDetailSourceType] = useState(material.taskDetailSourceType || '')
|
const [taskDetailSourceType, setTaskDetailSourceType] = useState(material.taskDetailSourceType || '')
|
||||||
const [taskDetailRequesterUserId, setTaskDetailRequesterUserId] = useState(material.taskDetailRequesterUserId || '')
|
const [taskDetailRequesterUserId, setTaskDetailRequesterUserId] = useState(material.taskDetailRequesterUserId || '')
|
||||||
const [taskDetailRequesterName, setTaskDetailRequesterName] = useState(material.taskDetailRequesterName || '')
|
const [taskDetailRequesterName, setTaskDetailRequesterName] = useState(material.taskDetailRequesterName || '')
|
||||||
@ -182,6 +186,14 @@ export default function EditMaterialModal({ material, onClose, onSuccess }: Edit
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showTaskNo) {
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo)
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
setError(dingtalkApprovalNoError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isFetchedTask) {
|
if (isFetchedTask) {
|
||||||
const confirmed = window.confirm('这条任务素材已经被外部系统获取过,继续保存可能影响对方使用,确认继续吗?')
|
const confirmed = window.confirm('这条任务素材已经被外部系统获取过,继续保存可能影响对方使用,确认继续吗?')
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
@ -205,6 +217,7 @@ export default function EditMaterialModal({ material, onClose, onSuccess }: Edit
|
|||||||
formData.append('authorName', authorName)
|
formData.append('authorName', authorName)
|
||||||
formData.append('userId', userId)
|
formData.append('userId', userId)
|
||||||
formData.append('taskNo', showTaskNo ? taskNo : '')
|
formData.append('taskNo', showTaskNo ? taskNo : '')
|
||||||
|
formData.append('dingtalkApprovalNo', showTaskNo ? dingtalkApprovalNo : '')
|
||||||
formData.append('taskDetailSourceType', materialType === '任务-详情' ? taskDetailSourceType : '')
|
formData.append('taskDetailSourceType', materialType === '任务-详情' ? taskDetailSourceType : '')
|
||||||
formData.append(
|
formData.append(
|
||||||
'taskDetailRequesterUserId',
|
'taskDetailRequesterUserId',
|
||||||
@ -389,16 +402,14 @@ export default function EditMaterialModal({ material, onClose, onSuccess }: Edit
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{showTaskNo && (
|
{showTaskNo && (
|
||||||
<div>
|
<TaskReferenceFields
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-700">任务号</label>
|
idPrefix={`edit-material-${material.id}`}
|
||||||
<input
|
taskNo={taskNo}
|
||||||
type="text"
|
onTaskNoChange={setTaskNo}
|
||||||
value={taskNo}
|
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||||
onChange={(event) => setTaskNo(event.target.value)}
|
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||||
className="input-apple"
|
disabled={updating}
|
||||||
placeholder="TASK + 12-18位数字,可带多级 -01"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{materialType === '任务-详情' && (
|
{materialType === '任务-详情' && (
|
||||||
|
|||||||
@ -51,7 +51,7 @@ describe('UploadModal - 任务-其他', () => {
|
|||||||
}) as any
|
}) as any
|
||||||
})
|
})
|
||||||
|
|
||||||
test('任务-其他应使用公共上传面板形式,只保留任务号 + 预览图片 + 素材文件', async () => {
|
test('任务-其他应使用公共上传面板形式,任务号与钉钉审批编号并列显示', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
render(<UploadModal onClose={jest.fn()} onSuccess={jest.fn()} />)
|
render(<UploadModal onClose={jest.fn()} onSuccess={jest.fn()} />)
|
||||||
@ -63,6 +63,7 @@ describe('UploadModal - 任务-其他', () => {
|
|||||||
expect(screen.getByLabelText('任务号 *')).toBeInTheDocument()
|
expect(screen.getByLabelText('任务号 *')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
expect(screen.getByLabelText('钉钉审批编号')).toBeInTheDocument()
|
||||||
expect(screen.getByText('预览图片 *')).toBeInTheDocument()
|
expect(screen.getByText('预览图片 *')).toBeInTheDocument()
|
||||||
expect(screen.getByText('素材文件 *')).toBeInTheDocument()
|
expect(screen.getByText('素材文件 *')).toBeInTheDocument()
|
||||||
expect(screen.getByText('支持格式:压缩包(.zip / .rar / .7z)')).toBeInTheDocument()
|
expect(screen.getByText('支持格式:压缩包(.zip / .rar / .7z)')).toBeInTheDocument()
|
||||||
|
|||||||
@ -60,11 +60,13 @@ import ProductPackageUploader from '@/components/upload/ProductPackageUploader'
|
|||||||
import VideoScreenshotUploader from '@/components/upload/VideoScreenshotUploader'
|
import VideoScreenshotUploader from '@/components/upload/VideoScreenshotUploader'
|
||||||
import TaskPageUploader from '@/components/upload/TaskPageUploader'
|
import TaskPageUploader from '@/components/upload/TaskPageUploader'
|
||||||
import CertificateReportUploader from '@/components/upload/CertificateReportUploader'
|
import CertificateReportUploader from '@/components/upload/CertificateReportUploader'
|
||||||
|
import TaskReferenceFields from '@/components/upload/TaskReferenceFields'
|
||||||
import {
|
import {
|
||||||
hasDragLeftDropZone,
|
hasDragLeftDropZone,
|
||||||
prepareFileDrag,
|
prepareFileDrag,
|
||||||
validateImageFiles,
|
validateImageFiles,
|
||||||
} from '@/components/upload/drag-drop-utils'
|
} from '@/components/upload/drag-drop-utils'
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no'
|
||||||
|
|
||||||
interface UploadModalProps {
|
interface UploadModalProps {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@ -155,6 +157,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
|||||||
|
|
||||||
// 任务号(所有素材类型支持)
|
// 任务号(所有素材类型支持)
|
||||||
const [taskNo, setTaskNo] = useState('')
|
const [taskNo, setTaskNo] = useState('')
|
||||||
|
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('')
|
||||||
|
|
||||||
// SKU号(视频类素材支持)
|
// SKU号(视频类素材支持)
|
||||||
const [sku, setSku] = useState('')
|
const [sku, setSku] = useState('')
|
||||||
@ -178,6 +181,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
|||||||
const [retryCount, setRetryCount] = useState(0)
|
const [retryCount, setRetryCount] = useState(0)
|
||||||
|
|
||||||
const isTaskOtherMaterial = materialType === '任务-其他'
|
const isTaskOtherMaterial = materialType === '任务-其他'
|
||||||
|
const isTaskMaterial = materialType.startsWith('任务-')
|
||||||
const usesDedicatedUploader = SPECIAL_UPLOAD_TYPES.includes(materialType as typeof SPECIAL_UPLOAD_TYPES[number])
|
const usesDedicatedUploader = SPECIAL_UPLOAD_TYPES.includes(materialType as typeof SPECIAL_UPLOAD_TYPES[number])
|
||||||
|
|
||||||
const previewInputRef = useRef<HTMLInputElement>(null)
|
const previewInputRef = useRef<HTMLInputElement>(null)
|
||||||
@ -239,6 +243,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
|||||||
setProductModel('')
|
setProductModel('')
|
||||||
setIsBatchMode(false)
|
setIsBatchMode(false)
|
||||||
setTaskNo('') // 切换素材类型时清空任务号
|
setTaskNo('') // 切换素材类型时清空任务号
|
||||||
|
setDingtalkApprovalNo('')
|
||||||
setSku('') // 切换素材类型时清空SKU号
|
setSku('') // 切换素材类型时清空SKU号
|
||||||
}, [materialType])
|
}, [materialType])
|
||||||
|
|
||||||
@ -1504,6 +1509,14 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
|||||||
setError('任务号格式错误,应为 TASK + 12-18位数字,可选的多个子任务号(-2位数字,如 TASK202601141713525624-01-01)')
|
setError('任务号格式错误,应为 TASK + 12-18位数字,可选的多个子任务号(-2位数字,如 TASK202601141713525624-01-01)')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isTaskMaterial) {
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo)
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
setError(dingtalkApprovalNoError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 任务号为空时仅提醒,不阻止上传(已在UI中显示提醒)
|
// 任务号为空时仅提醒,不阻止上传(已在UI中显示提醒)
|
||||||
|
|
||||||
@ -1613,6 +1626,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
|||||||
productCategoryName,
|
productCategoryName,
|
||||||
renderer: resolvedRenderer,
|
renderer: resolvedRenderer,
|
||||||
taskNo: resolvedTaskNo,
|
taskNo: resolvedTaskNo,
|
||||||
|
dingtalkApprovalNo: isTaskMaterial ? dingtalkApprovalNo.trim() : '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 分片后主文件已在服务器,添加路径信息
|
// 分片后主文件已在服务器,添加路径信息
|
||||||
@ -1793,23 +1807,36 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 任务号输入框(有专用上传器的任务类型不显示,因为它们有自己的任务号输入) */}
|
{/* 专用上传器会在自己的表单内渲染任务关联字段。 */}
|
||||||
{!usesDedicatedUploader && (
|
{!usesDedicatedUploader && isTaskMaterial && (
|
||||||
|
<TaskReferenceFields
|
||||||
|
idPrefix="generic-task-upload"
|
||||||
|
taskNo={taskNo}
|
||||||
|
onTaskNoChange={setTaskNo}
|
||||||
|
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||||
|
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||||
|
taskNoRequired={isTaskOtherMaterial}
|
||||||
|
disabled={uploading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!usesDedicatedUploader && !isTaskMaterial && (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label htmlFor="taskNo" className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{isTaskOtherMaterial ? '任务号 *' : <>任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span></>}
|
任务号
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="taskNo"
|
id="taskNo"
|
||||||
type="text"
|
type="text"
|
||||||
value={taskNo}
|
value={taskNo}
|
||||||
onChange={(e) => setTaskNo(e.target.value)}
|
onChange={(event) => setTaskNo(event.target.value)}
|
||||||
placeholder="TASK + 12-18位数字,如 TASK202601030184 或 TASK202601071437194808-01"
|
placeholder="例如:TASK202601030184"
|
||||||
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
|
className="w-full rounded-apple border border-gray-300 px-4 py-2 outline-none focus:border-gray-900 focus:ring-2 focus:ring-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:border-gray-400 dark:focus:ring-gray-600"
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
|
aria-invalid={Boolean(taskNo && !validateTaskNo(taskNo.trim().toUpperCase()))}
|
||||||
/>
|
/>
|
||||||
{taskNo && !validateTaskNo(taskNo.trim().toUpperCase()) && (
|
{taskNo && !validateTaskNo(taskNo.trim().toUpperCase()) && (
|
||||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
<p className="mt-1 text-sm text-red-600 dark:text-red-400">任务号格式错误</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import {
|
|||||||
prepareFileDrag,
|
prepareFileDrag,
|
||||||
validateImageFiles,
|
validateImageFiles,
|
||||||
} from './drag-drop-utils';
|
} from './drag-drop-utils';
|
||||||
|
import TaskReferenceFields from './TaskReferenceFields';
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||||
|
|
||||||
interface AssetFile {
|
interface AssetFile {
|
||||||
file: File;
|
file: File;
|
||||||
@ -43,6 +45,7 @@ export default function AIOriginalUploader({
|
|||||||
}: AIOriginalUploaderProps) {
|
}: AIOriginalUploaderProps) {
|
||||||
|
|
||||||
const [taskNo, setTaskNo] = useState('');
|
const [taskNo, setTaskNo] = useState('');
|
||||||
|
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('');
|
||||||
const [productModel, setProductModel] = useState('');
|
const [productModel, setProductModel] = useState('');
|
||||||
const [categoryName, setCategoryName] = useState('');
|
const [categoryName, setCategoryName] = useState('');
|
||||||
const [sceneName, setSceneName] = useState('');
|
const [sceneName, setSceneName] = useState('');
|
||||||
@ -203,6 +206,11 @@ export default function AIOriginalUploader({
|
|||||||
setError('任务号格式错误');
|
setError('任务号格式错误');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
setError(dingtalkApprovalNoError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!productModel.trim()) {
|
if (!productModel.trim()) {
|
||||||
setError('请输入产品型号(SPU)');
|
setError('请输入产品型号(SPU)');
|
||||||
return;
|
return;
|
||||||
@ -230,6 +238,7 @@ export default function AIOriginalUploader({
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
taskNo: taskNo || undefined,
|
taskNo: taskNo || undefined,
|
||||||
|
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||||
productModel,
|
productModel,
|
||||||
categoryName,
|
categoryName,
|
||||||
sceneName,
|
sceneName,
|
||||||
@ -287,6 +296,7 @@ export default function AIOriginalUploader({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
materialId,
|
materialId,
|
||||||
taskNo: taskNo || undefined,
|
taskNo: taskNo || undefined,
|
||||||
|
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||||
assets: {
|
assets: {
|
||||||
preview: [], // 不传预览图,后端自动生成
|
preview: [], // 不传预览图,后端自动生成
|
||||||
main: mainAssets.map((m, index) => ({
|
main: mainAssets.map((m, index) => ({
|
||||||
@ -330,24 +340,14 @@ export default function AIOriginalUploader({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 任务号字段 */}
|
<TaskReferenceFields
|
||||||
<div>
|
idPrefix="ai-original"
|
||||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
taskNo={taskNo}
|
||||||
任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span>
|
onTaskNoChange={setTaskNo}
|
||||||
</label>
|
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||||
<input
|
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||||
id="taskNo"
|
disabled={uploading}
|
||||||
type="text"
|
/>
|
||||||
value={taskNo}
|
|
||||||
onChange={(e) => setTaskNo(e.target.value)}
|
|
||||||
placeholder="TASK + 12-18位数字,如 TASK202601030184"
|
|
||||||
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
|
|
||||||
disabled={uploading}
|
|
||||||
/>
|
|
||||||
{taskNo && !validateTaskNo(taskNo) && (
|
|
||||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SPU型号 */}
|
{/* SPU型号 */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -18,6 +18,8 @@ import {
|
|||||||
prepareFileDrag,
|
prepareFileDrag,
|
||||||
validateImageFiles,
|
validateImageFiles,
|
||||||
} from './drag-drop-utils';
|
} from './drag-drop-utils';
|
||||||
|
import TaskReferenceFields from './TaskReferenceFields';
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||||
|
|
||||||
const TASK_DETAIL_SOURCE_FILE_EXTENSIONS = ['psd', 'psb', 'tif', 'tiff', 'zip', 'rar', '7z'] as const;
|
const TASK_DETAIL_SOURCE_FILE_EXTENSIONS = ['psd', 'psb', 'tif', 'tiff', 'zip', 'rar', '7z'] as const;
|
||||||
const TASK_DETAIL_SOURCE_FILE_ACCEPT = '.psd,.psb,.tif,.tiff,.zip,.rar,.7z';
|
const TASK_DETAIL_SOURCE_FILE_ACCEPT = '.psd,.psb,.tif,.tiff,.zip,.rar,.7z';
|
||||||
@ -100,6 +102,7 @@ export default function ProductPackageUploader({
|
|||||||
}: ProductPackageUploaderProps) {
|
}: ProductPackageUploaderProps) {
|
||||||
// 基本信息
|
// 基本信息
|
||||||
const [taskNo, setTaskNo] = useState('');
|
const [taskNo, setTaskNo] = useState('');
|
||||||
|
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('');
|
||||||
const [productModel, setProductModel] = useState('');
|
const [productModel, setProductModel] = useState('');
|
||||||
const [categoryName, setCategoryName] = useState('');
|
const [categoryName, setCategoryName] = useState('');
|
||||||
const [sceneName, setSceneName] = useState('');
|
const [sceneName, setSceneName] = useState('');
|
||||||
@ -442,6 +445,11 @@ export default function ProductPackageUploader({
|
|||||||
setError('任务号格式错误,应为 TASK + 12-18位数字,可选的多个子任务号(-2位数字,如 TASK202601141713525624-01-01)');
|
setError('任务号格式错误,应为 TASK + 12-18位数字,可选的多个子任务号(-2位数字,如 TASK202601141713525624-01-01)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
setError(dingtalkApprovalNoError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 主图改为非必选,只要至少有一张图片即可
|
// 主图改为非必选,只要至少有一张图片即可
|
||||||
if (mainImages.length === 0 && skuImages.length === 0 && detailImages.length === 0) {
|
if (mainImages.length === 0 && skuImages.length === 0 && detailImages.length === 0) {
|
||||||
@ -504,6 +512,7 @@ export default function ProductPackageUploader({
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
taskNo,
|
taskNo,
|
||||||
|
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||||
productModel,
|
productModel,
|
||||||
categoryName,
|
categoryName,
|
||||||
sceneName,
|
sceneName,
|
||||||
@ -613,6 +622,7 @@ export default function ProductPackageUploader({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
materialId,
|
materialId,
|
||||||
taskNo, // 传递taskNo,上传完成后写入数据库
|
taskNo, // 传递taskNo,上传完成后写入数据库
|
||||||
|
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||||
noBarcodes,
|
noBarcodes,
|
||||||
assets: {
|
assets: {
|
||||||
main: mainAssets,
|
main: mainAssets,
|
||||||
@ -689,24 +699,14 @@ export default function ProductPackageUploader({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 任务号字段(任务-详情独有) */}
|
<TaskReferenceFields
|
||||||
<div>
|
idPrefix="product-package"
|
||||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
taskNo={taskNo}
|
||||||
任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span>
|
onTaskNoChange={setTaskNo}
|
||||||
</label>
|
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||||
<input
|
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||||
id="taskNo"
|
disabled={uploading}
|
||||||
type="text"
|
/>
|
||||||
value={taskNo}
|
|
||||||
onChange={(e) => setTaskNo(e.target.value)}
|
|
||||||
placeholder="TASK + 12-18位数字,如 TASK202601030184 或 TASK202601071437194808-01"
|
|
||||||
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
|
|
||||||
disabled={uploading}
|
|
||||||
/>
|
|
||||||
{taskNo && !validateTaskNo(taskNo) && (
|
|
||||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import {
|
|||||||
prepareFileDrag,
|
prepareFileDrag,
|
||||||
validateImageFiles,
|
validateImageFiles,
|
||||||
} from './drag-drop-utils';
|
} from './drag-drop-utils';
|
||||||
|
import TaskReferenceFields from './TaskReferenceFields';
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||||
|
|
||||||
interface AssetFile {
|
interface AssetFile {
|
||||||
file: File;
|
file: File;
|
||||||
@ -43,6 +45,7 @@ export default function RealPhotoUploader({
|
|||||||
}: RealPhotoUploaderProps) {
|
}: RealPhotoUploaderProps) {
|
||||||
|
|
||||||
const [taskNo, setTaskNo] = useState('');
|
const [taskNo, setTaskNo] = useState('');
|
||||||
|
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('');
|
||||||
const [productModel, setProductModel] = useState('');
|
const [productModel, setProductModel] = useState('');
|
||||||
const [categoryName, setCategoryName] = useState('');
|
const [categoryName, setCategoryName] = useState('');
|
||||||
const [sceneName, setSceneName] = useState('');
|
const [sceneName, setSceneName] = useState('');
|
||||||
@ -203,6 +206,11 @@ export default function RealPhotoUploader({
|
|||||||
setError('任务号格式错误');
|
setError('任务号格式错误');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
setError(dingtalkApprovalNoError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!productModel.trim()) {
|
if (!productModel.trim()) {
|
||||||
setError('请输入产品型号(SPU)');
|
setError('请输入产品型号(SPU)');
|
||||||
return;
|
return;
|
||||||
@ -230,6 +238,7 @@ export default function RealPhotoUploader({
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
taskNo: taskNo || undefined,
|
taskNo: taskNo || undefined,
|
||||||
|
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||||
productModel,
|
productModel,
|
||||||
categoryName,
|
categoryName,
|
||||||
sceneName,
|
sceneName,
|
||||||
@ -286,6 +295,7 @@ export default function RealPhotoUploader({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
materialId,
|
materialId,
|
||||||
taskNo: taskNo || undefined,
|
taskNo: taskNo || undefined,
|
||||||
|
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||||
assets: {
|
assets: {
|
||||||
preview: [],
|
preview: [],
|
||||||
main: mainAssets.map((m, index) => ({
|
main: mainAssets.map((m, index) => ({
|
||||||
@ -327,24 +337,14 @@ export default function RealPhotoUploader({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 任务号字段 */}
|
<TaskReferenceFields
|
||||||
<div>
|
idPrefix="real-photo"
|
||||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
taskNo={taskNo}
|
||||||
任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span>
|
onTaskNoChange={setTaskNo}
|
||||||
</label>
|
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||||
<input
|
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||||
id="taskNo"
|
disabled={uploading}
|
||||||
type="text"
|
/>
|
||||||
value={taskNo}
|
|
||||||
onChange={(e) => setTaskNo(e.target.value)}
|
|
||||||
placeholder="TASK + 12-18位数字,如 TASK202601030184"
|
|
||||||
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
|
|
||||||
disabled={uploading}
|
|
||||||
/>
|
|
||||||
{taskNo && !validateTaskNo(taskNo) && (
|
|
||||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SPU型号 */}
|
{/* SPU型号 */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import {
|
|||||||
prepareFileDrag,
|
prepareFileDrag,
|
||||||
validateImageFiles,
|
validateImageFiles,
|
||||||
} from './drag-drop-utils';
|
} from './drag-drop-utils';
|
||||||
|
import TaskReferenceFields from './TaskReferenceFields';
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||||
|
|
||||||
interface AssetFile {
|
interface AssetFile {
|
||||||
file: File;
|
file: File;
|
||||||
@ -43,6 +45,7 @@ export default function TaskPageUploader({
|
|||||||
}: TaskPageUploaderProps) {
|
}: TaskPageUploaderProps) {
|
||||||
|
|
||||||
const [taskNo, setTaskNo] = useState('');
|
const [taskNo, setTaskNo] = useState('');
|
||||||
|
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('');
|
||||||
const [productModel, setProductModel] = useState('');
|
const [productModel, setProductModel] = useState('');
|
||||||
const [categoryName, setCategoryName] = useState('');
|
const [categoryName, setCategoryName] = useState('');
|
||||||
const [sceneName, setSceneName] = useState('');
|
const [sceneName, setSceneName] = useState('');
|
||||||
@ -181,6 +184,11 @@ export default function TaskPageUploader({
|
|||||||
setError('任务号格式错误,应为 TASK + 12-18位数字,可选的多个子任务号(-2位数字,如 TASK202601141713525624-01-01)');
|
setError('任务号格式错误,应为 TASK + 12-18位数字,可选的多个子任务号(-2位数字,如 TASK202601141713525624-01-01)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
setError(dingtalkApprovalNoError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 验证至少有一张预览图
|
// 验证至少有一张预览图
|
||||||
if (previewImages.length === 0) {
|
if (previewImages.length === 0) {
|
||||||
@ -201,6 +209,7 @@ export default function TaskPageUploader({
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
taskNo,
|
taskNo,
|
||||||
|
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||||
productModel,
|
productModel,
|
||||||
categoryName,
|
categoryName,
|
||||||
sceneName,
|
sceneName,
|
||||||
@ -262,6 +271,7 @@ export default function TaskPageUploader({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
materialId,
|
materialId,
|
||||||
taskNo: taskNo || undefined,
|
taskNo: taskNo || undefined,
|
||||||
|
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||||
assets: {
|
assets: {
|
||||||
main: previewAssets.map((m, index) => ({
|
main: previewAssets.map((m, index) => ({
|
||||||
uploadId: m.uploadId,
|
uploadId: m.uploadId,
|
||||||
@ -313,24 +323,14 @@ export default function TaskPageUploader({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 任务号字段 */}
|
<TaskReferenceFields
|
||||||
<div>
|
idPrefix="task-page"
|
||||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
taskNo={taskNo}
|
||||||
任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span>
|
onTaskNoChange={setTaskNo}
|
||||||
</label>
|
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||||
<input
|
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||||
id="taskNo"
|
disabled={uploading}
|
||||||
type="text"
|
/>
|
||||||
value={taskNo}
|
|
||||||
onChange={(e) => setTaskNo(e.target.value)}
|
|
||||||
placeholder="TASK + 12-18位数字,如 TASK202601030184 或 TASK202601071437194808-01"
|
|
||||||
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
|
|
||||||
disabled={uploading}
|
|
||||||
/>
|
|
||||||
{taskNo && !validateTaskNo(taskNo) && (
|
|
||||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SPU型号 */}
|
{/* SPU型号 */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
58
components/upload/TaskReferenceFields.test.tsx
Normal file
58
components/upload/TaskReferenceFields.test.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
|
||||||
|
import TaskReferenceFields from '@/components/upload/TaskReferenceFields'
|
||||||
|
|
||||||
|
describe('TaskReferenceFields', () => {
|
||||||
|
test('任务号与钉钉审批编号使用响应式双列布局', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<TaskReferenceFields
|
||||||
|
idPrefix="task-reference"
|
||||||
|
taskNo=""
|
||||||
|
onTaskNoChange={jest.fn()}
|
||||||
|
dingtalkApprovalNo=""
|
||||||
|
onDingtalkApprovalNoChange={jest.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(container.firstChild).toHaveClass('grid-cols-1', 'md:grid-cols-2')
|
||||||
|
expect(screen.getByLabelText('任务号')).toBeInTheDocument()
|
||||||
|
expect(screen.getByLabelText('钉钉审批编号')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('任务号输入会统一转为大写', () => {
|
||||||
|
const onTaskNoChange = jest.fn()
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskReferenceFields
|
||||||
|
idPrefix="task-reference"
|
||||||
|
taskNo=""
|
||||||
|
onTaskNoChange={onTaskNoChange}
|
||||||
|
dingtalkApprovalNo=""
|
||||||
|
onDingtalkApprovalNoChange={jest.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('任务号'), {
|
||||||
|
target: { value: 'task202601030184' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(onTaskNoChange).toHaveBeenCalledWith('TASK202601030184')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('非法审批编号应显示紧邻输入框的错误状态', () => {
|
||||||
|
render(
|
||||||
|
<TaskReferenceFields
|
||||||
|
idPrefix="task-reference"
|
||||||
|
taskNo=""
|
||||||
|
onTaskNoChange={jest.fn()}
|
||||||
|
dingtalkApprovalNo="20250230123"
|
||||||
|
onDingtalkApprovalNoChange={jest.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const input = screen.getByLabelText('钉钉审批编号')
|
||||||
|
fireEvent.blur(input)
|
||||||
|
expect(input).toHaveAttribute('aria-invalid', 'true')
|
||||||
|
expect(screen.getByText('钉钉审批编号前8位不是有效日期')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
112
components/upload/TaskReferenceFields.tsx
Normal file
112
components/upload/TaskReferenceFields.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
DINGTALK_APPROVAL_NO_MAX_LENGTH,
|
||||||
|
getDingtalkApprovalNoError,
|
||||||
|
} from '@/lib/dingtalk-approval-no'
|
||||||
|
|
||||||
|
interface TaskReferenceFieldsProps {
|
||||||
|
idPrefix: string
|
||||||
|
taskNo: string
|
||||||
|
onTaskNoChange: (value: string) => void
|
||||||
|
dingtalkApprovalNo: string
|
||||||
|
onDingtalkApprovalNoChange: (value: string) => void
|
||||||
|
taskNoRequired?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTaskNoError(value: string): string | null {
|
||||||
|
const normalized = value.trim().toUpperCase()
|
||||||
|
if (!normalized) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return /^TASK\d{12,18}(-\d{2})*$/.test(normalized)
|
||||||
|
? null
|
||||||
|
: '任务号格式错误'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TaskReferenceFields({
|
||||||
|
idPrefix,
|
||||||
|
taskNo,
|
||||||
|
onTaskNoChange,
|
||||||
|
dingtalkApprovalNo,
|
||||||
|
onDingtalkApprovalNoChange,
|
||||||
|
taskNoRequired = false,
|
||||||
|
disabled = false,
|
||||||
|
}: TaskReferenceFieldsProps) {
|
||||||
|
const [taskNoTouched, setTaskNoTouched] = useState(false)
|
||||||
|
const [approvalNoTouched, setApprovalNoTouched] = useState(false)
|
||||||
|
const taskNoId = `${idPrefix}-task-no`
|
||||||
|
const taskNoErrorId = `${taskNoId}-error`
|
||||||
|
const approvalNoId = `${idPrefix}-dingtalk-approval-no`
|
||||||
|
const approvalNoErrorId = `${approvalNoId}-error`
|
||||||
|
const taskNoError = getTaskNoError(taskNo)
|
||||||
|
const approvalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo)
|
||||||
|
const visibleTaskNoError = taskNoTouched ? taskNoError : null
|
||||||
|
const visibleApprovalNoError = approvalNoTouched ? approvalNoError : null
|
||||||
|
const inputClassName =
|
||||||
|
'w-full rounded-apple border border-gray-300 px-4 py-2 outline-none focus:border-gray-900 focus:ring-2 focus:ring-gray-200 disabled:cursor-not-allowed disabled:opacity-60 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:border-gray-400 dark:focus:ring-gray-600'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<label
|
||||||
|
htmlFor={taskNoId}
|
||||||
|
className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
任务号{taskNoRequired ? ' *' : ''}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={taskNoId}
|
||||||
|
type="text"
|
||||||
|
value={taskNo}
|
||||||
|
onChange={(event) => onTaskNoChange(event.target.value.toUpperCase())}
|
||||||
|
onBlur={() => setTaskNoTouched(true)}
|
||||||
|
placeholder="例如:TASK202601030184"
|
||||||
|
className={inputClassName}
|
||||||
|
disabled={disabled}
|
||||||
|
required={taskNoRequired}
|
||||||
|
aria-invalid={Boolean(visibleTaskNoError)}
|
||||||
|
aria-describedby={visibleTaskNoError ? taskNoErrorId : undefined}
|
||||||
|
/>
|
||||||
|
{visibleTaskNoError && (
|
||||||
|
<p id={taskNoErrorId} className="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||||
|
{visibleTaskNoError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0">
|
||||||
|
<label
|
||||||
|
htmlFor={approvalNoId}
|
||||||
|
className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
钉钉审批编号
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={approvalNoId}
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
autoComplete="off"
|
||||||
|
maxLength={DINGTALK_APPROVAL_NO_MAX_LENGTH}
|
||||||
|
value={dingtalkApprovalNo}
|
||||||
|
onChange={(event) => onDingtalkApprovalNoChange(event.target.value)}
|
||||||
|
onBlur={() => setApprovalNoTouched(true)}
|
||||||
|
placeholder="例如:202508211445000157693"
|
||||||
|
className={inputClassName}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-invalid={Boolean(visibleApprovalNoError)}
|
||||||
|
aria-describedby={visibleApprovalNoError ? approvalNoErrorId : undefined}
|
||||||
|
/>
|
||||||
|
{visibleApprovalNoError && (
|
||||||
|
<p id={approvalNoErrorId} className="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||||
|
{visibleApprovalNoError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -8,6 +8,8 @@ import {
|
|||||||
prepareFileDrag,
|
prepareFileDrag,
|
||||||
validateImageFiles,
|
validateImageFiles,
|
||||||
} from './drag-drop-utils';
|
} from './drag-drop-utils';
|
||||||
|
import TaskReferenceFields from './TaskReferenceFields';
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||||
|
|
||||||
interface AssetFile {
|
interface AssetFile {
|
||||||
file: File;
|
file: File;
|
||||||
@ -43,6 +45,7 @@ export default function ThreeDOriginalUploader({
|
|||||||
}: ThreeDOriginalUploaderProps) {
|
}: ThreeDOriginalUploaderProps) {
|
||||||
|
|
||||||
const [taskNo, setTaskNo] = useState('');
|
const [taskNo, setTaskNo] = useState('');
|
||||||
|
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('');
|
||||||
const [productModel, setProductModel] = useState('');
|
const [productModel, setProductModel] = useState('');
|
||||||
const [categoryName, setCategoryName] = useState('');
|
const [categoryName, setCategoryName] = useState('');
|
||||||
const [sceneName, setSceneName] = useState('');
|
const [sceneName, setSceneName] = useState('');
|
||||||
@ -203,6 +206,11 @@ export default function ThreeDOriginalUploader({
|
|||||||
setError('任务号格式错误');
|
setError('任务号格式错误');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
setError(dingtalkApprovalNoError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!productModel.trim()) {
|
if (!productModel.trim()) {
|
||||||
setError('请输入产品型号(SPU)');
|
setError('请输入产品型号(SPU)');
|
||||||
return;
|
return;
|
||||||
@ -230,6 +238,7 @@ export default function ThreeDOriginalUploader({
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
taskNo: taskNo || undefined,
|
taskNo: taskNo || undefined,
|
||||||
|
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||||
productModel,
|
productModel,
|
||||||
categoryName,
|
categoryName,
|
||||||
sceneName,
|
sceneName,
|
||||||
@ -287,6 +296,7 @@ export default function ThreeDOriginalUploader({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
materialId,
|
materialId,
|
||||||
taskNo: taskNo || undefined,
|
taskNo: taskNo || undefined,
|
||||||
|
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||||
assets: {
|
assets: {
|
||||||
preview: [], // 不传预览图,后端自动生成
|
preview: [], // 不传预览图,后端自动生成
|
||||||
main: mainAssets.map((m, index) => ({
|
main: mainAssets.map((m, index) => ({
|
||||||
@ -330,24 +340,14 @@ export default function ThreeDOriginalUploader({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 任务号字段 */}
|
<TaskReferenceFields
|
||||||
<div>
|
idPrefix="three-d-original"
|
||||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
taskNo={taskNo}
|
||||||
任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span>
|
onTaskNoChange={setTaskNo}
|
||||||
</label>
|
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||||
<input
|
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||||
id="taskNo"
|
disabled={uploading}
|
||||||
type="text"
|
/>
|
||||||
value={taskNo}
|
|
||||||
onChange={(e) => setTaskNo(e.target.value)}
|
|
||||||
placeholder="TASK + 12-18位数字,如 TASK202601030184"
|
|
||||||
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
|
|
||||||
disabled={uploading}
|
|
||||||
/>
|
|
||||||
{taskNo && !validateTaskNo(taskNo) && (
|
|
||||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SPU型号 */}
|
{/* SPU型号 */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import {
|
|||||||
prepareFileDrag,
|
prepareFileDrag,
|
||||||
validateImageFiles,
|
validateImageFiles,
|
||||||
} from './drag-drop-utils';
|
} from './drag-drop-utils';
|
||||||
|
import TaskReferenceFields from './TaskReferenceFields';
|
||||||
|
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||||
|
|
||||||
interface AssetFile {
|
interface AssetFile {
|
||||||
file: File;
|
file: File;
|
||||||
@ -43,6 +45,7 @@ export default function VideoScreenshotUploader({
|
|||||||
}: VideoScreenshotUploaderProps) {
|
}: VideoScreenshotUploaderProps) {
|
||||||
|
|
||||||
const [taskNo, setTaskNo] = useState('');
|
const [taskNo, setTaskNo] = useState('');
|
||||||
|
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('');
|
||||||
const [productModel, setProductModel] = useState('');
|
const [productModel, setProductModel] = useState('');
|
||||||
const [categoryName, setCategoryName] = useState('');
|
const [categoryName, setCategoryName] = useState('');
|
||||||
const [sceneName, setSceneName] = useState('');
|
const [sceneName, setSceneName] = useState('');
|
||||||
@ -204,6 +207,11 @@ export default function VideoScreenshotUploader({
|
|||||||
setError('任务号格式错误');
|
setError('任务号格式错误');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
setError(dingtalkApprovalNoError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!productModel.trim()) {
|
if (!productModel.trim()) {
|
||||||
setError('请输入产品型号(SPU)');
|
setError('请输入产品型号(SPU)');
|
||||||
return;
|
return;
|
||||||
@ -234,6 +242,7 @@ export default function VideoScreenshotUploader({
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
taskNo: taskNo || undefined,
|
taskNo: taskNo || undefined,
|
||||||
|
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||||
productModel,
|
productModel,
|
||||||
categoryName,
|
categoryName,
|
||||||
sceneName,
|
sceneName,
|
||||||
@ -293,6 +302,7 @@ export default function VideoScreenshotUploader({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
materialId,
|
materialId,
|
||||||
taskNo: taskNo || undefined,
|
taskNo: taskNo || undefined,
|
||||||
|
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||||
assets: {
|
assets: {
|
||||||
preview: [], // 不传预览图,后端自动生成
|
preview: [], // 不传预览图,后端自动生成
|
||||||
main: screenshotAssets.map((m, index) => ({
|
main: screenshotAssets.map((m, index) => ({
|
||||||
@ -345,24 +355,14 @@ export default function VideoScreenshotUploader({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 任务号字段 */}
|
<TaskReferenceFields
|
||||||
<div>
|
idPrefix="video-screenshot"
|
||||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
taskNo={taskNo}
|
||||||
任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span>
|
onTaskNoChange={setTaskNo}
|
||||||
</label>
|
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||||
<input
|
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||||
id="taskNo"
|
disabled={uploading}
|
||||||
type="text"
|
/>
|
||||||
value={taskNo}
|
|
||||||
onChange={(e) => setTaskNo(e.target.value)}
|
|
||||||
placeholder="TASK + 12-18位数字,如 TASK202601030184"
|
|
||||||
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
|
|
||||||
disabled={uploading}
|
|
||||||
/>
|
|
||||||
{taskNo && !validateTaskNo(taskNo) && (
|
|
||||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SPU型号 */}
|
{/* SPU型号 */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export function buildFormData(params: {
|
|||||||
productCategoryName: string
|
productCategoryName: string
|
||||||
renderer: string
|
renderer: string
|
||||||
taskNo?: string
|
taskNo?: string
|
||||||
|
dingtalkApprovalNo?: string
|
||||||
}): FormData {
|
}): FormData {
|
||||||
const {
|
const {
|
||||||
previewImages,
|
previewImages,
|
||||||
@ -37,6 +38,7 @@ export function buildFormData(params: {
|
|||||||
productCategoryName,
|
productCategoryName,
|
||||||
renderer,
|
renderer,
|
||||||
taskNo,
|
taskNo,
|
||||||
|
dingtalkApprovalNo,
|
||||||
} = params
|
} = params
|
||||||
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
@ -72,6 +74,9 @@ export function buildFormData(params: {
|
|||||||
if (taskNo) {
|
if (taskNo) {
|
||||||
formData.append('taskNo', taskNo)
|
formData.append('taskNo', taskNo)
|
||||||
}
|
}
|
||||||
|
if (dingtalkApprovalNo) {
|
||||||
|
formData.append('dingtalkApprovalNo', dingtalkApprovalNo)
|
||||||
|
}
|
||||||
|
|
||||||
// 统一处理renderer字段:根据素材类型判断是否需要传递
|
// 统一处理renderer字段:根据素材类型判断是否需要传递
|
||||||
const rendererInfo = shouldPassRenderer(materialType, renderer)
|
const rendererInfo = shouldPassRenderer(materialType, renderer)
|
||||||
|
|||||||
39
lib/__tests__/dingtalk-approval-no.test.ts
Normal file
39
lib/__tests__/dingtalk-approval-no.test.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
DINGTALK_APPROVAL_NO_MAX_LENGTH,
|
||||||
|
getDingtalkApprovalNoError,
|
||||||
|
normalizeDingtalkApprovalNo,
|
||||||
|
validateDingtalkApprovalNo,
|
||||||
|
} from '@/lib/dingtalk-approval-no'
|
||||||
|
|
||||||
|
describe('dingtalk approval number', () => {
|
||||||
|
test('允许不填写', () => {
|
||||||
|
expect(normalizeDingtalkApprovalNo(' ')).toBeNull()
|
||||||
|
expect(validateDingtalkApprovalNo('')).toBe(true)
|
||||||
|
expect(validateDingtalkApprovalNo(null)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('接受日期前缀加任意长度数字后缀', () => {
|
||||||
|
expect(validateDingtalkApprovalNo('202508211445000157693')).toBe(true)
|
||||||
|
expect(validateDingtalkApprovalNo('202402291')).toBe(true)
|
||||||
|
expect(validateDingtalkApprovalNo(' 20250821123 ')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('拒绝非数字内容和缺少后续编号', () => {
|
||||||
|
expect(getDingtalkApprovalNoError('20250821A1')).toBe('钉钉审批编号只能输入数字')
|
||||||
|
expect(getDingtalkApprovalNoError('20250821')).toBe('钉钉审批编号需包含8位日期和后续编号')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('拒绝无效日期前缀', () => {
|
||||||
|
expect(getDingtalkApprovalNoError('20250230123')).toBe('钉钉审批编号前8位不是有效日期')
|
||||||
|
expect(getDingtalkApprovalNoError('20230229123')).toBe('钉钉审批编号前8位不是有效日期')
|
||||||
|
expect(getDingtalkApprovalNoError('20251301123')).toBe('钉钉审批编号前8位不是有效日期')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('限制异常超长输入', () => {
|
||||||
|
const value = `20250821${'1'.repeat(DINGTALK_APPROVAL_NO_MAX_LENGTH - 7)}`
|
||||||
|
expect(value).toHaveLength(DINGTALK_APPROVAL_NO_MAX_LENGTH + 1)
|
||||||
|
expect(getDingtalkApprovalNoError(value)).toBe(
|
||||||
|
`钉钉审批编号不能超过${DINGTALK_APPROVAL_NO_MAX_LENGTH}位`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -63,6 +63,7 @@ describe('material-update shared rules', () => {
|
|||||||
renderer: 'CR',
|
renderer: 'CR',
|
||||||
authorName: '旧作者',
|
authorName: '旧作者',
|
||||||
taskNo: 'TASK202601141713525624',
|
taskNo: 'TASK202601141713525624',
|
||||||
|
dingtalkApprovalNo: null,
|
||||||
taskDetailRequesterUserId: null,
|
taskDetailRequesterUserId: null,
|
||||||
taskDetailRequesterName: null,
|
taskDetailRequesterName: null,
|
||||||
isFullSet: true,
|
isFullSet: true,
|
||||||
@ -684,6 +685,7 @@ describe('material-update shared rules', () => {
|
|||||||
authorName: '任务作者',
|
authorName: '任务作者',
|
||||||
userId: 'user-1',
|
userId: 'user-1',
|
||||||
taskNo: 'task202603310001',
|
taskNo: 'task202603310001',
|
||||||
|
dingtalkApprovalNo: '202508211445000157693',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -695,11 +697,65 @@ describe('material-update shared rules', () => {
|
|||||||
categoryName: '通用',
|
categoryName: '通用',
|
||||||
sceneName: '通用',
|
sceneName: '通用',
|
||||||
taskNo: 'TASK202603310001',
|
taskNo: 'TASK202603310001',
|
||||||
|
dingtalkApprovalNo: '202508211445000157693',
|
||||||
materialType: '任务-其他',
|
materialType: '任务-其他',
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('任务素材编辑时应拒绝无效的钉钉审批编号', async () => {
|
||||||
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
||||||
|
...baseMaterial,
|
||||||
|
materialType: '任务-页面',
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await updateMaterialWithSharedRules({
|
||||||
|
materialId: 'material-1',
|
||||||
|
operatorUserId: 'user-1',
|
||||||
|
operatorRole: 'USER',
|
||||||
|
input: {
|
||||||
|
productModel: 'SPU-001',
|
||||||
|
categoryName: '店铺A',
|
||||||
|
sceneName: '场景A',
|
||||||
|
materialType: '任务-页面',
|
||||||
|
renderer: '',
|
||||||
|
authorName: '任务作者',
|
||||||
|
dingtalkApprovalNo: '20250230123',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
error: '钉钉审批编号前8位不是有效日期',
|
||||||
|
status: 400,
|
||||||
|
})
|
||||||
|
expect(prisma.material.update).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('非任务素材不能写入钉钉审批编号', async () => {
|
||||||
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
|
||||||
|
|
||||||
|
const result = await updateMaterialWithSharedRules({
|
||||||
|
materialId: 'material-1',
|
||||||
|
operatorUserId: 'user-1',
|
||||||
|
operatorRole: 'USER',
|
||||||
|
input: {
|
||||||
|
productModel: 'SPU-001',
|
||||||
|
categoryName: '店铺A',
|
||||||
|
sceneName: '场景A',
|
||||||
|
materialType: '3D场景',
|
||||||
|
renderer: 'CR',
|
||||||
|
authorName: '作者',
|
||||||
|
dingtalkApprovalNo: '202508211445000157693',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
error: '钉钉审批编号仅适用于“任务-”素材',
|
||||||
|
status: 400,
|
||||||
|
})
|
||||||
|
expect(prisma.material.update).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
test('任务-详情编辑时应按已有资产自动回写 isFullSet', async () => {
|
test('任务-详情编辑时应按已有资产自动回写 isFullSet', async () => {
|
||||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
||||||
...baseMaterial,
|
...baseMaterial,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { compressImage, sanitizeFileName } from '@/lib/image-processor'
|
|||||||
import { createMaterialLog } from '@/lib/material-log'
|
import { createMaterialLog } from '@/lib/material-log'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { uploadPublicDisplayToS3 } from '@/lib/public-display'
|
import { uploadPublicDisplayToS3 } from '@/lib/public-display'
|
||||||
|
import { assignTaskNoOnComplete } from '@/lib/product-package'
|
||||||
|
|
||||||
jest.mock('next-auth', () => ({
|
jest.mock('next-auth', () => ({
|
||||||
getServerSession: jest.fn(),
|
getServerSession: jest.fn(),
|
||||||
@ -83,6 +84,7 @@ jest.mock('@/lib/public-display', () => ({
|
|||||||
jest.mock('@/lib/product-package', () => ({
|
jest.mock('@/lib/product-package', () => ({
|
||||||
validateTaskNo: jest.fn(() => true),
|
validateTaskNo: jest.fn(() => true),
|
||||||
isTaskNoExists: jest.fn().mockResolvedValue(false),
|
isTaskNoExists: jest.fn().mockResolvedValue(false),
|
||||||
|
assignTaskNoOnComplete: jest.fn().mockResolvedValue(null),
|
||||||
purgeDeletedMaterialsByTaskNo: jest.fn().mockResolvedValue({
|
purgeDeletedMaterialsByTaskNo: jest.fn().mockResolvedValue({
|
||||||
deletedMaterials: 0,
|
deletedMaterials: 0,
|
||||||
deletedFiles: 0,
|
deletedFiles: 0,
|
||||||
@ -143,6 +145,7 @@ describe('materials upload route - 任务-其他', () => {
|
|||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('materialType', '任务-其他')
|
formData.append('materialType', '任务-其他')
|
||||||
formData.append('taskNo', 'TASK202603310001')
|
formData.append('taskNo', 'TASK202603310001')
|
||||||
|
formData.append('dingtalkApprovalNo', '202508211445000157693')
|
||||||
formData.append('productModel', '')
|
formData.append('productModel', '')
|
||||||
formData.append('categoryName', '')
|
formData.append('categoryName', '')
|
||||||
formData.append('sceneName', '')
|
formData.append('sceneName', '')
|
||||||
@ -193,6 +196,18 @@ describe('materials upload route - 任务-其他', () => {
|
|||||||
expect(prisma.material.create).not.toHaveBeenCalled()
|
expect(prisma.material.create).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('钉钉审批编号日期无效时应在创建素材前返回失败', async () => {
|
||||||
|
const formData = buildTaskOtherFormData()
|
||||||
|
formData.set('dingtalkApprovalNo', '20250230123')
|
||||||
|
|
||||||
|
const response = await POST(createRequest(formData))
|
||||||
|
const body = await response.json()
|
||||||
|
|
||||||
|
expect(response.status).toBe(400)
|
||||||
|
expect(body.error).toBe('钉钉审批编号前8位不是有效日期')
|
||||||
|
expect(prisma.material.create).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
test('正常上传时应自动补通用店铺和场景,并保留原始压缩包文件名', async () => {
|
test('正常上传时应自动补通用店铺和场景,并保留原始压缩包文件名', async () => {
|
||||||
const formData = buildTaskOtherFormData()
|
const formData = buildTaskOtherFormData()
|
||||||
|
|
||||||
@ -226,14 +241,10 @@ describe('materials upload route - 任务-其他', () => {
|
|||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
expect(prisma.material.update).toHaveBeenCalledWith(
|
expect(assignTaskNoOnComplete).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
'material-task-other-1',
|
||||||
where: { id: 'material-task-other-1' },
|
'TASK202603310001',
|
||||||
data: expect.objectContaining({
|
'202508211445000157693',
|
||||||
taskNo: 'TASK202603310001',
|
|
||||||
taskStatus: 'completed',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
expect(createMaterialLog).toHaveBeenCalledWith(
|
expect(createMaterialLog).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
import { createMaterialLog } from '@/lib/material-log'
|
import { createMaterialLog } from '@/lib/material-log'
|
||||||
import { createZipFromFiles } from '@/lib/task-material-processor'
|
import { createZipFromFiles } from '@/lib/task-material-processor'
|
||||||
import { getOwnedUploadSession } from '@/lib/upload-sessions'
|
import { getOwnedUploadSession } from '@/lib/upload-sessions'
|
||||||
|
import { assignTaskNoOnComplete } from '@/lib/product-package'
|
||||||
|
|
||||||
jest.mock('@/lib/prisma', () => ({
|
jest.mock('@/lib/prisma', () => ({
|
||||||
prisma: {
|
prisma: {
|
||||||
@ -114,6 +115,7 @@ describe('product-package-completion', () => {
|
|||||||
const result = await completeProductPackageUpload({
|
const result = await completeProductPackageUpload({
|
||||||
materialId: 'material-1',
|
materialId: 'material-1',
|
||||||
taskNo: null,
|
taskNo: null,
|
||||||
|
dingtalkApprovalNo: '202508211445000157693',
|
||||||
operatorUserId: 'user-1',
|
operatorUserId: 'user-1',
|
||||||
assets: {
|
assets: {
|
||||||
main: [
|
main: [
|
||||||
@ -148,8 +150,14 @@ describe('product-package-completion', () => {
|
|||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
materialId: 'material-1',
|
materialId: 'material-1',
|
||||||
taskNo: null,
|
taskNo: null,
|
||||||
|
dingtalkApprovalNo: '202508211445000157693',
|
||||||
isFullSet: true,
|
isFullSet: true,
|
||||||
})
|
})
|
||||||
|
expect(assignTaskNoOnComplete).toHaveBeenCalledWith(
|
||||||
|
'material-1',
|
||||||
|
null,
|
||||||
|
'202508211445000157693',
|
||||||
|
)
|
||||||
expect(queueTaskDetailFullSetNotification).toHaveBeenCalledWith('material-1')
|
expect(queueTaskDetailFullSetNotification).toHaveBeenCalledWith('material-1')
|
||||||
expect(queueTaskDetailBarcodeImagesUpdatedNotification).toHaveBeenCalledWith('material-1', [
|
expect(queueTaskDetailBarcodeImagesUpdatedNotification).toHaveBeenCalledWith('material-1', [
|
||||||
'sku-preview-1',
|
'sku-preview-1',
|
||||||
@ -231,6 +239,7 @@ describe('product-package-completion', () => {
|
|||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
materialId: 'material-1',
|
materialId: 'material-1',
|
||||||
taskNo: null,
|
taskNo: null,
|
||||||
|
dingtalkApprovalNo: null,
|
||||||
isFullSet: true,
|
isFullSet: true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
69
lib/dingtalk-approval-no.ts
Normal file
69
lib/dingtalk-approval-no.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
export const DINGTALK_APPROVAL_NO_MAX_LENGTH = 64
|
||||||
|
|
||||||
|
function isLeapYear(year: number): boolean {
|
||||||
|
return year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDaysInMonth(year: number, month: number): number {
|
||||||
|
if (month === 2) {
|
||||||
|
return isLeapYear(year) ? 29 : 28
|
||||||
|
}
|
||||||
|
|
||||||
|
return [4, 6, 9, 11].includes(month) ? 30 : 31
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasValidDatePrefix(value: string): boolean {
|
||||||
|
const year = Number.parseInt(value.slice(0, 4), 10)
|
||||||
|
const month = Number.parseInt(value.slice(4, 6), 10)
|
||||||
|
const day = Number.parseInt(value.slice(6, 8), 10)
|
||||||
|
|
||||||
|
if (year < 1000 || year > 9999 || month < 1 || month > 12 || day < 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return day <= getDaysInMonth(year, month)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeDingtalkApprovalNo(
|
||||||
|
value: string | null | undefined,
|
||||||
|
): string | null {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = value.trim()
|
||||||
|
return normalized || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDingtalkApprovalNoError(
|
||||||
|
value: string | null | undefined,
|
||||||
|
): string | null {
|
||||||
|
const normalized = normalizeDingtalkApprovalNo(value)
|
||||||
|
if (!normalized) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d+$/.test(normalized)) {
|
||||||
|
return '钉钉审批编号只能输入数字'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.length < 9) {
|
||||||
|
return '钉钉审批编号需包含8位日期和后续编号'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.length > DINGTALK_APPROVAL_NO_MAX_LENGTH) {
|
||||||
|
return `钉钉审批编号不能超过${DINGTALK_APPROVAL_NO_MAX_LENGTH}位`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasValidDatePrefix(normalized)) {
|
||||||
|
return '钉钉审批编号前8位不是有效日期'
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateDingtalkApprovalNo(
|
||||||
|
value: string | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
return getDingtalkApprovalNoError(value) === null
|
||||||
|
}
|
||||||
@ -169,6 +169,7 @@ export async function buildMaterialSearchOrConditions(search: string): Promise<a
|
|||||||
{ productModel: { contains: keyword, mode: 'insensitive' } },
|
{ productModel: { contains: keyword, mode: 'insensitive' } },
|
||||||
{ reportTitle: { contains: keyword, mode: 'insensitive' } },
|
{ reportTitle: { contains: keyword, mode: 'insensitive' } },
|
||||||
{ taskNo: { contains: keyword, mode: 'insensitive' } },
|
{ taskNo: { contains: keyword, mode: 'insensitive' } },
|
||||||
|
{ dingtalkApprovalNo: { contains: keyword, mode: 'insensitive' } },
|
||||||
{ categoryName: { contains: keyword, mode: 'insensitive' } },
|
{ categoryName: { contains: keyword, mode: 'insensitive' } },
|
||||||
{ sceneName: { contains: keyword, mode: 'insensitive' } },
|
{ sceneName: { contains: keyword, mode: 'insensitive' } },
|
||||||
{ authorName: { contains: keyword, mode: 'insensitive' } },
|
{ authorName: { contains: keyword, mode: 'insensitive' } },
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { is3DOriginalType, isProductPackageType } from './material-type-mapper'
|
import { isProductPackageType, mapMaterialType } from './material-type-mapper'
|
||||||
import {
|
import {
|
||||||
DOUYIN_ACCOUNT_MAINTENANCE_VIDEO_TYPE,
|
DOUYIN_ACCOUNT_MAINTENANCE_VIDEO_TYPE,
|
||||||
DOUYIN_QIANCHUAN_VIDEO_TYPE,
|
DOUYIN_QIANCHUAN_VIDEO_TYPE,
|
||||||
@ -57,7 +57,7 @@ export function getSelectableMaterialTypeOptions(currentType?: string | null): s
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isTaskMaterialType(materialType: string): boolean {
|
export function isTaskMaterialType(materialType: string): boolean {
|
||||||
return isProductPackageType(materialType) || is3DOriginalType(materialType)
|
return mapMaterialType(materialType).startsWith('任务-')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function supportsTaskNo(materialType: string): boolean {
|
export function supportsTaskNo(materialType: string): boolean {
|
||||||
|
|||||||
@ -23,6 +23,10 @@ import { deriveTaskDetailIsFullSet } from '@/lib/task-detail-full-set'
|
|||||||
import { queueTaskDetailFullSetNotification } from '@/lib/external-auto-listing'
|
import { queueTaskDetailFullSetNotification } from '@/lib/external-auto-listing'
|
||||||
import { normalizeTaskDetailSourceType } from '@/lib/task-detail-source-type'
|
import { normalizeTaskDetailSourceType } from '@/lib/task-detail-source-type'
|
||||||
import { isCertificateReportMaterialType, normalizeReportTitle } from '@/lib/certificate-report'
|
import { isCertificateReportMaterialType, normalizeReportTitle } from '@/lib/certificate-report'
|
||||||
|
import {
|
||||||
|
getDingtalkApprovalNoError,
|
||||||
|
normalizeDingtalkApprovalNo,
|
||||||
|
} from '@/lib/dingtalk-approval-no'
|
||||||
|
|
||||||
export interface MaterialUpdateInput {
|
export interface MaterialUpdateInput {
|
||||||
productModel: string
|
productModel: string
|
||||||
@ -36,6 +40,7 @@ export interface MaterialUpdateInput {
|
|||||||
authorName: string
|
authorName: string
|
||||||
userId?: string | null
|
userId?: string | null
|
||||||
taskNo?: string | null
|
taskNo?: string | null
|
||||||
|
dingtalkApprovalNo?: string | null
|
||||||
taskDetailSourceType?: string | null
|
taskDetailSourceType?: string | null
|
||||||
taskDetailRequesterUserId?: string | null
|
taskDetailRequesterUserId?: string | null
|
||||||
// 兼容旧调用方传参,实际保存时会按资产自动计算
|
// 兼容旧调用方传参,实际保存时会按资产自动计算
|
||||||
@ -172,6 +177,7 @@ export async function updateMaterialWithSharedRules(options: UpdateMaterialOptio
|
|||||||
|
|
||||||
const material = editable.material as any
|
const material = editable.material as any
|
||||||
const hasTaskNoField = Object.prototype.hasOwnProperty.call(input, 'taskNo')
|
const hasTaskNoField = Object.prototype.hasOwnProperty.call(input, 'taskNo')
|
||||||
|
const hasDingtalkApprovalNoField = Object.prototype.hasOwnProperty.call(input, 'dingtalkApprovalNo')
|
||||||
const hasTaskDetailSourceTypeField = Object.prototype.hasOwnProperty.call(input, 'taskDetailSourceType')
|
const hasTaskDetailSourceTypeField = Object.prototype.hasOwnProperty.call(input, 'taskDetailSourceType')
|
||||||
const hasTaskDetailRequesterField = Object.prototype.hasOwnProperty.call(input, 'taskDetailRequesterUserId')
|
const hasTaskDetailRequesterField = Object.prototype.hasOwnProperty.call(input, 'taskDetailRequesterUserId')
|
||||||
const productModel = normalizeString(input.productModel)
|
const productModel = normalizeString(input.productModel)
|
||||||
@ -184,6 +190,7 @@ export async function updateMaterialWithSharedRules(options: UpdateMaterialOptio
|
|||||||
const platformName = normalizeOptionalString(input.platformName)
|
const platformName = normalizeOptionalString(input.platformName)
|
||||||
const productCategoryName = normalizeOptionalString(input.productCategoryName)
|
const productCategoryName = normalizeOptionalString(input.productCategoryName)
|
||||||
const taskNo = normalizeTaskNo(input.taskNo)
|
const taskNo = normalizeTaskNo(input.taskNo)
|
||||||
|
const dingtalkApprovalNo = normalizeDingtalkApprovalNo(input.dingtalkApprovalNo)
|
||||||
const rawTaskDetailSourceType = input.taskDetailSourceType
|
const rawTaskDetailSourceType = input.taskDetailSourceType
|
||||||
const taskDetailSourceType = normalizeTaskDetailSourceType(rawTaskDetailSourceType)
|
const taskDetailSourceType = normalizeTaskDetailSourceType(rawTaskDetailSourceType)
|
||||||
const requestedTaskDetailRequesterUserId = normalizeOptionalString(input.taskDetailRequesterUserId)
|
const requestedTaskDetailRequesterUserId = normalizeOptionalString(input.taskDetailRequesterUserId)
|
||||||
@ -219,6 +226,21 @@ export async function updateMaterialWithSharedRules(options: UpdateMaterialOptio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dingtalkApprovalNo && !materialType.startsWith('任务-')) {
|
||||||
|
return {
|
||||||
|
error: '钉钉审批编号仅适用于“任务-”素材',
|
||||||
|
status: 400 as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo)
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
return {
|
||||||
|
error: dingtalkApprovalNoError,
|
||||||
|
status: 400 as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasTaskDetailSourceTypeField &&
|
hasTaskDetailSourceTypeField &&
|
||||||
rawTaskDetailSourceType != null &&
|
rawTaskDetailSourceType != null &&
|
||||||
@ -330,6 +352,9 @@ export async function updateMaterialWithSharedRules(options: UpdateMaterialOptio
|
|||||||
? normalizeOptionalString(input.renderer) || material.renderer || getDefaultRenderer(materialType)
|
? normalizeOptionalString(input.renderer) || material.renderer || getDefaultRenderer(materialType)
|
||||||
: material.renderer,
|
: material.renderer,
|
||||||
taskNo: hasTaskNoField ? taskNo : material.taskNo,
|
taskNo: hasTaskNoField ? taskNo : material.taskNo,
|
||||||
|
dingtalkApprovalNo: materialType.startsWith('任务-')
|
||||||
|
? (hasDingtalkApprovalNoField ? dingtalkApprovalNo : material.dingtalkApprovalNo)
|
||||||
|
: null,
|
||||||
taskDetailSourceType:
|
taskDetailSourceType:
|
||||||
materialType === '任务-详情'
|
materialType === '任务-详情'
|
||||||
? (hasTaskDetailSourceTypeField ? taskDetailSourceType : material.taskDetailSourceType ?? null)
|
? (hasTaskDetailSourceTypeField ? taskDetailSourceType : material.taskDetailSourceType ?? null)
|
||||||
|
|||||||
@ -22,6 +22,10 @@ import {
|
|||||||
} from '@/lib/external-auto-listing'
|
} from '@/lib/external-auto-listing'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { mkdir, readFile, writeFile } from 'fs/promises'
|
import { mkdir, readFile, writeFile } from 'fs/promises'
|
||||||
|
import {
|
||||||
|
getDingtalkApprovalNoError,
|
||||||
|
normalizeDingtalkApprovalNo,
|
||||||
|
} from '@/lib/dingtalk-approval-no'
|
||||||
|
|
||||||
export interface ProductPackageImageInput {
|
export interface ProductPackageImageInput {
|
||||||
uploadId?: string
|
uploadId?: string
|
||||||
@ -52,6 +56,7 @@ export interface ProductPackageAssetsInput {
|
|||||||
export interface CompleteProductPackageUploadInput {
|
export interface CompleteProductPackageUploadInput {
|
||||||
materialId: string
|
materialId: string
|
||||||
taskNo?: string | null
|
taskNo?: string | null
|
||||||
|
dingtalkApprovalNo?: string | null
|
||||||
operatorUserId: string
|
operatorUserId: string
|
||||||
assets: ProductPackageAssetsInput
|
assets: ProductPackageAssetsInput
|
||||||
options?: {
|
options?: {
|
||||||
@ -462,7 +467,7 @@ async function movePsdFilesToPermanentDir(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function completeProductPackageUpload(input: CompleteProductPackageUploadInput) {
|
export async function completeProductPackageUpload(input: CompleteProductPackageUploadInput) {
|
||||||
const { materialId, taskNo, assets, operatorUserId, options } = input
|
const { materialId, taskNo, dingtalkApprovalNo, assets, operatorUserId, options } = input
|
||||||
const tempFiles: string[] = []
|
const tempFiles: string[] = []
|
||||||
const uploadIds: string[] = []
|
const uploadIds: string[] = []
|
||||||
const uploadedPreviewUrls: string[] = []
|
const uploadedPreviewUrls: string[] = []
|
||||||
@ -494,6 +499,11 @@ export async function completeProductPackageUpload(input: CompleteProductPackage
|
|||||||
if (normalizedTaskNo && !validateTaskNo(normalizedTaskNo)) {
|
if (normalizedTaskNo && !validateTaskNo(normalizedTaskNo)) {
|
||||||
throw new Error('任务号格式错误')
|
throw new Error('任务号格式错误')
|
||||||
}
|
}
|
||||||
|
const normalizedDingtalkApprovalNo = normalizeDingtalkApprovalNo(dingtalkApprovalNo)
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(normalizedDingtalkApprovalNo)
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
throw new Error(dingtalkApprovalNoError)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const material: any = await prisma.material.findUnique({
|
const material: any = await prisma.material.findUnique({
|
||||||
@ -595,7 +605,11 @@ export async function completeProductPackageUpload(input: CompleteProductPackage
|
|||||||
|
|
||||||
const cardPreviewUrl = previewAssets.main[0]?.fileUrl || material.previewUrl
|
const cardPreviewUrl = previewAssets.main[0]?.fileUrl || material.previewUrl
|
||||||
|
|
||||||
await assignTaskNoOnComplete(materialId, normalizedTaskNo)
|
await assignTaskNoOnComplete(
|
||||||
|
materialId,
|
||||||
|
normalizedTaskNo,
|
||||||
|
normalizedDingtalkApprovalNo,
|
||||||
|
)
|
||||||
await prisma.material.update({
|
await prisma.material.update({
|
||||||
where: { id: materialId },
|
where: { id: materialId },
|
||||||
data: {
|
data: {
|
||||||
@ -619,6 +633,10 @@ export async function completeProductPackageUpload(input: CompleteProductPackage
|
|||||||
oldValue: null,
|
oldValue: null,
|
||||||
newValue: normalizedTaskNo,
|
newValue: normalizedTaskNo,
|
||||||
},
|
},
|
||||||
|
dingtalkApprovalNo: {
|
||||||
|
oldValue: null,
|
||||||
|
newValue: normalizedDingtalkApprovalNo,
|
||||||
|
},
|
||||||
isFullSet: {
|
isFullSet: {
|
||||||
oldValue: false,
|
oldValue: false,
|
||||||
newValue: derivedIsFullSet,
|
newValue: derivedIsFullSet,
|
||||||
@ -658,6 +676,7 @@ export async function completeProductPackageUpload(input: CompleteProductPackage
|
|||||||
return {
|
return {
|
||||||
materialId,
|
materialId,
|
||||||
taskNo: normalizedTaskNo,
|
taskNo: normalizedTaskNo,
|
||||||
|
dingtalkApprovalNo: normalizedDingtalkApprovalNo,
|
||||||
isFullSet: derivedIsFullSet,
|
isFullSet: derivedIsFullSet,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -4,6 +4,10 @@
|
|||||||
|
|
||||||
import { prisma } from './prisma';
|
import { prisma } from './prisma';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import {
|
||||||
|
getDingtalkApprovalNoError,
|
||||||
|
normalizeDingtalkApprovalNo,
|
||||||
|
} from './dingtalk-approval-no';
|
||||||
|
|
||||||
// 任务号格式:TASK + 12-18位数字 + 可选的多个子任务号(-2位数字,可重复)
|
// 任务号格式:TASK + 12-18位数字 + 可选的多个子任务号(-2位数字,可重复)
|
||||||
// 示例:
|
// 示例:
|
||||||
@ -318,21 +322,32 @@ export async function purgeDeletedMaterialsByTaskNo(taskNo: string): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在素材成功完成后再写入任务号,避免失败中的半成品占用任务号
|
* 在素材成功完成后再写入任务关联字段,避免失败中的半成品占用任务号
|
||||||
*/
|
*/
|
||||||
export async function assignTaskNoOnComplete(
|
export async function assignTaskNoOnComplete(
|
||||||
materialId: string,
|
materialId: string,
|
||||||
taskNo?: string | null,
|
taskNo?: string | null,
|
||||||
|
dingtalkApprovalNo?: string | null,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
deletedMaterials: number;
|
deletedMaterials: number;
|
||||||
deletedFiles: number;
|
deletedFiles: number;
|
||||||
failedFiles: number;
|
failedFiles: number;
|
||||||
} | null> {
|
} | null> {
|
||||||
const normalizedTaskNo = taskNo?.trim();
|
const normalizedTaskNo = taskNo?.trim();
|
||||||
|
const hasDingtalkApprovalNo = dingtalkApprovalNo !== undefined;
|
||||||
|
const normalizedDingtalkApprovalNo = normalizeDingtalkApprovalNo(dingtalkApprovalNo);
|
||||||
|
const dingtalkApprovalNoError = getDingtalkApprovalNoError(normalizedDingtalkApprovalNo);
|
||||||
|
if (dingtalkApprovalNoError) {
|
||||||
|
throw new Error(dingtalkApprovalNoError);
|
||||||
|
}
|
||||||
|
|
||||||
if (!normalizedTaskNo) {
|
if (!normalizedTaskNo) {
|
||||||
await prisma.material.update({
|
await prisma.material.update({
|
||||||
where: { id: materialId },
|
where: { id: materialId },
|
||||||
data: {
|
data: {
|
||||||
|
...(hasDingtalkApprovalNo
|
||||||
|
? { dingtalkApprovalNo: normalizedDingtalkApprovalNo }
|
||||||
|
: {}),
|
||||||
taskStatus: TASK_STATUS.COMPLETED,
|
taskStatus: TASK_STATUS.COMPLETED,
|
||||||
} as any,
|
} as any,
|
||||||
});
|
});
|
||||||
@ -368,6 +383,9 @@ export async function assignTaskNoOnComplete(
|
|||||||
where: { id: materialId },
|
where: { id: materialId },
|
||||||
data: {
|
data: {
|
||||||
taskNo: normalizedTaskNo,
|
taskNo: normalizedTaskNo,
|
||||||
|
...(hasDingtalkApprovalNo
|
||||||
|
? { dingtalkApprovalNo: normalizedDingtalkApprovalNo }
|
||||||
|
: {}),
|
||||||
taskStatus: TASK_STATUS.COMPLETED,
|
taskStatus: TASK_STATUS.COMPLETED,
|
||||||
} as any,
|
} as any,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE "materials"
|
||||||
|
ADD COLUMN "dingtalk_approval_no" TEXT;
|
||||||
|
|
||||||
|
CREATE INDEX "materials_dingtalk_approval_no_idx"
|
||||||
|
ON "materials"("dingtalk_approval_no");
|
||||||
@ -98,6 +98,7 @@ model Material {
|
|||||||
transferredBy String?
|
transferredBy String?
|
||||||
transferNote String?
|
transferNote String?
|
||||||
taskNo String? @map("task_no")
|
taskNo String? @map("task_no")
|
||||||
|
dingtalkApprovalNo String? @map("dingtalk_approval_no")
|
||||||
taskStatus String? @map("task_status")
|
taskStatus String? @map("task_status")
|
||||||
taskFetchedAt DateTime? @map("task_fetched_at")
|
taskFetchedAt DateTime? @map("task_fetched_at")
|
||||||
isFullSet Boolean @default(false) @map("is_full_set")
|
isFullSet Boolean @default(false) @map("is_full_set")
|
||||||
@ -130,6 +131,7 @@ model Material {
|
|||||||
@@index([originalOwnerId])
|
@@index([originalOwnerId])
|
||||||
@@index([transferredAt])
|
@@index([transferredAt])
|
||||||
@@index([taskNo])
|
@@index([taskNo])
|
||||||
|
@@index([dingtalkApprovalNo])
|
||||||
@@index([taskStatus])
|
@@index([taskStatus])
|
||||||
@@index([isFullSet])
|
@@index([isFullSet])
|
||||||
@@index([productModel])
|
@@index([productModel])
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user