import React, { useState, useEffect, useCallback, useRef } from 'react' import { View, Text, Image, ScrollView, Input, Textarea } from '@tarojs/components' 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' import { getCompressedImageUrl } from '@/utils/image' definePageConfig({ navigationBarTitleText: '商品管理', }) // ─── Tab 配置 ────────────────────────────────────────────────── type TabKey = 'all' | 'on_sale' | 'pending' | 'sold_out' const TABS: { key: TabKey; label: string; param: Partial }[] = [ { key: 'all', label: '全部', param: {} }, { key: 'on_sale', label: '已上架', param: { status: 0 } }, { key: 'pending', label: '待上架', param: { status: 1 } }, { key: 'sold_out', label: '已售罄', param: { stock: 0 } }, ] // ─── 状态辅助函数 ────────────────────────────────────────────────── function getStatusText(status?: number): string { const map: Record = { 0: '已上架', 1: '待上架', 2: '待审核', 3: '审核不通过' } return map[status ?? 0] || '未知' } function getStatusColor(status?: number): string { const map: Record = { 0: '#16a34a', 1: '#ea580c', 2: '#9333ea', 3: '#dc2626' } return map[status ?? 0] || '#999' } // ─── 页面组件 ────────────────────────────────────────────────────── export default function StoreGoodsPage() { const [activeTab, setActiveTab] = useState('all') // 商品列表与分页 const [goodsList, setGoodsList] = useState([]) const [page, setPage] = useState(1) const [hasMore, setHasMore] = useState(true) const [loading, setLoading] = useState(false) // 搜索与筛选 const [keyword, setKeyword] = useState('') const [searchText, setSearchText] = useState('') const [categories, setCategories] = useState([]) const [selectedCategory, setSelectedCategory] = useState(undefined) const [showCategoryPicker, setShowCategoryPicker] = useState(false) // 编辑弹窗 const [showEditModal, setShowEditModal] = useState(false) const [editGoods, setEditGoods] = useState(null) const [editForm, setEditForm] = useState({ price: '', salePrice: '', dealerPrice: '', 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) /** 加载商品列表 */ const loadGoods = useCallback(async (tab: TabKey, pageNo: number = 1, append = false, categoryId?: number) => { if (loadingRef.current) return loadingRef.current = true setLoading(true) try { const tabConfig = TABS.find(t => t.key === tab) if (!tabConfig) return const params: ShopGoodsParam = { ...tabConfig.param, page: pageNo, limit: pageSize, } if (keyword) params.keywords = keyword const effectiveCategoryId = categoryId !== undefined ? categoryId : selectedCategory if (effectiveCategoryId) params.categoryId = effectiveCategoryId const res = await pageShopGoods(params) const list = res?.list || [] // 已售罄 Tab 需要客户端二次过滤 stock <= 0 const filtered = tab === 'sold_out' ? list.filter(g => (g.stock ?? 0) <= 0) : list setGoodsList(prev => append ? [...prev, ...filtered] : filtered) setPage(pageNo) setHasMore(list.length >= pageSize) } catch (e: any) { Taro.showToast({ title: e.message || '加载失败', icon: 'none' }) if (!append) setGoodsList([]) } finally { loadingRef.current = false setLoading(false) } }, [keyword, selectedCategory]) // 加载分类列表 useEffect(() => { listShopGoodsCategory({}) .then(data => setCategories(data || [])) .catch(() => {}) }, []) // 切换 tab 时重新加载 useEffect(() => { loadGoods(activeTab, 1) }, [activeTab, loadGoods]) // 页面重新显示时刷新 useDidShow(() => { loadGoods(activeTab, 1) }) /** 加载更多 */ const handleLoadMore = () => { if (hasMore && !loadingRef.current) { loadGoods(activeTab, page + 1, true) } } /** 执行搜索 */ const handleSearch = () => { setKeyword(searchText) loadGoods(activeTab, 1) } /** 选择分类 */ const handleSelectCategory = (cat: ShopGoodsCategory | undefined) => { setSelectedCategory(cat?.categoryId) setShowCategoryPicker(false) loadGoods(activeTab, 1, false, cat?.categoryId) } /** 快速上架/下架 */ const handleToggleStatus = async (goods: ShopGoods) => { const newStatus = goods.status === 0 ? 1 : 0 const actionText = newStatus === 0 ? '上架' : '下架' Taro.showModal({ title: '提示', content: `确定要${actionText}商品「${goods.name}」吗?`, success: async (res) => { if (!res.confirm) return try { Taro.showLoading({ title: `${actionText}中...` }) await updateShopGoods({ ...goods, status: newStatus }) Taro.hideLoading() Taro.showToast({ title: `${actionText}成功`, icon: 'success' }) loadGoods(activeTab, 1) } catch (e: any) { Taro.hideLoading() Taro.showToast({ title: e.message || `${actionText}失败`, icon: 'none' }) } }, }) } /** 切换推荐 */ const handleToggleRecommend = async (goods: ShopGoods) => { const newRecommend = goods.recommend === 1 ? 0 : 1 try { await updateShopGoods({ ...goods, recommend: newRecommend }) Taro.showToast({ title: newRecommend === 1 ? '已推荐' : '已取消推荐', icon: 'none' }) loadGoods(activeTab, 1) } catch (e: any) { Taro.showToast({ title: e.message || '操作失败', icon: 'none' }) } } /** 打开编辑弹窗 */ 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 || '', dealerPrice: goods.dealerPrice || '', stock: String(goods.stock ?? ''), sortNumber: String(goods.sortNumber ?? ''), comments: goods.comments || '', files: bannerFiles, }) setShowEditModal(true) } /** 关闭编辑弹窗 */ const closeEditModal = () => { setShowEditModal(false) 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 const price = parseFloat(editForm.price) if (isNaN(price) || price < 0) { Taro.showToast({ title: '请输入有效的价格', icon: 'none' }) return } const stock = parseInt(editForm.stock, 10) if (isNaN(stock) || stock < 0) { Taro.showToast({ title: '请输入有效的库存', icon: 'none' }) return } setSubmitting(true) 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() loadGoods(activeTab, 1) } catch (e: any) { Taro.showToast({ title: e.message || '保存失败', icon: 'none' }) } finally { setSubmitting(false) } } /** 渲染单个商品卡片 */ const renderGoodsCard = (goods: ShopGoods) => { const isSoldOut = (goods.stock ?? 0) <= 0 const isOnSale = goods.status === 0 return ( {/* 商品头部:图片 + 名称 + 状态 */} {goods.image ? ( Taro.previewImage({ urls: [goods.image!] })} /> ) : ( 📦 )} {getStatusText(goods.status)} {isSoldOut && isOnSale && ( 售罄 )} {goods.recommend === 1 && ( 推荐 )} {goods.name} {goods.categoryName && ( {goods.categoryName} )} {/* 价格信息 */} 到手价 ¥{goods.price || '0'} {goods.salePrice && ( 市场价 ¥{goods.salePrice} )} {goods.dealerPrice && ( VIP价 ¥{goods.dealerPrice} )} 销量 {goods.sales || 0} 库存 {goods.stock ?? 0} {/* 操作按钮 */} {/* 上架/下架 */} handleToggleStatus(goods)} > {isOnSale ? '下架' : '上架'} {/* 推荐 */} handleToggleRecommend(goods)} > {goods.recommend === 1 ? '取消推荐' : '推荐'} {/* 编辑 */} openEditModal(goods)} > 编辑 ) } return ( {/* 搜索栏 */} setSearchText(e.detail.value)} onConfirm={handleSearch} confirmType='search' /> {searchText ? ( { setSearchText(''); setKeyword(''); loadGoods(activeTab, 1) }} >× ) : null} setShowCategoryPicker(true)} > {selectedCategory ? categories.find(c => c.categoryId === selectedCategory)?.title || '分类' : '分类'} {/* Tab 栏 */} {TABS.map(tab => ( setActiveTab(tab.key)} > {tab.label} ))} {/* 商品列表 */} {loading && goodsList.length === 0 ? ( 加载中... ) : goodsList.length === 0 ? ( 暂无商品 ) : ( goodsList.map(renderGoodsCard) )} {loading && goodsList.length > 0 && ( 加载中... )} {!hasMore && goodsList.length > 0 && ( 没有更多了 )} {/* 分类选择弹窗 */} {showCategoryPicker && ( setShowCategoryPicker(false)}> e.stopPropagation()} > 选择分类 setShowCategoryPicker(false)}>× handleSelectCategory(undefined)} > 全部分类 {categories.map(cat => ( handleSelectCategory(cat)} > {cat.title} {selectedCategory === cat.categoryId && ( )} ))} )} {/* 编辑弹窗 */} {showEditModal && editGoods && ( 编辑商品 {/* 商品信息 */} {editGoods.image ? ( ) : ( 📷 )} {uploadingImage ? '上传中...' : '更换'} {editGoods.name} ID: {editGoods.goodsId} {/* 轮播图 */} 轮播图 {editForm.files.map((file, index) => ( handleRemoveBanner(index)} > × ))} {uploadingBanner ? '...' : '+'} {uploadingBanner ? '上传中' : '上传'} {/* 表单字段 */} 到手价 (¥) setEditForm(prev => ({ ...prev, price: e.detail.value }))} /> 市场价 (¥) setEditForm(prev => ({ ...prev, salePrice: e.detail.value }))} /> 会员价 (¥) setEditForm(prev => ({ ...prev, dealerPrice: e.detail.value }))} /> 库存 setEditForm(prev => ({ ...prev, stock: e.detail.value }))} /> 排序号 setEditForm(prev => ({ ...prev, sortNumber: e.detail.value }))} /> 备注