gongxiangp2/components/SkuList.tsx
chenpei 0ce59be34e 备份:组织架构设计方案讨论前的代码快照
- 包含所有当前修改和新功能
- 准备开始组织架构优化实施方案
2025-12-30 13:20:26 +08:00

289 lines
9.8 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import Image from 'next/image'
import { useToast } from '@/hooks/useToast'
import ToastBubble from './ToastBubble'
// SKU 数据类型
interface Sku {
id: string
skuName: string
skuCode?: string
previewUrl: string
zipFileName: string
zipFilePath: string
zipFileSize: string
sortOrder: number
uploaderId: string
uploader?: {
id: string
username: string
realName?: string
}
}
interface SkuListProps {
materialId: string
canEdit: boolean // 是否可以编辑(上传者或管理员)
onEditClick: () => void // 点击编辑按钮的回调
onSelectionChange?: (selectedIds: Set<string>) => void // 选中状态变化回调
}
export default function SkuList({ materialId, canEdit, onEditClick, onSelectionChange }: SkuListProps) {
const [skus, setSkus] = useState<Sku[]>([])
const [loading, setLoading] = useState(true)
const { toast, showToast } = useToast()
const [selectedSkuIds, setSelectedSkuIds] = useState<Set<string>>(new Set())
const [downloadingSkuId, setDownloadingSkuId] = useState<string | null>(null)
// 移除批量下载状态,由父组件处理
const [previewImage, setPreviewImage] = useState<string | null>(null) // 大图预览
// 加载 SKU 列表
useEffect(() => {
fetchSkus()
}, [materialId])
const fetchSkus = async () => {
try {
setLoading(true)
const response = await fetch(`/api/materials/${materialId}/skus`)
if (response.ok) {
const data = await response.json()
setSkus(data.skus || [])
}
} catch (error) {
console.error('加载 SKU 列表失败:', error)
} finally {
setLoading(false)
}
}
// 格式化文件大小
const formatFileSize = (bytes: string): string => {
const size = parseInt(bytes)
if (size === 0 || isNaN(size)) return '未知'
const units = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(size) / Math.log(1024))
return `${(size / Math.pow(1024, i)).toFixed(2)} ${units[i]}`
}
// 全选/取消全选
const handleSelectAll = () => {
const newSelected = selectedSkuIds.size === skus.length
? new Set<string>()
: new Set(skus.map(sku => sku.id))
setSelectedSkuIds(newSelected)
onSelectionChange?.(newSelected)
}
// 单选/取消单选
const handleSelectSku = (skuId: string) => {
const newSelected = new Set(selectedSkuIds)
if (newSelected.has(skuId)) {
newSelected.delete(skuId)
} else {
newSelected.add(skuId)
}
setSelectedSkuIds(newSelected)
onSelectionChange?.(newSelected)
}
// 下载单个 SKU
const handleDownloadSku = async (sku: Sku) => {
try {
setDownloadingSkuId(sku.id)
const response = await fetch(`/api/materials/${materialId}/skus/${sku.id}/download`)
if (!response.ok) {
const error = await response.json()
showToast(error.error || '下载失败', { variant: 'error' })
return
}
// 获取文件并下载
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = sku.zipFileName || `${sku.skuName}.zip`
document.body.appendChild(a)
a.click()
a.remove()
window.URL.revokeObjectURL(url)
} catch (error) {
console.error('下载 SKU 失败:', error)
showToast('下载失败,请稍后重试', { variant: 'error' })
} finally {
setDownloadingSkuId(null)
}
}
// 批量下载功能已移至父组件
if (loading) {
return (
<div className="py-4 text-center text-gray-500 dark:text-gray-400 text-sm">
加载中...
</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>
{canEdit && (
<button
onClick={onEditClick}
className="inline-flex items-center gap-1.5 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
添加 SKU
</button>
)}
</div>
)
}
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>
{canEdit && (
<button
onClick={onEditClick}
className="inline-flex items-center gap-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-xs rounded-lg transition-colors"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
编辑
</button>
)}
</div>
{/* 全选栏 */}
<div className="flex items-center justify-between py-2 px-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg mb-3">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={selectedSkuIds.size === skus.length && skus.length > 0}
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>
</label>
{selectedSkuIds.size > 0 && (
<span className="text-xs text-gray-500 dark:text-gray-400">
已选 {selectedSkuIds.size} 个
</span>
)}
</div>
{/* SKU 列表(可滚动区域) */}
<div
className="overflow-y-auto space-y-2 pr-1"
style={{ maxHeight: 'calc(100vh - 500px)' }}
>
{skus.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400 text-sm">
<svg className="w-12 h-12 mx-auto mb-3 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
<p>暂无 SKU</p>
{canEdit && (
<p className="text-xs mt-1 text-gray-400 dark:text-gray-500">点击上方&ldquo;编辑&rdquo;按钮添加</p>
)}
</div>
) : (
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"
>
{/* 复选框 */}
<input
type="checkbox"
checked={selectedSkuIds.has(sku.id)}
onChange={() => handleSelectSku(sku.id)}
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 flex-shrink-0"
/>
{/* 预览图(点击放大) */}
<div
className="w-10 h-10 flex-shrink-0 rounded overflow-hidden cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => setPreviewImage(sku.previewUrl)}
>
<Image
src={sku.previewUrl}
alt={sku.skuName}
width={40}
height={40}
className="w-full h-full object-cover"
unoptimized
/>
</div>
{/* SKU 信息 */}
<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">
{formatFileSize(sku.zipFileSize)}
</p>
</div>
{/* 下载按钮 */}
<button
onClick={() => handleDownloadSku(sku)}
disabled={downloadingSkuId === sku.id}
className="flex-shrink-0 px-2.5 py-1.5 text-xs bg-blue-600 hover:bg-blue-700 text-white rounded-lg disabled:opacity-50 transition-colors"
>
{downloadingSkuId === sku.id ? '下载中' : '下载'}
</button>
</div>
))
)}
</div>
{/* 批量下载按钮已移至父组件统一处理 */}
{/* 大图预览弹窗 */}
{previewImage && (
<div
className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4"
onClick={() => setPreviewImage(null)}
>
<div className="relative max-w-4xl max-h-[90vh]">
<Image
src={previewImage}
alt="预览图"
width={800}
height={800}
className="max-w-full max-h-[90vh] object-contain"
unoptimized
/>
<button
onClick={() => setPreviewImage(null)}
className="absolute top-2 right-2 w-8 h-8 bg-black/50 hover:bg-black/70 text-white rounded-full flex items-center justify-center"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
)}
</div>
)
}