feat: 全部素材详情页对齐看板,上传面板补上 GSAP

This commit is contained in:
chenpei 2026-08-19 10:57:00 +08:00
parent ae75bca301
commit 4efdccdd3c
9 changed files with 386 additions and 262 deletions

View File

@ -8,7 +8,7 @@ import Image from 'next/image'
import { format } from 'date-fns'
import gsap from 'gsap'
import { CalendarClock, CheckCircle2, RefreshCw, ShieldCheck } from 'lucide-react'
import Header from '@/components/Header'
import MaterialsHeader from '@/components/board/MaterialsHeader'
import VideoPlayerModal from '@/components/VideoPlayerModal'
import SkuList from '@/components/SkuList'
import SkuEditModal from '@/components/SkuEditModal'
@ -177,15 +177,15 @@ function VideoShareWaitPanel({ checking, onCheckAgain, waitState }: VideoShareWa
<section
ref={panelRef}
aria-labelledby="video-share-wait-title"
className="relative mx-auto max-w-2xl overflow-hidden rounded-2xl border border-gray-200 bg-white px-5 py-7 text-left shadow-lg dark:border-gray-700 dark:bg-gray-800 sm:px-9 sm:py-9"
className="relative mx-auto max-w-2xl overflow-hidden rounded-[12px] border border-[#e7e8ea] bg-white px-5 py-7 text-left sm:px-9 sm:py-9"
>
<div className="absolute inset-x-0 top-0 h-1 bg-blue-600" aria-hidden="true" />
<div className="absolute inset-x-0 top-0 h-px bg-[#e7e8ea]" aria-hidden="true" />
<div data-video-share-reveal className="flex items-center gap-3">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-50 text-emerald-600 dark:bg-emerald-950/50 dark:text-emerald-300">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[#eef0fa] text-[#5a66b8]">
<CheckCircle2 className="h-5 w-5" aria-hidden="true" />
</span>
<p className="text-sm font-semibold text-emerald-700 dark:text-emerald-300">
<p className="text-[13px] font-semibold text-[#1a1c1d]">
分享链接已生效
</p>
</div>
@ -193,21 +193,21 @@ function VideoShareWaitPanel({ checking, onCheckAgain, waitState }: VideoShareWa
<h1
id="video-share-wait-title"
data-video-share-reveal
className="mt-5 text-2xl font-semibold tracking-tight text-gray-950 dark:text-white sm:text-3xl"
className="mt-5 text-[24px] font-semibold tracking-[-0.03em] text-[#1a1c1d] sm:text-[28px]"
>
视频预计 {waitState.remainingDays} 天后开放
</h1>
<p
data-video-share-reveal
className="mt-3 max-w-xl text-base leading-7 text-gray-600 dark:text-gray-300"
className="mt-3 max-w-xl text-[15px] leading-7 text-[#6b7075]"
>
视频正处于 15 天保护期,期间暂不提供预览或下载。保护期结束后,当前链接会自动开放。
</p>
<div data-video-share-reveal className="mt-8">
<div className="flex items-center justify-between gap-4 text-sm">
<span className="font-medium text-gray-800 dark:text-gray-100">15 天保护期</span>
<span className="text-gray-500 dark:text-gray-400">还需约 {waitState.remainingDays} 天</span>
<span className="font-medium text-[#1a1c1d]">15 天保护期</span>
<span className="text-[#86898e]">还需约 {waitState.remainingDays} 天</span>
</div>
<div
role="progressbar"
@ -216,15 +216,15 @@ function VideoShareWaitPanel({ checking, onCheckAgain, waitState }: VideoShareWa
aria-valuemax={VIDEO_SHARE_PROTECTION_DAYS}
aria-valuenow={elapsedDays}
aria-valuetext={`预计还需 ${waitState.remainingDays} 天开放`}
className="mt-3 h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700"
className="mt-3 h-2 overflow-hidden rounded-full bg-[#e7e8ea]"
>
<div
data-video-share-progress
className="h-full w-full rounded-full bg-blue-600"
className="h-full w-full rounded-full bg-[#1a1c1d]"
style={{ transform: `scaleX(${progress})`, transformOrigin: 'left center' }}
/>
</div>
<div className="mt-2 flex items-start justify-between gap-4 text-xs text-gray-500 dark:text-gray-400">
<div className="mt-2 flex items-start justify-between gap-4 text-xs text-[#86898e]">
<span>链接已生效</span>
<span className="text-right">{availableAtLabel ?? '保护期结束后开放'}</span>
</div>
@ -233,14 +233,14 @@ function VideoShareWaitPanel({ checking, onCheckAgain, waitState }: VideoShareWa
{availableAtLabel && (
<div
data-video-share-reveal
className="mt-6 flex items-start gap-3 border-t border-gray-100 pt-5 dark:border-gray-700"
className="mt-6 flex items-start gap-3 border-t border-[#e7e8ea] pt-5"
>
<CalendarClock className="mt-0.5 h-5 w-5 shrink-0 text-blue-600 dark:text-blue-300" aria-hidden="true" />
<CalendarClock className="mt-0.5 h-5 w-5 shrink-0 text-[#6e79c8]" aria-hidden="true" />
<div>
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">预计开放时间</p>
<p className="text-sm font-medium text-[#1a1c1d]">预计开放时间</p>
<time
dateTime={waitState.availableAt ?? undefined}
className="mt-0.5 block text-sm text-gray-600 dark:text-gray-300"
className="mt-0.5 block text-sm text-[#6b7075]"
>
{availableAtLabel}
</time>
@ -249,10 +249,10 @@ function VideoShareWaitPanel({ checking, onCheckAgain, waitState }: VideoShareWa
)}
<div data-video-share-reveal className="mt-5 flex items-start gap-3">
<ShieldCheck className="mt-0.5 h-5 w-5 shrink-0 text-gray-400 dark:text-gray-500" aria-hidden="true" />
<ShieldCheck className="mt-0.5 h-5 w-5 shrink-0 text-[#86898e]" aria-hidden="true" />
<div>
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">无需重新分享</p>
<p className="mt-0.5 text-sm leading-6 text-gray-500 dark:text-gray-400">
<p className="text-sm font-medium text-[#1a1c1d]">无需重新分享</p>
<p className="mt-0.5 text-sm leading-6 text-[#6b7075]">
开放后,访问者继续使用这个链接即可查看视频。
</p>
</div>
@ -264,7 +264,7 @@ function VideoShareWaitPanel({ checking, onCheckAgain, waitState }: VideoShareWa
onClick={onCheckAgain}
disabled={checking}
aria-describedby="video-share-check-status"
className="inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-lg bg-blue-600 px-5 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-wait disabled:bg-blue-400 dark:focus-visible:ring-offset-gray-900 sm:w-auto"
className="inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-[8px] bg-[#1a1c1d] px-5 text-sm font-medium 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-wait disabled:opacity-50 sm:w-auto"
>
<RefreshCw className={`h-4 w-4 ${checking ? 'motion-safe:animate-spin' : ''}`} aria-hidden="true" />
{checking ? '正在检查…' : '重新检查'}
@ -273,7 +273,7 @@ function VideoShareWaitPanel({ checking, onCheckAgain, waitState }: VideoShareWa
id="video-share-check-status"
role="status"
aria-live="polite"
className="text-center text-xs text-gray-500 dark:text-gray-400 sm:text-left"
className="text-center text-xs text-[#86898e] sm:text-left"
>
{checking ? '正在确认视频是否已经开放' : '可以随时回来检查开放状态'}
</p>
@ -309,6 +309,35 @@ export default function SharePage() {
const [skuListRefreshKey, setSkuListRefreshKey] = useState(0)
const [detailActionBusy, setDetailActionBusy] = useState<'favorite' | 'share' | 'delete' | null>(null)
const { confirmAction, confirmActionDialog } = useConfirmActionDialog()
const pageRef = useRef<HTMLDivElement>(null)
useGSAP(() => {
if (loading || !material || typeof window.matchMedia !== 'function') return
if (!pageRef.current?.querySelector('[data-detail-preview], [data-detail-meta]')) return
const motion = gsap.matchMedia()
motion.add('(prefers-reduced-motion: no-preference)', () => {
gsap.fromTo(
'[data-detail-preview]',
{ y: 14, autoAlpha: 0 },
{ y: 0, autoAlpha: 1, duration: 0.42, ease: 'power3.out', clearProps: 'opacity,visibility,transform' },
)
gsap.fromTo(
'[data-detail-meta]',
{ y: 12, autoAlpha: 0 },
{
y: 0,
autoAlpha: 1,
duration: 0.38,
stagger: 0.05,
delay: 0.06,
ease: 'power2.out',
clearProps: 'opacity,visibility,transform',
},
)
})
return () => motion.revert()
}, { scope: pageRef, dependencies: [loading, material?.id] })
const materialDisplayTitle = material
? getCertificateReportDisplayTitle({
@ -881,10 +910,10 @@ export default function SharePage() {
if (loading) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<main className="container mx-auto px-4 py-8">
<div className="text-center py-16">
<p className="text-gray-500 dark:text-gray-400 text-lg">加载中...</p>
<div className="min-h-screen bg-[#fafafa] text-[#1a1c1d]">
<main className="mx-auto max-w-[1280px] px-6 py-16">
<div className="text-center">
<p className="text-[15px] text-[#86898e]">加载中...</p>
</div>
</main>
</div>
@ -893,9 +922,9 @@ export default function SharePage() {
if (videoShareWait) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<Header session={session} />
<main className="container mx-auto flex min-h-[calc(100vh-5rem)] items-start justify-center px-4 py-8 sm:items-center sm:py-12">
<div className="min-h-screen bg-[#fafafa] text-[#1a1c1d]">
<MaterialsHeader session={session} sectionLabel="创意资产库" />
<main className="mx-auto flex min-h-[calc(100vh-5rem)] max-w-[1280px] items-start justify-center px-6 py-8 sm:items-center sm:py-12">
<VideoShareWaitPanel
waitState={videoShareWait}
checking={checkingVideoAvailability}
@ -908,11 +937,11 @@ export default function SharePage() {
if (!material) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<Header session={session} />
<main className="container mx-auto px-4 py-8">
<div className="text-center py-16">
<p className="text-gray-500 dark:text-gray-400 text-lg">素材不存在</p>
<div className="min-h-screen bg-[#fafafa] text-[#1a1c1d]">
<MaterialsHeader session={session} sectionLabel="创意资产库" />
<main className="mx-auto max-w-[1280px] px-6 py-16">
<div className="text-center">
<p className="text-[15px] text-[#86898e]">素材不存在</p>
</div>
</main>
</div>
@ -935,9 +964,9 @@ export default function SharePage() {
const is3DOriginalType = ['任务-3D原图', '任务-AI原图', '任务-实拍原图', '任务-视频截图', '任务-其他'].includes(mappedMaterialType)
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<Header session={session} />
<main className="container mx-auto px-3 py-4 sm:px-4 sm:py-8">
<div ref={pageRef} className="min-h-screen bg-[#fafafa] text-[#1a1c1d]">
<MaterialsHeader session={session} sectionLabel="创意资产库" />
<main className="mx-auto max-w-[1280px] px-6 py-6 sm:py-8">
<MaterialDetailHeader
title={materialDisplayTitle || material.productModel || material.materialType}
materialType={mappedMaterialType}
@ -964,7 +993,7 @@ export default function SharePage() {
{/* 任务-详情类型:使用专用详情组件 */}
{mappedMaterialType === '任务-详情' ? (
<div className="max-w-7xl mx-auto">
<div data-detail-preview className="mx-auto">
<ProductPackageDetail
material={{
id: material.id,
@ -997,7 +1026,7 @@ export default function SharePage() {
/>
</div>
) : isTaskPageType ? (
<div className="max-w-7xl mx-auto">
<div data-detail-preview className="mx-auto">
<TaskPageDetail
material={{
id: material.id,
@ -1029,7 +1058,7 @@ export default function SharePage() {
</div>
) : is3DOriginalType ? (
/* 3D原图/AI原图/实拍原图/视频截图类型:使用专用详情组件 */
<div className="max-w-7xl mx-auto">
<div data-detail-preview className="mx-auto">
<ThreeDOriginalDetail
material={{
id: material.id,
@ -1064,10 +1093,10 @@ export default function SharePage() {
/* 其他素材类型:使用原有左右布局 */
<>
{/* 所有素材类型统一使用左右布局 */}
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_20rem] lg:gap-8">
<div className="mx-auto grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_20rem] lg:gap-8">
{/* 左侧:素材预览 */}
<div className="min-w-0">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden">
<div data-detail-preview className="min-w-0">
<div className="overflow-hidden rounded-[12px] border border-[#e7e8ea] bg-white">
{(() => {
// 判断是否为视频类型
const isVideoType = isVideoMaterialType(material.materialType)
@ -1105,7 +1134,7 @@ export default function SharePage() {
if (material.previewUrl) {
return (
<div className="w-full bg-gray-200 dark:bg-gray-700 flex flex-col items-center">
<div className="w-full bg-[#fafafa] flex flex-col items-center">
<div className="relative" style={{ maxWidth: '790px', width: '100%' }}>
<Image
src={material.previewUrl}
@ -1122,8 +1151,8 @@ export default function SharePage() {
}
return (
<div className="w-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center" style={{ minHeight: '400px' }}>
<p className="text-gray-500 dark:text-gray-400">PDF文件不存在</p>
<div className="w-full bg-[#fafafa] flex items-center justify-center" style={{ minHeight: '400px' }}>
<p className="text-[#86898e]">PDF文件不存在</p>
</div>
)
}
@ -1132,7 +1161,7 @@ export default function SharePage() {
if (allPreviewImages.length > 1 && !isVideoType && material.materialType !== '音乐') {
/* 显示多张预览图 */
return (
<div className="w-full bg-gray-200 dark:bg-gray-700 flex flex-col items-center">
<div className="w-full bg-[#fafafa] flex flex-col items-center">
{allPreviewImages.map((imageUrl, index) => (
<div key={index} className="relative" style={{ maxWidth: '790px', width: '100%' }}>
<Image
@ -1152,7 +1181,7 @@ export default function SharePage() {
/* 视频类型:显示视频预览(可点击播放完整视频) */
return (
<div
className="relative w-full bg-gray-200 dark:bg-gray-700 cursor-pointer group"
className="relative w-full bg-[#fafafa] cursor-pointer group"
onClick={() => {
// 优先使用720p预览,其次使用10秒片段,最后使用源视频
const playUrl = material.videoPreview540Url || material.videoPreviewClipUrl || material.videoSourceUrl
@ -1233,7 +1262,7 @@ export default function SharePage() {
}}
>
<div
className="h-full bg-blue-500 transition-all duration-100 relative"
className="relative h-full bg-[#1a1c1d] transition-all duration-100"
style={{ width: `${audioProgress}%` }}
>
{/* 拖动手柄 */}
@ -1286,14 +1315,14 @@ export default function SharePage() {
// 如果没有 previewUrl,不显示任何内容
if (!material.previewUrl) {
return (
<div className="w-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center" style={{ minHeight: '400px' }}>
<p className="text-gray-500 dark:text-gray-400">暂无预览图</p>
<div className="w-full bg-[#fafafa] flex items-center justify-center" style={{ minHeight: '400px' }}>
<p className="text-[#86898e]">暂无预览图</p>
</div>
)
}
return (
<div className="w-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<div className="w-full bg-[#fafafa] flex items-center justify-center">
<Image
src={material.previewUrl}
alt={materialDisplayTitle}
@ -1311,24 +1340,18 @@ export default function SharePage() {
</div>
{/* 右侧:素材信息 */}
<div className="min-w-0">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-5 space-y-6 lg:sticky lg:top-24 lg:p-6">
<div data-detail-meta className="min-w-0">
<div className="space-y-6 rounded-[12px] border border-[#e7e8ea] bg-white p-5 lg:sticky lg:top-24 lg:p-6">
{/* 标题和标签 - 统一间距 */}
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
<h2 className="mb-3 text-[20px] font-semibold tracking-[-0.03em] text-[#1a1c1d]">
{materialDisplayTitle}
</h2>
<div className="flex flex-wrap gap-2">
<span className="inline-flex items-center px-3 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-full text-sm font-medium">
{material.categoryName}
</span>
<span className="inline-flex items-center px-3 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-full text-sm font-medium">
{material.sceneName}
</span>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-[12px] text-[#86898e]">
<span>{material.categoryName}</span>
<span>{material.sceneName}</span>
{material.materialType && (
<span className="inline-flex items-center px-3 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded-full text-sm font-medium">
{material.materialType}
</span>
<span>{material.materialType}</span>
)}
</div>
</div>
@ -1346,49 +1369,49 @@ export default function SharePage() {
/* 其他类型:显示素材信息 */
<div className="space-y-4">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">作者</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.authorName}</p>
<p className="mb-1 text-[12px] text-[#86898e]">作者</p>
<p className="text-[15px] font-medium text-[#1a1c1d]">{material.authorName}</p>
</div>
{isCertificateReportMaterialType(material.materialType) ? (
<>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">报告名称 / 检测对象</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.reportTitle || '未填写'}</p>
<p className="mb-1 text-[12px] text-[#86898e]">报告名称 / 检测对象</p>
<p className="text-[15px] font-medium text-[#1a1c1d]">{material.reportTitle || '未填写'}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">文件数量</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{reportViewerItems.length}</p>
<p className="mb-1 text-[12px] text-[#86898e]">文件数量</p>
<p className="text-[15px] font-medium text-[#1a1c1d]">{reportViewerItems.length}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">总大小</p>
<p className="text-base font-medium text-blue-600 dark:text-blue-400">{formatFileSize(material.zipFileSize)}</p>
<p className="mb-1 text-[12px] text-[#86898e]">总大小</p>
<p className="font-en text-[15px] font-medium tabular-nums text-[#1a1c1d]">{formatFileSize(material.zipFileSize)}</p>
</div>
</>
) : (
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">文件大小</p>
<p className="text-base font-medium text-blue-600 dark:text-blue-400">{formatFileSize(material.zipFileSize)}</p>
<p className="mb-1 text-[12px] text-[#86898e]">文件大小</p>
<p className="font-en text-[15px] font-medium tabular-nums text-[#1a1c1d]">{formatFileSize(material.zipFileSize)}</p>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">下载次数</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.downloadCount}</p>
<p className="mb-1 text-[12px] text-[#86898e]">下载次数</p>
<p className="font-en text-[15px] font-medium tabular-nums text-[#1a1c1d]">{material.downloadCount}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">点赞数</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.likeCount}</p>
<p className="mb-1 text-[12px] text-[#86898e]">点赞数</p>
<p className="font-en text-[15px] font-medium tabular-nums text-[#1a1c1d]">{material.likeCount}</p>
</div>
</div>
{!isCertificateReportMaterialType(material.materialType) && (
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">文件名</p>
<p className="text-sm text-gray-700 dark:text-gray-300 break-all">{material.zipFileName}</p>
<p className="mb-1 text-[12px] text-[#86898e]">文件名</p>
<p className="break-all text-[13px] text-[#1a1c1d]">{material.zipFileName}</p>
</div>
)}
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">上传时间</p>
<p className="text-sm text-gray-700 dark:text-gray-300">
<p className="mb-1 text-[12px] text-[#86898e]">上传时间</p>
<p className="text-[13px] text-[#1a1c1d]">
{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}
</p>
</div>
@ -1408,7 +1431,7 @@ export default function SharePage() {
}
}}
disabled={downloading}
className="w-full py-3 bg-gray-600 hover:bg-gray-700 text-white rounded-xl text-base font-medium transition-colors disabled:opacity-75"
className="w-full rounded-[8px] bg-[#1a1c1d] py-3 text-[14px] font-medium text-white transition-colors hover:bg-[#2a2d2f] disabled:opacity-75"
>
{downloading && downloadingBatchType === 'model' ? (
<span className="flex items-center justify-center gap-2 whitespace-nowrap">
@ -1435,7 +1458,7 @@ export default function SharePage() {
}
}}
disabled={downloading}
className="w-full py-3 bg-gray-600 hover:bg-gray-700 text-white rounded-xl text-base font-medium transition-colors disabled:opacity-75"
className="w-full rounded-[8px] bg-[#1a1c1d] py-3 text-[14px] font-medium text-white transition-colors hover:bg-[#2a2d2f] disabled:opacity-75"
>
{downloading && downloadingBatchType === 'white-background' ? (
<span className="flex items-center justify-center gap-2 whitespace-nowrap">
@ -1455,19 +1478,19 @@ export default function SharePage() {
</button>
</div>
) : isCertificateReportMaterialType(material.materialType) ? (
<div className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600 dark:border-gray-700 dark:bg-gray-700/40 dark:text-gray-300">
<div className="rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] px-4 py-3 text-[13px] text-[#6b7075]">
报告文件请在左侧文件列表中打开或下载
</div>
) : (
<button
onClick={handleDownloadByDirectLink}
disabled={downloading}
className="w-full btn-primary py-3 text-base font-medium relative overflow-hidden disabled:opacity-75"
className="relative w-full overflow-hidden rounded-[8px] bg-[#1a1c1d] py-3 text-[14px] font-medium text-white hover:bg-[#2a2d2f] disabled:opacity-75"
>
{downloading ? (
<>
<div
className="absolute left-0 top-0 bottom-0 bg-blue-600 transition-all duration-300"
className="absolute bottom-0 left-0 top-0 bg-[#2a2d2f] transition-all duration-300"
style={{ width: `${downloadProgress}%` }}
/>
<span className="relative z-10 flex items-center justify-center gap-2">
@ -1500,7 +1523,7 @@ export default function SharePage() {
{showBackToTop && (
<button
onClick={scrollToTop}
className="fixed bottom-8 right-8 w-12 h-12 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all z-50"
className="fixed bottom-8 right-8 z-50 flex h-12 w-12 items-center justify-center rounded-[8px] border border-[#e7e8ea] bg-white text-[#1a1c1d] transition-colors hover:bg-[#1a1c1d] hover:text-white"
title="返回顶部"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View File

@ -75,16 +75,16 @@ export default function DocumentAssetViewer({ title, items }: DocumentAssetViewe
if (!selectedItem) {
return (
<div className="flex min-h-[400px] items-center justify-center bg-gray-100 text-sm text-gray-500 dark:bg-gray-700 dark:text-gray-300">
<div className="flex min-h-[400px] items-center justify-center bg-[#fafafa] text-sm text-[#86898e]">
暂无可预览文件
</div>
)
}
return (
<div className="w-full bg-white dark:bg-gray-800">
<div className="border-b border-gray-200 px-4 py-3 dark:border-gray-700">
<div className="mb-3 text-sm font-medium text-gray-700 dark:text-gray-200">{title}</div>
<div className="w-full bg-white">
<div className="border-b border-[#e7e8ea] px-4 py-3">
<div className="mb-3 text-sm font-medium text-[#1a1c1d]">{title}</div>
<div className="flex flex-wrap gap-2">
{normalizedItems.map((item) => {
const isActive = item.id === selectedItem.id
@ -93,10 +93,10 @@ export default function DocumentAssetViewer({ title, items }: DocumentAssetViewe
key={item.id}
type="button"
onClick={() => setSelectedId(item.id)}
className={`rounded-full px-3 py-1.5 text-xs transition-colors ${
className={`rounded-[8px] px-3 py-1.5 text-xs transition-colors ${
isActive
? 'bg-gray-900 text-white dark:bg-blue-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
? 'bg-[#1a1c1d] text-white'
: 'border border-[#e7e8ea] text-[#6b7075] hover:border-[#1a1c1d] hover:text-[#1a1c1d]'
}`}
>
{item.fileKind === 'pdf' ? 'PDF' : '图片'} · {item.fileName}
@ -106,10 +106,10 @@ export default function DocumentAssetViewer({ title, items }: DocumentAssetViewe
</div>
</div>
<div className="flex items-center justify-between border-b border-gray-100 px-4 py-3 dark:border-gray-700">
<div className="flex items-center justify-between border-b border-[#e7e8ea] px-4 py-3">
<div className="min-w-0">
<div className="truncate text-sm font-medium text-gray-900 dark:text-white">{selectedItem.fileName}</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
<div className="truncate text-sm font-medium text-[#1a1c1d]">{selectedItem.fileName}</div>
<div className="mt-1 text-xs text-[#86898e]">
{selectedItem.fileKind === 'pdf' ? 'PDF 文档预览' : '图片原图预览'}
</div>
</div>
@ -117,7 +117,7 @@ export default function DocumentAssetViewer({ title, items }: DocumentAssetViewe
<button
type="button"
onClick={handleOpenFile}
className="rounded-lg px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
className="rounded-[8px] border border-[#e7e8ea] px-3 py-2 text-sm text-[#1a1c1d] transition-colors hover:border-[#1a1c1d]"
>
打开原文件
</button>
@ -126,14 +126,14 @@ export default function DocumentAssetViewer({ title, items }: DocumentAssetViewe
onClick={() => {
void handleDownloadFile()
}}
className="rounded-lg bg-gray-900 px-3 py-2 text-sm text-white transition-colors hover:bg-gray-800 dark:bg-blue-600 dark:hover:bg-blue-700"
className="rounded-[8px] bg-[#1a1c1d] px-3 py-2 text-sm text-white transition-colors hover:bg-[#2a2d2f]"
>
下载原文件
</button>
</div>
</div>
<div className="bg-gray-100 dark:bg-gray-700">
<div className="bg-[#fafafa]">
{selectedItem.fileKind === 'pdf' ? (
<iframe
src={`${selectedItem.fileUrl}#toolbar=0&navpanes=0&scrollbar=1`}

View File

@ -167,13 +167,13 @@ export default function SkuList({ materialId, canEdit, onEditClick, onSelectionC
}
if (loading) {
return <div className="py-4 text-center text-gray-500 dark:text-gray-400 text-sm">加载中...</div>
return <div className="py-4 text-center text-[13px] text-[#86898e]">加载中...</div>
}
if (skus.length === 0) {
return (
<div className="py-4 flex items-center justify-between">
<span className="text-gray-500 dark:text-gray-400 text-sm">暂无 SKU</span>
<span className="text-[13px] text-[#86898e]">暂无 SKU</span>
{canEdit && (
<button
onClick={onEditClick}
@ -192,7 +192,7 @@ export default function SkuList({ materialId, canEdit, onEditClick, onSelectionC
return (
<div className="flex flex-col">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-gray-900 dark:text-white">SKU 列表 ({skus.length}个)</span>
<span className="text-[13px] font-medium text-[#1a1c1d]">SKU 列表 ({skus.length}个)</span>
{canEdit && (
<button
onClick={onEditClick}
@ -206,7 +206,7 @@ export default function SkuList({ materialId, canEdit, onEditClick, onSelectionC
)}
</div>
<div className="flex items-center justify-between py-2 px-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg mb-3">
<div className="mb-3 flex items-center justify-between rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] px-3 py-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
@ -214,10 +214,10 @@ export default function SkuList({ materialId, canEdit, onEditClick, onSelectionC
onChange={handleSelectAll}
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-xs text-gray-600 dark:text-gray-300">全选</span>
<span className="text-[12px] text-[#1a1c1d]">全选</span>
</label>
{selectedSkuIds.size > 0 && (
<span className="text-xs text-gray-500 dark:text-gray-400">已选 {selectedSkuIds.size} 个</span>
<span className="text-[12px] text-[#86898e]">已选 {selectedSkuIds.size} 个</span>
)}
</div>
@ -225,7 +225,7 @@ export default function SkuList({ materialId, canEdit, onEditClick, onSelectionC
{skus.map((sku) => (
<div
key={sku.id}
className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-700/30 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700/50 transition-colors"
className="flex items-center gap-2 rounded-[8px] border border-[#e7e8ea] p-2 transition-colors hover:bg-[#fafafa]"
>
<input
type="checkbox"
@ -249,15 +249,15 @@ export default function SkuList({ materialId, canEdit, onEditClick, onSelectionC
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">{sku.skuName}</p>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
<p className="truncate text-[13px] font-medium text-[#1a1c1d]">{sku.skuName}</p>
<p className="truncate text-[12px] text-[#86898e]">
模型:{sku.zipFilePath && sku.zipFilePath.trim() ? '已上传' : '未上传'}
<span className="mx-1.5 text-gray-300 dark:text-gray-600">·</span>
<span className="mx-1.5 text-[#e7e8ea]">·</span>
白底图:{sku.whiteBackgroundUploaded ? `${sku.whiteBackgroundCount || 0}张` : '未上传'}
{sku.whiteBackgroundUploaded && sku.whiteBackgroundComplete === false ? '(未补齐)' : ''}
</p>
{sku.whiteBackgroundUploaded && (
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400 truncate">
<p className="mt-0.5 truncate text-[12px] text-[#86898e]">
{[
sku.whiteBackgroundSourceType ? `出图:${sku.whiteBackgroundSourceType}` : null,
sku.whiteBackgroundUploadedAt

View File

@ -1,6 +1,8 @@
'use client'
import { useState, useRef, useEffect, useCallback } from 'react'
import { useGSAP } from '@gsap/react'
import gsap from 'gsap'
import Image from 'next/image'
import { chunkUpload } from '@/lib/chunk-upload'
import {
@ -84,6 +86,8 @@ import {
normalizeSpuProductModel,
} from '@/lib/product-model'
gsap.registerPlugin(useGSAP)
interface UploadModalProps {
onClose: () => void
onSuccess: () => void
@ -219,6 +223,62 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
const singleUploadXhrsRef = useRef<Set<XMLHttpRequest>>(new Set()) // 单文件上传的所有活跃XHR
const singleUploadServerStateRef = useRef<{ uploadId?: string; existingFilePath?: string } | null>(null)
const batchFileInputRef = useRef<HTMLInputElement>(null) // 批量文件选择input
const overlayRef = useRef<HTMLDivElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const onCloseRef = useRef(onClose)
const closeAnimatedRef = useRef(onClose)
onCloseRef.current = onClose
useGSAP((context, contextSafe) => {
if (typeof window.matchMedia !== 'function') return
const motion = gsap.matchMedia()
motion.add('(prefers-reduced-motion: no-preference)', () => {
gsap.fromTo(
overlayRef.current,
{ autoAlpha: 0 },
{ autoAlpha: 1, duration: 0.28, ease: 'power2.out' },
)
gsap.fromTo(
panelRef.current,
{ y: 16, autoAlpha: 0 },
{
y: 0,
autoAlpha: 1,
duration: 0.4,
ease: 'power3.out',
clearProps: 'opacity,visibility,transform',
},
)
gsap.fromTo(
'[data-upload-reveal]',
{ y: 8, autoAlpha: 0 },
{
y: 0,
autoAlpha: 1,
duration: 0.32,
stagger: 0.04,
delay: 0.06,
ease: 'power2.out',
clearProps: 'opacity,visibility,transform',
},
)
})
closeAnimatedRef.current = contextSafe(() => {
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (reduced || !overlayRef.current || !panelRef.current) {
onCloseRef.current()
return
}
gsap.timeline({ onComplete: () => onCloseRef.current() })
.to(panelRef.current, { y: 10, autoAlpha: 0, duration: 0.2, ease: 'power2.in' })
.to(overlayRef.current, { autoAlpha: 0, duration: 0.16, ease: 'power2.in' }, '<')
})
return () => motion.revert()
}, { scope: overlayRef })
const singleUploadTotalBytes =
(zipFile?.size || 0) +
previewImages.reduce((sum, file) => sum + file.size, 0) +
@ -1888,19 +1948,20 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
setUploading(false)
setError('上传已取消')
} else {
onClose()
closeAnimatedRef.current()
}
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[#1a1c1d]/40 p-4">
<div ref={overlayRef} className="fixed inset-0 z-50 flex items-center justify-center bg-[#1a1c1d]/40 p-4">
<div
ref={panelRef}
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="border-b border-[#e7e8ea] px-8 pb-5 pt-7">
<div data-upload-reveal 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
@ -1915,7 +1976,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
</div>
<button
type="button"
onClick={onClose}
onClick={() => closeAnimatedRef.current()}
aria-label="关闭上传素材窗口"
title="关闭"
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"
@ -1925,7 +1986,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6 px-8 py-6">
<form data-upload-reveal onSubmit={handleSubmit} className="space-y-6 px-8 py-6">
<div>
<label htmlFor="upload-material-type" className={fieldLabelClass}>
素材类型 *

View File

@ -53,12 +53,12 @@ function ActionButton({
disabled={disabled}
aria-label={title}
aria-pressed={pressed}
className={`inline-flex min-h-11 items-center justify-center gap-2 whitespace-nowrap rounded-lg border px-3 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-wait disabled:opacity-50 dark:focus-visible:ring-offset-gray-900 ${
className={`inline-flex min-h-11 items-center justify-center gap-2 whitespace-nowrap rounded-[8px] border px-3 text-[13px] font-medium tracking-[-0.02em] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 disabled:cursor-wait disabled:opacity-50 ${
danger
? 'border-red-200 text-red-600 hover:bg-red-50 dark:border-red-900/70 dark:text-red-400 dark:hover:bg-red-950/30'
? 'border-[#e7e8ea] text-[#b42318] hover:bg-[#fafafa]'
: pressed
? 'border-blue-600 bg-blue-600 text-white hover:bg-blue-700 dark:border-blue-500 dark:bg-blue-600'
: 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800'
? 'border-[#1a1c1d] bg-[#1a1c1d] text-white hover:bg-[#2a2d2f]'
: 'border-[#e7e8ea] bg-white text-[#1a1c1d] hover:border-[#1a1c1d]'
}`}
>
{children}
@ -85,7 +85,7 @@ export default function MaterialDetailHeader({
return (
<section
aria-label="素材详情操作"
className="mx-auto mb-4 max-w-7xl rounded-xl border border-gray-200 bg-white px-3 py-3 dark:border-gray-800 dark:bg-gray-900 sm:mb-6 sm:px-4"
className="mx-auto mb-6 max-w-[1280px] border-b border-[#e7e8ea] px-0 pb-5"
>
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
<div className="flex min-w-0 flex-1 items-center gap-3">
@ -93,7 +93,7 @@ export default function MaterialDetailHeader({
type="button"
onClick={onBack}
aria-label="返回上一页"
className="inline-flex h-11 w-11 flex-none items-center justify-center rounded-lg border border-gray-200 text-gray-700 transition-colors hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800 dark:focus-visible:ring-offset-gray-900"
className="inline-flex h-11 w-11 flex-none items-center justify-center rounded-[8px] border border-[#e7e8ea] text-[#1a1c1d] transition-colors hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
@ -101,13 +101,13 @@ export default function MaterialDetailHeader({
</button>
<div className="min-w-0">
<h1 className="truncate text-base font-semibold text-gray-950 dark:text-white sm:text-lg" title={title}>
<h1 className="truncate text-[20px] font-semibold tracking-[-0.03em] text-[#1a1c1d] sm:text-[22px]" title={title}>
{title}
</h1>
<p className="mt-0.5 flex min-w-0 items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
<p className="mt-1 flex min-w-0 items-center gap-2 text-[12px] text-[#86898e]">
<span className="flex-none">{materialType}</span>
<span aria-hidden="true">·</span>
<span className="truncate" title={materialId}>ID:{materialId}</span>
<span className="truncate font-en tabular-nums tracking-[0.04em]" title={materialId}>ID {materialId}</span>
</p>
</div>
</div>

View File

@ -7,6 +7,16 @@
import { useState, useEffect, useRef } from 'react';
import { parseDownloadError } from '@/lib/download-errors';
import { getTaskDetailSourceTypeLabel } from '@/lib/task-detail-source-type';
import {
detailInkButtonClass,
detailOutlineButtonClass,
detailMetaLabelClass,
detailMetaValueClass,
detailPanelClass,
detailSectionTitleClass,
detailThumbActiveClass,
detailThumbIdleClass,
} from '@/components/detail/detail-styles';
interface Asset {
id: string;
@ -102,17 +112,14 @@ export default function ProductPackageDetail({
<div className="space-y-6">
{/* 警告:已被外部系统获取 */}
{material.taskStatus === 'fetched' && material.taskFetchedAt && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className={`${detailPanelClass} p-4`}>
<div className="flex items-start">
<svg className="w-5 h-5 text-yellow-600 mt-0.5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<div>
<h4 className="text-sm font-medium text-yellow-800">注意:此素材已被外部系统获取</h4>
<p className="text-sm text-yellow-700 mt-1">
<h4 className="text-[13px] font-medium text-[#1a1c1d]">此素材已被外部系统获取</h4>
<p className="mt-1 text-[13px] text-[#6b7075]">
获取时间:{new Date(material.taskFetchedAt).toLocaleString('zh-CN')}
</p>
<p className="text-sm text-yellow-700">编辑或删除此素材可能影响外部系统的使用</p>
<p className="text-[13px] text-[#6b7075]">编辑或删除可能影响外部系统使用</p>
</div>
</div>
</div>
@ -123,15 +130,15 @@ export default function ProductPackageDetail({
<div className="min-w-0 space-y-6">
{/* 图片预览区(可显示主图或SKU图) */}
{((previewType === 'main' && mainImages.length > 0) || (previewType === 'sku' && skuImages.length > 0)) && (
<div className="bg-white rounded-lg shadow p-4">
<div className={`${detailPanelClass} p-4`}>
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium">
<h4 className={detailSectionTitleClass}>
{previewType === 'main' ? `主图 (${currentMainIndex + 1}/${mainImages.length})` : `SKU图 (${currentSkuIndex + 1}/${skuImages.length})`}
</h4>
{((previewType === 'main' && mainImages.length > 1) || (previewType === 'sku' && skuImages.length > 1)) && (
<button
onClick={() => setAutoPlayEnabled(!autoPlayEnabled)}
className="text-xs text-gray-500 hover:text-gray-700"
className="text-[12px] text-[#86898e] hover:text-[#1a1c1d]"
type="button"
>
{autoPlayEnabled ? '暂停轮播' : '开始轮播'}
@ -146,7 +153,7 @@ export default function ProductPackageDetail({
: skuImages[currentSkuIndex]?.fileUrl
}
alt={previewType === 'main' ? `主图 ${currentMainIndex + 1}` : `SKU图 ${currentSkuIndex + 1}`}
className="w-full rounded-lg"
className="w-full"
/>
</div>
</div>
@ -154,8 +161,8 @@ export default function ProductPackageDetail({
{/* 主图小卡片展示区 */}
{mainImages.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h4 className="text-sm font-medium mb-3 text-gray-900 dark:text-white">主图 ({mainImages.length})</h4>
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>主图 ({mainImages.length})</h4>
<div className="flex flex-wrap gap-2">
{mainImages.map((img, index) => (
<button
@ -177,10 +184,10 @@ export default function ProductPackageDetail({
// 阻止默认的鼠标按下行为
e.preventDefault();
}}
className={`relative flex-shrink-0 border-2 rounded overflow-hidden ${
className={`relative flex-shrink-0 overflow-hidden rounded-[8px] ${
previewType === 'main' && index === currentMainIndex
? 'border-blue-500 ring-2 ring-blue-200'
: 'border-gray-200 hover:border-gray-300'
? detailThumbActiveClass
: detailThumbIdleClass
}`}
style={{ width: '84px', height: '84px' }}
type="button"
@ -191,8 +198,8 @@ export default function ProductPackageDetail({
className="w-full h-full object-cover"
/>
{previewType === 'main' && index === currentMainIndex && (
<div className="absolute inset-0 bg-blue-500 bg-opacity-20 flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<div className="absolute inset-0 flex items-center justify-center bg-[#1a1c1d]/20">
<svg className="h-6 w-6 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
@ -205,8 +212,8 @@ export default function ProductPackageDetail({
{/* 详情图区域(使用页面整体滚动,图片无缝拼接) */}
{detailImages.length > 0 && (
<div className="bg-white rounded-lg shadow p-4">
<h4 className="text-sm font-medium mb-3">详情图 ({detailImages.length})</h4>
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>详情图 ({detailImages.length})</h4>
<div className="space-y-0">
{detailImages.map((img, index) => (
<div key={img.id} className="relative">
@ -216,7 +223,7 @@ export default function ProductPackageDetail({
className="w-full block"
style={{ display: 'block' }}
/>
<div className="absolute top-2 left-2 bg-black bg-opacity-60 text-white text-xs px-2 py-1 rounded">
<div className="absolute left-2 top-2 bg-[#1a1c1d] px-2 py-1 text-[11px] text-white">
{index + 1}
</div>
</div>
@ -229,36 +236,36 @@ export default function ProductPackageDetail({
{/* 右侧:基本信息 + SKU图 + 下载按钮 */}
<div className="space-y-6">
{/* 基本信息卡片 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-medium mb-4 text-gray-900 dark:text-white">素材详情</h3>
<div data-detail-meta className={`${detailPanelClass} p-6`}>
<h3 className="mb-4 text-[15px] font-semibold tracking-[-0.03em] text-[#1a1c1d]">素材详情</h3>
<div className="space-y-4">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">任务号</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.taskNo}</p>
<p className={detailMetaLabelClass}>任务号</p>
<p className={detailMetaValueClass}>{material.taskNo}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">SPU型号</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.productModel}</p>
<p className={detailMetaLabelClass}>SPU型号</p>
<p className={detailMetaValueClass}>{material.productModel}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">店铺</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.categoryName}</p>
<p className={detailMetaLabelClass}>店铺</p>
<p className={detailMetaValueClass}>{material.categoryName}</p>
</div>
{material.sceneName && (
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">场景</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.sceneName}</p>
<p className={detailMetaLabelClass}>场景</p>
<p className={detailMetaValueClass}>{material.sceneName}</p>
</div>
)}
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">出图方式</p>
<p className="text-base font-medium text-gray-900 dark:text-white">
<p className={detailMetaLabelClass}>出图方式</p>
<p className={detailMetaValueClass}>
{getTaskDetailSourceTypeLabel(material.taskDetailSourceType)}
</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">是否整套</p>
<p className="text-base font-medium text-gray-900 dark:text-white">
<p className={detailMetaLabelClass}>是否整套</p>
<p className={detailMetaValueClass}>
{material.isFullSet ? '是' : '否'}
</p>
</div>
@ -267,8 +274,8 @@ export default function ProductPackageDetail({
{/* SKU图小卡片展示区 */}
{skuImages.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h4 className="text-sm font-medium mb-3 text-gray-900 dark:text-white">SKU图 ({skuImages.length})</h4>
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>SKU图 ({skuImages.length})</h4>
<div className="grid grid-cols-4 gap-2">
{skuImages.map((img, index) => (
<button
@ -280,10 +287,10 @@ export default function ProductPackageDetail({
setCurrentSkuIndex(index);
setAutoPlayEnabled(false); // 手动切换时暂停轮播
}}
className={`relative w-full aspect-square border-2 rounded overflow-hidden ${
className={`relative aspect-square w-full overflow-hidden rounded-[8px] ${
previewType === 'sku' && index === currentSkuIndex
? 'border-blue-500 ring-2 ring-blue-200'
: 'border-gray-200 hover:border-gray-300'
? detailThumbActiveClass
: detailThumbIdleClass
}`}
type="button"
>
@ -293,8 +300,8 @@ export default function ProductPackageDetail({
className="w-full h-full object-cover"
/>
{previewType === 'sku' && index === currentSkuIndex && (
<div className="absolute inset-0 bg-blue-500 bg-opacity-20 flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<div className="absolute inset-0 flex items-center justify-center bg-[#1a1c1d]/20">
<svg className="h-6 w-6 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
@ -309,14 +316,14 @@ export default function ProductPackageDetail({
<div className="space-y-6 lg:sticky lg:top-24 lg:z-10">
{/* PSD 文件信息 */}
{psdFiles.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h4 className="text-sm font-medium mb-3 text-gray-900 dark:text-white">PSD 源文件 ({psdFiles.length})</h4>
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>PSD 源文件 ({psdFiles.length})</h4>
<div className="space-y-2">
{psdFiles.map((file) => (
<div key={file.id} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded">
<div key={file.id} className="flex items-center justify-between rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] p-3">
<div className="flex-1">
<p className="text-sm font-medium text-gray-900 dark:text-white">{file.fileName}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{formatBytes(file.fileSize)}</p>
<p className="text-[13px] font-medium text-[#1a1c1d]">{file.fileName}</p>
<p className="text-[12px] text-[#86898e]">{formatBytes(file.fileSize)}</p>
</div>
{canDownloadSource ? (
<button
@ -340,13 +347,13 @@ export default function ProductPackageDetail({
}
}}
disabled={downloading}
className="px-3 py-1 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
className={detailInkButtonClass}
type="button"
>
下载
</button>
) : (
<span className="text-xs text-gray-400">无权限</span>
<span className="text-[12px] text-[#86898e]">无权限</span>
)}
</div>
))}
@ -355,16 +362,16 @@ export default function ProductPackageDetail({
)}
{/* 下载按钮组 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h4 className="text-sm font-medium mb-3 text-gray-900 dark:text-white">批量下载</h4>
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>批量下载</h4>
{downloading && (
<div className="mb-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<div className="mb-3 rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] p-3">
<div className="flex items-center space-x-2">
<svg className="animate-spin h-4 w-4 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<svg className="h-4 w-4 animate-spin text-[#1a1c1d]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span className="text-sm text-blue-600 dark:text-blue-400">
<span className="text-[13px] text-[#6b7075]">
{downloadType === 'all' ? '正在准备全部文件,请稍候...' :
downloadType === 'main' ? '正在下载主图,请稍候...' :
downloadType === 'sku' ? '正在下载SKU图,请稍候...' :
@ -399,7 +406,7 @@ export default function ProductPackageDetail({
}
}}
disabled={downloading}
className="px-4 py-2 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
className={detailInkButtonClass}
type="button"
>
下载主图
@ -427,7 +434,7 @@ export default function ProductPackageDetail({
}
}}
disabled={downloading}
className="px-4 py-2 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
className={detailInkButtonClass}
type="button"
>
下载SKU图
@ -455,7 +462,7 @@ export default function ProductPackageDetail({
}
}}
disabled={downloading}
className="px-4 py-2 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
className={detailInkButtonClass}
type="button"
>
下载详情图
@ -482,7 +489,7 @@ export default function ProductPackageDetail({
}
}}
disabled={downloading}
className="px-4 py-2 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
className={detailInkButtonClass}
type="button"
>
下载全部
@ -494,7 +501,7 @@ export default function ProductPackageDetail({
{onBack && (
<button
onClick={onBack}
className="w-full py-3 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-xl text-base font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className={`w-full ${detailOutlineButtonClass}`}
>
返回上一页
</button>

View File

@ -6,6 +6,16 @@
import { useState, useEffect, useRef } from 'react';
import { parseDownloadError } from '@/lib/download-errors';
import {
detailInkButtonClass,
detailMetaLabelClass,
detailMetaValueClass,
detailOutlineButtonClass,
detailPanelClass,
detailSectionTitleClass,
detailThumbActiveClass,
detailThumbIdleClass,
} from '@/components/detail/detail-styles';
interface Asset {
id: string;
@ -89,18 +99,13 @@ export default function TaskPageDetail({
<div className="space-y-6">
{/* 警告:已被外部系统获取 */}
{material.taskStatus === 'fetched' && material.taskFetchedAt && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-start">
<svg className="w-5 h-5 text-yellow-600 mt-0.5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<div>
<h4 className="text-sm font-medium text-yellow-800">注意:此素材已被外部系统获取</h4>
<p className="text-sm text-yellow-700 mt-1">
获取时间:{new Date(material.taskFetchedAt).toLocaleString('zh-CN')}
</p>
<p className="text-sm text-yellow-700">编辑或删除此素材可能影响外部系统的使用</p>
</div>
<div className={`${detailPanelClass} p-4`}>
<div>
<h4 className="text-[13px] font-medium text-[#1a1c1d]">此素材已被外部系统获取</h4>
<p className="mt-1 text-[13px] text-[#6b7075]">
获取时间:{new Date(material.taskFetchedAt).toLocaleString('zh-CN')}
</p>
<p className="text-[13px] text-[#6b7075]">编辑或删除可能影响外部系统使用</p>
</div>
</div>
)}
@ -110,7 +115,7 @@ export default function TaskPageDetail({
<div className="min-w-0 space-y-6">
{/* 预览图预览区 */}
{mainImages.length > 0 && (
<div className="bg-white rounded-lg shadow p-4">
<div className={`${detailPanelClass} p-4`}>
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium">
预览图 ({currentImageIndex + 1}/{mainImages.length})
@ -118,7 +123,7 @@ export default function TaskPageDetail({
{mainImages.length > 1 && (
<button
onClick={() => setAutoPlayEnabled(!autoPlayEnabled)}
className="text-xs text-gray-500 hover:text-gray-700"
className="text-[12px] text-[#86898e] hover:text-[#1a1c1d]"
type="button"
>
{autoPlayEnabled ? '暂停轮播' : '开始轮播'}
@ -137,8 +142,8 @@ export default function TaskPageDetail({
{/* 预览图小卡片展示区 */}
{mainImages.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h4 className="text-sm font-medium mb-3 text-gray-900 dark:text-white">所有预览图 ({mainImages.length})</h4>
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>所有预览图 ({mainImages.length})</h4>
<div className="flex flex-wrap gap-2">
{mainImages.map((img, index) => (
<button
@ -161,8 +166,8 @@ export default function TaskPageDetail({
}}
className={`relative flex-shrink-0 border-2 rounded overflow-hidden ${
index === currentImageIndex
? 'border-blue-500 ring-2 ring-blue-200'
: 'border-gray-200 hover:border-gray-300'
? detailThumbActiveClass
: detailThumbIdleClass
}`}
style={{ width: '84px', height: '84px' }}
type="button"
@ -173,8 +178,8 @@ export default function TaskPageDetail({
className="w-full h-full object-cover"
/>
{index === currentImageIndex && (
<div className="absolute inset-0 bg-blue-500 bg-opacity-20 flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<div className="absolute inset-0 flex items-center justify-center bg-[#1a1c1d]/20">
<svg className="h-6 w-6 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
@ -189,25 +194,25 @@ export default function TaskPageDetail({
{/* 右侧:基本信息 + 下载按钮 */}
<div className="space-y-6">
{/* 基本信息卡片 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-medium mb-4 text-gray-900 dark:text-white">素材详情</h3>
<div className={`${detailPanelClass} p-6`}>
<h3 className="mb-4 text-[15px] font-semibold tracking-[-0.03em] text-[#1a1c1d]">素材详情</h3>
<div className="space-y-4">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">任务号</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.taskNo}</p>
<p className={detailMetaLabelClass}>任务号</p>
<p className={detailMetaValueClass}>{material.taskNo}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">SPU型号</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.productModel}</p>
<p className={detailMetaLabelClass}>SPU型号</p>
<p className={detailMetaValueClass}>{material.productModel}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">店铺</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.categoryName}</p>
<p className={detailMetaLabelClass}>店铺</p>
<p className={detailMetaValueClass}>{material.categoryName}</p>
</div>
{material.sceneName && (
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">场景</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.sceneName}</p>
<p className={detailMetaLabelClass}>场景</p>
<p className={detailMetaValueClass}>{material.sceneName}</p>
</div>
)}
</div>
@ -217,12 +222,12 @@ export default function TaskPageDetail({
<div className="space-y-6 lg:sticky lg:top-24 lg:z-10">
{/* ZIP 源文件 */}
{zipFile && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h4 className="text-sm font-medium mb-3 text-gray-900 dark:text-white">源文件</h4>
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded">
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>源文件</h4>
<div className="flex items-center justify-between rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] p-3">
<div className="flex-1">
<p className="text-sm font-medium text-gray-900 dark:text-white">{zipFile.fileName}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{formatBytes(zipFile.fileSize)}</p>
<p className="text-[13px] font-medium text-[#1a1c1d]">{zipFile.fileName}</p>
<p className="text-[12px] text-[#86898e]">{formatBytes(zipFile.fileSize)}</p>
</div>
{canDownloadSource ? (
<button
@ -246,7 +251,7 @@ export default function TaskPageDetail({
}
}}
disabled={downloading}
className="px-3 py-1 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
className={detailInkButtonClass}
type="button"
>
{downloading && downloadType === 'zip' ? (
@ -262,7 +267,7 @@ export default function TaskPageDetail({
)}
</button>
) : (
<span className="text-xs text-gray-400">无权限</span>
<span className="text-[12px] text-[#86898e]">无权限</span>
)}
</div>
</div>
@ -272,7 +277,7 @@ export default function TaskPageDetail({
{onBack && (
<button
onClick={onBack}
className="w-full py-3 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-xl text-base font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className={`w-full ${detailOutlineButtonClass}`}
>
返回上一页
</button>

View File

@ -6,6 +6,16 @@
import { useState, useEffect, useRef } from 'react';
import { parseDownloadError } from '@/lib/download-errors';
import {
detailInkButtonClass,
detailMetaLabelClass,
detailMetaValueClass,
detailOutlineButtonClass,
detailPanelClass,
detailSectionTitleClass,
detailThumbActiveClass,
detailThumbIdleClass,
} from '@/components/detail/detail-styles';
interface Asset {
id: string;
@ -91,18 +101,13 @@ export default function ThreeDOriginalDetail({
<div className="space-y-6">
{/* 警告:已被外部系统获取 */}
{material.taskStatus === 'fetched' && material.taskFetchedAt && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-start">
<svg className="w-5 h-5 text-yellow-600 mt-0.5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<div>
<h4 className="text-sm font-medium text-yellow-800">注意:此素材已被外部系统获取</h4>
<p className="text-sm text-yellow-700 mt-1">
获取时间:{new Date(material.taskFetchedAt).toLocaleString('zh-CN')}
</p>
<p className="text-sm text-yellow-700">编辑或删除此素材可能影响外部系统的使用</p>
</div>
<div className={`${detailPanelClass} p-4`}>
<div>
<h4 className="text-[13px] font-medium text-[#1a1c1d]">此素材已被外部系统获取</h4>
<p className="mt-1 text-[13px] text-[#6b7075]">
获取时间:{new Date(material.taskFetchedAt).toLocaleString('zh-CN')}
</p>
<p className="text-[13px] text-[#6b7075]">编辑或删除可能影响外部系统使用</p>
</div>
</div>
)}
@ -112,7 +117,7 @@ export default function ThreeDOriginalDetail({
<div className="min-w-0 space-y-6">
{/* 原图预览区 */}
{mainImages.length > 0 && (
<div className="bg-white rounded-lg shadow p-4">
<div className={`${detailPanelClass} p-4`}>
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium">
{isTaskOther ? '预览图' : '原图'} ({currentImageIndex + 1}/{mainImages.length})
@ -120,7 +125,7 @@ export default function ThreeDOriginalDetail({
{mainImages.length > 1 && (
<button
onClick={() => setAutoPlayEnabled(!autoPlayEnabled)}
className="text-xs text-gray-500 hover:text-gray-700"
className="text-[12px] text-[#86898e] hover:text-[#1a1c1d]"
type="button"
>
{autoPlayEnabled ? '暂停轮播' : '开始轮播'}
@ -139,10 +144,10 @@ export default function ThreeDOriginalDetail({
{/* 原图小卡片展示区 */}
{mainImages.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h4 className="text-sm font-medium mb-3 text-gray-900 dark:text-white">所有原图 ({mainImages.length})</h4>
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>所有原图 ({mainImages.length})</h4>
{isTaskOther && (
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">任务-其他只展示上传时提交的预览图</p>
<p className="mb-3 text-[12px] text-[#86898e]">任务-其他只展示上传时提交的预览图</p>
)}
<div className="flex flex-wrap gap-2">
{mainImages.map((img, index) => (
@ -166,8 +171,8 @@ export default function ThreeDOriginalDetail({
}}
className={`relative flex-shrink-0 border-2 rounded overflow-hidden ${
index === currentImageIndex
? 'border-blue-500 ring-2 ring-blue-200'
: 'border-gray-200 hover:border-gray-300'
? detailThumbActiveClass
: detailThumbIdleClass
}`}
style={{ width: '84px', height: '84px' }}
type="button"
@ -178,8 +183,8 @@ export default function ThreeDOriginalDetail({
className="w-full h-full object-cover"
/>
{index === currentImageIndex && (
<div className="absolute inset-0 bg-blue-500 bg-opacity-20 flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<div className="absolute inset-0 flex items-center justify-center bg-[#1a1c1d]/20">
<svg className="h-6 w-6 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
@ -194,29 +199,29 @@ export default function ThreeDOriginalDetail({
{/* 右侧:基本信息 + 下载按钮 */}
<div className="space-y-6">
{/* 基本信息卡片 */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-medium mb-4 text-gray-900 dark:text-white">素材详情</h3>
<div className={`${detailPanelClass} p-6`}>
<h3 className="mb-4 text-[15px] font-semibold tracking-[-0.03em] text-[#1a1c1d]">素材详情</h3>
<div className="space-y-4">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">任务号</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.taskNo}</p>
<p className={detailMetaLabelClass}>任务号</p>
<p className={detailMetaValueClass}>{material.taskNo}</p>
</div>
{!isTaskOther && (
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">SPU型号</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.productModel}</p>
<p className={detailMetaLabelClass}>SPU型号</p>
<p className={detailMetaValueClass}>{material.productModel}</p>
</div>
)}
{!isTaskOther && (
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">店铺</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.categoryName}</p>
<p className={detailMetaLabelClass}>店铺</p>
<p className={detailMetaValueClass}>{material.categoryName}</p>
</div>
)}
{!isTaskOther && material.sceneName && (
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">场景</p>
<p className="text-base font-medium text-gray-900 dark:text-white">{material.sceneName}</p>
<p className={detailMetaLabelClass}>场景</p>
<p className={detailMetaValueClass}>{material.sceneName}</p>
</div>
)}
</div>
@ -226,12 +231,12 @@ export default function ThreeDOriginalDetail({
<div className="space-y-6 lg:sticky lg:top-24 lg:z-10">
{/* ZIP 源文件 */}
{zipFile && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h4 className="text-sm font-medium mb-3 text-gray-900 dark:text-white">源文件</h4>
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded">
<div className={`${detailPanelClass} p-4`}>
<h4 className={detailSectionTitleClass}>源文件</h4>
<div className="flex items-center justify-between rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] p-3">
<div className="flex-1">
<p className="text-sm font-medium text-gray-900 dark:text-white">{zipFile.fileName}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{formatBytes(zipFile.fileSize)}</p>
<p className="text-[13px] font-medium text-[#1a1c1d]">{zipFile.fileName}</p>
<p className="text-[12px] text-[#86898e]">{formatBytes(zipFile.fileSize)}</p>
</div>
{canDownloadSource ? (
<button
@ -255,7 +260,7 @@ export default function ThreeDOriginalDetail({
}
}}
disabled={downloading}
className="px-3 py-1 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
className={detailInkButtonClass}
type="button"
>
{downloading && downloadType === 'zip' ? (
@ -271,7 +276,7 @@ export default function ThreeDOriginalDetail({
)}
</button>
) : (
<span className="text-xs text-gray-400">无权限</span>
<span className="text-[12px] text-[#86898e]">无权限</span>
)}
</div>
</div>
@ -281,7 +286,7 @@ export default function ThreeDOriginalDetail({
{onBack && (
<button
onClick={onBack}
className="w-full py-3 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-xl text-base font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className={`w-full ${detailOutlineButtonClass}`}
>
返回上一页
</button>

View File

@ -0,0 +1,23 @@
export const detailPanelClass =
'rounded-[12px] border border-[#e7e8ea] bg-white'
export const detailMetaLabelClass =
'mb-1 text-[12px] tracking-[-0.02em] text-[#86898e]'
export const detailMetaValueClass =
'text-[15px] font-medium tracking-[-0.02em] text-[#1a1c1d]'
export const detailSectionTitleClass =
'mb-3 text-[13px] font-medium tracking-[-0.02em] text-[#1a1c1d]'
export const detailInkButtonClass =
'inline-flex h-10 items-center justify-center rounded-[8px] bg-[#1a1c1d] px-4 text-[13px] 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 detailOutlineButtonClass =
'inline-flex h-10 items-center justify-center rounded-[8px] border border-[#1a1c1d] bg-white px-4 text-[13px] 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 detailThumbIdleClass =
'border border-[#e7e8ea] hover:border-[#1a1c1d]'
export const detailThumbActiveClass =
'border border-[#1a1c1d]'