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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user