feat: 上传面板对齐看板视觉,通用类型可填钉钉审批编号
This commit is contained in:
parent
e1669b90b6
commit
ae75bca301
@ -483,9 +483,7 @@ export async function POST(request: NextRequest) {
|
||||
const rawTaskNo = (formData.get('taskNo') as string) || '';
|
||||
const taskNo = isTaskOtherMaterial ? rawTaskNo.trim().toUpperCase() : rawTaskNo;
|
||||
const rawDingtalkApprovalNo = (formData.get('dingtalkApprovalNo') as string) || '';
|
||||
const dingtalkApprovalNo = isTaskMaterial
|
||||
? normalizeDingtalkApprovalNo(rawDingtalkApprovalNo)
|
||||
: null;
|
||||
const dingtalkApprovalNo = normalizeDingtalkApprovalNo(rawDingtalkApprovalNo);
|
||||
const rawProductModel = productModelInput || (isTaskOtherMaterial ? taskNo : '');
|
||||
const productModel = VIDEO_TYPES.has(materialType)
|
||||
? normalizeSpuProductModel(rawProductModel)
|
||||
@ -653,13 +651,6 @@ 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(
|
||||
@ -1200,6 +1191,7 @@ export async function POST(request: NextRequest) {
|
||||
videoSourceUrl, // 视频类型时保存视频URL
|
||||
videoCoverUrl,
|
||||
videoProcessingStatus: videoSourceUrl ? 'pending' : null, // 视频类型时初始化为pending状态
|
||||
dingtalkApprovalNo,
|
||||
// taskNo: 不写入,上传完成后再写入
|
||||
// taskStatus: 不写入,上传完成后再写入
|
||||
userId: (session.user as any).id,
|
||||
|
||||
@ -51,6 +51,14 @@ describe('UploadModal - 任务-其他', () => {
|
||||
}) as any
|
||||
})
|
||||
|
||||
test('3D场景公共上传面板应显示任务号与钉钉审批编号', async () => {
|
||||
render(<UploadModal onClose={jest.fn()} onSuccess={jest.fn()} />)
|
||||
|
||||
expect(await screen.findByLabelText('任务号')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('钉钉审批编号')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('素材类型 *')).toHaveValue('3D场景')
|
||||
})
|
||||
|
||||
test('任务-其他应使用公共上传面板形式,任务号与钉钉审批编号并列显示', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
|
||||
@ -69,6 +69,15 @@ import {
|
||||
validateImageFiles,
|
||||
} from '@/components/upload/drag-drop-utils'
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no'
|
||||
import {
|
||||
dropZoneActiveClass,
|
||||
dropZoneIdleClass,
|
||||
fieldControlClass,
|
||||
fieldHintClass,
|
||||
fieldLabelClass,
|
||||
inkButtonClass,
|
||||
outlineButtonClass,
|
||||
} from '@/components/upload/upload-field-styles'
|
||||
import {
|
||||
getSpuProductModelValidationError,
|
||||
isPlausibleSpuProductModel,
|
||||
@ -197,7 +206,6 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
const [retryCount, setRetryCount] = useState(0)
|
||||
|
||||
const isTaskOtherMaterial = materialType === '任务-其他'
|
||||
const isTaskMaterial = materialType.startsWith('任务-')
|
||||
const usesDedicatedUploader = SPECIAL_UPLOAD_TYPES.includes(materialType as typeof SPECIAL_UPLOAD_TYPES[number])
|
||||
const interactionLocked = uploading || previewProcessingCount > 0
|
||||
|
||||
@ -1646,12 +1654,10 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isTaskMaterial) {
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo)
|
||||
if (dingtalkApprovalNoError) {
|
||||
setError(dingtalkApprovalNoError)
|
||||
return
|
||||
}
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo)
|
||||
if (dingtalkApprovalNoError) {
|
||||
setError(dingtalkApprovalNoError)
|
||||
return
|
||||
}
|
||||
|
||||
// 任务号为空时仅提醒,不阻止上传(已在UI中显示提醒)
|
||||
@ -1762,7 +1768,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
productCategoryName,
|
||||
renderer: resolvedRenderer,
|
||||
taskNo: resolvedTaskNo,
|
||||
dingtalkApprovalNo: isTaskMaterial ? dingtalkApprovalNo.trim() : '',
|
||||
dingtalkApprovalNo: dingtalkApprovalNo.trim(),
|
||||
completedChunkUpload: chunkResult || undefined,
|
||||
})
|
||||
|
||||
@ -1887,52 +1893,55 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 rounded-apple max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-apple-lg"
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[#1a1c1d]/40 p-4">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="upload-material-title"
|
||||
className="max-h-[90vh] w-full max-w-2xl overflow-y-auto rounded-[12px] border border-[#e7e8ea] bg-white"
|
||||
>
|
||||
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">上传素材</h2>
|
||||
<div className="border-b border-[#e7e8ea] px-8 pb-5 pt-7">
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="min-w-0">
|
||||
<h2
|
||||
id="upload-material-title"
|
||||
className="text-[24px] font-semibold tracking-[-0.03em] text-[#1a1c1d]"
|
||||
>
|
||||
上传素材
|
||||
</h2>
|
||||
<p className="mt-2 text-[13px] leading-relaxed text-[#6b7075]">
|
||||
把文件拖进虚线框即可。内网请先切到顶部提示里的内网地址,上传会更快。
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="关闭上传素材窗口"
|
||||
title="关闭"
|
||||
className="inline-flex h-10 w-10 items-center justify-center text-2xl leading-none text-gray-400 transition-colors hover:text-gray-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:hover:text-gray-300"
|
||||
className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-[8px] text-[22px] leading-none text-[#86898e] transition-colors hover:bg-[#fafafa] hover:text-[#1a1c1d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1 mt-2">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
💡 提示:可拖拽文件到对应的虚线框内上传
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
提示:如在内网环境,请通过顶部提示切换到内网地址以获得更快速度
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||
{/* 素材类型 - 第一栏,让用户先选择类型 */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6 px-8 py-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label htmlFor="upload-material-type" className={fieldLabelClass}>
|
||||
素材类型 *
|
||||
</label>
|
||||
<select
|
||||
id="upload-material-type"
|
||||
value={materialType}
|
||||
onChange={(e) => setMaterialType(e.target.value)}
|
||||
disabled={interactionLocked}
|
||||
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:cursor-not-allowed disabled:opacity-60"
|
||||
className={fieldControlClass}
|
||||
>
|
||||
{availableMaterialTypes.length > 0 ? (
|
||||
availableMaterialTypes.map((type) => (
|
||||
<option key={type} value={type}>{type}</option>
|
||||
))
|
||||
) : (
|
||||
// 加载中或失败时的后备选项(排除有专用上传组件的类型)
|
||||
<>
|
||||
{MATERIAL_TYPE_OPTIONS.filter((type) => !SPECIAL_UPLOAD_TYPES.includes(type as typeof SPECIAL_UPLOAD_TYPES[number])).map((type) => (
|
||||
<option key={type} value={type}>{type}</option>
|
||||
@ -1942,8 +1951,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 专用上传器会在自己的表单内渲染任务关联字段。 */}
|
||||
{!usesDedicatedUploader && isTaskMaterial && (
|
||||
{!usesDedicatedUploader && (
|
||||
<TaskReferenceFields
|
||||
idPrefix="generic-task-upload"
|
||||
taskNo={taskNo}
|
||||
@ -1955,30 +1963,9 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!usesDedicatedUploader && !isTaskMaterial && (
|
||||
<div>
|
||||
<label htmlFor="taskNo" className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
任务号
|
||||
</label>
|
||||
<input
|
||||
id="taskNo"
|
||||
type="text"
|
||||
value={taskNo}
|
||||
onChange={(event) => setTaskNo(event.target.value)}
|
||||
placeholder="例如:TASK202601030184"
|
||||
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={interactionLocked}
|
||||
aria-invalid={Boolean(taskNo && !validateTaskNo(taskNo.trim().toUpperCase()))}
|
||||
/>
|
||||
{taskNo && !validateTaskNo(taskNo.trim().toUpperCase()) && (
|
||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">任务号格式错误</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 任务-详情上传组件 */}
|
||||
{materialType === '任务-详情' && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<div className="border-t border-[#e7e8ea] pt-6">
|
||||
<ProductPackageUploader
|
||||
onSuccess={() => {
|
||||
onSuccess();
|
||||
@ -1997,7 +1984,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
|
||||
{/* 任务-视频截图上传组件 */}
|
||||
{materialType === '任务-视频截图' && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<div className="border-t border-[#e7e8ea] pt-6">
|
||||
<VideoScreenshotUploader
|
||||
onSuccess={() => {
|
||||
onSuccess();
|
||||
@ -2016,7 +2003,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
|
||||
{/* 任务-页面上传组件 */}
|
||||
{materialType === '任务-页面' && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<div className="border-t border-[#e7e8ea] pt-6">
|
||||
<TaskPageUploader
|
||||
onSuccess={() => {
|
||||
onSuccess();
|
||||
@ -2035,7 +2022,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
|
||||
{/* 证书-检测报告上传组件 */}
|
||||
{materialType === '证书-检测报告' && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<div className="border-t border-[#e7e8ea] pt-6">
|
||||
<CertificateReportUploader
|
||||
onSuccess={() => {
|
||||
onSuccess();
|
||||
@ -2244,9 +2231,9 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
|
||||
{/* 品牌VI灵感提示:预览图会自动从PDF提取 */}
|
||||
{materialType === '品牌VI灵感' && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
💡 提示:系统会自动从PDF文件的第一页提取预览图,无需手动上传预览图或展示图。
|
||||
<div className="rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] p-4">
|
||||
<p className="text-[13px] leading-relaxed text-[#6b7075]">
|
||||
系统会从 PDF 第一页自动抽出预览图,不用再单独上传预览图或展示图。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@ -2311,37 +2298,36 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
{/* Material File(有专用上传器的任务类型不显示) */}
|
||||
{!usesDedicatedUploader && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label className={fieldLabelClass}>
|
||||
素材文件 {(materialType === '模特' || materialType === '标准模型') ? '(可选)' : '*'}
|
||||
{/* 批量上传提示 */}
|
||||
{materialType === '音乐' && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2">
|
||||
(支持批量上传,最多100个)
|
||||
<span className={`ml-2 font-normal ${fieldHintClass}`}>
|
||||
(支持批量,最多100个)
|
||||
</span>
|
||||
)}
|
||||
{isBatchVideoMaterialType(materialType) && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2">
|
||||
(支持批量上传,最多8个)
|
||||
<span className={`ml-2 font-normal ${fieldHintClass}`}>
|
||||
(支持批量,最多8个)
|
||||
</span>
|
||||
)}
|
||||
{materialType === '3D场景' && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2">
|
||||
(支持批量上传,最多5个)
|
||||
<span className={`ml-2 font-normal ${fieldHintClass}`}>
|
||||
(支持批量,最多5个)
|
||||
</span>
|
||||
)}
|
||||
{materialType === '买家秀图片' && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2">
|
||||
(支持批量上传,每批次至少8张)
|
||||
<span className={`ml-2 font-normal ${fieldHintClass}`}>
|
||||
(支持批量,每批次至少8张)
|
||||
</span>
|
||||
)}
|
||||
{materialType === '小红书种草' && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2">
|
||||
(支持批量上传)
|
||||
<span className={`ml-2 font-normal ${fieldHintClass}`}>
|
||||
(支持批量)
|
||||
</span>
|
||||
)}
|
||||
{materialType === '品牌VI灵感' && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2">
|
||||
(支持批量上传,最多50个)
|
||||
<span className={`ml-2 font-normal ${fieldHintClass}`}>
|
||||
(支持批量,最多50个)
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
@ -2466,9 +2452,9 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
/>
|
||||
{/* 批量上传时显示品类选择提示 */}
|
||||
{productCategories.length > 0 && (
|
||||
<div className="mb-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
⚠️ 请在上方选择品类(所有PDF文件将使用同一个品类)
|
||||
<div className="mb-4 rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] p-3">
|
||||
<p className="text-[13px] text-[#6b7075]">
|
||||
请在上方选择品类,这一批 PDF 会用同一个品类。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@ -2531,13 +2517,9 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
<div
|
||||
ref={dropZoneRef}
|
||||
aria-disabled={interactionLocked}
|
||||
className={`border-2 border-dashed rounded-apple p-6 text-center transition-colors ${
|
||||
className={`p-6 text-center ${
|
||||
interactionLocked ? 'cursor-not-allowed opacity-60' : ''
|
||||
} ${
|
||||
isDragging
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
} ${isDragging ? dropZoneActiveClass : dropZoneIdleClass}`}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
@ -2545,20 +2527,20 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
>
|
||||
{zipFile ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-gray-700 dark:text-gray-300 font-medium">{zipFile.name}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<p className="font-medium text-[#1a1c1d]">{zipFile.name}</p>
|
||||
<p className="text-[13px] text-[#6b7075]">
|
||||
{formatBytes(zipFile.size)}
|
||||
</p>
|
||||
{detectMaterialType(zipFile.name) && (
|
||||
<p className="text-sm text-green-600 dark:text-green-400">
|
||||
已自动识别类型: {detectMaterialType(zipFile.name)}
|
||||
<p className="text-[13px] text-[#157a4b]">
|
||||
已识别类型:{detectMaterialType(zipFile.name)}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
disabled={interactionLocked}
|
||||
onClick={() => setZipFile(null)}
|
||||
className="text-sm text-red-600 hover:text-red-700 disabled:cursor-not-allowed disabled:opacity-50 dark:text-red-400 dark:hover:text-red-300"
|
||||
className="text-[13px] text-[#b42318] hover:underline disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
@ -2589,7 +2571,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
type="button"
|
||||
disabled={interactionLocked}
|
||||
onClick={() => batchFileInputRef.current?.click()}
|
||||
className="btn-secondary mr-2"
|
||||
className={`${outlineButtonClass} mr-2`}
|
||||
>
|
||||
{materialType === '音乐' && '选择音乐文件(可多选)'}
|
||||
{isBatchVideoMaterialType(materialType) && '选择视频文件(可多选,最多8个)'}
|
||||
@ -2602,11 +2584,11 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
type="button"
|
||||
disabled={interactionLocked}
|
||||
onClick={() => zipInputRef.current?.click()}
|
||||
className="btn-secondary"
|
||||
className={outlineButtonClass}
|
||||
>
|
||||
选择素材文件
|
||||
</button>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
<p className={`mt-3 ${fieldHintClass}`}>
|
||||
支持格式:
|
||||
{materialType === '音乐' && '音频文件(.mp3 / .wav / .flac / .m4a / .aac / .ogg)'}
|
||||
{isVideoMaterialType(materialType) &&
|
||||
@ -2626,7 +2608,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
|
||||
{/* 上传进度 */}
|
||||
{uploading && (
|
||||
<div className="space-y-4 border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
|
||||
<div className="mt-4 space-y-4 border-t border-[#e7e8ea] pt-4">
|
||||
<UploadProgress
|
||||
uploading={uploading}
|
||||
isBatchMode={isBatchMode}
|
||||
@ -2643,24 +2625,23 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div role="alert" className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 px-4 py-3 rounded-apple">
|
||||
<div role="alert" className="rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] px-4 py-3 text-[13px] text-[#b42318]">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Buttons(有专用上传器的任务类型不显示,因为它们有自己的按钮) */}
|
||||
{!usesDedicatedUploader && (
|
||||
<div className="flex justify-end space-x-3 pt-4">
|
||||
<div className="flex justify-end gap-3 border-t border-[#e7e8ea] pt-5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelUpload}
|
||||
className="btn-secondary"
|
||||
className={outlineButtonClass}
|
||||
>
|
||||
{uploading ? '取消上传' : '取消'}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
className={inkButtonClass}
|
||||
disabled={uploading || previewProcessingCount > 0}
|
||||
>
|
||||
{uploading
|
||||
|
||||
@ -2,6 +2,12 @@
|
||||
|
||||
import Image from 'next/image'
|
||||
import { PREVIEW_OPTIONAL_TYPES } from './upload-constants'
|
||||
import {
|
||||
dropZoneActiveClass,
|
||||
dropZoneIdleClass,
|
||||
fieldHintClass,
|
||||
fieldLabelClass,
|
||||
} from './upload-field-styles'
|
||||
import VideoPreviewImageUpload from './VideoPreviewImageUpload'
|
||||
import { isVideoMaterialType } from '@/lib/video-material-types'
|
||||
|
||||
@ -59,20 +65,16 @@ export default function PreviewImageUpload({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label className={fieldLabelClass}>
|
||||
预览图片
|
||||
{PREVIEW_OPTIONAL_TYPES.includes(materialType) ? '(可选)' : ' *'}
|
||||
</label>
|
||||
<div
|
||||
ref={previewDropZoneRef}
|
||||
aria-disabled={disabled}
|
||||
className={`border-2 border-dashed rounded-apple p-4 transition-colors ${
|
||||
className={`p-4 ${
|
||||
disabled ? 'cursor-not-allowed opacity-60' : ''
|
||||
} ${
|
||||
isPreviewDragging
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
} ${isPreviewDragging ? dropZoneActiveClass : dropZoneIdleClass}`}
|
||||
onDragEnter={onPreviewDragEnter}
|
||||
onDragLeave={onPreviewDragLeave}
|
||||
onDragOver={onPreviewDragOver}
|
||||
@ -87,10 +89,10 @@ export default function PreviewImageUpload({
|
||||
className="hidden"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
{previewUrls.map((url, index) => (
|
||||
<div key={index} className="relative group">
|
||||
<div className="relative w-full h-32 rounded-apple overflow-hidden border-2 border-gray-200 dark:border-gray-600">
|
||||
<div key={index} className="group relative">
|
||||
<div className="relative h-32 w-full overflow-hidden rounded-[8px] border border-[#e7e8ea]">
|
||||
<Image
|
||||
src={url}
|
||||
alt={`预览图 ${index + 1}`}
|
||||
@ -99,7 +101,7 @@ export default function PreviewImageUpload({
|
||||
className="object-cover"
|
||||
/>
|
||||
{index === 0 && (
|
||||
<div className="absolute top-1 left-1 bg-blue-500 text-white text-xs px-2 py-1 rounded">
|
||||
<div className="absolute left-1 top-1 bg-[#1a1c1d] px-2 py-0.5 text-[11px] tracking-[-0.02em] text-white">
|
||||
主预览图
|
||||
</div>
|
||||
)}
|
||||
@ -108,7 +110,8 @@ export default function PreviewImageUpload({
|
||||
type="button"
|
||||
onClick={() => onRemovePreviewImage(index)}
|
||||
disabled={disabled}
|
||||
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
aria-label={`移除预览图 ${index + 1}`}
|
||||
className="absolute -right-2 -top-2 flex h-6 w-6 items-center justify-center rounded-full bg-[#1a1c1d] text-white opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@ -118,19 +121,19 @@ export default function PreviewImageUpload({
|
||||
type="button"
|
||||
onClick={() => previewInputRef.current?.click()}
|
||||
disabled={disabled}
|
||||
className="group w-full h-32 rounded-apple bg-gray-100 dark:bg-gray-700 border-2 border-dashed border-gray-300 dark:border-gray-600 flex items-center justify-center hover:border-gray-400 dark:hover:border-gray-500 transition-colors"
|
||||
className="group flex h-32 w-full items-center justify-center rounded-[8px] border border-dashed border-[#e7e8ea] bg-white transition-colors hover:border-[#1a1c1d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2"
|
||||
>
|
||||
<svg className="w-12 h-12 text-gray-400 group-hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
<svg className="h-8 w-8 text-[#86898e] transition-colors group-hover:text-[#1a1c1d]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-3 space-y-1">
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
支持 JPG、PNG、WebP 等格式,可一次选择多张或拖拽上传
|
||||
<p className={fieldHintClass}>
|
||||
支持 JPG、PNG、WebP,可一次选多张或拖进来。
|
||||
</p>
|
||||
<p className="text-xs text-blue-600 dark:text-blue-400">
|
||||
💡 第一张图会自动作为主预览图
|
||||
<p className={fieldHintClass}>
|
||||
第一张图会作为主预览图。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,6 +6,11 @@ import {
|
||||
DINGTALK_APPROVAL_NO_MAX_LENGTH,
|
||||
getDingtalkApprovalNoError,
|
||||
} from '@/lib/dingtalk-approval-no'
|
||||
import {
|
||||
fieldControlClass,
|
||||
fieldErrorClass,
|
||||
fieldLabelClass,
|
||||
} from '@/components/upload/upload-field-styles'
|
||||
|
||||
interface TaskReferenceFieldsProps {
|
||||
idPrefix: string
|
||||
@ -47,16 +52,11 @@ export default function TaskReferenceFields({
|
||||
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"
|
||||
>
|
||||
<label htmlFor={taskNoId} className={fieldLabelClass}>
|
||||
任务号{taskNoRequired ? ' *' : ''}
|
||||
</label>
|
||||
<input
|
||||
@ -66,24 +66,21 @@ export default function TaskReferenceFields({
|
||||
onChange={(event) => onTaskNoChange(event.target.value.toUpperCase())}
|
||||
onBlur={() => setTaskNoTouched(true)}
|
||||
placeholder="例如:TASK202601030184"
|
||||
className={inputClassName}
|
||||
className={fieldControlClass}
|
||||
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">
|
||||
<p id={taskNoErrorId} className={fieldErrorClass}>
|
||||
{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 htmlFor={approvalNoId} className={fieldLabelClass}>
|
||||
钉钉审批编号
|
||||
</label>
|
||||
<input
|
||||
@ -96,13 +93,13 @@ export default function TaskReferenceFields({
|
||||
onChange={(event) => onDingtalkApprovalNoChange(event.target.value)}
|
||||
onBlur={() => setApprovalNoTouched(true)}
|
||||
placeholder="例如:202508211445000157693"
|
||||
className={inputClassName}
|
||||
className={fieldControlClass}
|
||||
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">
|
||||
<p id={approvalNoErrorId} className={fieldErrorClass}>
|
||||
{visibleApprovalNoError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -7,6 +7,14 @@
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { requiresCategoryAndScene, requiresProductCategoryName, requiresRenderer, shouldAutoFillGeneral } from '@/lib/material-validation'
|
||||
import {
|
||||
fieldControlClass,
|
||||
fieldErrorClass,
|
||||
fieldHintClass,
|
||||
fieldLabelClass,
|
||||
inkButtonClass,
|
||||
outlineButtonClass,
|
||||
} from '@/components/upload/upload-field-styles'
|
||||
import { parseSPU, type SPUParseResult } from '@/lib/spu-parser'
|
||||
import { isVideoMaterialType } from '@/lib/video-material-types'
|
||||
import {
|
||||
@ -190,7 +198,7 @@ export default function UploadForm({
|
||||
<>
|
||||
{/* SPU 或材质名称 */}
|
||||
<div>
|
||||
<label htmlFor="productModel" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label htmlFor="productModel" className={fieldLabelClass}>
|
||||
{materialType === '材质' ? '材质名称 *' : 'SPU型号 *'}
|
||||
</label>
|
||||
<input
|
||||
@ -209,16 +217,16 @@ export default function UploadForm({
|
||||
maxLength={100}
|
||||
aria-invalid={isVideoMaterialType(materialType) && productModel.length > 0 && !isPlausibleSpuProductModel(productModel)}
|
||||
aria-describedby={isVideoMaterialType(materialType) ? 'product-model-hint' : undefined}
|
||||
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={fieldControlClass}
|
||||
placeholder="例如:DF-A3DZ0008"
|
||||
/>
|
||||
{isVideoMaterialType(materialType) && (
|
||||
<p
|
||||
id="product-model-hint"
|
||||
className={`mt-2 text-xs ${
|
||||
className={`mt-2 ${
|
||||
productModel && !isPlausibleSpuProductModel(productModel)
|
||||
? 'text-red-600 dark:text-red-400'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
? fieldErrorClass
|
||||
: fieldHintClass
|
||||
}`}
|
||||
role={productModel && !isPlausibleSpuProductModel(productModel) ? 'alert' : undefined}
|
||||
>
|
||||
@ -232,15 +240,15 @@ export default function UploadForm({
|
||||
{/* SKU字段(视频类素材使用) */}
|
||||
{isVideoMaterialType(materialType) && (
|
||||
<div>
|
||||
<label htmlFor="sku" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
SKU <span className="text-gray-500 dark:text-gray-400 text-xs">(可选,多个SKU请用逗号分隔)</span>
|
||||
<label htmlFor="sku" className={fieldLabelClass}>
|
||||
SKU <span className={`ml-1 font-normal ${fieldHintClass}`}>(可选,多个用逗号分隔)</span>
|
||||
</label>
|
||||
<input
|
||||
id="sku"
|
||||
type="text"
|
||||
value={sku}
|
||||
onChange={(e) => onSkuChange?.(e.target.value)}
|
||||
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={fieldControlClass}
|
||||
placeholder="例如:SKU001,SKU002,SKU003"
|
||||
/>
|
||||
</div>
|
||||
@ -249,11 +257,11 @@ export default function UploadForm({
|
||||
{/* 品类分类(标准模型必填,其他类型可选) */}
|
||||
{productCategories.length > 0 && (
|
||||
<div>
|
||||
<label htmlFor="productCategoryName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label htmlFor="productCategoryName" className={fieldLabelClass}>
|
||||
品类{requiresProductCategoryName(materialType) ? ' *' : '(可选)'}
|
||||
{categoryAutoFilled && (
|
||||
<span className="ml-2 text-xs text-green-600 dark:text-green-400 font-normal">
|
||||
✓ 自动识别
|
||||
<span className={`ml-2 font-normal ${fieldHintClass}`}>
|
||||
已自动识别
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
@ -262,10 +270,9 @@ export default function UploadForm({
|
||||
value={productCategoryName}
|
||||
onChange={(e) => {
|
||||
onProductCategoryChange(e.target.value)
|
||||
// 手动修改时清除自动填充标记
|
||||
setCategoryAutoFilled(false)
|
||||
}}
|
||||
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={fieldControlClass}
|
||||
>
|
||||
<option value="">{requiresProductCategoryName(materialType) ? '请选择品类' : '不选择品类'}</option>
|
||||
{productCategories.map((category) => (
|
||||
@ -280,27 +287,27 @@ export default function UploadForm({
|
||||
{/* 使用店铺(品牌VI灵感不显示,其他类型按需显示) */}
|
||||
{!isBrandVI && requiresCategoryAndScene(materialType) && (
|
||||
<div>
|
||||
<label htmlFor="categoryName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label htmlFor="categoryName" className={fieldLabelClass}>
|
||||
使用店铺 *
|
||||
{shopAutoFilled && (
|
||||
<span className="ml-2 text-xs text-green-600 dark:text-green-400 font-normal">
|
||||
✓ 自动识别
|
||||
<span className={`ml-2 font-normal ${fieldHintClass}`}>
|
||||
已自动识别
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
{showNewCategoryInput ? (
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newCategoryName}
|
||||
onChange={(e) => setNewCategoryName(e.target.value)}
|
||||
className="flex-1 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={`flex-1 ${fieldControlClass}`}
|
||||
placeholder="输入新店铺名称"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddNewCategory}
|
||||
className="btn-primary"
|
||||
className={inkButtonClass}
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
@ -310,23 +317,22 @@ export default function UploadForm({
|
||||
setShowNewCategoryInput(false)
|
||||
setNewCategoryName('')
|
||||
}}
|
||||
className="btn-secondary"
|
||||
className={outlineButtonClass}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
id="categoryName"
|
||||
value={categoryName}
|
||||
onChange={(e) => {
|
||||
onCategoryChange(e.target.value)
|
||||
// 手动修改时清除自动填充标记
|
||||
setShopAutoFilled(false)
|
||||
}}
|
||||
disabled={isAutoGeneralType}
|
||||
className={`flex-1 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 ${isAutoGeneralType ? 'opacity-70 cursor-not-allowed' : ''}`}
|
||||
className={`flex-1 ${fieldControlClass} ${isAutoGeneralType ? 'cursor-not-allowed opacity-70' : ''}`}
|
||||
>
|
||||
{isAutoGeneralType ? (
|
||||
<option value="通用">通用</option>
|
||||
@ -344,10 +350,10 @@ export default function UploadForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowNewCategoryInput(true)}
|
||||
className="btn-secondary whitespace-nowrap"
|
||||
className={`${outlineButtonClass} whitespace-nowrap`}
|
||||
disabled={isAutoGeneralType}
|
||||
>
|
||||
+ 新建店铺
|
||||
新建店铺
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@ -357,22 +363,22 @@ export default function UploadForm({
|
||||
{/* 场景类别(品牌VI灵感、材质类型和标准模型不需要) */}
|
||||
{!isBrandVI && requiresCategoryAndScene(materialType) && (
|
||||
<div>
|
||||
<label htmlFor="sceneName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label htmlFor="sceneName" className={fieldLabelClass}>
|
||||
场景类别 *
|
||||
</label>
|
||||
{showNewSceneInput ? (
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newSceneName}
|
||||
onChange={(e) => setNewSceneName(e.target.value)}
|
||||
className="flex-1 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={`flex-1 ${fieldControlClass}`}
|
||||
placeholder="输入新场景类别"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddNewScene}
|
||||
className="btn-primary"
|
||||
className={inkButtonClass}
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
@ -382,19 +388,19 @@ export default function UploadForm({
|
||||
setShowNewSceneInput(false)
|
||||
setNewSceneName('')
|
||||
}}
|
||||
className="btn-secondary"
|
||||
className={outlineButtonClass}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
id="sceneName"
|
||||
value={sceneName}
|
||||
onChange={(e) => onSceneChange(e.target.value)}
|
||||
disabled={isAutoGeneralType}
|
||||
className={`flex-1 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 ${isAutoGeneralType ? 'opacity-70 cursor-not-allowed' : ''}`}
|
||||
className={`flex-1 ${fieldControlClass} ${isAutoGeneralType ? 'cursor-not-allowed opacity-70' : ''}`}
|
||||
>
|
||||
{isAutoGeneralType ? (
|
||||
<option value="通用">通用</option>
|
||||
@ -412,10 +418,10 @@ export default function UploadForm({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowNewSceneInput(true)}
|
||||
className="btn-secondary whitespace-nowrap"
|
||||
className={`${outlineButtonClass} whitespace-nowrap`}
|
||||
disabled={isAutoGeneralType}
|
||||
>
|
||||
+ 新建场景类别
|
||||
新建场景
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@ -425,14 +431,14 @@ export default function UploadForm({
|
||||
{/* 平台分类(可选,品牌VI灵感不显示,其他类型按需显示) */}
|
||||
{!isBrandVI && platforms.length > 0 && !requiresProductCategoryName(materialType) && (
|
||||
<div>
|
||||
<label htmlFor="platformName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label htmlFor="platformName" className={fieldLabelClass}>
|
||||
平台分类(可选)
|
||||
</label>
|
||||
<select
|
||||
id="platformName"
|
||||
value={platformName}
|
||||
onChange={(e) => onPlatformChange(e.target.value)}
|
||||
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={fieldControlClass}
|
||||
>
|
||||
<option value="">不选择平台</option>
|
||||
{platforms.map((platform) => (
|
||||
@ -447,14 +453,14 @@ export default function UploadForm({
|
||||
{/* 渲染器(仅3D场景显示) */}
|
||||
{requiresRenderer(materialType) && (
|
||||
<div>
|
||||
<label htmlFor="renderer" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label htmlFor="renderer" className={fieldLabelClass}>
|
||||
渲染器 *
|
||||
</label>
|
||||
<select
|
||||
id="renderer"
|
||||
value={renderer}
|
||||
onChange={(e) => onRendererChange(e.target.value)}
|
||||
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={fieldControlClass}
|
||||
>
|
||||
<option value="CR">CR</option>
|
||||
<option value="VR">VR</option>
|
||||
@ -465,11 +471,11 @@ export default function UploadForm({
|
||||
|
||||
{/* 标准模型说明 */}
|
||||
{requiresProductCategoryName(materialType) && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-apple p-4">
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300 font-medium mb-2">
|
||||
💡 标准模型类型说明:
|
||||
<div className="rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] p-4">
|
||||
<p className="mb-2 text-[13px] font-medium text-[#1a1c1d]">
|
||||
标准模型怎么传
|
||||
</p>
|
||||
<ul className="text-xs text-blue-600 dark:text-blue-400 space-y-1 list-disc list-inside">
|
||||
<ul className={`list-inside list-disc space-y-1 ${fieldHintClass}`}>
|
||||
<li>以产品型号(SPU)为基准创建模型合集</li>
|
||||
<li>上传时只需填写 SPU 型号和预览图</li>
|
||||
<li>创建后在详情页添加各个 SKU 的模型文件</li>
|
||||
|
||||
21
components/upload/upload-field-styles.ts
Normal file
21
components/upload/upload-field-styles.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export const fieldLabelClass =
|
||||
'mb-2 block text-[13px] font-medium tracking-[-0.02em] text-[#1a1c1d]'
|
||||
|
||||
export const fieldHintClass = 'text-[12px] leading-relaxed text-[#86898e]'
|
||||
|
||||
export const fieldControlClass =
|
||||
'w-full rounded-[8px] border border-[#e7e8ea] bg-white px-4 py-2.5 text-[14px] tracking-[-0.02em] text-[#1a1c1d] placeholder:text-[#86898e] outline-none transition-colors focus:border-[#1a1c1d] focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60'
|
||||
|
||||
export const fieldErrorClass = 'mt-1 text-[13px] text-[#b42318]'
|
||||
|
||||
export const inkButtonClass =
|
||||
'inline-flex h-11 items-center justify-center rounded-[8px] bg-[#1a1c1d] px-6 text-[14px] font-medium tracking-[-0.02em] text-white transition-colors hover:bg-[#2a2d2f] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'
|
||||
|
||||
export const outlineButtonClass =
|
||||
'inline-flex h-11 items-center justify-center rounded-[8px] border border-[#1a1c1d] bg-white px-6 text-[14px] font-medium tracking-[-0.02em] text-[#1a1c1d] transition-colors hover:bg-[#1a1c1d] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'
|
||||
|
||||
export const dropZoneIdleClass =
|
||||
'rounded-[12px] border border-dashed border-[#e7e8ea] bg-[#fafafa] transition-colors'
|
||||
|
||||
export const dropZoneActiveClass =
|
||||
'rounded-[12px] border border-dashed border-[#1a1c1d] bg-[#eef0fa] transition-colors'
|
||||
@ -816,8 +816,12 @@ describe('material-update shared rules', () => {
|
||||
expect(prisma.material.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('非任务素材不能写入钉钉审批编号', async () => {
|
||||
test('3D场景可以写入钉钉审批编号', async () => {
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
|
||||
;(prisma.material.update as jest.Mock).mockImplementation(async ({ data }) => ({
|
||||
...baseMaterial,
|
||||
...data,
|
||||
}))
|
||||
|
||||
const result = await updateMaterialWithSharedRules({
|
||||
materialId: 'material-1',
|
||||
@ -834,11 +838,14 @@ describe('material-update shared rules', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
error: '钉钉审批编号仅适用于“任务-”素材',
|
||||
status: 400,
|
||||
expect('error' in result).toBe(false)
|
||||
expect(prisma.material.update).toHaveBeenCalledWith({
|
||||
where: { id: 'material-1' },
|
||||
data: expect.objectContaining({
|
||||
materialType: '3D场景',
|
||||
dingtalkApprovalNo: '202508211445000157693',
|
||||
}),
|
||||
})
|
||||
expect(prisma.material.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('任务-详情编辑时应按已有资产自动回写 isFullSet', async () => {
|
||||
|
||||
@ -60,8 +60,8 @@ export function isTaskMaterialType(materialType: string): boolean {
|
||||
return mapMaterialType(materialType).startsWith('任务-')
|
||||
}
|
||||
|
||||
export function supportsTaskNo(materialType: string): boolean {
|
||||
return isTaskMaterialType(materialType)
|
||||
export function supportsTaskNo(_materialType: string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
export function supportsIsFullSet(materialType: string): boolean {
|
||||
|
||||
@ -236,13 +236,6 @@ async function updateMaterialWithSharedRulesLocked(options: UpdateMaterialOption
|
||||
}
|
||||
}
|
||||
|
||||
if (dingtalkApprovalNo && !materialType.startsWith('任务-')) {
|
||||
return {
|
||||
error: '钉钉审批编号仅适用于“任务-”素材',
|
||||
status: 400 as const,
|
||||
}
|
||||
}
|
||||
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo)
|
||||
if (dingtalkApprovalNoError) {
|
||||
return {
|
||||
@ -362,9 +355,7 @@ async function updateMaterialWithSharedRulesLocked(options: UpdateMaterialOption
|
||||
? normalizeOptionalString(input.renderer) || material.renderer || getDefaultRenderer(materialType)
|
||||
: material.renderer,
|
||||
taskNo: hasTaskNoField ? taskNo : material.taskNo,
|
||||
dingtalkApprovalNo: materialType.startsWith('任务-')
|
||||
? (hasDingtalkApprovalNoField ? dingtalkApprovalNo : material.dingtalkApprovalNo)
|
||||
: null,
|
||||
dingtalkApprovalNo: hasDingtalkApprovalNoField ? dingtalkApprovalNo : material.dingtalkApprovalNo,
|
||||
taskDetailSourceType:
|
||||
materialType === '任务-详情'
|
||||
? (hasTaskDetailSourceTypeField ? taskDetailSourceType : material.taskDetailSourceType ?? null)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user