feat(store-center): 在门店中心添加扫码登录PC后台功能

- 门店中心右上角新增扫码登录按钮,使用base64 SVG图标兼容小程序Image组件渲染
- 扫码后通过解析二维码内容调用confirmWechatQRLogin接口完成PC端登录确认
- 支持扫码取消静默处理,扫码失败则弹窗提示错误信息
- 优化门店中心页面布局,调整扫码按钮绝对定位及样式
- 优化了用户管理页面的小型样式,包括按钮和文本间距调整
- 调整用户页面部分Badge显示逻辑,排除tabIndex为4的项不显示角标
This commit is contained in:
2026-07-17 12:38:27 +08:00
parent 21b8403fab
commit bd3589af61
5 changed files with 1322 additions and 1230 deletions

View File

@@ -94,3 +94,13 @@
### 验证 ### 验证
- `npx taro build --type weapp` 构建成功 - `npx taro build --type weapp` 构建成功
## 门店中心右上角扫码登录 PC 后台src/pages/store/center/index.tsx
- **需求**:门店中心顶部绿色渐变区右上角加一个二维码扫码图标,点击后调起微信扫码,扫码结果解析出 token 后调 `confirmWechatQRLogin` 确认 PC 端登录
- **参考**`/Users/gxwebsoft/VUE/template-10584/src/pages/user/components/UserCard.tsx` 中的 `UnifiedQRButton` 组件
- **实现**
- 复用已有 API`parseQRContent` + `confirmWechatQRLogin``@/api/passport/qr-login`),与 `src/components/QRLoginScanner.tsx` 同款
- 新增 `handleScanLogin` callback`Taro.scanCode``parseQRContent` → 校验 token/userId → `confirmWechatQRLogin` → 成功弹窗
- 右上角按钮用 `absolute top-3 right-4 z-20` 定位,半透明白色圆形背景 + base64 SVG 图标
- 小程序不支持 `<svg>` 标签,用 `Image` 组件 + `data:image/svg+xml;base64,...` data URI 渲染图标
- 用户取消扫码errMsg 含 'cancel')静默处理

View File

@@ -1,11 +1,15 @@
import React, { useState, useEffect, useCallback } from 'react' import React, {useState, useEffect, useCallback} from 'react'
import { View, Text, Image } from '@tarojs/components' import {View, Text, Image} from '@tarojs/components'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { getMyClerk } from '@/api/shop/shopStoreUser' import {getMyClerk} from '@/api/shop/shopStoreUser'
import { listShopDealerApply } from '@/api/shop/shopDealerApply' import {listShopDealerApply} from '@/api/shop/shopDealerApply'
import { pageShopOrder } from '@/api/shop/shopOrder' import {pageShopOrder} from '@/api/shop/shopOrder'
import {confirmWechatQRLogin, parseQRContent} from '@/api/passport/qr-login'
import Badge from '@/components/common/Badge' import Badge from '@/components/common/Badge'
// 二维码扫码图标base64 SVG data URI兼容小程序 Image 组件)
const QR_SCAN_ICON = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMyA3VjVhMiAyIDAgMCAxIDItMmgyTTE3IDNoMmEyIDIgMCAwIDEgMiAydjJNMjEgMTd2MmEyIDIgMCAwIDEtMiAyaC0yTTcgMjFINWEyIDIgMCAwIDEtMi0ydi0yIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiAvPjxyZWN0IHg9IjciIHk9IjciIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiAvPjxyZWN0IHg9IjEzIiB5PSI3IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgLz48cmVjdCB4PSI3IiB5PSIxMyIgd2lkdGg9IjQiIGhlaWdodD0iNCIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIC8+PHBhdGggZD0iTTEzIDEzaDJ2Mk0xNyAxM3YyTTE3IDE3djJNMTMgMTd2Mk0xMyAxOWgyIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiAvPjwvc3ZnPg=="
definePageConfig({ definePageConfig({
navigationBarTitleText: '门店中心', navigationBarTitleText: '门店中心',
}) })
@@ -71,7 +75,7 @@ export default function StoreCenterPage() {
getMyClerk() getMyClerk()
.then(data => { .then(data => {
if (!data) { if (!data) {
Taro.showToast({ title: '仅门店店员可访问', icon: 'none' }) Taro.showToast({title: '仅门店店员可访问', icon: 'none'})
setTimeout(() => Taro.navigateBack(), 1500) setTimeout(() => Taro.navigateBack(), 1500)
return return
} }
@@ -80,7 +84,7 @@ export default function StoreCenterPage() {
loadBadgeCounts() loadBadgeCounts()
}) })
.catch(() => { .catch(() => {
Taro.showToast({ title: '仅门店店员可访问', icon: 'none' }) Taro.showToast({title: '仅门店店员可访问', icon: 'none'})
setTimeout(() => Taro.navigateBack(), 1500) setTimeout(() => Taro.navigateBack(), 1500)
}) })
.finally(() => setLoading(false)) .finally(() => setLoading(false))
@@ -90,15 +94,17 @@ export default function StoreCenterPage() {
const loadBadgeCounts = async () => { const loadBadgeCounts = async () => {
try { try {
// VIP 待审核 // VIP 待审核
const vipData = await listShopDealerApply({ applyStatus: 10 }) const vipData = await listShopDealerApply({applyStatus: 10})
setPendingVipCount((vipData || []).length) setPendingVipCount((vipData || []).length)
} catch { /* ignore */ } } catch { /* ignore */
}
try { try {
// 待处理订单:未付款/待发货/待收货 // 待处理订单:未付款/待发货/待收货
const orderData = await pageShopOrder({ statusFilter: 1, page: 1, limit: 1 }) const orderData = await pageShopOrder({statusFilter: 1, page: 1, limit: 1})
setPendingOrderCount(orderData?.total || 0) setPendingOrderCount(orderData?.total || 0)
} catch { /* ignore */ } } catch { /* ignore */
}
} }
/** 获取卡片角标数字 */ /** 获取卡片角标数字 */
@@ -108,10 +114,43 @@ export default function StoreCenterPage() {
return 0 return 0
} }
/** 扫码登录 PC 后台 */
const handleScanLogin = useCallback(async () => {
try {
Taro.showLoading({title: '请扫码...', mask: true})
const scanRes = await Taro.scanCode({onlyFromCamera: false, scanType: ['qrCode']})
Taro.hideLoading()
const rawContent = scanRes.result || ''
const token = parseQRContent(rawContent)
const userId = Number(Taro.getStorageSync('UserId'))
if (!token) {
Taro.showModal({title: '提示', content: '未识别到有效的登录二维码', showCancel: false})
return
}
if (!userId) {
Taro.showModal({title: '提示', content: '当前用户未登录', showCancel: false})
return
}
Taro.showLoading({title: '正在确认登录...', mask: true})
await confirmWechatQRLogin(token, userId)
Taro.hideLoading()
Taro.showModal({title: '登录成功', content: 'PC 端后台登录已确认,请返回网页端查看', showCancel: false})
} catch (err: any) {
Taro.hideLoading()
if (err?.errMsg?.includes('cancel')) {
return // 用户取消扫码,静默处理
}
Taro.showModal({title: '登录失败', content: err?.message || '扫码登录失败,请重试', showCancel: false})
}
}, [])
/** 请求订阅消息授权 */ /** 请求订阅消息授权 */
const handleSubscribe = useCallback(() => { const handleSubscribe = useCallback(() => {
if (SUBSCRIBE_TMPL_IDS.length === 0) { if (SUBSCRIBE_TMPL_IDS.length === 0) {
Taro.showToast({ title: '暂无可订阅的消息模板', icon: 'none' }) Taro.showToast({title: '暂无可订阅的消息模板', icon: 'none'})
return return
} }
Taro.requestSubscribeMessage({ Taro.requestSubscribeMessage({
@@ -120,7 +159,7 @@ export default function StoreCenterPage() {
// res[templateId] === 'accept' 表示用户同意订阅 // res[templateId] === 'accept' 表示用户同意订阅
const accepted = SUBSCRIBE_TMPL_IDS.filter(id => res[id] === 'accept') const accepted = SUBSCRIBE_TMPL_IDS.filter(id => res[id] === 'accept')
if (accepted.length > 0) { if (accepted.length > 0) {
Taro.showToast({ title: '订阅成功', icon: 'success' }) Taro.showToast({title: '订阅成功', icon: 'success'})
} }
}, },
fail: (err: any) => { fail: (err: any) => {
@@ -134,7 +173,7 @@ export default function StoreCenterPage() {
}) })
} else if (err.errCode === 20004 || err.errCode === 20002) { } else if (err.errCode === 20004 || err.errCode === 20002) {
// 用户拒绝/关闭了主开关,静默处理 // 用户拒绝/关闭了主开关,静默处理
Taro.showToast({ title: '已取消订阅', icon: 'none' }) Taro.showToast({title: '已取消订阅', icon: 'none'})
} }
}, },
}) })
@@ -156,15 +195,25 @@ export default function StoreCenterPage() {
{/* 顶部信息区:店员头像 + 门店名称 */} {/* 顶部信息区:店员头像 + 门店名称 */}
<View <View
className='pt-8 pb-12 px-5 rounded-b-3xl relative overflow-hidden' className='pt-8 pb-12 px-5 rounded-b-3xl relative overflow-hidden'
style={{ background: 'linear-gradient(135deg, #15803d 0%, #22c55e 60%, #4ade80 100%)' }} style={{background: 'linear-gradient(135deg, #15803d 0%, #22c55e 60%, #4ade80 100%)'}}
> >
<View className='absolute -top-10 -right-10 w-40 h-40 rounded-full opacity-10' <View className='absolute -top-10 -right-10 w-40 h-40 rounded-full opacity-10'
style={{ background: 'radial-gradient(circle, #ffffff, transparent)' }} /> style={{background: 'radial-gradient(circle, #ffffff, transparent)'}}/>
<View className='absolute -bottom-6 -left-6 w-24 h-24 rounded-full opacity-15' <View className='absolute -bottom-6 -left-6 w-24 h-24 rounded-full opacity-15'
style={{ background: 'radial-gradient(circle, #ffffff, transparent)' }} /> style={{background: 'radial-gradient(circle, #ffffff, transparent)'}}/>
{/* 右上角扫码登录按钮 */}
<View
className='absolute top-3 right-4 z-20 w-9 h-9 rounded-full bg-white/20 border border-white/30 flex items-center justify-center active:opacity-70'
onClick={handleScanLogin}
>
<Image src={QR_SCAN_ICON} className='w-5 h-5' mode='aspectFit'/>
</View>
<View className='relative z-10 flex items-center gap-4'> <View className='relative z-10 flex items-center gap-4'>
{/* 店员头像 */} {/* 店员头像 */}
<View className='w-16 h-16 rounded-full bg-white/20 border-2 border-white/40 overflow-hidden flex-shrink-0'> <View
className='w-16 h-16 rounded-full bg-white/20 border-2 border-white/40 overflow-hidden flex-shrink-0'>
{storeInfo.avatar ? ( {storeInfo.avatar ? (
<Image <Image
className='w-full h-full' className='w-full h-full'
@@ -198,12 +247,12 @@ export default function StoreCenterPage() {
<View <View
key={card.key} key={card.key}
className='bg-white rounded-xl p-4 flex items-center gap-4 active:opacity-80 relative' className='bg-white rounded-xl p-4 flex items-center gap-4 active:opacity-80 relative'
onClick={() => Taro.navigateTo({ url: card.url })} onClick={() => Taro.navigateTo({url: card.url})}
> >
{/* 图标 */} {/* 图标 */}
<View <View
className='w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0' className='w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0'
style={{ background: card.bgColor }} style={{background: card.bgColor}}
> >
<Text className='text-2xl'>{card.icon}</Text> <Text className='text-2xl'>{card.icon}</Text>
</View> </View>
@@ -216,7 +265,8 @@ export default function StoreCenterPage() {
{/* 待处理角标 */} {/* 待处理角标 */}
{card.showBadge && ( {card.showBadge && (
<Badge count={getBadgeCount(card.key)} className='absolute -top-1 -right-1' size={20} fontSize={11} fontWeight='bold' /> <Badge count={getBadgeCount(card.key)} className='absolute -top-1 -right-1' size={20}
fontSize={11} fontWeight='bold'/>
)} )}
{/* 箭头 */} {/* 箭头 */}
@@ -227,13 +277,14 @@ export default function StoreCenterPage() {
</View> </View>
{/* 底部提示 */} {/* 底部提示 */}
<View className='mx-3 mt-6 mb-6'> <View className='mx-3 mt-3 mb-6'>
{/* 订阅消息提醒 */} {/* 订阅消息提醒 */}
<View <View
className='bg-white rounded-xl p-4 flex items-center gap-3 active:opacity-80 mb-3' className='bg-white rounded-xl p-4 flex items-center gap-3 active:opacity-80 mb-3'
onClick={handleSubscribe} onClick={handleSubscribe}
> >
<View className='w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center flex-shrink-0'> <View
className='w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center flex-shrink-0'>
<Text className='text-xl'>🔔</Text> <Text className='text-xl'>🔔</Text>
</View> </View>
<View className='flex-1 min-w-0'> <View className='flex-1 min-w-0'>
@@ -245,9 +296,10 @@ export default function StoreCenterPage() {
<Text className='text-orange-500 text-sm flex-shrink-0'></Text> <Text className='text-orange-500 text-sm flex-shrink-0'></Text>
</View> </View>
{/*<Text className='text-gray-300 text-xs text-center block'>*/} <Text className='text-gray-300 text-xs text-center block'>
{/* 未来更多功能将持续上线*/} 线
{/*</Text>*/} </Text>
<View className={'h-4'}></View>
</View> </View>
</View> </View>
) )

View File

@@ -1,13 +1,13 @@
import React, { useState, useEffect, useCallback, useRef } from 'react' import React, {useState, useEffect, useCallback, useRef} from 'react'
import { View, Text, Image, ScrollView, Input, Textarea } from '@tarojs/components' import {View, Text, Image, ScrollView, Input, Textarea} from '@tarojs/components'
import Taro, { useDidShow } from '@tarojs/taro' import Taro, {useDidShow} from '@tarojs/taro'
import { pageShopGoods, updateShopGoods, addShopGoods } from '@/api/shop/shopGoods' import {pageShopGoods, updateShopGoods, addShopGoods} from '@/api/shop/shopGoods'
import { listShopGoodsCategory } from '@/api/shop/shopGoodsCategory' import {listShopGoodsCategory} from '@/api/shop/shopGoodsCategory'
import { getMyClerk } from '@/api/shop/shopStoreUser' import {getMyClerk} from '@/api/shop/shopStoreUser'
import { uploadFile } from '@/api/system/file' import {uploadFile} from '@/api/system/file'
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model' import type {ShopGoods, ShopGoodsParam} from '@/api/shop/shopGoods/model'
import type { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model' import type {ShopGoodsCategory} from '@/api/shop/shopGoodsCategory/model'
import { getCompressedImageUrl } from '@/utils/image' import {getCompressedImageUrl} from '@/utils/image'
definePageConfig({ definePageConfig({
navigationBarTitleText: '商品管理', navigationBarTitleText: '商品管理',
@@ -17,20 +17,20 @@ definePageConfig({
type TabKey = 'all' | 'on_sale' | 'pending' | 'sold_out' type TabKey = 'all' | 'on_sale' | 'pending' | 'sold_out'
const TABS: { key: TabKey; label: string; param: Partial<ShopGoodsParam> }[] = [ const TABS: { key: TabKey; label: string; param: Partial<ShopGoodsParam> }[] = [
{ key: 'all', label: '全部', param: {} }, {key: 'all', label: '全部', param: {}},
{ key: 'on_sale', label: '已上架', param: { status: 0 } }, {key: 'on_sale', label: '已上架', param: {status: 0}},
{ key: 'pending', label: '待上架', param: { status: 1 } }, {key: 'pending', label: '待上架', param: {status: 1}},
{ key: 'sold_out', label: '已售罄', param: { stock: 0 } }, {key: 'sold_out', label: '已售罄', param: {stock: 0}},
] ]
// ─── 状态辅助函数 ────────────────────────────────────────────────── // ─── 状态辅助函数 ──────────────────────────────────────────────────
function getStatusText(status?: number): string { function getStatusText(status?: number): string {
const map: Record<number, string> = { 0: '已上架', 1: '待上架', 2: '待审核', 3: '审核不通过' } const map: Record<number, string> = {0: '已上架', 1: '待上架', 2: '待审核', 3: '审核不通过'}
return map[status ?? 0] || '未知' return map[status ?? 0] || '未知'
} }
function getStatusColor(status?: number): string { function getStatusColor(status?: number): string {
const map: Record<number, string> = { 0: '#16a34a', 1: '#ea580c', 2: '#9333ea', 3: '#dc2626' } const map: Record<number, string> = {0: '#16a34a', 1: '#ea580c', 2: '#9333ea', 3: '#dc2626'}
return map[status ?? 0] || '#999' return map[status ?? 0] || '#999'
} }
@@ -121,7 +121,7 @@ export default function StoreGoodsPage() {
setPage(pageNo) setPage(pageNo)
setHasMore(list.length >= pageSize) setHasMore(list.length >= pageSize)
} catch (e: any) { } catch (e: any) {
Taro.showToast({ title: e.message || '加载失败', icon: 'none' }) Taro.showToast({title: e.message || '加载失败', icon: 'none'})
if (!append) setGoodsList([]) if (!append) setGoodsList([])
} finally { } finally {
loadingRef.current = false loadingRef.current = false
@@ -133,7 +133,8 @@ export default function StoreGoodsPage() {
useEffect(() => { useEffect(() => {
listShopGoodsCategory({}) listShopGoodsCategory({})
.then(data => setCategories(data || [])) .then(data => setCategories(data || []))
.catch(() => {}) .catch(() => {
})
}, []) }, [])
// 获取当前登录店员的门店ID // 获取当前登录店员的门店ID
@@ -142,7 +143,8 @@ export default function StoreGoodsPage() {
.then(data => { .then(data => {
if (data?.storeId) setStoreId(data.storeId) if (data?.storeId) setStoreId(data.storeId)
}) })
.catch(() => {}) .catch(() => {
})
}, []) }, [])
// 切换 tab 时重新加载 // 切换 tab 时重新加载
@@ -186,14 +188,14 @@ export default function StoreGoodsPage() {
success: async (res) => { success: async (res) => {
if (!res.confirm) return if (!res.confirm) return
try { try {
Taro.showLoading({ title: `${actionText}中...` }) Taro.showLoading({title: `${actionText}中...`})
await updateShopGoods({ ...goods, status: newStatus }) await updateShopGoods({...goods, status: newStatus})
Taro.hideLoading() Taro.hideLoading()
Taro.showToast({ title: `${actionText}成功`, icon: 'success' }) Taro.showToast({title: `${actionText}成功`, icon: 'success'})
loadGoods(activeTab, 1) loadGoods(activeTab, 1)
} catch (e: any) { } catch (e: any) {
Taro.hideLoading() Taro.hideLoading()
Taro.showToast({ title: e.message || `${actionText}失败`, icon: 'none' }) Taro.showToast({title: e.message || `${actionText}失败`, icon: 'none'})
} }
}, },
}) })
@@ -203,11 +205,11 @@ export default function StoreGoodsPage() {
const handleToggleRecommend = async (goods: ShopGoods) => { const handleToggleRecommend = async (goods: ShopGoods) => {
const newRecommend = goods.recommend === 1 ? 0 : 1 const newRecommend = goods.recommend === 1 ? 0 : 1
try { try {
await updateShopGoods({ ...goods, recommend: newRecommend }) await updateShopGoods({...goods, recommend: newRecommend})
Taro.showToast({ title: newRecommend === 1 ? '已推荐' : '已取消推荐', icon: 'none' }) Taro.showToast({title: newRecommend === 1 ? '已推荐' : '已取消推荐', icon: 'none'})
loadGoods(activeTab, 1) loadGoods(activeTab, 1)
} catch (e: any) { } catch (e: any) {
Taro.showToast({ title: e.message || '操作失败', icon: 'none' }) Taro.showToast({title: e.message || '操作失败', icon: 'none'})
} }
} }
@@ -223,15 +225,15 @@ export default function StoreGoodsPage() {
...parsed ...parsed
.map((item: any) => .map((item: any) =>
typeof item === 'string' typeof item === 'string'
? { url: item } ? {url: item}
: { uid: item.uid, url: item.url || '', status: item.status } : {uid: item.uid, url: item.url || '', status: item.status}
) )
.filter((item: any) => item.url) .filter((item: any) => item.url)
) )
} }
} catch { } catch {
// 非 JSON 数组时按逗号分隔兜底 // 非 JSON 数组时按逗号分隔兜底
bannerFiles.push(...goods.files.split(',').filter(Boolean).map(url => ({ url }))) bannerFiles.push(...goods.files.split(',').filter(Boolean).map(url => ({url})))
} }
} }
setEditForm({ setEditForm({
@@ -260,11 +262,11 @@ export default function StoreGoodsPage() {
const res = await uploadFile() const res = await uploadFile()
const imageUrl = res.path || '' const imageUrl = res.path || ''
if (imageUrl) { if (imageUrl) {
setEditGoods(prev => prev ? { ...prev, image: imageUrl } : null) setEditGoods(prev => prev ? {...prev, image: imageUrl} : null)
Taro.showToast({ title: '上传成功', icon: 'success' }) Taro.showToast({title: '上传成功', icon: 'success'})
} }
} catch (e: any) { } catch (e: any) {
Taro.showToast({ title: e.message || '上传失败', icon: 'none' }) Taro.showToast({title: e.message || '上传失败', icon: 'none'})
} finally { } finally {
setUploadingImage(false) setUploadingImage(false)
} }
@@ -280,12 +282,12 @@ export default function StoreGoodsPage() {
if (imageUrl) { if (imageUrl) {
setEditForm(prev => ({ setEditForm(prev => ({
...prev, ...prev,
files: [...prev.files, { uid: res.id, url: imageUrl, status: 'done' }], files: [...prev.files, {uid: res.id, url: imageUrl, status: 'done'}],
})) }))
Taro.showToast({ title: '上传成功', icon: 'success' }) Taro.showToast({title: '上传成功', icon: 'success'})
} }
} catch (e: any) { } catch (e: any) {
Taro.showToast({ title: e.message || '上传失败', icon: 'none' }) Taro.showToast({title: e.message || '上传失败', icon: 'none'})
} finally { } finally {
setUploadingBanner(false) setUploadingBanner(false)
} }
@@ -305,13 +307,13 @@ export default function StoreGoodsPage() {
const price = parseFloat(editForm.price) const price = parseFloat(editForm.price)
if (isNaN(price) || price < 0) { if (isNaN(price) || price < 0) {
Taro.showToast({ title: '请输入有效的价格', icon: 'none' }) Taro.showToast({title: '请输入有效的价格', icon: 'none'})
return return
} }
const stock = parseInt(editForm.stock, 10) const stock = parseInt(editForm.stock, 10)
if (isNaN(stock) || stock < 0) { if (isNaN(stock) || stock < 0) {
Taro.showToast({ title: '请输入有效的库存', icon: 'none' }) Taro.showToast({title: '请输入有效的库存', icon: 'none'})
return return
} }
@@ -328,11 +330,11 @@ export default function StoreGoodsPage() {
comments: editForm.comments || undefined, comments: editForm.comments || undefined,
files: JSON.stringify(editForm.files), files: JSON.stringify(editForm.files),
}) })
Taro.showToast({ title: '保存成功', icon: 'success' }) Taro.showToast({title: '保存成功', icon: 'success'})
closeEditModal() closeEditModal()
loadGoods(activeTab, 1) loadGoods(activeTab, 1)
} catch (e: any) { } catch (e: any) {
Taro.showToast({ title: e.message || '保存失败', icon: 'none' }) Taro.showToast({title: e.message || '保存失败', icon: 'none'})
} finally { } finally {
setSubmitting(false) setSubmitting(false)
} }
@@ -370,11 +372,11 @@ export default function StoreGoodsPage() {
const res = await uploadFile() const res = await uploadFile()
const imageUrl = res.path || '' const imageUrl = res.path || ''
if (imageUrl) { if (imageUrl) {
setAddForm(prev => ({ ...prev, image: imageUrl })) setAddForm(prev => ({...prev, image: imageUrl}))
Taro.showToast({ title: '上传成功', icon: 'success' }) Taro.showToast({title: '上传成功', icon: 'success'})
} }
} catch (e: any) { } catch (e: any) {
Taro.showToast({ title: e.message || '上传失败', icon: 'none' }) Taro.showToast({title: e.message || '上传失败', icon: 'none'})
} finally { } finally {
setAddUploadingImage(false) setAddUploadingImage(false)
} }
@@ -390,12 +392,12 @@ export default function StoreGoodsPage() {
if (imageUrl) { if (imageUrl) {
setAddForm(prev => ({ setAddForm(prev => ({
...prev, ...prev,
files: [...prev.files, { uid: res.id, url: imageUrl, status: 'done' }], files: [...prev.files, {uid: res.id, url: imageUrl, status: 'done'}],
})) }))
Taro.showToast({ title: '上传成功', icon: 'success' }) Taro.showToast({title: '上传成功', icon: 'success'})
} }
} catch (e: any) { } catch (e: any) {
Taro.showToast({ title: e.message || '上传失败', icon: 'none' }) Taro.showToast({title: e.message || '上传失败', icon: 'none'})
} finally { } finally {
setAddUploadingBanner(false) setAddUploadingBanner(false)
} }
@@ -412,26 +414,26 @@ export default function StoreGoodsPage() {
/** 提交新增商品 */ /** 提交新增商品 */
const submitAdd = async () => { const submitAdd = async () => {
if (!addForm.name.trim()) { if (!addForm.name.trim()) {
Taro.showToast({ title: '请输入商品名称', icon: 'none' }) Taro.showToast({title: '请输入商品名称', icon: 'none'})
return return
} }
if (!addForm.image) { if (!addForm.image) {
Taro.showToast({ title: '请上传商品图片', icon: 'none' }) Taro.showToast({title: '请上传商品图片', icon: 'none'})
return return
} }
const price = parseFloat(addForm.price) const price = parseFloat(addForm.price)
if (isNaN(price) || price < 0) { if (isNaN(price) || price < 0) {
Taro.showToast({ title: '请输入有效的到手价', icon: 'none' }) Taro.showToast({title: '请输入有效的到手价', icon: 'none'})
return return
} }
const stock = parseInt(addForm.stock, 10) const stock = parseInt(addForm.stock, 10)
if (isNaN(stock) || stock < 0) { if (isNaN(stock) || stock < 0) {
Taro.showToast({ title: '请输入有效的库存', icon: 'none' }) Taro.showToast({title: '请输入有效的库存', icon: 'none'})
return return
} }
if (!storeId) { if (!storeId) {
Taro.showToast({ title: '未获取到门店信息,请重试', icon: 'none' }) Taro.showToast({title: '未获取到门店信息,请重试', icon: 'none'})
return return
} }
@@ -453,11 +455,11 @@ export default function StoreGoodsPage() {
recommend: 0, recommend: 0,
type: 1, // 实物商品 type: 1, // 实物商品
}) })
Taro.showToast({ title: '添加成功', icon: 'success' }) Taro.showToast({title: '添加成功', icon: 'success'})
closeAddModal() closeAddModal()
loadGoods(activeTab, 1) loadGoods(activeTab, 1)
} catch (e: any) { } catch (e: any) {
Taro.showToast({ title: e.message || '添加失败', icon: 'none' }) Taro.showToast({title: e.message || '添加失败', icon: 'none'})
} finally { } finally {
setAddSubmitting(false) setAddSubmitting(false)
} }
@@ -475,12 +477,13 @@ export default function StoreGoodsPage() {
{goods.image ? ( {goods.image ? (
<Image <Image
className='w-20 h-20 rounded-lg bg-gray-50 flex-shrink-0' className='w-20 h-20 rounded-lg bg-gray-50 flex-shrink-0'
src={getCompressedImageUrl(goods.image,{ width: 120, quality: 90 })} src={getCompressedImageUrl(goods.image, {width: 120, quality: 90})}
mode='aspectFill' mode='aspectFill'
onClick={() => Taro.previewImage({ urls: [goods.image!] })} 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'> <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> <Text className='text-2xl text-gray-300'>📦</Text>
</View> </View>
)} )}
@@ -488,7 +491,10 @@ export default function StoreGoodsPage() {
<View className='flex items-center gap-2 mb-1'> <View className='flex items-center gap-2 mb-1'>
<Text <Text
className='text-xs px-1.5 py-0.5 rounded' className='text-xs px-1.5 py-0.5 rounded'
style={{ color: getStatusColor(goods.status), background: getStatusColor(goods.status) + '15' }} style={{
color: getStatusColor(goods.status),
background: getStatusColor(goods.status) + '15'
}}
> >
{getStatusText(goods.status)} {getStatusText(goods.status)}
</Text> </Text>
@@ -499,7 +505,8 @@ export default function StoreGoodsPage() {
<Text className='text-xs px-1.5 py-0.5 rounded bg-amber-50 text-amber-600'></Text> <Text className='text-xs px-1.5 py-0.5 rounded bg-amber-50 text-amber-600'></Text>
)} )}
</View> </View>
<Text className='text-sm text-gray-800 block' style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <Text className='text-sm text-gray-800 block'
style={{overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>
{goods.name} {goods.name}
</Text> </Text>
{goods.categoryName && ( {goods.categoryName && (
@@ -520,19 +527,18 @@ export default function StoreGoodsPage() {
<Text className='text-sm text-gray-400 line-through'>¥{goods.salePrice}</Text> <Text className='text-sm text-gray-400 line-through'>¥{goods.salePrice}</Text>
</View> </View>
)} )}
{goods.dealerPrice && (
<View className='flex-1'> <View className='flex-1'>
<Text className='text-xs text-gray-400 block'>VIP价</Text> <Text className='text-xs text-gray-400 block'>VIP价</Text>
<Text className='text-sm text-purple-500'>¥{goods.dealerPrice}</Text> <Text className='text-sm text-purple-500'>¥{goods.dealerPrice}</Text>
</View> </View>
)}
<View className='flex-1'> <View className='flex-1'>
<Text className='text-xs text-gray-400 block'></Text> <Text className='text-xs text-gray-400 block'></Text>
<Text className='text-sm text-gray-700'>{goods.sales || 0}</Text> <Text className='text-sm text-gray-700'>{goods.sales || 0}</Text>
</View> </View>
<View className='flex-1'> <View className='flex-1'>
<Text className='text-xs text-gray-400 block'></Text> <Text className='text-xs text-gray-400 block'></Text>
<Text className={`text-sm ${isSoldOut ? 'text-red-500' : 'text-gray-700'}`}>{goods.stock ?? 0}</Text> <Text
className={`text-sm ${isSoldOut ? 'text-red-500' : 'text-gray-700'}`}>{goods.stock ?? 0}</Text>
</View> </View>
</View> </View>
@@ -586,7 +592,11 @@ export default function StoreGoodsPage() {
{searchText ? ( {searchText ? (
<Text <Text
className='text-gray-400 text-lg px-1' className='text-gray-400 text-lg px-1'
onClick={() => { setSearchText(''); setKeyword(''); loadGoods(activeTab, 1) }} onClick={() => {
setSearchText('');
setKeyword('');
loadGoods(activeTab, 1)
}}
>×</Text> >×</Text>
) : null} ) : null}
</View> </View>
@@ -625,7 +635,7 @@ export default function StoreGoodsPage() {
{/* 商品列表 */} {/* 商品列表 */}
<ScrollView <ScrollView
scrollY scrollY
style={{ height: 'calc(100vh - 100px)' }} style={{height: 'calc(100vh - 100px)'}}
onScrollToLower={handleLoadMore} onScrollToLower={handleLoadMore}
lowerThreshold={100} lowerThreshold={100}
> >
@@ -650,29 +660,31 @@ export default function StoreGoodsPage() {
<Text className='text-gray-300 text-xs'></Text> <Text className='text-gray-300 text-xs'></Text>
</View> </View>
)} )}
<View className='h-20' /> <View className='h-20'/>
</ScrollView> </ScrollView>
{/* 新增商品浮动按钮 */} {/* 新增商品浮动按钮 */}
<View <View
className='fixed right-4 bottom-8 w-14 h-14 rounded-full bg-cyan-500 flex items-center justify-center active:opacity-80' className='fixed right-4 bottom-8 w-14 h-14 rounded-full bg-cyan-500 flex items-center justify-center active:opacity-80'
style={{ boxShadow: '0 4px 12px rgba(8, 145, 178, 0.4)' }} style={{boxShadow: '0 4px 12px rgba(8, 145, 178, 0.4)'}}
onClick={openAddModal} onClick={openAddModal}
> >
<Text className='text-white text-3xl leading-none' style={{ marginTop: '-2px' }}>+</Text> <Text className='text-white text-3xl leading-none' style={{marginTop: '-2px'}}>+</Text>
</View> </View>
{/* 分类选择弹窗 */} {/* 分类选择弹窗 */}
{showCategoryPicker && ( {showCategoryPicker && (
<View className='fixed inset-0 z-50' onClick={() => setShowCategoryPicker(false)}> <View className='fixed inset-0 z-50' onClick={() => setShowCategoryPicker(false)}>
<View className='absolute inset-0 bg-black/50' /> <View className='absolute inset-0 bg-black/50'/>
<View <View
className='absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl max-h-[60vh] overflow-y-auto' className='absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl max-h-[60vh] overflow-y-auto'
onClick={(e) => e.stopPropagation()} 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'> <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-base font-medium text-gray-800'></Text>
<Text className='text-gray-400 text-lg' onClick={() => setShowCategoryPicker(false)}>×</Text> <Text className='text-gray-400 text-lg'
onClick={() => setShowCategoryPicker(false)}>×</Text>
</View> </View>
<View <View
className='px-5 py-3.5 border-b border-gray-50' className='px-5 py-3.5 border-b border-gray-50'
@@ -692,7 +704,7 @@ export default function StoreGoodsPage() {
)} )}
</View> </View>
))} ))}
<View className='h-8' /> <View className='h-8'/>
</View> </View>
</View> </View>
)} )}
@@ -700,28 +712,32 @@ export default function StoreGoodsPage() {
{/* 编辑弹窗 */} {/* 编辑弹窗 */}
{showEditModal && editGoods && ( {showEditModal && editGoods && (
<View className='fixed inset-0 z-50 flex items-end justify-center'> <View className='fixed inset-0 z-50 flex items-end justify-center'>
<View className='absolute inset-0 bg-black/50' onClick={closeEditModal} /> <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'> <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> <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'> <View className='bg-gray-50 rounded-xl p-4 mb-5 flex items-center gap-3'>
<View className='relative' onClick={handleUploadImage}> <View className='relative' onClick={handleUploadImage}>
{editGoods.image ? ( {editGoods.image ? (
<Image className='w-16 h-16 rounded-lg' src={getCompressedImageUrl(editGoods.image)} mode='aspectFill' /> <Image className='w-16 h-16 rounded-lg' src={getCompressedImageUrl(editGoods.image)}
mode='aspectFill'/>
) : ( ) : (
<View className='w-16 h-16 rounded-lg bg-gray-200 flex items-center justify-center'> <View className='w-16 h-16 rounded-lg bg-gray-200 flex items-center justify-center'>
<Text className='text-2xl text-gray-300'>📷</Text> <Text className='text-2xl text-gray-300'>📷</Text>
</View> </View>
)} )}
<View className='absolute inset-0 rounded-lg bg-black/30 flex items-center justify-center'> <View
className='absolute inset-0 rounded-lg bg-black/30 flex items-center justify-center'>
<Text className='text-white text-xs'> <Text className='text-white text-xs'>
{uploadingImage ? '上传中...' : '更换'} {uploadingImage ? '上传中...' : '更换'}
</Text> </Text>
</View> </View>
</View> </View>
<View className='flex-1 min-w-0'> <View className='flex-1 min-w-0'>
<Text className='text-sm text-gray-800 block' style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <Text className='text-sm text-gray-800 block'
style={{overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>
{editGoods.name} {editGoods.name}
</Text> </Text>
<Text className='text-xs text-gray-400 mt-1 block'>ID: {editGoods.goodsId}</Text> <Text className='text-xs text-gray-400 mt-1 block'>ID: {editGoods.goodsId}</Text>
@@ -734,7 +750,8 @@ export default function StoreGoodsPage() {
<View className='flex flex-wrap gap-3'> <View className='flex flex-wrap gap-3'>
{editForm.files.map((file, index) => ( {editForm.files.map((file, index) => (
<View key={file.url + index} className='relative w-20 h-20'> <View key={file.url + index} className='relative w-20 h-20'>
<Image className='w-20 h-20 rounded-lg bg-gray-100' src={getCompressedImageUrl(file.url)} mode='aspectFill' /> <Image className='w-20 h-20 rounded-lg bg-gray-100'
src={getCompressedImageUrl(file.url)} mode='aspectFill'/>
<View <View
className='absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center' 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)} onClick={() => handleRemoveBanner(index)}
@@ -747,7 +764,8 @@ export default function StoreGoodsPage() {
className='w-20 h-20 rounded-lg border border-dashed border-gray-300 flex flex-col items-center justify-center' className='w-20 h-20 rounded-lg border border-dashed border-gray-300 flex flex-col items-center justify-center'
onClick={handleAddBanner} onClick={handleAddBanner}
> >
<Text className='text-2xl text-gray-300 mb-0.5'>{uploadingBanner ? '...' : '+'}</Text> <Text
className='text-2xl text-gray-300 mb-0.5'>{uploadingBanner ? '...' : '+'}</Text>
<Text className='text-xs text-gray-400'>{uploadingBanner ? '上传中' : '上传'}</Text> <Text className='text-xs text-gray-400'>{uploadingBanner ? '上传中' : '上传'}</Text>
</View> </View>
</View> </View>
@@ -761,7 +779,7 @@ export default function StoreGoodsPage() {
type='digit' type='digit'
placeholder='请输入价格' placeholder='请输入价格'
value={editForm.price} value={editForm.price}
onInput={(e) => setEditForm(prev => ({ ...prev, price: e.detail.value }))} onInput={(e) => setEditForm(prev => ({...prev, price: e.detail.value}))}
/> />
</View> </View>
@@ -772,7 +790,7 @@ export default function StoreGoodsPage() {
type='digit' type='digit'
placeholder='选填,划线价' placeholder='选填,划线价'
value={editForm.salePrice} value={editForm.salePrice}
onInput={(e) => setEditForm(prev => ({ ...prev, salePrice: e.detail.value }))} onInput={(e) => setEditForm(prev => ({...prev, salePrice: e.detail.value}))}
/> />
</View> </View>
@@ -783,7 +801,7 @@ export default function StoreGoodsPage() {
type='digit' type='digit'
placeholder='VIP/经销商专享价' placeholder='VIP/经销商专享价'
value={editForm.dealerPrice} value={editForm.dealerPrice}
onInput={(e) => setEditForm(prev => ({ ...prev, dealerPrice: e.detail.value }))} onInput={(e) => setEditForm(prev => ({...prev, dealerPrice: e.detail.value}))}
/> />
</View> </View>
@@ -794,7 +812,7 @@ export default function StoreGoodsPage() {
type='number' type='number'
placeholder='请输入库存数量' placeholder='请输入库存数量'
value={editForm.stock} value={editForm.stock}
onInput={(e) => setEditForm(prev => ({ ...prev, stock: e.detail.value }))} onInput={(e) => setEditForm(prev => ({...prev, stock: e.detail.value}))}
/> />
</View> </View>
@@ -805,7 +823,7 @@ export default function StoreGoodsPage() {
type='number' type='number'
placeholder='数字越小越靠前' placeholder='数字越小越靠前'
value={editForm.sortNumber} value={editForm.sortNumber}
onInput={(e) => setEditForm(prev => ({ ...prev, sortNumber: e.detail.value }))} onInput={(e) => setEditForm(prev => ({...prev, sortNumber: e.detail.value}))}
/> />
</View> </View>
@@ -815,9 +833,9 @@ export default function StoreGoodsPage() {
className='bg-gray-50 rounded-lg px-4 py-3 text-sm w-full' className='bg-gray-50 rounded-lg px-4 py-3 text-sm w-full'
placeholder='选填' placeholder='选填'
value={editForm.comments} value={editForm.comments}
onInput={(e) => setEditForm(prev => ({ ...prev, comments: e.detail.value }))} onInput={(e) => setEditForm(prev => ({...prev, comments: e.detail.value}))}
maxlength={200} maxlength={200}
style={{ minHeight: '60px' }} style={{minHeight: '60px'}}
/> />
</View> </View>
@@ -842,22 +860,26 @@ export default function StoreGoodsPage() {
{/* 新增商品弹窗 */} {/* 新增商品弹窗 */}
{showAddModal && ( {showAddModal && (
<View className='fixed inset-0 z-50 flex items-end justify-center'> <View className='fixed inset-0 z-50 flex items-end justify-center'>
<View className='absolute inset-0 bg-black/50' onClick={closeAddModal} /> <View className='absolute inset-0 bg-black/50' onClick={closeAddModal}/>
<View className='relative bg-white rounded-t-2xl w-full px-5 pt-6 pb-10 max-h-[85vh] overflow-y-auto'> <View
className='relative bg-white rounded-t-2xl w-full px-5 pt-6 pb-10 max-h-[85vh] overflow-y-auto'>
<Text className='text-lg font-medium text-gray-800 text-center mb-5 block'></Text> <Text className='text-lg font-medium text-gray-800 text-center mb-5 block'></Text>
{/* 商品图片 */} {/* 商品图片 */}
<View className='mb-4'> <View className='mb-4'>
<Text className='text-sm text-gray-600 mb-2 block'> <Text className='text-red-500'>*</Text></Text> <Text className='text-sm text-gray-600 mb-2 block'> <Text
className='text-red-500'>*</Text></Text>
<View className='relative w-20 h-20' onClick={handleAddUploadImage}> <View className='relative w-20 h-20' onClick={handleAddUploadImage}>
{addForm.image ? ( {addForm.image ? (
<Image className='w-20 h-20 rounded-lg' src={getCompressedImageUrl(addForm.image)} mode='aspectFill' /> <Image className='w-20 h-20 rounded-lg' src={getCompressedImageUrl(addForm.image)}
mode='aspectFill'/>
) : ( ) : (
<View className='w-20 h-20 rounded-lg bg-gray-100 flex items-center justify-center'> <View className='w-20 h-20 rounded-lg bg-gray-100 flex items-center justify-center'>
<Text className='text-2xl text-gray-300'>📷</Text> <Text className='text-2xl text-gray-300'>📷</Text>
</View> </View>
)} )}
<View className='absolute inset-0 rounded-lg bg-black/30 flex items-center justify-center'> <View
className='absolute inset-0 rounded-lg bg-black/30 flex items-center justify-center'>
<Text className='text-white text-xs'> <Text className='text-white text-xs'>
{addUploadingImage ? '上传中...' : '上传'} {addUploadingImage ? '上传中...' : '上传'}
</Text> </Text>
@@ -867,12 +889,13 @@ export default function StoreGoodsPage() {
{/* 商品名称 */} {/* 商品名称 */}
<View className='mb-4'> <View className='mb-4'>
<Text className='text-sm text-gray-600 mb-2 block'> <Text className='text-red-500'>*</Text></Text> <Text className='text-sm text-gray-600 mb-2 block'> <Text
className='text-red-500'>*</Text></Text>
<Input <Input
className='bg-gray-50 rounded-lg px-4 py-3 text-sm' className='bg-gray-50 rounded-lg px-4 py-3 text-sm'
placeholder='请输入商品名称' placeholder='请输入商品名称'
value={addForm.name} value={addForm.name}
onInput={(e) => setAddForm(prev => ({ ...prev, name: e.detail.value }))} onInput={(e) => setAddForm(prev => ({...prev, name: e.detail.value}))}
maxlength={100} maxlength={100}
/> />
</View> </View>
@@ -899,7 +922,8 @@ export default function StoreGoodsPage() {
<View className='flex flex-wrap gap-3'> <View className='flex flex-wrap gap-3'>
{addForm.files.map((file, index) => ( {addForm.files.map((file, index) => (
<View key={file.url + index} className='relative w-20 h-20'> <View key={file.url + index} className='relative w-20 h-20'>
<Image className='w-20 h-20 rounded-lg bg-gray-100' src={getCompressedImageUrl(file.url)} mode='aspectFill' /> <Image className='w-20 h-20 rounded-lg bg-gray-100'
src={getCompressedImageUrl(file.url)} mode='aspectFill'/>
<View <View
className='absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center' className='absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center'
onClick={() => handleAddRemoveBanner(index)} onClick={() => handleAddRemoveBanner(index)}
@@ -912,21 +936,24 @@ export default function StoreGoodsPage() {
className='w-20 h-20 rounded-lg border border-dashed border-gray-300 flex flex-col items-center justify-center' className='w-20 h-20 rounded-lg border border-dashed border-gray-300 flex flex-col items-center justify-center'
onClick={handleAddUploadBanner} onClick={handleAddUploadBanner}
> >
<Text className='text-2xl text-gray-300 mb-0.5'>{addUploadingBanner ? '...' : '+'}</Text> <Text
<Text className='text-xs text-gray-400'>{addUploadingBanner ? '上传中' : '上传'}</Text> className='text-2xl text-gray-300 mb-0.5'>{addUploadingBanner ? '...' : '+'}</Text>
<Text
className='text-xs text-gray-400'>{addUploadingBanner ? '上传中' : '上传'}</Text>
</View> </View>
</View> </View>
</View> </View>
{/* 到手价 */} {/* 到手价 */}
<View className='mb-4'> <View className='mb-4'>
<Text className='text-sm text-gray-600 mb-2 block'> (¥) <Text className='text-red-500'>*</Text></Text> <Text className='text-sm text-gray-600 mb-2 block'> (¥) <Text
className='text-red-500'>*</Text></Text>
<Input <Input
className='bg-gray-50 rounded-lg px-4 py-3 text-sm' className='bg-gray-50 rounded-lg px-4 py-3 text-sm'
type='digit' type='digit'
placeholder='请输入价格' placeholder='请输入价格'
value={addForm.price} value={addForm.price}
onInput={(e) => setAddForm(prev => ({ ...prev, price: e.detail.value }))} onInput={(e) => setAddForm(prev => ({...prev, price: e.detail.value}))}
/> />
</View> </View>
@@ -938,7 +965,7 @@ export default function StoreGoodsPage() {
type='digit' type='digit'
placeholder='选填,划线价' placeholder='选填,划线价'
value={addForm.salePrice} value={addForm.salePrice}
onInput={(e) => setAddForm(prev => ({ ...prev, salePrice: e.detail.value }))} onInput={(e) => setAddForm(prev => ({...prev, salePrice: e.detail.value}))}
/> />
</View> </View>
@@ -950,19 +977,20 @@ export default function StoreGoodsPage() {
type='digit' type='digit'
placeholder='VIP/经销商专享价' placeholder='VIP/经销商专享价'
value={addForm.dealerPrice} value={addForm.dealerPrice}
onInput={(e) => setAddForm(prev => ({ ...prev, dealerPrice: e.detail.value }))} onInput={(e) => setAddForm(prev => ({...prev, dealerPrice: e.detail.value}))}
/> />
</View> </View>
{/* 库存 */} {/* 库存 */}
<View className='mb-4'> <View className='mb-4'>
<Text className='text-sm text-gray-600 mb-2 block'> <Text className='text-red-500'>*</Text></Text> <Text className='text-sm text-gray-600 mb-2 block'> <Text
className='text-red-500'>*</Text></Text>
<Input <Input
className='bg-gray-50 rounded-lg px-4 py-3 text-sm' className='bg-gray-50 rounded-lg px-4 py-3 text-sm'
type='number' type='number'
placeholder='请输入库存数量' placeholder='请输入库存数量'
value={addForm.stock} value={addForm.stock}
onInput={(e) => setAddForm(prev => ({ ...prev, stock: e.detail.value }))} onInput={(e) => setAddForm(prev => ({...prev, stock: e.detail.value}))}
/> />
</View> </View>
@@ -974,7 +1002,7 @@ export default function StoreGoodsPage() {
type='number' type='number'
placeholder='数字越小越靠前' placeholder='数字越小越靠前'
value={addForm.sortNumber} value={addForm.sortNumber}
onInput={(e) => setAddForm(prev => ({ ...prev, sortNumber: e.detail.value }))} onInput={(e) => setAddForm(prev => ({...prev, sortNumber: e.detail.value}))}
/> />
</View> </View>
@@ -985,9 +1013,9 @@ export default function StoreGoodsPage() {
className='bg-gray-50 rounded-lg px-4 py-3 text-sm w-full' className='bg-gray-50 rounded-lg px-4 py-3 text-sm w-full'
placeholder='选填' placeholder='选填'
value={addForm.comments} value={addForm.comments}
onInput={(e) => setAddForm(prev => ({ ...prev, comments: e.detail.value }))} onInput={(e) => setAddForm(prev => ({...prev, comments: e.detail.value}))}
maxlength={200} maxlength={200}
style={{ minHeight: '60px' }} style={{minHeight: '60px'}}
/> />
</View> </View>
@@ -1012,19 +1040,21 @@ export default function StoreGoodsPage() {
{/* 新增商品分类选择弹窗 */} {/* 新增商品分类选择弹窗 */}
{showAddCategoryPicker && ( {showAddCategoryPicker && (
<View className='fixed inset-0 z-[60]' onClick={() => setShowAddCategoryPicker(false)}> <View className='fixed inset-0 z-[60]' onClick={() => setShowAddCategoryPicker(false)}>
<View className='absolute inset-0 bg-black/50' /> <View className='absolute inset-0 bg-black/50'/>
<View <View
className='absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl max-h-[60vh] overflow-y-auto' className='absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl max-h-[60vh] overflow-y-auto'
onClick={(e) => e.stopPropagation()} 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'> <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-base font-medium text-gray-800'></Text>
<Text className='text-gray-400 text-lg' onClick={() => setShowAddCategoryPicker(false)}>×</Text> <Text className='text-gray-400 text-lg'
onClick={() => setShowAddCategoryPicker(false)}>×</Text>
</View> </View>
<View <View
className='px-5 py-3.5 border-b border-gray-50' className='px-5 py-3.5 border-b border-gray-50'
onClick={() => { onClick={() => {
setAddForm(prev => ({ ...prev, categoryId: undefined })) setAddForm(prev => ({...prev, categoryId: undefined}))
setShowAddCategoryPicker(false) setShowAddCategoryPicker(false)
}} }}
> >
@@ -1035,7 +1065,7 @@ export default function StoreGoodsPage() {
key={cat.categoryId} key={cat.categoryId}
className='px-5 py-3.5 border-b border-gray-50 flex justify-between items-center' className='px-5 py-3.5 border-b border-gray-50 flex justify-between items-center'
onClick={() => { onClick={() => {
setAddForm(prev => ({ ...prev, categoryId: cat.categoryId })) setAddForm(prev => ({...prev, categoryId: cat.categoryId}))
setShowAddCategoryPicker(false) setShowAddCategoryPicker(false)
}} }}
> >
@@ -1045,7 +1075,7 @@ export default function StoreGoodsPage() {
)} )}
</View> </View>
))} ))}
<View className='h-8' /> <View className='h-8'/>
</View> </View>
</View> </View>
)} )}

View File

@@ -183,7 +183,7 @@ export default function StoreUsersPage() {
{displayName} {displayName}
</Text> </Text>
<View <View
className='px-1.5 py-0.5 rounded text-[10px]' className='px-1 py-1 rounded text-xs'
style={{ style={{
color: isDisabled ? '#dc2626' : '#16a34a', color: isDisabled ? '#dc2626' : '#16a34a',
background: isDisabled ? '#fef2f2' : '#f0fdf4', background: isDisabled ? '#fef2f2' : '#f0fdf4',
@@ -192,14 +192,14 @@ export default function StoreUsersPage() {
{isDisabled ? '已禁用' : '正常'} {isDisabled ? '已禁用' : '正常'}
</View> </View>
</View> </View>
<Text className='text-xs text-gray-400 mt-0.5 block'> <Text className='text-xs text-gray-400 mt-1 block'>
{user.phone || user.mobile || '未绑定手机'} {user.phone || user.mobile || '未绑定手机'}
</Text> </Text>
</View> </View>
{/* 禁用/启用按钮 */} {/* 禁用/启用按钮 */}
<View <View
className={`px-3 py-1.5 rounded-lg flex-shrink-0 ${isDisabled ? 'bg-green-50' : 'bg-red-50'}`} className={`px-3 py-1 rounded-lg flex-shrink-0 ${isDisabled ? 'bg-green-50' : 'bg-red-50'}`}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
handleToggleStatus(user) handleToggleStatus(user)
@@ -215,17 +215,17 @@ export default function StoreUsersPage() {
<View className='flex items-center gap-4 mt-3 pt-3 border-t border-gray-50'> <View className='flex items-center gap-4 mt-3 pt-3 border-t border-gray-50'>
{user.memberLevelName && ( {user.memberLevelName && (
<View className='flex items-center gap-1'> <View className='flex items-center gap-1'>
<Text className='text-[10px] text-gray-400'>:</Text> <Text className='text-xs text-gray-400'>:</Text>
<Text className='text-xs text-purple-500'>{user.memberLevelName}</Text> <Text className='text-xs text-purple-500'>{user.memberLevelName}</Text>
</View> </View>
)} )}
<View className='flex items-center gap-1'> <View className='flex items-center gap-1'>
<Text className='text-[10px] text-gray-400'>:</Text> <Text className='text-xs text-gray-400'>:</Text>
<Text className='text-xs text-gray-500'>{formatTime(user.createTime)}</Text> <Text className='text-xs text-gray-500'>{formatTime(user.createTime)}</Text>
</View> </View>
{user.balance !== undefined && Number(user.balance) > 0 && ( {user.balance !== undefined && Number(user.balance) > 0 && (
<View className='flex items-center gap-1'> <View className='flex items-center gap-1'>
<Text className='text-[10px] text-gray-400'>:</Text> <Text className='text-xs text-gray-400'>:</Text>
<Text className='text-xs text-orange-500'>¥{user.balance}</Text> <Text className='text-xs text-orange-500'>¥{user.balance}</Text>
</View> </View>
)} )}
@@ -238,7 +238,7 @@ export default function StoreUsersPage() {
<View className='min-h-full bg-gray-50'> <View className='min-h-full bg-gray-50'>
{/* 搜索栏 */} {/* 搜索栏 */}
<View className='bg-white px-3 py-2 flex items-center gap-2'> <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'> <View className='flex-1 flex items-center bg-gray-100 rounded-lg px-3 py-1'>
<Input <Input
className='flex-1 text-sm' className='flex-1 text-sm'
placeholder='搜索昵称 / 手机号' placeholder='搜索昵称 / 手机号'
@@ -255,7 +255,7 @@ export default function StoreUsersPage() {
) : null} ) : null}
</View> </View>
<View <View
className='px-3 py-1.5 rounded-lg bg-blue-500' className='px-3 py-1 rounded-lg bg-blue-500'
onClick={handleSearch} onClick={handleSearch}
> >
<Text className='text-sm text-white'></Text> <Text className='text-sm text-white'></Text>
@@ -342,7 +342,7 @@ export default function StoreUsersPage() {
{detailUser.nickname || detailUser.realName || '未设置昵称'} {detailUser.nickname || detailUser.realName || '未设置昵称'}
</Text> </Text>
<View <View
className='px-2 py-0.5 rounded text-xs' className='px-2 py-1 rounded text-xs'
style={{ style={{
color: detailUser.status === 1 ? '#dc2626' : '#16a34a', color: detailUser.status === 1 ? '#dc2626' : '#16a34a',
background: detailUser.status === 1 ? '#fef2f2' : '#f0fdf4', background: detailUser.status === 1 ? '#fef2f2' : '#f0fdf4',

View File

@@ -281,7 +281,7 @@ const UserPage: React.FC = () => {
<Text className='text-xl mb-1'>{item.icon}</Text> <Text className='text-xl mb-1'>{item.icon}</Text>
<Text className='text-xs text-gray-600'>{item.label}</Text> <Text className='text-xs text-gray-600'>{item.label}</Text>
{/* 数量角标count 为 0 时组件内部不渲染 */} {/* 数量角标count 为 0 时组件内部不渲染 */}
<Badge count={item.count} className='absolute -top-1 -right-1' /> {item.tabIndex != 4 && <Badge count={item.count} className='absolute -top-1 -right-1' />}
</View> </View>
))} ))}
</View> </View>