feat(store): 添加商品图片及轮播图上传功能
- 商品编辑弹窗新增商品主图上传和更换功能,支持上传进度反馈 - 实现轮播图(files字段)上传、删除和显示,支持多格式files解析 - 编辑提交时携带图片和轮播图数据进行保存 - 新增图片压缩工具函数getCompressedImageUrl,用于生成缩略图压缩URL - 统一修正上传接口URL,确保图片上传服务可用
This commit is contained in:
@@ -3,6 +3,7 @@ import { View, Text, Image, ScrollView, Input, Textarea } from '@tarojs/componen
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { pageShopGoods, updateShopGoods } from '@/api/shop/shopGoods'
|
||||
import { listShopGoodsCategory } from '@/api/shop/shopGoodsCategory'
|
||||
import { uploadFile } from '@/api/system/file'
|
||||
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model'
|
||||
import type { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model'
|
||||
|
||||
@@ -58,8 +59,11 @@ export default function StoreGoodsPage() {
|
||||
stock: '',
|
||||
sortNumber: '',
|
||||
comments: '',
|
||||
files: [] as Array<{ uid?: number; url: string; status?: string }>,
|
||||
})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [uploadingImage, setUploadingImage] = useState(false)
|
||||
const [uploadingBanner, setUploadingBanner] = useState(false)
|
||||
|
||||
const pageSize = 10
|
||||
const loadingRef = useRef(false)
|
||||
@@ -177,6 +181,26 @@ export default function StoreGoodsPage() {
|
||||
/** 打开编辑弹窗 */
|
||||
const openEditModal = (goods: ShopGoods) => {
|
||||
setEditGoods(goods)
|
||||
const bannerFiles: Array<{ uid?: number; url: string; status?: string }> = []
|
||||
if (goods.files) {
|
||||
try {
|
||||
const parsed = JSON.parse(goods.files)
|
||||
if (Array.isArray(parsed)) {
|
||||
bannerFiles.push(
|
||||
...parsed
|
||||
.map((item: any) =>
|
||||
typeof item === 'string'
|
||||
? { url: item }
|
||||
: { uid: item.uid, url: item.url || '', status: item.status }
|
||||
)
|
||||
.filter((item: any) => item.url)
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// 非 JSON 数组时按逗号分隔兜底
|
||||
bannerFiles.push(...goods.files.split(',').filter(Boolean).map(url => ({ url })))
|
||||
}
|
||||
}
|
||||
setEditForm({
|
||||
price: goods.price || '',
|
||||
salePrice: goods.salePrice || '',
|
||||
@@ -184,6 +208,7 @@ export default function StoreGoodsPage() {
|
||||
stock: String(goods.stock ?? ''),
|
||||
sortNumber: String(goods.sortNumber ?? ''),
|
||||
comments: goods.comments || '',
|
||||
files: bannerFiles,
|
||||
})
|
||||
setShowEditModal(true)
|
||||
}
|
||||
@@ -194,6 +219,53 @@ export default function StoreGoodsPage() {
|
||||
setEditGoods(null)
|
||||
}
|
||||
|
||||
/** 上传/更换商品图片 */
|
||||
const handleUploadImage = async () => {
|
||||
if (uploadingImage) return
|
||||
setUploadingImage(true)
|
||||
try {
|
||||
const res = await uploadFile()
|
||||
const imageUrl = res.path || ''
|
||||
if (imageUrl) {
|
||||
setEditGoods(prev => prev ? { ...prev, image: imageUrl } : null)
|
||||
Taro.showToast({ title: '上传成功', icon: 'success' })
|
||||
}
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||
} finally {
|
||||
setUploadingImage(false)
|
||||
}
|
||||
}
|
||||
|
||||
/** 上传轮播图 */
|
||||
const handleAddBanner = async () => {
|
||||
if (uploadingBanner) return
|
||||
setUploadingBanner(true)
|
||||
try {
|
||||
const res = await uploadFile()
|
||||
const imageUrl = res.path || ''
|
||||
if (imageUrl) {
|
||||
setEditForm(prev => ({
|
||||
...prev,
|
||||
files: [...prev.files, { uid: res.id, url: imageUrl, status: 'done' }],
|
||||
}))
|
||||
Taro.showToast({ title: '上传成功', icon: 'success' })
|
||||
}
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||
} finally {
|
||||
setUploadingBanner(false)
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除轮播图 */
|
||||
const handleRemoveBanner = (index: number) => {
|
||||
setEditForm(prev => ({
|
||||
...prev,
|
||||
files: prev.files.filter((_, i) => i !== index),
|
||||
}))
|
||||
}
|
||||
|
||||
/** 提交编辑 */
|
||||
const submitEdit = async () => {
|
||||
if (!editGoods) return
|
||||
@@ -214,12 +286,14 @@ export default function StoreGoodsPage() {
|
||||
try {
|
||||
await updateShopGoods({
|
||||
...editGoods,
|
||||
image: editGoods.image,
|
||||
price: editForm.price,
|
||||
salePrice: editForm.salePrice || undefined,
|
||||
dealerPrice: editForm.dealerPrice || undefined,
|
||||
stock,
|
||||
sortNumber: parseInt(editForm.sortNumber, 10) || 0,
|
||||
comments: editForm.comments || undefined,
|
||||
files: JSON.stringify(editForm.files),
|
||||
})
|
||||
Taro.showToast({ title: '保存成功', icon: 'success' })
|
||||
closeEditModal()
|
||||
@@ -465,9 +539,20 @@ export default function StoreGoodsPage() {
|
||||
|
||||
{/* 商品信息 */}
|
||||
<View className='bg-gray-50 rounded-xl p-4 mb-5 flex items-center gap-3'>
|
||||
{editGoods.image && (
|
||||
<Image className='w-14 h-14 rounded-lg bg-gray-100' src={editGoods.image} mode='aspectFill' />
|
||||
)}
|
||||
<View className='relative' onClick={handleUploadImage}>
|
||||
{editGoods.image ? (
|
||||
<Image className='w-16 h-16 rounded-lg' src={editGoods.image} mode='aspectFill' />
|
||||
) : (
|
||||
<View className='w-16 h-16 rounded-lg bg-gray-200 flex items-center justify-center'>
|
||||
<Text className='text-2xl text-gray-300'>📷</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className='absolute inset-0 rounded-lg bg-black/30 flex items-center justify-center'>
|
||||
<Text className='text-white text-xs'>
|
||||
{uploadingImage ? '上传中...' : '更换'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='flex-1 min-w-0'>
|
||||
<Text className='text-sm text-gray-800 block' style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{editGoods.name}
|
||||
@@ -476,6 +561,31 @@ export default function StoreGoodsPage() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 轮播图 */}
|
||||
<View className='mb-5'>
|
||||
<Text className='text-sm text-gray-600 mb-2 block'>轮播图</Text>
|
||||
<View className='flex flex-wrap gap-3'>
|
||||
{editForm.files.map((file, index) => (
|
||||
<View key={file.url + index} className='relative w-20 h-20'>
|
||||
<Image className='w-20 h-20 rounded-lg bg-gray-100' src={file.url} mode='aspectFill' />
|
||||
<View
|
||||
className='absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center'
|
||||
onClick={() => handleRemoveBanner(index)}
|
||||
>
|
||||
<Text className='text-white text-xs'>×</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
<View
|
||||
className='w-20 h-20 rounded-lg border border-dashed border-gray-300 flex flex-col items-center justify-center'
|
||||
onClick={handleAddBanner}
|
||||
>
|
||||
<Text className='text-2xl text-gray-300 mb-0.5'>{uploadingBanner ? '...' : '+'}</Text>
|
||||
<Text className='text-xs text-gray-400'>{uploadingBanner ? '上传中' : '上传'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 表单字段 */}
|
||||
<View className='mb-4'>
|
||||
<Text className='text-sm text-gray-600 mb-2 block'>到手价 (¥)</Text>
|
||||
|
||||
Reference in New Issue
Block a user