feat: 包材列表对齐修复、操作精简、图片上传功能
- 所有列统一 flex items-center 垂直居中对齐 - 表头图片列加上"图片"文字 - 操作列精简为编辑/复制/删除三个图标按钮 - 图片列支持点击上传(JPG/PNG/WebP,最大10MB) - 新增 /api/asset-center/upload-image 上传接口
This commit is contained in:
parent
72f866de12
commit
f83c587794
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 包材图片上传 API
|
||||
* POST /api/asset-center/upload-image
|
||||
*/
|
||||
|
||||
import { NextRequest } from 'next/server'
|
||||
import { writeFile, mkdir } from 'fs/promises'
|
||||
import { existsSync } from 'fs'
|
||||
import path from 'path'
|
||||
import { handleRequest, jsonSuccess, jsonError } from '@/server/core/response'
|
||||
import { ErrorCode } from '@/server/core/errors'
|
||||
|
||||
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp']
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return handleRequest(request, async ({ requestId }) => {
|
||||
const formData = await request.formData()
|
||||
const file = formData.get('file') as File | null
|
||||
|
||||
if (!file) {
|
||||
return jsonError(ErrorCode.VALIDATION_REQUIRED_FIELD, '请选择要上传的图片', 400, requestId)
|
||||
}
|
||||
|
||||
if (!ALLOWED_TYPES.includes(file.type)) {
|
||||
return jsonError(ErrorCode.FILE_TYPE_NOT_ALLOWED, '仅支持 JPG、PNG、WebP 格式', 400, requestId)
|
||||
}
|
||||
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
return jsonError(ErrorCode.FILE_TOO_LARGE, '图片大小不能超过 10MB', 400, requestId)
|
||||
}
|
||||
|
||||
const uploadDir = path.join(process.cwd(), 'public', 'packaging-images')
|
||||
|
||||
if (!existsSync(uploadDir)) {
|
||||
await mkdir(uploadDir, { recursive: true })
|
||||
}
|
||||
|
||||
const ext = path.extname(file.name) || '.jpg'
|
||||
const timestamp = Date.now()
|
||||
const random = Math.random().toString(36).slice(2, 8)
|
||||
const fileName = `${timestamp}_${random}${ext}`
|
||||
const filePath = path.join(uploadDir, fileName)
|
||||
|
||||
const bytes = await file.arrayBuffer()
|
||||
await writeFile(filePath, Buffer.from(bytes))
|
||||
|
||||
const publicUrl = `/packaging-images/${fileName}`
|
||||
|
||||
return jsonSuccess({
|
||||
url: publicUrl,
|
||||
fileName,
|
||||
size: file.size,
|
||||
}, 201, requestId)
|
||||
}, { requireAuth: true })
|
||||
}
|
||||
@ -19,8 +19,6 @@ import {
|
||||
Package2,
|
||||
Pencil,
|
||||
Plus,
|
||||
Power,
|
||||
PowerOff,
|
||||
ToggleLeft,
|
||||
ToggleRight,
|
||||
Trash2,
|
||||
@ -777,6 +775,33 @@ export default function PackagingMaterialsClient({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUploadImage(variant: AssetVariantDTO, file: File) {
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await fetch('/api/asset-center/upload-image', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
if (!response.ok || !result.data?.url) {
|
||||
throw new Error(result.message || '上传失败')
|
||||
}
|
||||
|
||||
const updatedVariant = await updateAssetVariant(variant.id, {
|
||||
thumbnailUrl: result.data.url,
|
||||
})
|
||||
|
||||
setVariants((previous) =>
|
||||
previous.map((item) => (item.id === updatedVariant.id ? updatedVariant : item)),
|
||||
)
|
||||
} catch (error) {
|
||||
setDetailError(error instanceof Error ? error.message : '图片上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const selectedFamilyCanDelete =
|
||||
selectedFamily != null && (familyVariantCountMap.get(selectedFamily.id) ?? 0) === 0
|
||||
|
||||
@ -1006,8 +1031,8 @@ export default function PackagingMaterialsClient({
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden rounded-[24px] border border-black/5">
|
||||
<div className="grid grid-cols-[48px_1.4fr_1fr_0.9fr_1fr_0.8fr_0.7fr_auto] gap-2 bg-apple-bg-secondary/65 px-4 py-3 text-xs font-apple-medium uppercase tracking-[0.18em] text-apple-label-tertiary">
|
||||
<span></span>
|
||||
<div className="grid grid-cols-[48px_1.4fr_1fr_0.9fr_1fr_0.8fr_0.7fr_auto] items-center gap-2 bg-apple-bg-secondary/65 px-4 py-3 text-xs font-apple-medium uppercase tracking-[0.18em] text-apple-label-tertiary">
|
||||
<span>图片</span>
|
||||
<span>名称</span>
|
||||
<span>编码</span>
|
||||
<span>模板类型</span>
|
||||
@ -1072,32 +1097,47 @@ export default function PackagingMaterialsClient({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
{variant.thumbnailUrl ? (
|
||||
<img
|
||||
src={variant.thumbnailUrl}
|
||||
alt={variant.name}
|
||||
className="h-9 w-9 rounded-apple-md border border-black/5 object-cover"
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<label className="relative cursor-pointer">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
className="hidden"
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (file) void handleUploadImage(variant, file)
|
||||
event.target.value = ''
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-apple-md border border-black/5 bg-apple-bg-secondary/60">
|
||||
<ImageIcon className="h-4 w-4 text-apple-label-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
{variant.thumbnailUrl ? (
|
||||
<img
|
||||
src={variant.thumbnailUrl}
|
||||
alt={variant.name}
|
||||
className="h-9 w-9 rounded-apple-md border border-black/5 object-cover transition hover:opacity-80"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-apple-md border border-dashed border-black/15 bg-apple-bg-secondary/60 transition hover:border-apple-blue/40 hover:bg-apple-blue/5">
|
||||
<ImageIcon className="h-4 w-4 text-apple-label-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-apple-semibold">{variant.name}</p>
|
||||
<div className="min-w-0 flex items-center">
|
||||
<span className="truncate text-sm font-apple-semibold">{variant.name}</span>
|
||||
</div>
|
||||
<div className="min-w-0 text-sm text-apple-label-secondary">
|
||||
<span className="block truncate font-mono text-xs">{variant.code}</span>
|
||||
<div className="min-w-0 flex items-center text-apple-label-secondary">
|
||||
<span className="truncate font-mono text-xs">{variant.code}</span>
|
||||
</div>
|
||||
<div className="min-w-0 text-sm text-apple-label-secondary">
|
||||
<span className="block truncate">{template?.label ?? '未设置'}</span>
|
||||
<div className="min-w-0 flex items-center text-apple-label-secondary">
|
||||
<span className="truncate text-sm">{template?.label ?? '未设置'}</span>
|
||||
</div>
|
||||
<div className="min-w-0 text-sm text-apple-label-secondary">
|
||||
<span className="block truncate font-mono text-xs">{specLabel}</span>
|
||||
<div className="min-w-0 flex items-center text-apple-label-secondary">
|
||||
<span className="truncate font-mono text-xs">{specLabel}</span>
|
||||
</div>
|
||||
<div className="min-w-0 text-sm">
|
||||
<div className="min-w-0 flex items-center text-sm">
|
||||
{variant.costPrice != null ? (
|
||||
<span className="font-apple-medium">¥ {variant.costPrice}</span>
|
||||
) : rowComputedCost != null ? (
|
||||
@ -1106,7 +1146,7 @@ export default function PackagingMaterialsClient({
|
||||
<span className="text-xs text-apple-label-tertiary">缺成本</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<Badge type={getStatusTone(variant.status)} variant="subtle" size="small">
|
||||
{getStatusLabel(variant.status)}
|
||||
</Badge>
|
||||
@ -1131,18 +1171,6 @@ export default function PackagingMaterialsClient({
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={variant.status === 'AVAILABLE' ? '停用' : '启用'}
|
||||
className="rounded-apple-md p-1.5 text-apple-label-secondary transition hover:bg-apple-bg-secondary hover:text-apple-label-primary"
|
||||
onClick={() => void handleToggleVariantStatus(variant)}
|
||||
>
|
||||
{variant.status === 'AVAILABLE' ? (
|
||||
<PowerOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Power className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="删除"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user