feat(auth): 添加iframe轮播消息支持并优化地址簿和订单状态处理

- 添加HostWheelMessage类型定义和IFRAME_WHEEL消息常量
- 实现iframe轮播事件监听和消息发送功能
- 移除address.type.ts中的region和region_text字段
- 更新英文本地化文件中的completed状态为Approved
- 添加订单状态通知相关的国际化文案
- 更新常量文件中的订单状态描述注释
- 在index.ts中添加HostWheelMessage和OrderStatusCode类型
- 修改record页面的订单状态处理逻辑
- 实现订单状态变更通知弹窗功能
- 优化地址簿获取地址文本的方法
- 修复地址列表API响应数据访问问题
- 添加playx.html测试页面和相关配置文件
This commit is contained in:
JiaJun
2026-07-27 14:23:45 +08:00
parent 8002650b95
commit 3eb03da0da
12 changed files with 442 additions and 61 deletions

View File

@@ -1,4 +1,4 @@
import {useState} from 'react'
import {useCallback, useEffect, useRef, useState} from 'react'
import {useQuery} from '@tanstack/react-query'
import {useTranslation} from 'react-i18next'
@@ -9,7 +9,7 @@ import { cn } from '@/lib'
import {MotionButton, MotionDiv, tapMotionPropsIcon, tapMotionPropsSoft} from '@/lib/motion'
import Modal from '@/components/modal'
import Button from '@/components/button'
import type { OrderCardProps, OrderRecord, PointsCardProps, RecordButtonType, TabButtonProps } from '@/types'
import type { OrderCardProps, OrderRecord, OrderStatusCode, PointsCardProps, RecordButtonType, TabButtonProps } from '@/types'
import {Link} from 'react-router-dom'
import { ArrowLeft, ChevronRight, Coins, PackageSearch } from 'lucide-react'
import {orders, pointsLogs} from '@/api/business.ts'
@@ -22,6 +22,16 @@ const pointsRecordToneClassName = {
negative: 'bg-[#FF9BA4] text-[#7B2634]',
} as const
const ORDER_POLL_INTERVAL_MS = 10_000
const EMPTY_ORDER_RECORDS: OrderRecord[] = []
type OrderStatusNotice = {
id: string
title: string
orderNumber?: string
statusCode: Extract<OrderStatusCode, 'COMPLETED' | 'REJECTED'>
}
function formatDatePart(value: number) {
return String(value).padStart(2, '0')
}
@@ -59,48 +69,53 @@ function toTitleCase(value: string) {
.join(' ')
}
function getOrderStatus(status?: string | number) {
function normalizeOrderStatus(status?: string | number): OrderStatusCode {
if (typeof status === 'string') {
const normalizedStatus = status.trim().toUpperCase()
const matchedStatus = ORDER_STATUS.find((item) => item === normalizedStatus)
if (matchedStatus) {
switch (matchedStatus) {
case 'PENDING':
return i18n.t('record.statusLabel.pending')
case 'COMPLETED':
return i18n.t('record.statusLabel.completed')
case 'SHIPPED':
return i18n.t('record.statusLabel.shipped')
case 'REJECTED':
return i18n.t('record.statusLabel.rejected')
}
if (ORDER_STATUS.includes(normalizedStatus)) {
return normalizedStatus as OrderStatusCode
}
}
const normalizedStatus = typeof status === 'string' && /^\d+$/.test(status)
? Number(status)
const normalizedStatus = typeof status === 'string' && /^\d+$/.test(status.trim())
? Number(status.trim())
: status
if (typeof normalizedStatus === 'number') {
switch (normalizedStatus) {
case 0:
return i18n.t('record.statusLabel.pending')
return 'PENDING'
case 1:
return i18n.t('record.statusLabel.completed')
return 'COMPLETED'
case 2:
return i18n.t('record.statusLabel.shipped')
return 'SHIPPED'
case 3:
return i18n.t('record.statusLabel.rejected')
return 'REJECTED'
default:
return String(normalizedStatus)
return 'UNKNOWN'
}
}
if (!normalizedStatus) {
return i18n.t('record.statusLabel.pending')
}
return 'UNKNOWN'
}
return toTitleCase(normalizedStatus)
function getOrderStatusLabel(statusCode: OrderStatusCode, fallbackStatus?: string | number) {
switch (statusCode) {
case 'PENDING':
return i18n.t('record.statusLabel.pending')
case 'COMPLETED':
return i18n.t('record.statusLabel.completed')
case 'SHIPPED':
return i18n.t('record.statusLabel.shipped')
case 'REJECTED':
return i18n.t('record.statusLabel.rejected')
case 'UNKNOWN':
if (!fallbackStatus) {
return i18n.t('record.statusLabel.pending')
}
return typeof fallbackStatus === 'number' ? String(fallbackStatus) : toTitleCase(fallbackStatus)
}
}
function getOrderCategory(item: OrderItem) {
@@ -175,6 +190,7 @@ function mapOrderItemToRecord(item: OrderItem): OrderRecord {
const { date, time } = getDateTimeParts(item.create_time ?? item.created_at)
const orderNumber = item.external_transaction_id?.trim() || item.order_no ? String(item.external_transaction_id?.trim() || item.order_no) : undefined
const points = getOrderPoints(item)
const statusCode = normalizeOrderStatus(item.status)
return {
id: String(item.id),
@@ -184,7 +200,8 @@ function mapOrderItemToRecord(item: OrderItem): OrderRecord {
category: getOrderCategory(item),
title: item.item_title ?? item.title ?? item.mallItem?.title ?? i18n.t('record.untitledOrder'),
trackingNumber: getTrackingNumber(item),
status: getOrderStatus(item.status),
status: getOrderStatusLabel(statusCode, item.status),
statusCode,
points: points.display,
}
}
@@ -244,25 +261,19 @@ function mapPointsLogItemToRecord(item: PointsLogItem) {
}
}
function getOrderStatusClassName(status: string) {
const normalizedStatus = status.toLowerCase()
if (normalizedStatus === 'completed' || status === i18n.t('record.statusLabel.completed')) {
return 'bg-[#9BFFC0] text-[#176640]'
function getOrderStatusClassName(statusCode: OrderStatusCode) {
switch (statusCode) {
case 'COMPLETED':
return 'bg-[#9BFFC0] text-[#176640]'
case 'SHIPPED':
return 'bg-[#95F0FF] text-[#116A79]'
case 'PENDING':
return 'bg-[#FFF18C] text-[#7F6A0D]'
case 'REJECTED':
return 'bg-[#FFB1C0] text-[#7C2941]'
case 'UNKNOWN':
return 'bg-white/15 text-white/80'
}
if (normalizedStatus === 'shipped' || status === i18n.t('record.statusLabel.shipped')) {
return 'bg-[#95F0FF] text-[#116A79]'
}
if (normalizedStatus === 'pending' || status === i18n.t('record.statusLabel.pending')) {
return 'bg-[#FFF18C] text-[#7F6A0D]'
}
if (normalizedStatus === 'rejected' || status === i18n.t('record.statusLabel.rejected')) {
return 'bg-[#FFB1C0] text-[#7C2941]'
}
return 'bg-white/15 text-white/80'
}
function TabButton({ active, label, icon: Icon, onClick }: TabButtonProps) {
@@ -314,7 +325,7 @@ function OrderCard({ record, onOpenDetails }: OrderCardProps) {
<div
className={cn(
'rounded-[6px] px-[8px] py-[3px] text-[11px] leading-none',
getOrderStatusClassName(record.status),
getOrderStatusClassName(record.statusCode),
)}
>
{record.status}
@@ -352,24 +363,87 @@ function PointsCard({ record }: PointsCardProps) {
function OrdersTabContent({
sessionId,
onOpenDetails,
onOrdersChange,
onStatusNotices,
}: {
sessionId: string
onOpenDetails: (record: OrderRecord) => void
onOrdersChange: (records: OrderRecord[]) => void
onStatusNotices: (notices: OrderStatusNotice[]) => void
}) {
const {t} = useTranslation()
const initializedRef = useRef(false)
const pendingOrderIdsRef = useRef<Set<string>>(new Set())
const ordersQuery = useQuery({
queryKey: queryKeys.orders(sessionId),
enabled: Boolean(sessionId),
gcTime: 0,
refetchInterval: sessionId ? ORDER_POLL_INTERVAL_MS : false,
refetchIntervalInBackground: true,
queryFn: async () => {
const response = await orders({
session_id: sessionId,
})
return (response.data.list ?? []).map(mapOrderItemToRecord)
return (response.data?.list ?? []).map(mapOrderItemToRecord)
},
})
const orderRecords = ordersQuery.data ?? []
const orderRecords = ordersQuery.data ?? EMPTY_ORDER_RECORDS
useEffect(() => {
initializedRef.current = false
pendingOrderIdsRef.current = new Set()
}, [sessionId])
useEffect(() => {
if (!ordersQuery.isSuccess) {
return
}
onOrdersChange(orderRecords)
if (!initializedRef.current) {
pendingOrderIdsRef.current = new Set(
orderRecords
.filter((record) => record.statusCode === 'PENDING')
.map((record) => record.id),
)
initializedRef.current = true
return
}
const pendingOrderIds = pendingOrderIdsRef.current
const nextPendingOrderIds = new Set(pendingOrderIds)
const notices: OrderStatusNotice[] = []
orderRecords.forEach((record) => {
if (record.statusCode === 'PENDING') {
nextPendingOrderIds.add(record.id)
return
}
if (!pendingOrderIds.has(record.id)) {
return
}
nextPendingOrderIds.delete(record.id)
if (record.statusCode === 'COMPLETED' || record.statusCode === 'REJECTED') {
notices.push({
id: record.id,
title: record.title,
orderNumber: record.orderNumber,
statusCode: record.statusCode,
})
}
})
pendingOrderIdsRef.current = nextPendingOrderIds
if (notices.length) {
onStatusNotices(notices)
}
}, [orderRecords, ordersQuery.isSuccess, onOrdersChange, onStatusNotices])
if (!sessionId) {
return (
@@ -456,11 +530,31 @@ function RecordPage() {
const sessionId = useUserStore((state) => state.authInfo?.session_id ?? '')
const [tab, setTab] = useState<RecordButtonType>('order')
const [selectedOrder, setSelectedOrder] = useState<OrderRecord | null>(null)
const [orderStatusNoticeQueue, setOrderStatusNoticeQueue] = useState<OrderStatusNotice[]>([])
const currentOrderStatusNotice = orderStatusNoticeQueue[0]
const handleCloseDetails = () => {
setSelectedOrder(null)
}
const handleOrdersChange = useCallback((records: OrderRecord[]) => {
setSelectedOrder((currentOrder) => {
if (!currentOrder) {
return null
}
return records.find((record) => record.id === currentOrder.id) ?? currentOrder
})
}, [])
const handleStatusNotices = useCallback((notices: OrderStatusNotice[]) => {
setOrderStatusNoticeQueue((currentQueue) => [...currentQueue, ...notices])
}, [])
const handleCloseOrderStatusNotice = () => {
setOrderStatusNoticeQueue((currentQueue) => currentQueue.slice(1))
}
return (
<PageLayout contentClassName="flex h-[100svh] w-full flex-col overflow-hidden px-4 pb-8 sm:px-6 lg:px-8">
<div className="mx-auto w-full max-w-[980px]">
@@ -510,7 +604,13 @@ function RecordPage() {
<div className="mt-[14px] min-h-0 flex-1 overflow-y-auto">
<div className="mx-auto w-full max-w-[860px]">
{tab === 'order' ? (
<OrdersTabContent key="order" sessionId={sessionId} onOpenDetails={setSelectedOrder} />
<OrdersTabContent
key="order"
sessionId={sessionId}
onOpenDetails={setSelectedOrder}
onOrdersChange={handleOrdersChange}
onStatusNotices={handleStatusNotices}
/>
) : (
<PointsTabContent key="record" sessionId={sessionId} />
)}
@@ -551,6 +651,38 @@ function RecordPage() {
</div>
) : null}
</Modal>
<Modal
open={Boolean(currentOrderStatusNotice)}
title={currentOrderStatusNotice?.statusCode === 'COMPLETED'
? t('record.statusNotice.approvedTitle')
: t('record.statusNotice.rejectedTitle')}
onClose={handleCloseOrderStatusNotice}
className="max-w-[420px]"
bodyClassName="pt-[0px]"
footer={
<Button
type="button"
className="h-[36px] w-full sm:min-w-[94px] sm:w-auto"
onClick={handleCloseOrderStatusNotice}
>
{t('record.statusNotice.acknowledge')}
</Button>
}
>
{currentOrderStatusNotice ? (
<div className="mt-[10px] rounded-[10px] bg-[#1C1818]/78 px-[12px] py-[16px] text-[14px] leading-[1.65] text-white">
<div>
{currentOrderStatusNotice.statusCode === 'COMPLETED'
? t('record.statusNotice.approvedDescription')
: t('record.statusNotice.rejectedDescription')}
</div>
<div className="mt-[10px] text-white/58">
{currentOrderStatusNotice.orderNumber ?? currentOrderStatusNotice.title}
</div>
</div>
) : null}
</Modal>
</PageLayout>
)
}