From 8ca893efabdbdfe15470a30b9311966d018514a5 Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Wed, 5 Aug 2026 23:03:41 +0800 Subject: [PATCH] feat(mini-user): show shipping logistics preview and detail timeline (v3.4.13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Order detail displays latest XFX route under 配送中 with link to full timeline page; docs updated. Co-authored-by: Cursor --- apps/mini-user/src/lib/order-logistics.ts | 56 +++++++++++ .../src/pages/order-detail/index.tsx | 57 ++++++++++- .../src/pages/order-logistics/index.tsx | 94 ++++++++++--------- apps/mini-user/src/styles/order.css | 91 ++++++++++++++++++ 杜康好客-v3-PRD.md | 2 +- 杜康好客-v3-现状对照.md | 2 +- 杜康好客-v3.4.13-体验优化开发文档.md | 23 +++-- 7 files changed, 270 insertions(+), 55 deletions(-) create mode 100644 apps/mini-user/src/lib/order-logistics.ts diff --git a/apps/mini-user/src/lib/order-logistics.ts b/apps/mini-user/src/lib/order-logistics.ts new file mode 100644 index 0000000..bc2967a --- /dev/null +++ b/apps/mini-user/src/lib/order-logistics.ts @@ -0,0 +1,56 @@ +import { isXfxProviderCode } from '@dukang/shared-types'; +import { formatShanghaiDateTime } from './datetime'; +import { request } from './api'; + +export type OrderDelivery = { + provider?: string; + trackingNo?: string; + logisticsCompany?: string; + manualQueryUrl?: string; +}; + +export type OrderTrackNode = { + trackInfo?: string; + createTime?: string; + createdAt?: string; + statusName?: string; +}; + +export type OrderTrackResponse = { + nodes?: OrderTrackNode[]; + manualQueryUrl?: string | null; + provider?: string; + trackingNo?: string; + logisticsCompany?: string; +}; + +/** 订单详情页展示物流摘要的状态(配送中) */ +export const ORDER_LOGISTICS_PREVIEW_STATUSES = ['SHIPPING', 'SHIPPED'] as const; + +export function deliveryProviderLabel(provider?: string, company?: string) { + if (company) return company; + if (!provider || provider === 'MOCK' || isXfxProviderCode(provider)) { + return '小飞侠配送'; + } + if (provider === 'LOGISTICS') return '快递配送'; + return provider; +} + +export function shouldLoadOrderTrack(delivery?: OrderDelivery | null) { + if (!delivery) return false; + if (delivery.trackingNo) return true; + return isXfxProviderCode(delivery.provider || ''); +} + +export function formatTrackNodeTime(node: OrderTrackNode) { + return formatShanghaiDateTime(node.createTime || node.createdAt); +} + +export function latestTrackNode(nodes: OrderTrackNode[]) { + if (nodes.length === 0) return null; + return nodes[0]; +} + +export async function fetchOrderTrack(orderId: string) { + return request(`/trade/orders/${orderId}/track`); +} diff --git a/apps/mini-user/src/pages/order-detail/index.tsx b/apps/mini-user/src/pages/order-detail/index.tsx index 69eb403..5e26fed 100644 --- a/apps/mini-user/src/pages/order-detail/index.tsx +++ b/apps/mini-user/src/pages/order-detail/index.tsx @@ -8,6 +8,13 @@ import WechatShareReady from '../../components/WechatShareReady'; import ContactCsButton from '../../components/ContactCsButton'; import { request, toast } from '../../lib/api'; import { buildPayUrl } from '../../lib/checkout-nav'; +import { + fetchOrderTrack, + latestTrackNode, + ORDER_LOGISTICS_PREVIEW_STATUSES, + shouldLoadOrderTrack, + type OrderTrackNode, +} from '../../lib/order-logistics'; import { maskPhone } from '../../lib/phone'; import { confirmOrderReceive, type WechatConfirmPayload } from '../../lib/wechat-order-confirm'; import { @@ -85,11 +92,32 @@ export default function OrderDetailPage() { usePageView('order_detail_view', orderId ? { orderId } : undefined); const [order, setOrder] = useState(null); const [confirming, setConfirming] = useState(false); + const [latestTrack, setLatestTrack] = useState(null); + const [trackLoading, setTrackLoading] = useState(false); + + async function loadOrderTrack(delivery: OrderDetail['delivery']) { + if (!orderId || !shouldLoadOrderTrack(delivery)) { + setLatestTrack(null); + return; + } + setTrackLoading(true); + try { + const track = await fetchOrderTrack(orderId); + setLatestTrack(latestTrackNode(track.nodes ?? [])); + } catch { + setLatestTrack(null); + } finally { + setTrackLoading(false); + } + } useEffect(() => { if (!orderId) return; request(`/trade/orders/${orderId}`) - .then(setOrder) + .then((data) => { + setOrder(data); + void loadOrderTrack(data.delivery); + }) .catch((e) => toast(e instanceof Error ? e.message : '加载失败')); }, [orderId]); @@ -97,7 +125,10 @@ export default function OrderDetailPage() { if (!orderId) return; // 从微信确认收货组件返回后刷新 request(`/trade/orders/${orderId}`) - .then(setOrder) + .then((data) => { + setOrder(data); + void loadOrderTrack(data.delivery); + }) .catch(() => {}); }); @@ -113,6 +144,11 @@ export default function OrderDetailPage() { ['PENDING_SHIP', 'OUT_WAREHOUSE', 'SHIPPING', 'SHIPPED', 'PENDING_RECEIVE', 'DELIVERED', 'COMPLETED'].includes( order.status || '', ); + const showLogisticsPreview = + canViewLogistics && + ORDER_LOGISTICS_PREVIEW_STATUSES.includes( + (order?.status || '') as (typeof ORDER_LOGISTICS_PREVIEW_STATUSES)[number], + ); const item = order?.items?.[0]; const productName = item?.productName || order?.productName || '杜康商品'; @@ -230,7 +266,22 @@ export default function OrderDetailPage() { {isProxy && order.proxyPartnerName ? ( 由合伙人 {order.proxyPartnerName} 代下 ) : null} - {canViewLogistics ? ( + {showLogisticsPreview ? ( + + + {trackLoading ? ( + 物流信息加载中… + ) : latestTrack?.trackInfo ? ( + {latestTrack.trackInfo} + ) : order.delivery?.manualQueryUrl ? ( + 物流已更新,点击查看详情 + ) : ( + 物流信息待更新,请稍后查看 + )} + + 物流详情 + + ) : canViewLogistics ? ( 查看物流追踪 diff --git a/apps/mini-user/src/pages/order-logistics/index.tsx b/apps/mini-user/src/pages/order-logistics/index.tsx index 3ec531c..06534b0 100644 --- a/apps/mini-user/src/pages/order-logistics/index.tsx +++ b/apps/mini-user/src/pages/order-logistics/index.tsx @@ -4,20 +4,16 @@ import Taro, { useRouter } from '@tarojs/taro'; import PageShell from '../../components/PageShell'; import SubPageHeader from '../../components/SubPageHeader'; import { request, toast } from '../../lib/api'; +import { + deliveryProviderLabel, + fetchOrderTrack, + formatTrackNodeTime, + shouldLoadOrderTrack, + type OrderDelivery, + type OrderTrackNode, +} from '../../lib/order-logistics'; import { usePageView } from '../../lib/usePageView'; -type OrderDelivery = { - provider?: string; - trackingNo?: string; - logisticsCompany?: string; - manualQueryUrl?: string; -}; - -type TrackNode = { - trackInfo?: string; - createdAt?: string; -}; - type OrderDetail = { id: string; orderNo?: string; @@ -35,25 +31,13 @@ const STATUS_LABELS: Record = { COMPLETED: '已完成', }; -function deliveryProviderLabel(provider?: string, company?: string) { - if (company) return company; - if (!provider || provider === 'MOCK' || provider === 'XFX' || provider === 'XIAOFEIXIA') { - return '小飞侠配送'; - } - if (provider === 'LOGISTICS') return '快递配送'; - return provider; -} - -function formatDateTime(value: string) { - return String(value).slice(0, 19).replace('T', ' '); -} - export default function OrderLogisticsPage() { const router = useRouter(); const orderId = router.params.id ?? ''; usePageView('order_logistics_view', orderId ? { orderId } : undefined); const [order, setOrder] = useState(null); - const [trackNodes, setTrackNodes] = useState([]); + const [trackNodes, setTrackNodes] = useState([]); + const [trackLoading, setTrackLoading] = useState(false); useEffect(() => { if (!orderId) return; @@ -63,13 +47,18 @@ export default function OrderLogisticsPage() { const data = await request(`/trade/orders/${orderId}`); if (cancelled) return; setOrder(data); - if (data.delivery?.trackingNo || data.delivery?.provider === 'XFX') { - try { - const track = await request<{ nodes?: TrackNode[] }>(`/trade/orders/${orderId}/track`); - if (!cancelled) setTrackNodes(track.nodes ?? []); - } catch { - if (!cancelled) setTrackNodes([]); - } + if (!shouldLoadOrderTrack(data.delivery)) { + setTrackNodes([]); + return; + } + setTrackLoading(true); + try { + const track = await fetchOrderTrack(orderId); + if (!cancelled) setTrackNodes(track.nodes ?? []); + } catch { + if (!cancelled) setTrackNodes([]); + } finally { + if (!cancelled) setTrackLoading(false); } } catch (e) { if (!cancelled) toast(e instanceof Error ? e.message : '加载失败'); @@ -99,7 +88,7 @@ export default function OrderLogisticsPage() { return ( - + {!order ? ( 加载中… @@ -139,17 +128,36 @@ export default function OrderLogisticsPage() { ) : null} - {trackNodes.length > 0 ? ( + {trackLoading ? ( 物流动态 - {trackNodes.map((node, index) => ( - - - {node.createdAt ? formatDateTime(node.createdAt) : '—'} - - {node.trackInfo || '—'} - - ))} + 加载中… + + ) : trackNodes.length > 0 ? ( + + 物流动态 + + {trackNodes.map((node, index) => ( + + + + {node.statusName ? ( + {node.statusName} + ) : null} + {node.trackInfo || '—'} + {formatTrackNodeTime(node)} + + + ))} + + + ) : shouldLoadOrderTrack(delivery) ? ( + + 物流动态 + 暂无路由信息,请稍后刷新 ) : null} diff --git a/apps/mini-user/src/styles/order.css b/apps/mini-user/src/styles/order.css index 5d0eebe..cdf2c7a 100644 --- a/apps/mini-user/src/styles/order.css +++ b/apps/mini-user/src/styles/order.css @@ -501,10 +501,101 @@ color: var(--color-heritage-red); } +.order-logistics-preview { + display: flex; + align-items: center; + gap: 12px; + margin-top: 12px; + padding: 12px; + border-radius: 10px; + background: rgba(166, 29, 36, 0.04); +} + +.order-logistics-preview-main { + flex: 1; + min-width: 0; +} + +.order-logistics-preview-info { + font-size: 13px; + color: var(--color-on-surface); + line-height: 1.5; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.order-logistics-preview-link { + flex-shrink: 0; + font-size: 13px; + font-weight: 600; + color: var(--color-heritage-red); +} + .order-row-value--link { color: var(--color-heritage-red); } +.order-logistics-timeline { + padding-top: 4px; +} + +.order-logistics-timeline-item { + display: flex; + gap: 12px; + position: relative; + padding-bottom: 16px; +} + +.order-logistics-timeline-item:last-child { + padding-bottom: 0; +} + +.order-logistics-timeline-item:not(:last-child)::before { + content: ''; + position: absolute; + left: 5px; + top: 14px; + bottom: 0; + width: 1px; + background: rgba(0, 0, 0, 0.08); +} + +.order-logistics-timeline-dot { + flex-shrink: 0; + width: 11px; + height: 11px; + margin-top: 4px; + border-radius: 50%; + background: var(--color-surface-container-high, #e8e4df); + border: 2px solid #fff; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08); +} + +.order-logistics-timeline-item--latest .order-logistics-timeline-dot { + background: var(--color-heritage-red); + box-shadow: 0 0 0 1px var(--color-heritage-red); +} + +.order-logistics-timeline-body { + flex: 1; + min-width: 0; +} + +.order-logistics-timeline-status { + display: block; + font-size: 13px; + font-weight: 600; + color: var(--color-on-surface); + margin-bottom: 4px; +} + +.order-logistics-timeline-item--latest .order-logistics-timeline-status { + color: var(--color-heritage-red); +} + .order-logistics-node { padding: 10px 0; border-bottom: 1px solid rgba(0, 0, 0, 0.06); diff --git a/杜康好客-v3-PRD.md b/杜康好客-v3-PRD.md index 93486b9..dad5b67 100644 --- a/杜康好客-v3-PRD.md +++ b/杜康好客-v3-PRD.md @@ -48,7 +48,7 @@ | 版本 | 日期 | 说明 | |------|------|------| -| **3.4.13** | 2026-08-05 | 推广码归因统计、核销用户信息、技术支持工单优先级、mini-user 门店/商品/提货/版本/物流体验、H5 登录校验、合伙人微信暂停禁登、**OSS 图片超 10MB 客户端压缩**;开发设计见 [`杜康好客-v3.4.13-体验优化开发文档.md`](./杜康好客-v3.4.13-体验优化开发文档.md) | +| **3.4.13** | 2026-08-05 | 推广码归因统计、核销用户信息、技术支持工单优先级、mini-user 门店/商品/提货/版本/**配送中物流摘要与详情时间线**、H5 登录校验、合伙人微信暂停禁登、**OSS 图片超 10MB 客户端压缩**;开发设计见 [`杜康好客-v3.4.13-体验优化开发文档.md`](./杜康好客-v3.4.13-体验优化开发文档.md) | --- diff --git a/杜康好客-v3-现状对照.md b/杜康好客-v3-现状对照.md index fa10ccf..6855998 100644 --- a/杜康好客-v3-现状对照.md +++ b/杜康好客-v3-现状对照.md @@ -341,7 +341,7 @@ C2~C7、C14 见 §1.3。 | H5 登录前端校验 | ✅ | partner/shop LoginPage | | mini-user 门店体验 | ✅ | 电话脱敏/埋点、门头 aspectFit、套餐折叠 | | mini-user 商品/提货 | ✅ | 去分享、首图 preview、提货确认弹框 | -| mini-user 版本/物流 | ✅ | minClientVersion + UpdateManager;order-logistics | +| mini-user 版本/物流 | ✅ | minClientVersion + UpdateManager;配送中最新路由摘要 + 物流详情时间线(小飞侠 `/track`) | | OSS 大图压缩 | ✅ | shared-ui compressImage;四端 upload + mini 头像 | | 文档 | ✅ | PRD §0.5 + v3.4.13 开发文档 | diff --git a/杜康好客-v3.4.13-体验优化开发文档.md b/杜康好客-v3.4.13-体验优化开发文档.md index 37b691a..1fa8796 100644 --- a/杜康好客-v3.4.13-体验优化开发文档.md +++ b/杜康好客-v3.4.13-体验优化开发文档.md @@ -8,7 +8,7 @@ | 模块 | 内容 | |------|------| | admin-web | 推广码 attributionCount;核销记录用户信息;技术支持工单优先级;**大图上传前压缩** | -| mini-user | 门店电话脱敏+拨打埋点;门头/套餐展示;商品去分享+首图 preview;提货确认弹框;版本更新提示;物流追踪页;**头像超 10MB 压缩** | +| mini-user | 门店电话脱敏+拨打埋点;门头/套餐展示;商品去分享+首图 preview;提货确认弹框;版本更新提示;**订单详情配送中物流摘要+详情页时间线**;**头像超 10MB 压缩** | | h5-partner / h5-shop / h5-user | 登录 phone/code 前端校验;**OSS 图片超 10MB 自动 canvas 压缩后上传** | | 后端 | 工单 priority 字段;client-config minClientVersion;合伙人微信登录暂停拦截 | @@ -34,7 +34,7 @@ | ST1785901775231811 | 门店套餐遮挡 | ✅ 折叠/行数限制 | | ST1785901711627824 | SHOP login 校验 | ✅ 前端空字段拦截 | | ST1785901314893145 | 门头照裁剪 | ✅ aspectFit + preview | -| ST1785939375449985 | 订单物流追踪页 | ✅ order-logistics | +| ST1785939375449985 | 订单物流追踪页 | ✅ 配送中摘要 + 物流详情页时间线(小飞侠路由) | | — | 上传图片超 10MB 先压缩 | ✅ `@dukang/shared-ui/compressImage` + 各端 upload | ## 3. 后端 API / 配置 @@ -44,23 +44,32 @@ | GET | `/common/client-config` | 新增 `minClientVersion`(env `MINI_USER_MIN_VERSION`) | | — | `CommonSupportTicket.priority` | `LOW \| NORMAL \| HIGH \| URGENT`,默认 NORMAL | | — | `loginPartnerWechat` | 非 ACTIVE 账号抛出「合伙人账号已停用」 | -| GET | `/trade/orders/:id/track` | 物流追踪页复用 | +| GET | `/trade/orders/:id/track` | 物流路由;小飞侠(`XFX`/`XIAOFEIXIA`)走小飞侠 TRACK_ROUTE,其他承运商返回 manualQueryUrl | -## 4. 数据表 +### 4. mini-user 物流(ST1785939375449985) + +| 页面 | 行为 | +|------|------| +| 订单详情 | 状态为**配送中**(`SHIPPING`/`SHIPPED`)时,状态下方展示最新一条路由信息;右侧「物流详情」进入详情页 | +| 物流详情 | 配送方式、运单号、完整物流动态时间线(状态名 + 描述 + 时间);最新节点高亮 | +| 接口 | `GET /trade/orders/:id` + `GET /trade/orders/:id/track`;`shouldLoadOrderTrack`:有运单号或小飞侠 provider 时拉路由 | +| 公共模块 | `apps/mini-user/src/lib/order-logistics.ts` | + +## 5. 数据表 - `common_support_ticket.priority` ENUM,默认 `NORMAL` -## 5. HQ 开发计划 +## 6. HQ 开发计划 在 admin-web **开发计划 → 版本列表** 创建 `v3.4.13`(状态 `IN_PROGRESS`),审批 ST 后关联 `dev_plan_task`。 -## 6. 验收 ACC +## 7. 验收 ACC - [ ] 推广码详情展示 attributionCount - [ ] 核销记录含 userNo/nickname/phone - [ ] 技术支持可创建/筛选/编辑优先级 - [ ] 合伙人/门店登录空字段前端提示;暂停合伙人微信登录被拒 - [ ] mini-user:电话脱敏、拨打埋点、提货确认、无分享按钮、首图 preview、门头/套餐正常 -- [ ] mini-user:低版本弹窗、UpdateManager、物流追踪页 +- [ ] mini-user:低版本弹窗、UpdateManager;配送中展示最新物流 + 物流详情页完整时间线(小飞侠路由) - [ ] 各端上传 >10MB 图片自动压缩后可上传;仍超限有明确报错 - [ ] `pnpm lint` 无新增错误