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:
12
.cloudbase-sites/preview.json
Normal file
12
.cloudbase-sites/preview.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"pid": 62123,
|
||||
"port": 17175,
|
||||
"host": "0.0.0.0",
|
||||
"base": "/",
|
||||
"framework": "vite-react",
|
||||
"command": "/Users/jiaunun/.nvm/versions/node/v24.15.0/bin/node /Users/jiaunun/Desktop/JiaJun/palyX/node_modules/vite/bin/vite.js --host 0.0.0.0 --port 17175 --strictPort --base /",
|
||||
"internalUrl": "http://127.0.0.1:17175/",
|
||||
"logPath": "/Users/jiaunun/Desktop/JiaJun/palyX/.cloudbase-sites/logs/preview-1785130890070.log",
|
||||
"cwd": "/Users/jiaunun/Desktop/JiaJun/palyX",
|
||||
"startedAt": "2026-07-27T05:41:31.101Z"
|
||||
}
|
||||
105
public/playx.html
Normal file
105
public/playx.html
Normal file
@@ -0,0 +1,105 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
|
||||
>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>PlayX</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
height: calc(100% - 80px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: red;
|
||||
height: 80px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
aside {
|
||||
background-color: blue;
|
||||
height: 100%;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
section {
|
||||
background-color: yellow;
|
||||
height: 100%;
|
||||
width: calc(100% - 200px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>header</header>
|
||||
<main>
|
||||
<aside>aside</aside>
|
||||
<section id="host-scroll-region">
|
||||
<div id="iframe-container" style="height: 100%">
|
||||
<iframe
|
||||
id="playx-frame"
|
||||
src="https://playx-mall.cjdhr.top"
|
||||
width="100%"
|
||||
height="100%"
|
||||
></iframe>
|
||||
</div>
|
||||
<footer style="height: 80px; background-color: green;">footer</footer>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const frame = document.getElementById('playx-frame')
|
||||
const iframeContainer = document.getElementById('iframe-container')
|
||||
const hostScrollRegion = document.getElementById('host-scroll-region')
|
||||
|
||||
frame.addEventListener('load', () => {
|
||||
iframeContainer.style.height = '100%'
|
||||
hostScrollRegion.scrollTop = 0
|
||||
})
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.source !== frame.contentWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.data?.type === 'IFRAME_HEIGHT') {
|
||||
const height = Number(event.data?.payload?.height)
|
||||
|
||||
if (!Number.isFinite(height) || height <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
iframeContainer.style.height = `${height}px`
|
||||
return
|
||||
}
|
||||
|
||||
if (event.data?.type === 'IFRAME_WHEEL') {
|
||||
const downTotalY = Number(event.data?.payload?.downTotalY)
|
||||
|
||||
if (!Number.isFinite(downTotalY)) {
|
||||
return
|
||||
}
|
||||
|
||||
const maxScrollTop = Math.max(hostScrollRegion.scrollHeight - hostScrollRegion.clientHeight, 0)
|
||||
hostScrollRegion.scrollTop = Math.min(Math.max(downTotalY, 0), maxScrollTop)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -330,6 +330,20 @@
|
||||
scheduleHeightLog(event.data)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.source === frame.contentWindow && event.data?.type === 'IFRAME_WHEEL') {
|
||||
const downTotalY = Number(event.data?.payload?.downTotalY)
|
||||
const upTotalY = Number(event.data?.payload?.upTotalY)
|
||||
|
||||
if (!Number.isFinite(downTotalY) || !Number.isFinite(upTotalY)) {
|
||||
return
|
||||
}
|
||||
|
||||
appendLog('Received IFRAME_WHEEL from iframe', {
|
||||
downTotalY,
|
||||
upTotalY,
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -35,5 +35,5 @@ export const HOME_GOOD_TYPE_ORDER = ['WITHDRAW', 'BONUS', 'PHYSICAL'] as const s
|
||||
|
||||
export const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ?? 'https://playx-api.cjdhr.top'
|
||||
|
||||
/**@description 待处理 / 已完成 / 已发货 / 已驳回 */
|
||||
/**@description 待处理 / 已通过 / 已发货 / 已拒绝 */
|
||||
export const ORDER_STATUS = ['PENDING','COMPLETED','SHIPPED','REJECTED']
|
||||
|
||||
@@ -22,8 +22,7 @@ const emptyAddressForm: AddAddressForm = {
|
||||
}
|
||||
|
||||
export function getAddressText(item: AddressListItem) {
|
||||
const regionText = item.region_text || item.region.map((part) => part.trim()).join(', ')
|
||||
return [regionText, item.detail_address].filter(Boolean).join(', ')
|
||||
return item.detail_address.trim()
|
||||
}
|
||||
|
||||
export function mapAddressToOption(item: AddressListItem): AddressOption {
|
||||
@@ -56,7 +55,7 @@ export function useAddressBook(options?: UseAddressBookOptions) {
|
||||
enabled: Boolean(sessionId) && Boolean(options?.autoLoad),
|
||||
queryFn: async () => {
|
||||
const response = await addressList({session_id: sessionId!})
|
||||
return response.data.list
|
||||
return response.data?.list ?? []
|
||||
},
|
||||
})
|
||||
|
||||
@@ -108,7 +107,7 @@ export function useAddressBook(options?: UseAddressBookOptions) {
|
||||
queryKey: queryKeys.addressList(sessionId),
|
||||
queryFn: async () => {
|
||||
const response = await addressList({session_id: sessionId})
|
||||
return response.data.list
|
||||
return response.data?.list ?? []
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -6,10 +6,11 @@ import {userAssets} from '@/api/user.ts'
|
||||
import {normalizeLanguage} from '@/lib/i18n'
|
||||
import {queryKeys} from '@/lib/queryKeys.ts'
|
||||
import {useUserStore} from '@/store/user.ts'
|
||||
import type {HostContextMessage} from '@/types'
|
||||
import type {HostContextMessage, HostWheelMessage} from '@/types'
|
||||
|
||||
const HOST_READY_MESSAGE = 'IFRAME_READY'
|
||||
const HOST_HEIGHT_MESSAGE = 'IFRAME_HEIGHT'
|
||||
const HOST_WHEEL_MESSAGE = 'IFRAME_WHEEL'
|
||||
const HOST_READY_INTERVAL = 500
|
||||
const HOST_READY_RETRY_LIMIT = 20
|
||||
const HOST_HEIGHT_REPORT_INTERVAL = 250
|
||||
@@ -240,5 +241,93 @@ export function AuthGuide({children}: PropsWithChildren) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (window.parent === window) {
|
||||
return
|
||||
}
|
||||
|
||||
let frameId = 0
|
||||
let hasPendingWheelUpdate = false
|
||||
let virtualScrollY = 0
|
||||
|
||||
const measurePageHeight = () =>
|
||||
Math.max(
|
||||
document.body.scrollHeight,
|
||||
document.documentElement.scrollHeight,
|
||||
document.body.offsetHeight,
|
||||
document.documentElement.offsetHeight,
|
||||
)
|
||||
|
||||
const getPageHeight = () =>
|
||||
Math.max(window.innerHeight, document.documentElement.clientHeight, 1)
|
||||
|
||||
const normalizeWheelDeltaY = (event: WheelEvent) => {
|
||||
switch (event.deltaMode) {
|
||||
case WheelEvent.DOM_DELTA_LINE:
|
||||
return event.deltaY * 16
|
||||
case WheelEvent.DOM_DELTA_PAGE:
|
||||
return event.deltaY * getPageHeight()
|
||||
case WheelEvent.DOM_DELTA_PIXEL:
|
||||
default:
|
||||
return event.deltaY
|
||||
}
|
||||
}
|
||||
|
||||
const flushWheelDelta = () => {
|
||||
frameId = 0
|
||||
|
||||
if (!hasPendingWheelUpdate) {
|
||||
return
|
||||
}
|
||||
|
||||
const message: HostWheelMessage = {
|
||||
type: HOST_WHEEL_MESSAGE,
|
||||
payload: {
|
||||
downTotalY: virtualScrollY,
|
||||
upTotalY: Math.max(measurePageHeight() - virtualScrollY, 0),
|
||||
},
|
||||
}
|
||||
|
||||
console.log('[playx] posting wheel position to host:', message)
|
||||
hasPendingWheelUpdate = false
|
||||
window.parent.postMessage(message, '*')
|
||||
}
|
||||
|
||||
const scheduleWheelPost = () => {
|
||||
if (frameId) {
|
||||
return
|
||||
}
|
||||
|
||||
frameId = window.requestAnimationFrame(flushWheelDelta)
|
||||
}
|
||||
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
const deltaY = normalizeWheelDeltaY(event)
|
||||
if (!Number.isFinite(deltaY) || deltaY === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const pageHeight = measurePageHeight()
|
||||
const previousVirtualScrollY = virtualScrollY
|
||||
virtualScrollY = Math.min(Math.max(virtualScrollY + deltaY, 0), pageHeight)
|
||||
|
||||
if (virtualScrollY === previousVirtualScrollY) {
|
||||
return
|
||||
}
|
||||
|
||||
hasPendingWheelUpdate = true
|
||||
scheduleWheelPost()
|
||||
}
|
||||
|
||||
window.addEventListener('wheel', handleWheel, {passive: true, capture: true})
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('wheel', handleWheel, true)
|
||||
if (frameId) {
|
||||
window.cancelAnimationFrame(frameId)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
@@ -130,10 +130,17 @@ const en = {
|
||||
},
|
||||
statusLabel: {
|
||||
pending: 'Pending',
|
||||
completed: 'Completed',
|
||||
completed: 'Approved',
|
||||
shipped: 'Shipped',
|
||||
rejected: 'Rejected',
|
||||
},
|
||||
statusNotice: {
|
||||
approvedTitle: 'Order Approved',
|
||||
rejectedTitle: 'Order Rejected',
|
||||
approvedDescription: 'Your order has been approved.',
|
||||
rejectedDescription: 'Your order has been rejected.',
|
||||
acknowledge: 'Got it',
|
||||
},
|
||||
},
|
||||
validation: {
|
||||
sessionExpired: 'Session expired. Please log in again.',
|
||||
|
||||
@@ -130,10 +130,17 @@ const ms = {
|
||||
},
|
||||
statusLabel: {
|
||||
pending: 'Menunggu',
|
||||
completed: 'Selesai',
|
||||
completed: 'Diluluskan',
|
||||
shipped: 'Dihantar',
|
||||
rejected: 'Ditolak',
|
||||
},
|
||||
statusNotice: {
|
||||
approvedTitle: 'Pesanan Diluluskan',
|
||||
rejectedTitle: 'Pesanan Ditolak',
|
||||
approvedDescription: 'Pesanan anda telah diluluskan.',
|
||||
rejectedDescription: 'Pesanan anda telah ditolak.',
|
||||
acknowledge: 'Faham',
|
||||
},
|
||||
},
|
||||
validation: {
|
||||
sessionExpired: 'Sesi telah tamat. Sila log masuk semula.',
|
||||
|
||||
@@ -130,9 +130,16 @@ const zh = {
|
||||
},
|
||||
statusLabel: {
|
||||
pending: '待处理',
|
||||
completed: '已完成',
|
||||
completed: '已通过',
|
||||
shipped: '已发货',
|
||||
rejected: '已驳回',
|
||||
rejected: '已拒绝',
|
||||
},
|
||||
statusNotice: {
|
||||
approvedTitle: '订单已通过',
|
||||
rejectedTitle: '订单已拒绝',
|
||||
approvedDescription: '订单已经通过。',
|
||||
rejectedDescription: '订单已拒绝。',
|
||||
acknowledge: '知道了',
|
||||
},
|
||||
},
|
||||
validation: {
|
||||
|
||||
@@ -12,14 +12,12 @@ export interface AddressInfo {
|
||||
export type AddressListItem = {
|
||||
id: number
|
||||
phone: string
|
||||
region: string[]
|
||||
detail_address: string
|
||||
default_setting: 0 | 1
|
||||
create_time: number
|
||||
update_time: number
|
||||
playx_user_asset_id: number
|
||||
receiver_name: string
|
||||
region_text: string
|
||||
}
|
||||
|
||||
export type AddressListData = {
|
||||
|
||||
@@ -12,6 +12,14 @@ export type HostContextMessage = {
|
||||
}
|
||||
}
|
||||
|
||||
export type HostWheelMessage = {
|
||||
type: 'IFRAME_WHEEL'
|
||||
payload: {
|
||||
downTotalY: number
|
||||
upTotalY: number
|
||||
}
|
||||
}
|
||||
|
||||
export type ButtonVariant = 'orange' | 'gray'
|
||||
|
||||
export type ButtonProps = PropsWithChildren<ButtonHTMLAttributes<HTMLButtonElement>> & {
|
||||
@@ -94,6 +102,8 @@ export type AccountTableRow = {
|
||||
|
||||
export type PointsRecordTone = 'positive' | 'negative'
|
||||
|
||||
export type OrderStatusCode = 'PENDING' | 'COMPLETED' | 'SHIPPED' | 'REJECTED' | 'UNKNOWN'
|
||||
|
||||
export type OrderRecord = {
|
||||
id: string
|
||||
orderNumber?: string
|
||||
@@ -103,6 +113,7 @@ export type OrderRecord = {
|
||||
title: string
|
||||
trackingNumber?: string
|
||||
status: string
|
||||
statusCode: OrderStatusCode
|
||||
points: string
|
||||
}
|
||||
|
||||
|
||||
@@ -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