diff --git a/.workbuddy/memory/2026-07-04.md b/.workbuddy/memory/2026-07-04.md index 7722d5f..20cc3e0 100644 --- a/.workbuddy/memory/2026-07-04.md +++ b/.workbuddy/memory/2026-07-04.md @@ -74,3 +74,44 @@ ### 涉及文件(补充) - 修改:`src/pages/shop/product-detail.tsx` + +--- + +### 10. 新订单提醒功能(前端轮询 + 订阅消息) + +#### 10.1 创建 useNewOrderDetector Hook +- 新建 `src/hooks/useNewOrderDetector.ts` +- 封装新订单轮询检测逻辑: + - 首次加载自动建立订单 ID 基准线,不触发提醒 + - 后续轮询对比发现新订单后触发 `onNewOrders` 回调 + - 页面隐藏时自动暂停轮询,显示时恢复 + - 提供 `clearUnread()` 方法清除未读计数 +- 导出类型:`NewOrderItem`、`UseNewOrderDetectorOptions`、`UseNewOrderDetectorReturn` +- 注册到 `src/hooks/index.ts` + +#### 10.2 改造订单管理页(前端轮询) +- 修改 `src/pages/store/orders/index.tsx` +- 集成 `useNewOrderDetector`:30 秒轮询,静默检测新订单 +- 发现新订单时:震动提醒(`Taro.vibrateShort`)+ Toast 提示("您有 N 条新订单") +- "全部" Tab 增加红色角标显示未读新订单数(>99 显示 99+) +- 切换 Tab 时自动清除未读计数 + +#### 10.3 改造门店中心页 +- 修改 `src/pages/store/center/index.tsx` +- 订单管理卡片增加待处理订单角标(查询 statusFilter=1 的 total) +- VIP 审核角标同步增强,统一使用 `getBadgeCount()` 方法 +- 底部新增"接收新订单提醒"卡片: + - 调用 `Taro.requestSubscribeMessage` 请求微信订阅消息授权 + - 模板 ID 通过 `SUBSCRIBE_TMPL_IDS` 常量配置(待微信公众平台申请后填入) + - 授权成功后 Toast 提示 + +#### 待后端配合 +- 微信公众平台申请订阅消息模板("新订单通知"类目) +- 将模板 ID 填入 `store/center/index.tsx` 的 `SUBSCRIBE_TMPL_IDS` 数组 +- 后端在订单创建成功后调用 `subscribeMessage.send` API 向店员推送 + +#### 涉及文件 +- 新建:`src/hooks/useNewOrderDetector.ts` +- 修改:`src/hooks/index.ts` +- 修改:`src/pages/store/orders/index.tsx` +- 修改:`src/pages/store/center/index.tsx` diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index f7ee4ff..bd7afa5 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -25,3 +25,9 @@ - VIP 审核页:`src/pages/user/vip-review/index.tsx` - VIP 升级页:`src/pages/user/vip-upgrade/index.tsx` - 门店中心:`src/pages/store/center/index.tsx` + +## 订阅消息 +- 模板 ID:`sh1K9iK7vZjebUNFu6OsMsnsJxm4whThWGrhN7I4zVg`(交易提醒) +- 字段:订单号(`character_string1`)、商品名称(`thing4`)、联系人(`thing10`)、联系电话(`phone_number7`)、送货地址(`thing8`) +- 场景说明:下单提醒 +- 新订单检测 Hook:`src/hooks/useNewOrderDetector.ts`(30秒轮询,页面隐藏暂停) diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 4f2ea4e..3600e31 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -26,3 +26,5 @@ export { useCoupon } from './useCoupon' export { usePagination } from './usePagination' export { usePayment } from './usePayment' export { useScrollHeight } from './useScrollHeight' +export { useNewOrderDetector } from './useNewOrderDetector' +export type { NewOrderItem, UseNewOrderDetectorOptions, UseNewOrderDetectorReturn } from './useNewOrderDetector' diff --git a/src/hooks/useNewOrderDetector.ts b/src/hooks/useNewOrderDetector.ts new file mode 100644 index 0000000..5c8d55c --- /dev/null +++ b/src/hooks/useNewOrderDetector.ts @@ -0,0 +1,129 @@ +import { useRef, useState, useCallback } from 'react' +import { useDidShow, useDidHide } from '@tarojs/taro' + +export interface NewOrderItem { + orderId?: number + orderNo?: string + [key: string]: unknown +} + +export interface UseNewOrderDetectorOptions { + /** 轮询间隔(毫秒),默认 30000(30秒) */ + interval?: number + /** 获取最新订单列表(按时间降序),返回前几条用于对比 */ + fetchLatestOrders: () => Promise + /** 发现新订单时的回调 */ + onNewOrders?: (count: number, newOrders: NewOrderItem[]) => void +} + +export interface UseNewOrderDetectorReturn { + /** 未读新订单数 */ + newOrderCount: number + /** 新订单列表 */ + newOrders: NewOrderItem[] + /** 清除未读计数(用户查看后调用) */ + clearUnread: () => void + /** 手动触发一次检测 */ + checkNow: () => Promise +} + +/** + * 新订单轮询检测 Hook + * + * 用于门店订单管理页面,轮询检测是否有新订单产生。 + * 页面显示时自动启动轮询,隐藏时暂停,避免无效请求。 + * + * @example + * const { newOrderCount, newOrders, clearUnread } = useNewOrderDetector({ + * fetchLatestOrders: () => pageShopOrder({ page: 1, limit: 5 }), + * onNewOrders: (count) => Taro.vibrateShort({ type: 'medium' }), + * }) + */ +export function useNewOrderDetector( + options: UseNewOrderDetectorOptions, +): UseNewOrderDetectorReturn { + const { interval = 30000, fetchLatestOrders, onNewOrders } = options + + // 已见过的所有 orderId 集合(初始化时需要先加载一次建立基准) + const knownOrderIdsRef = useRef | null>(null) + // 轮询定时器 + const timerRef = useRef | null>(null) + // 是否正在检测中(防止并发) + const detectingRef = useRef(false) + + const [newOrderCount, setNewOrderCount] = useState(0) + const [newOrders, setNewOrders] = useState([]) + + /** 执行一次检测 */ + const doCheck = useCallback(async () => { + if (detectingRef.current) return + detectingRef.current = true + + try { + const list = await fetchLatestOrders() + if (!list || list.length === 0) return + + // 首次加载:建立基准线,不触发提醒 + if (knownOrderIdsRef.current === null) { + knownOrderIdsRef.current = new Set( + list.map(o => o.orderId).filter(Boolean) as number[], + ) + return + } + + // 对比检测新订单 + const freshOrders: NewOrderItem[] = [] + const newIds: number[] = [] + + for (const order of list) { + const id = order.orderId + if (id != null && !knownOrderIdsRef.current.has(id)) { + freshOrders.push(order) + newIds.push(id) + knownOrderIdsRef.current.add(id) + } + } + + if (freshOrders.length > 0) { + setNewOrderCount(prev => prev + freshOrders.length) + setNewOrders(prev => [...freshOrders, ...prev]) + onNewOrders?.(freshOrders.length, freshOrders) + } + } catch { + // 静默失败,不影响页面正常使用 + } finally { + detectingRef.current = false + } + }, [fetchLatestOrders, onNewOrders]) + + /** 清除未读 */ + const clearUnread = useCallback(() => { + setNewOrderCount(0) + setNewOrders([]) + }, []) + + /** 手动触发检测 */ + const checkNow = useCallback(async () => { + await doCheck() + }, [doCheck]) + + // 页面显示时:初始化基准线 + 启动轮询 + useDidShow(() => { + // 首次加载建立基准(不触发提醒) + doCheck() + + // 启动定时轮询 + if (timerRef.current) clearInterval(timerRef.current) + timerRef.current = setInterval(doCheck, interval) + }) + + // 页面隐藏时:停止轮询 + useDidHide(() => { + if (timerRef.current) { + clearInterval(timerRef.current) + timerRef.current = null + } + }) + + return { newOrderCount, newOrders, clearUnread, checkNow } +} diff --git a/src/pages/store/center/index.tsx b/src/pages/store/center/index.tsx index aff6453..e7e318a 100644 --- a/src/pages/store/center/index.tsx +++ b/src/pages/store/center/index.tsx @@ -1,8 +1,9 @@ -import React, { useState, useEffect } from 'react' +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: '门店中心', @@ -18,6 +19,8 @@ const FEATURE_CARDS = [ url: '/pages/store/orders/index', color: '#15803d', bgColor: '#dcfce7', + // 显示待处理订单数量角标 + showBadge: true, }, { key: 'vip-review', @@ -27,14 +30,19 @@ const FEATURE_CARDS = [ 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(null) - const [pendingCount, setPendingCount] = useState(0) + const [pendingVipCount, setPendingVipCount] = useState(0) + const [pendingOrderCount, setPendingOrderCount] = useState(0) const [loading, setLoading] = useState(true) useEffect(() => { @@ -47,8 +55,8 @@ export default function StoreCenterPage() { return } setStoreInfo(data) - // 加载待审核 VIP 申请数量 - loadPendingCount() + // 加载待审核数量 + loadBadgeCounts() }) .catch(() => { Taro.showToast({ title: '仅门店店员可访问', icon: 'none' }) @@ -57,13 +65,49 @@ export default function StoreCenterPage() { .finally(() => setLoading(false)) }, []) - const loadPendingCount = async () => { + /** 加载各类待处理角标数量 */ + const loadBadgeCounts = async () => { try { - const data = await listShopDealerApply({ applyStatus: 10 }) - setPendingCount((data || []).length) + // 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 ( @@ -138,10 +182,12 @@ export default function StoreCenterPage() { {card.desc} - {/* 待审核角标 */} - {card.showBadge && pendingCount > 0 && ( - - {pendingCount > 99 ? '99+' : pendingCount} + {/* 待处理角标 */} + {card.showBadge && getBadgeCount(card.key) > 0 && ( + + + {getBadgeCount(card.key) > 99 ? '99+' : getBadgeCount(card.key)} + )} @@ -154,6 +200,23 @@ export default function StoreCenterPage() { {/* 底部提示 */} + {/* 订阅消息提醒 */} + + + 🔔 + + + 接收新订单提醒 + + 开启后新订单将通过微信服务通知提醒您 + + + 去开启 + + 未来更多功能将持续上线 diff --git a/src/pages/store/orders/index.tsx b/src/pages/store/orders/index.tsx index 43cce12..1610d77 100644 --- a/src/pages/store/orders/index.tsx +++ b/src/pages/store/orders/index.tsx @@ -4,6 +4,7 @@ import Taro, {useDidShow} from '@tarojs/taro' import {pageShopOrder, updateShopOrder, removeShopOrder} from '@/api/shop/shopOrder' import type {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model' import {TenantId} from '@/config/app' +import {useNewOrderDetector} from '@/hooks/useNewOrderDetector' definePageConfig({ navigationBarTitleText: '订单管理', @@ -141,6 +142,25 @@ export default function StoreOrdersPage() { const pageSize = 10 const loadingRef = useRef(false) + // ─── 新订单轮询检测 ────────────────────────────────────────── + const { newOrderCount, clearUnread } = useNewOrderDetector({ + interval: 30000, // 30 秒轮询一次 + fetchLatestOrders: useCallback( + () => pageShopOrder({ page: 1, limit: 5 }).then(r => r?.list || []), + [], + ), + onNewOrders: useCallback((count) => { + // 震动提醒 + Taro.vibrateShort({ type: 'medium' }) + // Toast 提醒 + Taro.showToast({ + title: `您有 ${count} 条新订单`, + icon: 'none', + duration: 2500, + }) + }, []), + }) + /** 加载订单列表(待处理 Tab 会合并多个 statusFilter 的结果) */ const loadOrders = useCallback(async (tab: TabKey, pageNo: number = 1, append = false) => { if (loadingRef.current) return @@ -442,15 +462,26 @@ export default function StoreOrdersPage() { {TABS.map(tab => ( setActiveTab(tab.key)} + onClick={() => { + setActiveTab(tab.key) + clearUnread() + }} > {tab.label} + {/* 新订单角标:在"全部"Tab 上显示 */} + {tab.key === 'all' && newOrderCount > 0 && ( + + + {newOrderCount > 99 ? '99+' : newOrderCount} + + + )} ))}