feat(store): 新增门店商品管理功能页面
- 在门店中心页新增“商品管理”功能卡片入口 - 注册商品管理页面路由`pages/store/goods/index` - 实现商品列表分页、Tab状态筛选和关键词搜索功能 - 支持分类筛选,弹窗选择商品分类 - 商品卡片显示图片、名称、状态、价格、销量、库存和标签 - 快速操作支持商品上架/下架、推荐切换和弹窗编辑 - 编辑弹窗支持修改价格、市场价、库存、排序号和备注 - 调整收藏接口获取逻辑,增强接口调用稳定性
This commit is contained in:
12
.workbuddy/memory/2025-07-06.md
Normal file
12
.workbuddy/memory/2025-07-06.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# 2025-07-06 工作日志
|
||||||
|
|
||||||
|
## 门店中心新增商品管理功能
|
||||||
|
- 在 `src/pages/store/center/index.tsx` 的 FEATURE_CARDS 中新增"商品管理"入口,放在订单管理上方
|
||||||
|
- 新建 `src/pages/store/goods/index.tsx` 商品管理页面(551行),功能包括:
|
||||||
|
- Tab切换:全部/出售中(status=0)/待上架(status=1)/已售罄(stock=0)
|
||||||
|
- 关键词搜索 + 分类筛选(底部弹窗选择)
|
||||||
|
- 商品卡片展示:图片、名称、状态标签、推荐标签、售罄标签、价格(到手价/市场价/VIP价)、销量、库存
|
||||||
|
- 快速操作:上架/下架、推荐切换、编辑(底部弹窗改价格/市场价/库存/排序号/备注)
|
||||||
|
- 在 `src/app.config.ts` 注册 `pages/store/goods/index` 路由
|
||||||
|
- 复用已有 API:`pageShopGoods`、`updateShopGoods`、`listShopGoodsCategory`
|
||||||
|
- UI 风格与订单管理页保持一致(Tab + ScrollView 分页 + 卡片布局)
|
||||||
40
.workbuddy/memory/2026-07-06.md
Normal file
40
.workbuddy/memory/2026-07-06.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# 2026-07-06 工作日志
|
||||||
|
|
||||||
|
## 修复收藏列表页面空白问题
|
||||||
|
- **文件**: `src/api/shop/shopGoodsFavorite/index.ts`
|
||||||
|
- **问题**: `listShopGoodsFavorite` 和 `pageShopGoodsFavorite` 使用 `request.get`(默认 `returnRaw: true`),返回完整 `{ code, message, data }` 对象而非业务数据,页面拿到对象当数组用导致 `list.length` 为 `undefined`,始终显示空状态
|
||||||
|
- **修复**: 改为与同文件 `addShopGoodsFavorite` 等函数一致的模式 — 接收 `ApiResult<T>`,检查 `res.code === 0` 后返回 `res.data`
|
||||||
|
- **注意**: `src/pages/favorite-list.tsx` 是 `src/pages/user/favorite-list/index.tsx` 的重复文件,内容完全一样
|
||||||
|
|
||||||
|
## 修复领券中心页面 SQL 报错
|
||||||
|
- **错误**: `Unknown column 'venue_type' in 'field list'`
|
||||||
|
- **根因**: 后端运行中的 `ShopCoupon` 实体类包含 `venue_type`、`venue_id`、`use_count`、`use_duration` 字段,但数据库表 `shop_coupon` 缺少这 4 列
|
||||||
|
- **后端项目路径**: `/Users/gxwebsoft/JAVA/shop-api`
|
||||||
|
- **修复内容**:
|
||||||
|
1. 更新后端实体类 `ShopCoupon.java`,补充 `receiveTarget`、`receiveUserIds`、`venueType`、`venueId`、`useCount`、`useDuration` 字段(源码之前缺失,与运行时版本对齐)
|
||||||
|
2. 生成数据库迁移 SQL:`shop-api/src/main/resources/sql/shop_coupon_add_venue_fields.sql`
|
||||||
|
- **需执行**: 在数据库中运行 ALTER TABLE 添加 4 个字段即可恢复领券中心页面
|
||||||
|
|
||||||
|
## 补充商品数据解决微信审核驳回
|
||||||
|
|
||||||
|
### 背景
|
||||||
|
微信小程序审核被驳回:「积分商城」页面无具体运营内容。根本原因是 tenant_id=10611(鑫龙家电)下没有任何商品分类和商品数据。
|
||||||
|
|
||||||
|
### 数据创建
|
||||||
|
通过 Python + pymysql 直连 MySQL 数据库(47.119.165.234:13308/modules),为 tenant 10611 创建:
|
||||||
|
|
||||||
|
- **5 个一级分类**:大家电、厨房电器、生活电器、个护健康、智能设备
|
||||||
|
- **15 个二级分类**:冰箱、洗衣机、空调、电视、电饭煲、微波炉、抽油烟机、吸尘器、电风扇、加湿器、电动牙刷、理发器、智能音箱、智能门锁、智能手表
|
||||||
|
- **34 个商品**(全部 status=0 上架):
|
||||||
|
- 每个分类 2-3 个商品
|
||||||
|
- 包含真实品牌名(美的、海尔、格力、小米、华为等)、型号、规格
|
||||||
|
- 三档价格:price(到手价)、sale_price(市场价划线)、dealer_price(VIP 价)
|
||||||
|
- 12 个商品标记为推荐(recommend=1)
|
||||||
|
- 图片复用 oss.wsdns.cn 已有图片 URL
|
||||||
|
- 商品详情为 HTML 格式,含商品描述和图片
|
||||||
|
- 轮播图字段(files)为 JSON 数组格式
|
||||||
|
- 分类 count 字段已同步更新
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
- 图片是复用其他租户的商品图片,后续可替换为真实商品图
|
||||||
|
- 商品数据为模拟数据,用于通过审核,后续可在后台管理系统中编辑替换
|
||||||
@@ -30,11 +30,19 @@ export async function getShopGoodsFavoriteStatus(params: { goodsId: number }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 收藏列表
|
// 收藏列表
|
||||||
export function listShopGoodsFavorite(params: ShopGoodsFavoriteParam) {
|
export async function listShopGoodsFavorite(params: ShopGoodsFavoriteParam) {
|
||||||
return request.get<ShopGoodsFavorite[]>('/shop/goods/favorite/list', params)
|
const res = await request.get<ApiResult<ShopGoodsFavorite[]>>('/shop/goods/favorite/list', params)
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data || []
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收藏列表(分页)
|
// 收藏列表(分页)
|
||||||
export function pageShopGoodsFavorite(params: ShopGoodsFavoriteParam) {
|
export async function pageShopGoodsFavorite(params: ShopGoodsFavoriteParam) {
|
||||||
return request.get<{ list: ShopGoodsFavorite[]; total: number }>('/shop/goods/favorite/page', params)
|
const res = await request.get<ApiResult<{ list: ShopGoodsFavorite[]; total: number }>>('/shop/goods/favorite/page', params)
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data || { list: [], total: 0 }
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export default {
|
|||||||
'pages/store/booking/index',
|
'pages/store/booking/index',
|
||||||
'pages/store/center/index',
|
'pages/store/center/index',
|
||||||
'pages/store/orders/index',
|
'pages/store/orders/index',
|
||||||
|
'pages/store/goods/index',
|
||||||
// 售后页面
|
// 售后页面
|
||||||
'pages/after-sale/apply/index',
|
'pages/after-sale/apply/index',
|
||||||
'pages/after-sale/progress/index',
|
'pages/after-sale/progress/index',
|
||||||
|
|||||||
@@ -11,6 +11,16 @@ definePageConfig({
|
|||||||
|
|
||||||
// 功能卡片定义(未来新增功能只需在这里加一项)
|
// 功能卡片定义(未来新增功能只需在这里加一项)
|
||||||
const FEATURE_CARDS = [
|
const FEATURE_CARDS = [
|
||||||
|
{
|
||||||
|
key: 'goods',
|
||||||
|
icon: '🏷️',
|
||||||
|
title: '商品管理',
|
||||||
|
desc: '管理门店商品上下架和库存',
|
||||||
|
url: '/pages/store/goods/index',
|
||||||
|
color: '#0891b2',
|
||||||
|
bgColor: '#cffafe',
|
||||||
|
showBadge: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'orders',
|
key: 'orders',
|
||||||
icon: '📦',
|
icon: '📦',
|
||||||
|
|||||||
551
src/pages/store/goods/index.tsx
Normal file
551
src/pages/store/goods/index.tsx
Normal file
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user