Files
xinlong-shop-taro/src/pages/store/center/index.tsx
赵忠林 5c8acfd19c feat(store): 新增门店新订单提醒功能
- 新增 useNewOrderDetector Hook,实现30秒轮询检测新订单
- 订单管理页集成该 Hook,新增订单时震动+Toast提醒
- “全部”Tab 显示新订单红点,切换Tab清空未读计数
- 门店中心页增加待处理订单角标,统一待审核和待处理显示逻辑
- 底部新增“接收新订单提醒”订阅消息授权入口,调用微信接口申请授权
- 文档中补充订阅消息模板ID及字段说明,待后端配合公众号推送消息
2026-07-04 12:10:02 +08:00

227 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useCallback } from 'react'
import { View, Text, Image } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { getMyClerk } from '@/api/shop/shopStoreUser'
import { listShopDealerApply } from '@/api/shop/shopDealerApply'
import { pageShopOrder } from '@/api/shop/shopOrder'
definePageConfig({
navigationBarTitleText: '门店中心',
})
// 功能卡片定义(未来新增功能只需在这里加一项)
const FEATURE_CARDS = [
{
key: 'orders',
icon: '📦',
title: '订单管理',
desc: '查看和处理门店订单',
url: '/pages/store/orders/index',
color: '#15803d',
bgColor: '#dcfce7',
// 显示待处理订单数量角标
showBadge: true,
},
{
key: 'vip-review',
icon: '👑',
title: 'VIP会员审核',
desc: '审核客户VIP会员申请',
url: '/pages/user/vip-review/index',
color: '#7c3aed',
bgColor: '#ede9fe',
showBadge: true,
},
]
// 订阅消息模板 ID
const SUBSCRIBE_TMPL_IDS = [
'sh1K9iK7vZjebUNFu6OsMsnsJxm4whThWGrhN7I4zVg', // 交易提醒:订单号、商品名称、联系人、联系电话、送货地址
]
export default function StoreCenterPage() {
const [storeInfo, setStoreInfo] = useState<any>(null)
const [pendingVipCount, setPendingVipCount] = useState(0)
const [pendingOrderCount, setPendingOrderCount] = useState(0)
const [loading, setLoading] = useState(true)
useEffect(() => {
// 验证店员身份
getMyClerk()
.then(data => {
if (!data) {
Taro.showToast({ title: '仅门店店员可访问', icon: 'none' })
setTimeout(() => Taro.navigateBack(), 1500)
return
}
setStoreInfo(data)
// 加载待审核数量
loadBadgeCounts()
})
.catch(() => {
Taro.showToast({ title: '仅门店店员可访问', icon: 'none' })
setTimeout(() => Taro.navigateBack(), 1500)
})
.finally(() => setLoading(false))
}, [])
/** 加载各类待处理角标数量 */
const loadBadgeCounts = async () => {
try {
// VIP 待审核
const vipData = await listShopDealerApply({ applyStatus: 10 })
setPendingVipCount((vipData || []).length)
} catch { /* ignore */ }
try {
// 待处理订单:未付款/待发货/待收货
const orderData = await pageShopOrder({ statusFilter: 1, page: 1, limit: 1 })
setPendingOrderCount(orderData?.total || 0)
} catch { /* ignore */ }
}
/** 获取卡片角标数字 */
const getBadgeCount = (key: string) => {
if (key === 'orders') return pendingOrderCount
if (key === 'vip-review') return pendingVipCount
return 0
}
/** 请求订阅消息授权 */
const handleSubscribe = useCallback(() => {
if (SUBSCRIBE_TMPL_IDS.length === 0) {
Taro.showToast({ title: '暂无可订阅的消息模板', icon: 'none' })
return
}
Taro.requestSubscribeMessage({
tmplIds: SUBSCRIBE_TMPL_IDS,
success: (res) => {
// res[templateId] === 'accept' 表示用户同意订阅
const accepted = SUBSCRIBE_TMPL_IDS.filter(id => res[id] === 'accept')
if (accepted.length > 0) {
Taro.showToast({ title: '订阅成功', icon: 'success' })
}
},
fail: (err) => {
console.error('订阅失败:', err)
},
})
}, [])
if (loading) {
return (
<View className='min-h-full bg-gray-50 flex items-center justify-center'>
<Text className='text-gray-400'>...</Text>
</View>
)
}
if (!storeInfo) return null
return (
<View className='min-h-full bg-gray-50'>
{/* 顶部信息区:店员头像 + 门店名称 */}
<View
className='pt-8 pb-12 px-5 rounded-b-3xl relative overflow-hidden'
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'
style={{ background: 'radial-gradient(circle, #ffffff, transparent)' }} />
<View className='absolute -bottom-6 -left-6 w-24 h-24 rounded-full opacity-15'
style={{ background: 'radial-gradient(circle, #ffffff, transparent)' }} />
<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'>
{storeInfo.avatar ? (
<Image
className='w-full h-full'
src={storeInfo.avatar}
mode='aspectFill'
/>
) : (
<View className='w-full h-full flex items-center justify-center'>
<Text className='text-2xl'>👤</Text>
</View>
)}
</View>
{/* 门店信息 */}
<View className='flex-1 min-w-0'>
<Text className='text-white text-lg font-bold block truncate'>
{storeInfo.storeName || '门店中心'}
</Text>
<Text className='text-white text-opacity-80 text-sm mt-1 block truncate'>
{storeInfo.name} {storeInfo.phone ? `· ${storeInfo.phone}` : ''}
</Text>
</View>
</View>
</View>
{/* 功能卡片区 */}
<View className='mx-3 -mt-6 relative z-20'>
<View className='flex flex-col gap-3'>
{FEATURE_CARDS.map(card => (
<View
key={card.key}
className='bg-white rounded-xl p-4 flex items-center gap-4 active:opacity-80 relative'
onClick={() => Taro.navigateTo({ url: card.url })}
>
{/* 图标 */}
<View
className='w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0'
style={{ background: card.bgColor }}
>
<Text className='text-2xl'>{card.icon}</Text>
</View>
{/* 文字 */}
<View className='flex-1 min-w-0'>
<Text className='text-gray-800 text-base font-medium block'>{card.title}</Text>
<Text className='text-gray-400 text-xs mt-0.5 block'>{card.desc}</Text>
</View>
{/* 待处理角标 */}
{card.showBadge && getBadgeCount(card.key) > 0 && (
<View className='absolute -top-1 -right-1 min-w-[20px] h-[20px] rounded-full bg-red-500 flex items-center justify-center px-1.5'>
<Text className='text-white text-[11px] font-bold'>
{getBadgeCount(card.key) > 99 ? '99+' : getBadgeCount(card.key)}
</Text>
</View>
)}
{/* 箭头 */}
<Text className='text-gray-300 text-lg flex-shrink-0'></Text>
</View>
))}
</View>
</View>
{/* 底部提示 */}
<View className='mx-3 mt-6 mb-4'>
{/* 订阅消息提醒 */}
<View
className='bg-white rounded-xl p-4 flex items-center gap-3 active:opacity-80 mb-3'
onClick={handleSubscribe}
>
<View className='w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center flex-shrink-0'>
<Text className='text-xl'>🔔</Text>
</View>
<View className='flex-1 min-w-0'>
<Text className='text-gray-800 text-sm font-medium block'></Text>
<Text className='text-gray-400 text-xs mt-0.5 block'>
</Text>
</View>
<Text className='text-orange-500 text-sm flex-shrink-0'></Text>
</View>
<Text className='text-gray-300 text-xs text-center block'>
线
</Text>
</View>
</View>
)
}