@@ -0,0 +1,551 @@
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 type { ShopGoods , ShopGoodsParam } from '@/api/shop/shopGoods/model'
import type { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model'
definePageConfig ( {
navigationBarTitleText : '商品管理' ,
} )
// ─── Tab 配置 ──────────────────────────────────────────────────
type TabKey = 'all' | 'on_sale' | 'pending' | 'sold_out'
const TABS : { key : TabKey ; label : string ; param : Partial < ShopGoodsParam > } [ ] = [
{ 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 < number , string > = { 0 : '已上架' , 1 : '待上架' , 2 : '待审核' , 3 : '审核不通过' }
return map [ status ? ? 0 ] || '未知'
}
function getStatusColor ( status? : number ) : string {
const map : Record < number , string > = { 0 : '#16a34a' , 1 : '#ea580c' , 2 : '#9333ea' , 3 : '#dc2626' }
return map [ status ? ? 0 ] || '#999'
}
// ─── 页面组件 ──────────────────────────────────────────────────────
export default function StoreGoodsPage() {
const [ activeTab , setActiveTab ] = useState < TabKey > ( 'all' )
// 商品列表与分页
const [ goodsList , setGoodsList ] = useState < ShopGoods [ ] > ( [ ] )
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 < ShopGoodsCategory [ ] > ( [ ] )
const [ selectedCategory , setSelectedCategory ] = useState < number | undefined > ( undefined )
const [ showCategoryPicker , setShowCategoryPicker ] = useState ( false )
// 编辑弹窗
const [ showEditModal , setShowEditModal ] = useState ( false )
const [ editGoods , setEditGoods ] = useState < ShopGoods | null > ( null )
const [ editForm , setEditForm ] = useState ( {
price : '' ,
salePrice : '' ,
stock : '' ,
sortNumber : '' ,
comments : '' ,
} )
const [ submitting , setSubmitting ] = useState ( false )
const pageSize = 10
const loadingRef = useRef ( false )
/** 加载商品列表 */
const loadGoods = useCallback ( async ( tab : TabKey , pageNo : number = 1 , append = false ) = > {
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
if ( selectedCategory ) params . categoryId = selectedCategory
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 )
}
/** 快速上架/下架 */
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 )
setEditForm ( {
price : goods.price || '' ,
salePrice : goods.salePrice || '' ,
stock : String ( goods . stock ? ? '' ) ,
sortNumber : String ( goods . sortNumber ? ? '' ) ,
comments : goods.comments || '' ,
} )
setShowEditModal ( true )
}
/** 关闭编辑弹窗 */
const closeEditModal = ( ) = > {
setShowEditModal ( false )
setEditGoods ( null )
}
/** 提交编辑 */
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 ,
price : editForm.price ,
salePrice : editForm.salePrice || undefined ,
stock ,
sortNumber : parseInt ( editForm . sortNumber , 10 ) || 0 ,
comments : editForm.comments || undefined ,
} )
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 (
< View key = { goods . goodsId } className = 'bg-white rounded-xl mx-3 mt-3 p-4' >
{ /* 商品头部:图片 + 名称 + 状态 */ }
< View className = 'flex items-start gap-3 mb-3' >
{ goods . image ? (
< Image
className = 'w-20 h-20 rounded-lg bg-gray-50 flex-shrink-0'
src = { goods . image }
mode = 'aspectFill'
onClick = { ( ) = > Taro . previewImage ( { urls : [ goods . image ! ] } ) }
/ >
) : (
< View className = 'w-20 h-20 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0' >
< Text className = 'text-2xl text-gray-300' > 📦 < / Text >
< / View >
) }
< View className = 'flex-1 min-w-0' >
< View className = 'flex items-center gap-2 mb-1' >
< Text
className = 'text-xs px-1.5 py-0.5 rounded'
style = { { color : getStatusColor ( goods . status ) , background : getStatusColor ( goods . status ) + '15' } }
>
{ getStatusText ( goods . status ) }
< / Text >
{ isSoldOut && isOnSale && (
< Text className = 'text-xs px-1.5 py-0.5 rounded bg-red-50 text-red-500' > 售 罄 < / Text >
) }
{ goods . recommend === 1 && (
< Text className = 'text-xs px-1.5 py-0.5 rounded bg-amber-50 text-amber-600' > 推 荐 < / Text >
) }
< / View >
< Text className = 'text-sm text-gray-800 block' style = { { overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } >
{ goods . name }
< / Text >
{ goods . categoryName && (
< Text className = 'text-xs text-gray-400 mt-1 block' > { goods . categoryName } < / Text >
) }
< / View >
< / View >
{ /* 价格信息 */ }
< View className = 'flex items-center gap-4 mb-3 bg-gray-50 rounded-lg p-3' >
< View className = 'flex-1' >
< Text className = 'text-xs text-gray-400 block' > 到 手 价 < / Text >
< Text className = 'text-base text-red-500 font-medium' > ¥ { goods . price || '0' } < / Text >
< / View >
{ goods . salePrice && (
< View className = 'flex-1' >
< Text className = 'text-xs text-gray-400 block' > 市 场 价 < / Text >
< Text className = 'text-sm text-gray-400 line-through' > ¥ { goods . salePrice } < / Text >
< / View >
) }
{ goods . dealerPrice && (
< View className = 'flex-1' >
< Text className = 'text-xs text-gray-400 block' > VIP价 < / Text >
< Text className = 'text-sm text-purple-500' > ¥ { goods . dealerPrice } < / Text >
< / View >
) }
< View className = 'flex-1' >
< Text className = 'text-xs text-gray-400 block' > 销 量 < / Text >
< Text className = 'text-sm text-gray-700' > { goods . sales || 0 } < / Text >
< / View >
< View className = 'flex-1' >
< Text className = 'text-xs text-gray-400 block' > 库 存 < / Text >
< Text className = { ` text-sm ${ isSoldOut ? 'text-red-500' : 'text-gray-700' } ` } > { goods . stock ? ? 0 } < / Text >
< / View >
< / View >
{ /* 操作按钮 */ }
< View className = 'flex items-center gap-2 border-t border-gray-50 pt-3' >
{ /* 上架/下架 */ }
< View
className = { ` flex-1 py-2 rounded-lg text-center ${ isOnSale ? 'bg-orange-50' : 'bg-green-50' } ` }
onClick = { ( ) = > handleToggleStatus ( goods ) }
>
< Text className = { ` text-sm ${ isOnSale ? 'text-orange-500' : 'text-green-500' } ` } >
{ isOnSale ? '下架' : '上架' }
< / Text >
< / View >
{ /* 推荐 */ }
< View
className = { ` flex-1 py-2 rounded-lg text-center ${ goods . recommend === 1 ? 'bg-amber-50' : 'bg-gray-50' } ` }
onClick = { ( ) = > handleToggleRecommend ( goods ) }
>
< Text className = { ` text-sm ${ goods . recommend === 1 ? 'text-amber-600' : 'text-gray-500' } ` } >
{ goods . recommend === 1 ? '取消推荐' : '推荐' }
< / Text >
< / View >
{ /* 编辑 */ }
< View
className = 'flex-1 py-2 rounded-lg text-center bg-blue-50'
onClick = { ( ) = > openEditModal ( goods ) }
>
< Text className = 'text-sm text-blue-500' > 编 辑 < / Text >
< / View >
< / View >
< / View >
)
}
return (
< View className = 'min-h-full bg-gray-50' >
{ /* 搜索栏 */ }
< View className = 'bg-white px-3 py-2 flex items-center gap-2' >
< View className = 'flex-1 flex items-center bg-gray-100 rounded-lg px-3 py-1.5' >
< Input
className = 'flex-1 text-sm'
placeholder = '搜索商品名称'
value = { searchText }
onInput = { ( e ) = > setSearchText ( e . detail . value ) }
onConfirm = { handleSearch }
confirmType = 'search'
/ >
{ searchText ? (
< Text
className = 'text-gray-400 text-lg px-1'
onClick = { ( ) = > { setSearchText ( '' ) ; setKeyword ( '' ) ; loadGoods ( activeTab , 1 ) } }
> × < / Text >
) : null }
< / View >
< View
className = 'px-3 py-1.5 rounded-lg bg-gray-100 flex items-center'
onClick = { ( ) = > setShowCategoryPicker ( true ) }
>
< Text className = 'text-sm text-gray-600' >
{ selectedCategory
? categories . find ( c = > c . categoryId === selectedCategory ) ? . title || '分类'
: '分类' }
< / Text >
< Text className = 'text-gray-400 text-xs ml-1' > ▾ < / Text >
< / View >
< / View >
{ /* Tab 栏 */ }
< View className = 'bg-white flex border-b border-gray-50' >
{ TABS . map ( tab = > (
< View
key = { tab . key }
className = { ` flex-1 text-center py-3 border-b-2 ${
activeTab === tab . key ? 'border-cyan-500' : 'border-transparent'
} ` }
onClick = { ( ) = > setActiveTab ( tab . key ) }
>
< Text
className = { ` text-sm ${ activeTab === tab . key ? 'text-cyan-500 font-medium' : 'text-gray-500' } ` }
>
{ tab . label }
< / Text >
< / View >
) ) }
< / View >
{ /* 商品列表 */ }
< ScrollView
scrollY
style = { { height : 'calc(100vh - 100px)' } }
onScrollToLower = { handleLoadMore }
lowerThreshold = { 100 }
>
{ loading && goodsList . length === 0 ? (
< View className = 'flex justify-center items-center py-20' >
< Text className = 'text-gray-400' > 加 载 中 . . . < / Text >
< / View >
) : goodsList . length === 0 ? (
< View className = 'flex justify-center items-center py-20' >
< Text className = 'text-gray-400' > 暂 无 商 品 < / Text >
< / View >
) : (
goodsList . map ( renderGoodsCard )
) }
{ loading && goodsList . length > 0 && (
< View className = 'flex justify-center items-center py-4' >
< Text className = 'text-gray-400 text-sm' > 加 载 中 . . . < / Text >
< / View >
) }
{ ! hasMore && goodsList . length > 0 && (
< View className = 'flex justify-center items-center py-4' >
< Text className = 'text-gray-300 text-xs' > 没 有 更 多 了 < / Text >
< / View >
) }
< View className = 'h-6' / >
< / ScrollView >
{ /* 分类选择弹窗 */ }
{ showCategoryPicker && (
< View className = 'fixed inset-0 z-50' onClick = { ( ) = > setShowCategoryPicker ( false ) } >
< View className = 'absolute inset-0 bg-black/50' / >
< View
className = 'absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl max-h-[60vh] overflow-y-auto'
onClick = { ( e ) = > e . stopPropagation ( ) }
>
< View className = 'sticky top-0 bg-white border-b border-gray-50 px-5 py-4 flex justify-between items-center' >
< Text className = 'text-base font-medium text-gray-800' > 选 择 分 类 < / Text >
< Text className = 'text-gray-400 text-lg' onClick = { ( ) = > setShowCategoryPicker ( false ) } > × < / Text >
< / View >
< View
className = 'px-5 py-3.5 border-b border-gray-50'
onClick = { ( ) = > handleSelectCategory ( undefined ) }
>
< Text className = 'text-sm text-gray-600' > 全 部 分 类 < / Text >
< / View >
{ categories . map ( cat = > (
< View
key = { cat . categoryId }
className = 'px-5 py-3.5 border-b border-gray-50 flex justify-between items-center'
onClick = { ( ) = > handleSelectCategory ( cat ) }
>
< Text className = 'text-sm text-gray-700' > { cat . title } < / Text >
{ selectedCategory === cat . categoryId && (
< Text className = 'text-cyan-500' > ✓ < / Text >
) }
< / View >
) ) }
< View className = 'h-8' / >
< / View >
< / View >
) }
{ /* 编辑弹窗 */ }
{ showEditModal && editGoods && (
< View className = 'fixed inset-0 z-50 flex items-end justify-center' >
< View className = 'absolute inset-0 bg-black/50' onClick = { closeEditModal } / >
< View className = 'relative bg-white rounded-t-2xl w-full px-5 pt-6 pb-10 max-h-[80vh] overflow-y-auto' >
< Text className = 'text-lg font-medium text-gray-800 text-center mb-5 block' > 编 辑 商 品 < / Text >
{ /* 商品信息 */ }
< 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 = 'flex-1 min-w-0' >
< Text className = 'text-sm text-gray-800 block' style = { { overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } >
{ editGoods . name }
< / Text >
< Text className = 'text-xs text-gray-400 mt-1 block' > ID : { editGoods . goodsId } < / Text >
< / View >
< / View >
{ /* 表单字段 */ }
< View className = 'mb-4' >
< Text className = 'text-sm text-gray-600 mb-2 block' > 到 手 价 ( ¥ ) < / Text >
< Input
className = 'bg-gray-50 rounded-lg px-4 py-3 text-sm'
type = 'digit'
placeholder = '请输入价格'
value = { editForm . price }
onInput = { ( e ) = > setEditForm ( prev = > ( { . . . prev , price : e.detail.value } ) ) }
/ >
< / View >
< View className = 'mb-4' >
< Text className = 'text-sm text-gray-600 mb-2 block' > 市 场 价 ( ¥ ) < / Text >
< Input
className = 'bg-gray-50 rounded-lg px-4 py-3 text-sm'
type = 'digit'
placeholder = '选填,划线价'
value = { editForm . salePrice }
onInput = { ( e ) = > setEditForm ( prev = > ( { . . . prev , salePrice : e.detail.value } ) ) }
/ >
< / View >
< View className = 'mb-4' >
< Text className = 'text-sm text-gray-600 mb-2 block' > 库 存 < / Text >
< Input
className = 'bg-gray-50 rounded-lg px-4 py-3 text-sm'
type = 'number'
placeholder = '请输入库存数量'
value = { editForm . stock }
onInput = { ( e ) = > setEditForm ( prev = > ( { . . . prev , stock : e.detail.value } ) ) }
/ >
< / View >
< View className = 'mb-4' >
< Text className = 'text-sm text-gray-600 mb-2 block' > 排 序 号 < / Text >
< Input
className = 'bg-gray-50 rounded-lg px-4 py-3 text-sm'
type = 'number'
placeholder = '数字越小越靠前'
value = { editForm . sortNumber }
onInput = { ( e ) = > setEditForm ( prev = > ( { . . . prev , sortNumber : e.detail.value } ) ) }
/ >
< / View >
< View className = 'mb-6' >
< Text className = 'text-sm text-gray-600 mb-2 block' > 备 注 < / Text >
< Textarea
className = 'bg-gray-50 rounded-lg px-4 py-3 text-sm w-full'
placeholder = '选填'
value = { editForm . comments }
onInput = { ( e ) = > setEditForm ( prev = > ( { . . . prev , comments : e.detail.value } ) ) }
maxlength = { 200 }
style = { { minHeight : '60px' } }
/ >
< / View >
{ /* 操作按钮 */ }
< View className = 'flex gap-3' >
< View className = 'flex-1 py-3 rounded-xl bg-gray-100 text-center' onClick = { closeEditModal } >
< Text className = 'text-sm text-gray-600' > 取 消 < / Text >
< / View >
< View
className = 'flex-1 py-3 rounded-xl bg-cyan-500 text-center'
onClick = { submitting ? undefined : submitEdit }
>
< Text className = 'text-sm text-white' >
{ submitting ? '保存中...' : '保存' }
< / Text >
< / View >
< / View >
< / View >
< / View >
) }
< / View >
)
}