- 添加HostWheelMessage类型定义和IFRAME_WHEEL消息常量 - 实现iframe轮播事件监听和消息发送功能 - 移除address.type.ts中的region和region_text字段 - 更新英文本地化文件中的completed状态为Approved - 添加订单状态通知相关的国际化文案 - 更新常量文件中的订单状态描述注释 - 在index.ts中添加HostWheelMessage和OrderStatusCode类型 - 修改record页面的订单状态处理逻辑 - 实现订单状态变更通知弹窗功能 - 优化地址簿获取地址文本的方法 - 修复地址列表API响应数据访问问题 - 添加playx.html测试页面和相关配置文件
691 lines
25 KiB
TypeScript
691 lines
25 KiB
TypeScript
import {useCallback, useEffect, useRef, useState} from 'react'
|
|
import {useQuery} from '@tanstack/react-query'
|
|
import {useTranslation} from 'react-i18next'
|
|
|
|
import PageLayout from '@/components/layout'
|
|
import {ORDER_STATUS} from '@/constant'
|
|
import i18n from '@/lib/i18n'
|
|
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, 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'
|
|
import {queryKeys} from '@/lib/queryKeys.ts'
|
|
import { useUserStore } from '@/store/user.ts'
|
|
import type {OrderItem, PointsLogItem} from '@/types/business.type.ts'
|
|
|
|
const pointsRecordToneClassName = {
|
|
positive: 'bg-[#9BFFC0] text-[#176640]',
|
|
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')
|
|
}
|
|
|
|
function getDateTimeParts(value?: number | string) {
|
|
if (value == null) {
|
|
return { date: '--', time: '--:--' }
|
|
}
|
|
|
|
const numericValue = typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value
|
|
const date = new Date(
|
|
typeof numericValue === 'number'
|
|
? String(numericValue).length === 13
|
|
? numericValue
|
|
: numericValue * 1000
|
|
: numericValue,
|
|
)
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
return { date: '--', time: '--:--' }
|
|
}
|
|
|
|
return {
|
|
date: `${date.getFullYear()}-${formatDatePart(date.getMonth() + 1)}-${formatDatePart(date.getDate())}`,
|
|
time: `${formatDatePart(date.getHours())}:${formatDatePart(date.getMinutes())}`,
|
|
}
|
|
}
|
|
|
|
function toTitleCase(value: string) {
|
|
return value
|
|
.toLowerCase()
|
|
.split(/[\s_-]+/)
|
|
.filter(Boolean)
|
|
.map((part) => part[0]?.toUpperCase() + part.slice(1))
|
|
.join(' ')
|
|
}
|
|
|
|
function normalizeOrderStatus(status?: string | number): OrderStatusCode {
|
|
if (typeof status === 'string') {
|
|
const normalizedStatus = status.trim().toUpperCase()
|
|
if (ORDER_STATUS.includes(normalizedStatus)) {
|
|
return normalizedStatus as OrderStatusCode
|
|
}
|
|
}
|
|
|
|
const normalizedStatus = typeof status === 'string' && /^\d+$/.test(status.trim())
|
|
? Number(status.trim())
|
|
: status
|
|
|
|
if (typeof normalizedStatus === 'number') {
|
|
switch (normalizedStatus) {
|
|
case 0:
|
|
return 'PENDING'
|
|
case 1:
|
|
return 'COMPLETED'
|
|
case 2:
|
|
return 'SHIPPED'
|
|
case 3:
|
|
return 'REJECTED'
|
|
default:
|
|
return 'UNKNOWN'
|
|
}
|
|
}
|
|
|
|
return 'UNKNOWN'
|
|
}
|
|
|
|
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) {
|
|
if (item.type_title) {
|
|
return item.type_title
|
|
}
|
|
|
|
if (item.category_title) {
|
|
return item.category_title
|
|
}
|
|
|
|
if (item.type) {
|
|
const normalizedType = item.type.trim().toUpperCase()
|
|
switch (normalizedType) {
|
|
case 'BONUS':
|
|
return i18n.t('record.categories.bonus')
|
|
case 'PHYSICAL':
|
|
return i18n.t('record.categories.physical')
|
|
case 'WITHDRAW':
|
|
return i18n.t('record.categories.withdraw')
|
|
case '1':
|
|
return i18n.t('record.categories.withdraw')
|
|
case '2':
|
|
return i18n.t('record.categories.physical')
|
|
case '3':
|
|
return i18n.t('record.categories.bonus')
|
|
default:
|
|
return toTitleCase(normalizedType)
|
|
}
|
|
}
|
|
|
|
if (item.category) {
|
|
return toTitleCase(item.category)
|
|
}
|
|
|
|
return i18n.t('record.categories.order')
|
|
}
|
|
|
|
function getOrderPoints(item: OrderItem) {
|
|
const rawValue = item.points_cost
|
|
|
|
if (rawValue == null || rawValue === '') {
|
|
return {display: '--'}
|
|
}
|
|
|
|
const numericValue = typeof rawValue === 'string' ? Number(rawValue) : rawValue
|
|
if (Number.isNaN(numericValue)) {
|
|
const textValue = String(rawValue)
|
|
return {display: `${textValue} ${i18n.t('common.points')}`}
|
|
}
|
|
|
|
return {
|
|
display: `${numericValue} ${i18n.t('common.points')}`,
|
|
}
|
|
}
|
|
|
|
function getTrackingNumber(item: OrderItem) {
|
|
const shippingNumber = item.shipping_no?.trim()
|
|
const logisticsNumber = item.logistics_no?.trim()
|
|
const trackingNumber = item.tracking_no?.trim()
|
|
const shippingCompany = item.shipping_company?.trim()
|
|
const resolvedNumber = shippingNumber || logisticsNumber || trackingNumber
|
|
|
|
if (!resolvedNumber) {
|
|
return undefined
|
|
}
|
|
|
|
return shippingCompany ? `${shippingCompany} ${resolvedNumber}` : resolvedNumber
|
|
}
|
|
|
|
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),
|
|
orderNumber,
|
|
date,
|
|
time,
|
|
category: getOrderCategory(item),
|
|
title: item.item_title ?? item.title ?? item.mallItem?.title ?? i18n.t('record.untitledOrder'),
|
|
trackingNumber: getTrackingNumber(item),
|
|
status: getOrderStatusLabel(statusCode, item.status),
|
|
statusCode,
|
|
points: points.display,
|
|
}
|
|
}
|
|
|
|
function getPointsRecordAmount(item: PointsLogItem) {
|
|
const rawValue = item.points
|
|
|
|
if (rawValue == null || rawValue === '') {
|
|
return {
|
|
amount: '--',
|
|
tone: 'negative' as const,
|
|
}
|
|
}
|
|
|
|
const numericValue = typeof rawValue === 'string' ? Number(rawValue) : rawValue
|
|
if (Number.isNaN(numericValue)) {
|
|
const textValue = String(rawValue).trim()
|
|
return {
|
|
amount: item.direction === 'IN'
|
|
? textValue.startsWith('+') ? textValue : `+${textValue.replace(/^[+-]/, '')}`
|
|
: item.direction === 'OUT'
|
|
? textValue.startsWith('-') ? textValue : `-${textValue.replace(/^[+-]/, '')}`
|
|
: textValue,
|
|
tone: item.direction === 'IN' ? 'positive' as const : 'negative' as const,
|
|
}
|
|
}
|
|
|
|
return {
|
|
amount: `${item.direction === 'IN' ? '+' : item.direction === 'OUT' ? '-' : ''}${Math.abs(numericValue)}`,
|
|
tone: item.direction === 'IN' ? 'positive' as const : 'negative' as const,
|
|
}
|
|
}
|
|
|
|
function getPointsRecordTitle(item: PointsLogItem) {
|
|
return [
|
|
item.item_title,
|
|
item.title,
|
|
item.mallItem?.title,
|
|
item.biz_type,
|
|
item.type,
|
|
item.description,
|
|
item.remark?.split('\n')[0],
|
|
].find((value) => typeof value === 'string' && value.trim())?.trim() ?? i18n.t('record.pointsRecordFallback')
|
|
}
|
|
|
|
function mapPointsLogItemToRecord(item: PointsLogItem) {
|
|
const {date, time} = getDateTimeParts(item.ts ?? item.create_time ?? item.created_at ?? item.update_time)
|
|
const amount = getPointsRecordAmount(item)
|
|
|
|
return {
|
|
id: String(item.id),
|
|
title: getPointsRecordTitle(item),
|
|
date,
|
|
time,
|
|
amount: amount.amount,
|
|
tone: amount.tone,
|
|
}
|
|
}
|
|
|
|
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'
|
|
}
|
|
}
|
|
|
|
function TabButton({ active, label, icon: Icon, onClick }: TabButtonProps) {
|
|
return (
|
|
<MotionButton
|
|
type="button"
|
|
className={cn(
|
|
'inline-flex min-w-[140px] cursor-pointer items-center justify-center gap-[8px] rounded-[10px] border px-[14px] py-[9px] text-[13px] transition-colors focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]',
|
|
active
|
|
? 'border-[#F99A0B] bg-linear-to-r from-[#F96C02] to-[#FE9F00] text-white shadow-[0_0_16px_rgba(249,108,2,0.22)]'
|
|
: 'border-white/35 bg-white/3 text-[#B8B1AA] hover:bg-white/6',
|
|
)}
|
|
onClick={onClick}
|
|
{...tapMotionPropsSoft}
|
|
>
|
|
<Icon className="h-[15px] w-[15px]" aria-hidden="true" />
|
|
{label}
|
|
</MotionButton>
|
|
)
|
|
}
|
|
|
|
function OrderCard({ record, onOpenDetails }: OrderCardProps) {
|
|
const {t} = useTranslation()
|
|
return (
|
|
<div className="shrink-0 overflow-hidden rounded-[12px] shadow-[0_10px_30px_rgba(0,0,0,0.24)]">
|
|
<div className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[12px] py-[9px] text-[14px] text-white">
|
|
{record.date} {record.time} • {record.category}
|
|
</div>
|
|
<div className="liquid-glass-bg !rounded-t-none flex items-center justify-between gap-[14px] px-[12px] py-[14px]">
|
|
<div className="min-w-0 flex-1 pr-[16px]">
|
|
<div className="text-[16px] font-medium text-white">{record.title}</div>
|
|
{record.trackingNumber ? (
|
|
<div className="mt-[4px] text-[13px] text-white/45">
|
|
{t('record.trackingNumber')} {record.trackingNumber}
|
|
</div>
|
|
) : null}
|
|
<MotionButton
|
|
type="button"
|
|
className="mt-[10px] inline-flex items-center gap-[5px] text-[13px] text-[#FA6A00] focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
|
|
onClick={() => onOpenDetails(record)}
|
|
{...tapMotionPropsSoft}
|
|
>
|
|
{t('record.checkDetails')}
|
|
<ChevronRight className="h-[14px] w-[14px]" aria-hidden="true" />
|
|
</MotionButton>
|
|
</div>
|
|
|
|
<div className="flex shrink-0 flex-col items-end gap-[10px]">
|
|
<div
|
|
className={cn(
|
|
'rounded-[6px] px-[8px] py-[3px] text-[11px] leading-none',
|
|
getOrderStatusClassName(record.statusCode),
|
|
)}
|
|
>
|
|
{record.status}
|
|
</div>
|
|
<div className="text-[13px] text-white/85">{record.points.replace(/^-/, '')}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function PointsCard({ record }: PointsCardProps) {
|
|
return (
|
|
<div className="shrink-0 overflow-hidden rounded-[12px] shadow-[0_10px_30px_rgba(0,0,0,0.24)]">
|
|
<div className="bg-linear-to-r from-[#F96C02] to-[#FE9F00] px-[12px] py-[9px] text-[15px] text-white">
|
|
{record.title}
|
|
</div>
|
|
<div className="liquid-glass-bg !rounded-t-none flex items-center justify-between px-[12px] py-[22px]">
|
|
<div className="text-[13px] text-white/40">
|
|
{record.date} {record.time}
|
|
</div>
|
|
<div
|
|
className={cn(
|
|
'inline-flex w-fit rounded-[6px] px-[10px] py-[3px] text-[12px] leading-none',
|
|
pointsRecordToneClassName[record.tone],
|
|
)}
|
|
>
|
|
{record.amount}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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)
|
|
},
|
|
})
|
|
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 (
|
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
|
{t('validation.verificationRequired')}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (ordersQuery.isPending) {
|
|
return (
|
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
|
{t('record.loading')}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!orderRecords.length) {
|
|
return (
|
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
|
{t('record.noData')}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-[12px] pb-[4px]">
|
|
{orderRecords.map((record) => (
|
|
<OrderCard key={record.id} record={record} onOpenDetails={onOpenDetails} />
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function PointsTabContent({sessionId}: {sessionId: string}) {
|
|
const {t} = useTranslation()
|
|
const pointsLogsQuery = useQuery({
|
|
queryKey: queryKeys.pointsLogs(sessionId),
|
|
enabled: Boolean(sessionId),
|
|
gcTime: 0,
|
|
queryFn: async () => {
|
|
const response = await pointsLogs({
|
|
session_id: sessionId,
|
|
})
|
|
|
|
return (response.data.list ?? []).map(mapPointsLogItemToRecord)
|
|
},
|
|
})
|
|
const pointsRecords = pointsLogsQuery.data ?? []
|
|
|
|
if (!sessionId) {
|
|
return (
|
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
|
{t('validation.verificationRequired')}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (pointsLogsQuery.isPending) {
|
|
return (
|
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
|
{t('record.loading')}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!pointsRecords.length) {
|
|
return (
|
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
|
{t('record.noData')}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-[12px] pb-[4px]">
|
|
{pointsRecords.map((record) => <PointsCard key={record.id} record={record} />)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function RecordPage() {
|
|
const {t} = useTranslation()
|
|
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]">
|
|
<Link
|
|
to="/"
|
|
className="mt-[12px] flex h-[44px] items-center justify-between rounded-[12px] bg-[#08070E]/72 px-[14px] text-[#F56E10] transition-colors hover:bg-[#0D0A14]/80 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E] sm:mt-[16px]"
|
|
>
|
|
<div className="flex items-center gap-[8px]">
|
|
<MotionDiv {...tapMotionPropsIcon}>
|
|
<ArrowLeft className="h-[16px] w-[16px]" aria-hidden="true" />
|
|
</MotionDiv>
|
|
<span className="text-[14px] font-medium text-white/92">{t('common.back')}</span>
|
|
</div>
|
|
<div className="text-[15px] font-semibold text-[#F56E10]">{t('record.title')}</div>
|
|
<div className="w-[52px]"></div>
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="flex min-h-0 flex-1 flex-col pt-[18px] pb-[24px]">
|
|
<div className="mx-auto w-full max-w-[860px]">
|
|
<div className="mb-[12px] flex justify-end">
|
|
<div className="flex gap-[8px]">
|
|
<TabButton
|
|
active={tab === 'order'}
|
|
icon={PackageSearch}
|
|
label={t('record.myOrders')}
|
|
onClick={() => {
|
|
setSelectedOrder(null)
|
|
setTab('order')
|
|
}}
|
|
/>
|
|
<TabButton
|
|
active={tab === 'record'}
|
|
icon={Coins}
|
|
label={t('record.pointsRecord')}
|
|
onClick={() => {
|
|
setSelectedOrder(null)
|
|
setTab('record')
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="h-px bg-white/16"></div>
|
|
</div>
|
|
|
|
<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}
|
|
onOrdersChange={handleOrdersChange}
|
|
onStatusNotices={handleStatusNotices}
|
|
/>
|
|
) : (
|
|
<PointsTabContent key="record" sessionId={sessionId} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Modal
|
|
open={Boolean(selectedOrder)}
|
|
title={t('record.orderDetails')}
|
|
onClose={handleCloseDetails}
|
|
className="max-w-[420px]"
|
|
bodyClassName="pt-[0px]"
|
|
footer={
|
|
<Button type="button" className="h-[36px] w-full sm:min-w-[94px] sm:w-auto" onClick={handleCloseDetails}>
|
|
{t('common.close')}
|
|
</Button>
|
|
}
|
|
>
|
|
{selectedOrder ? (
|
|
<div className="mt-[10px] rounded-[10px] bg-[#1C1818]/78 px-[12px] py-[6px]">
|
|
{[
|
|
{ label: t('record.orderNumber'), value: selectedOrder.orderNumber ?? '--' },
|
|
{ label: t('record.orderTime'), value: `${selectedOrder.date} ${selectedOrder.time}` },
|
|
{ label: t('record.orderType'), value: selectedOrder.category },
|
|
{ label: t('record.itemName'), value: selectedOrder.title },
|
|
...(selectedOrder.trackingNumber
|
|
? [{ label: t('record.trackingNumber'), value: selectedOrder.trackingNumber }]
|
|
: []),
|
|
{ label: t('record.points'), value: selectedOrder.points.replace(/^-/, '') },
|
|
{ label: t('record.status'), value: selectedOrder.status },
|
|
].map((item) => (
|
|
<div key={item.label} className="border-b border-white/8 py-[10px] last:border-b-0">
|
|
<div className="text-[13px] text-white/48">{item.label}</div>
|
|
<div className="mt-[4px] text-[14px] text-white">{item.value}</div>
|
|
</div>
|
|
))}
|
|
</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>
|
|
)
|
|
}
|
|
|
|
export default RecordPage
|