<template>
|
<view class="page">
|
<view class="page-header">
|
<view class="safe-space" :style="'height:' + navTop + 'px'"></view>
|
<view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'">
|
<view class="back-button" hover-class="back-button-active" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view>
|
<text class="nav-title">使用记录</text>
|
<view class="capsule-space" :style="'width:' + navWidth + 'px'"></view>
|
</view>
|
<view class="tabs">
|
<view class="tab" v-for="item in statuses" :key="item.value" :class="activeStatus == item.value ? 'active' : ''" @tap="changeTab(item.value)"><text>{{item.label}}</text><view class="active-line"></view></view>
|
</view>
|
</view>
|
|
<scroll-view class="record-scroll" scroll-y="true" show-scrollbar="false" lower-threshold="80"
|
refresher-enabled="true" :refresher-triggered="refreshing" refresher-background="#F4F6F9"
|
@refresherrefresh="handlePullDownRefresh" @refresherabort="finishPullRefresh" @scrolltolower="loadMoreRecords">
|
<view v-if="initialLoading" class="empty-state"><view class="loading-spinner"></view><text class="empty-title">正在加载使用记录</text></view>
|
<view v-else-if="initialError" class="empty-state"><text class="empty-title">使用记录加载失败</text><text class="retry-text" @tap="retryInitialLoad">重新加载</text></view>
|
<view v-else-if="hasLoaded && visibleRecords.length == 0" class="empty-state"><text class="empty-title">暂无相关使用记录</text><text class="empty-desc">切换其他状态查看记录</text></view>
|
<view v-else-if="hasLoaded" class="record-list">
|
<view class="record-card" v-for="item in visibleRecords" :key="item.id" hover-class="record-card-active" @tap="openRecord(item)">
|
<view class="record-icon" :class="item.iconTone"><image :src="item.icon" mode="aspectFit"></image></view>
|
<view class="record-info"><text class="record-name">{{item.name}}</text><text class="record-time">{{item.time}}</text></view>
|
<view class="record-side"><text class="status-pill" :class="item.statusTone">{{item.status}}</text><text v-if="item.amount.length > 0" class="record-amount">{{item.amount}}</text></view>
|
</view>
|
</view>
|
<view v-if="loadingMore" class="loading-row"><view class="loading-spinner"></view><text>正在加载</text></view>
|
<view v-else-if="loadMoreError" class="loading-row load-error" @tap="retryLoadMore"><text>加载失败,点击重试</text></view>
|
<view v-else-if="hasLoaded && !hasMore && visibleRecords.length > 0" class="no-more"><view></view><text>已经到底了</text><view></view></view>
|
<view class="bottom-space"></view>
|
</scroll-view>
|
</view>
|
</template>
|
|
<script setup lang="uts">
|
import { apiEnvironment } from '../../config/api.uts'
|
import { getUsageRecords } from '../../services/portal.uts'
|
import { clearSession, getSession, hasActiveSession } from '../../services/session.uts'
|
import { safeBack } from '../../utils/navigation.uts'
|
|
const navTop = ref(28)
|
const navHeight = ref(32)
|
const navWidth = ref(88)
|
const navRight = ref(10)
|
const statuses = [
|
{ label: '全部', value: '' },
|
{ label: '待支付', value: 'pending_payment' },
|
{ label: '待核销', value: 'pending_verification' },
|
{ label: '已完成', value: 'completed' },
|
{ label: '已取消/已退款', value: 'cancelled' },
|
{ label: '待处理', value: 'processing' },
|
{ label: '已领取', value: 'received' }
|
]
|
const activeStatus = ref('')
|
const pageSize = 8
|
const autoRefreshInterval = 15000
|
const currentPage = ref(1)
|
const talentId = ref('')
|
const isPreviewMember = ref(false)
|
const initialLoading = ref(true)
|
const loadingMore = ref(false)
|
const hasLoaded = ref(false)
|
const hasMore = ref(true)
|
const initialError = ref(false)
|
const loadMoreError = ref(false)
|
const refreshing = ref(false)
|
const failInitialOnce = ref(false)
|
const failLoadMoreOnce = ref(false)
|
const listRequestVersion = ref(0)
|
const visibleRecords = ref([])
|
let cancelRecordsRequest: (() => void) | null = null
|
let autoRefreshTimer = 0
|
let hasShown = false
|
let pageVisible = false
|
let refreshInProgress = false
|
|
const getMerchantIcon = (merchantType: string) => {
|
if (merchantType == 'food') return '/static/order-assets/utensils.svg'
|
if (merchantType == 'hotel') return '/static/order-assets/hotel.svg'
|
if (merchantType == 'cinema' || merchantType == 'theatre') return '/static/order-assets/film.svg'
|
if (merchantType == 'medical') return '/static/order-assets/medical.svg'
|
if (merchantType == 'scenic') return '/static/order-assets/ticket.svg'
|
if (merchantType == 'parking') return '/static/order-assets/parking.svg'
|
if (merchantType == 'fuel') return '/static/order-assets/fuel.svg'
|
if (merchantType == 'carWash') return '/static/merchant-assets/car-wash-orange.svg'
|
if (merchantType == 'shopping') return '/static/order-assets/shopping.svg'
|
return '/static/order-assets/service.svg'
|
}
|
const getMerchantIconTone = (merchantType: string) => {
|
if (merchantType == 'hotel') return 'icon-green'
|
if (merchantType == 'cinema' || merchantType == 'theatre' || merchantType == 'food' || merchantType == 'parking') return 'icon-blue'
|
if (merchantType == 'shopping') return 'icon-pink'
|
if (merchantType == 'service') return 'icon-purple'
|
return 'icon-orange'
|
}
|
const statusText = (status: string): string => {
|
if (status == 'pending_payment') return '待支付'
|
if (status == 'pending_verification') return '待核销'
|
if (status == 'completed') return '已完成'
|
if (status == 'cancelled') return '已取消'
|
if (status == 'processing') return '待处理'
|
if (status == 'received') return '已领取'
|
return '未知状态'
|
}
|
const statusTone = (status: string): string => {
|
if (status == 'completed') return 'status-green'
|
if (status == 'received' || status == 'pending_payment') return 'status-blue'
|
if (status == 'cancelled') return 'status-gray'
|
return 'status-orange'
|
}
|
const statusValue = (label: string): string => {
|
for (let index = 0; index < statuses.length; index++) {
|
if (statuses[index].label == label) return statuses[index].value
|
}
|
return ''
|
}
|
const formatTime = (value: any): string => {
|
if (value == null) return ''
|
const text = value as string
|
return text.replace('T', ' ').substring(0, 16)
|
}
|
const formatAmount = (value: any): string => {
|
if (value == null || value === '') return ''
|
const amount = Number(value)
|
return isNaN(amount) ? '' : '¥' + amount.toFixed(2)
|
}
|
/**
|
* 已完成/待核销/待支付之外的订单都归在「已取消」这一桶里,但离开支付流程的原因不同:未支付被取消,
|
* 还是钱已经退回来了。退款维度只改这一桶的文案,状态码与点击跳转不变。
|
*/
|
const statusLabel = (status: string, source: any): string => {
|
if (status == 'cancelled') {
|
const refundStatus = source != null && source.refundStatus != null ? Number(source.refundStatus) : 0
|
if (refundStatus == 1) return '退款中'
|
if (refundStatus == 2) return '已退款'
|
}
|
return statusText(status)
|
}
|
const mapRecord = (source: any): any => {
|
const merchantType = source != null && source.merchantType != null ? source.merchantType as string : 'service'
|
const status = source != null && source.status != null ? source.status as string : ''
|
return {
|
id: source != null && source.recordId != null ? source.recordId as string : '',
|
businessId: source != null && source.businessId != null ? source.businessId as string : '',
|
recordType: source != null && source.recordType != null ? source.recordType as string : '',
|
targetDetailId: source != null && source.targetDetailId != null ? '' + source.targetDetailId : '',
|
targetDetailType: source != null && source.targetDetailType != null ? source.targetDetailType as string : '',
|
merchantId: source != null && source.merchantId != null ? '' + source.merchantId : '',
|
name: source != null && source.merchantName != null ? source.merchantName as string : '人才权益服务',
|
detailName: source != null && source.detailName != null ? source.detailName as string : '',
|
merchantType: merchantType,
|
time: formatTime(source != null ? source.occurredAt : null),
|
statusCode: status,
|
status: statusLabel(status, source),
|
amount: formatAmount(source != null ? source.amount : null),
|
icon: getMerchantIcon(merchantType),
|
iconTone: getMerchantIconTone(merchantType),
|
statusTone: statusTone(status)
|
}
|
}
|
onLoad((options: any) => {
|
// #ifdef MP-WEIXIN
|
const capsule = uni.getMenuButtonBoundingClientRect()
|
const systemInfo = uni.getSystemInfoSync()
|
navTop.value = capsule.top
|
navHeight.value = capsule.height
|
navWidth.value = capsule.width
|
navRight.value = systemInfo.windowWidth - capsule.right
|
// #endif
|
const previewMember = apiEnvironment == 'development' && options != null && options['preview'] == 'member'
|
isPreviewMember.value = previewMember
|
if (previewMember) talentId.value = options['talentId'] != null ? options['talentId'] as string : '2'
|
if (!hasActiveSession() && !previewMember) {
|
clearSession()
|
uni.reLaunch({ url: '/pages/login/login' })
|
return
|
}
|
const session = getSession()
|
if (!previewMember && session != null && session.talentId != null) talentId.value = session.talentId as string
|
if (options != null && options['previewState'] != null) {
|
const previewState = options['previewState'] as string
|
failInitialOnce.value = previewState == 'initialError'
|
failLoadMoreOnce.value = previewState == 'loadMoreError'
|
}
|
if (options != null && options.status != null) {
|
let initialStatus = decodeURIComponent(options.status as string)
|
if (initialStatus == '待使用') initialStatus = '待核销'
|
activeStatus.value = statusValue(initialStatus)
|
}
|
resetRecords()
|
})
|
onShow(() => {
|
pageVisible = true
|
if (hasShown) refreshRecords(false)
|
else hasShown = true
|
startAutoRefresh()
|
})
|
onHide(() => {
|
pageVisible = false
|
stopAutoRefresh()
|
})
|
onUnload(() => {
|
pageVisible = false
|
refreshInProgress = false
|
stopAutoRefresh()
|
listRequestVersion.value = listRequestVersion.value + 1
|
cancelActiveRequest()
|
})
|
|
const cancelActiveRequest = () => {
|
if (cancelRecordsRequest == null) return
|
cancelRecordsRequest()
|
cancelRecordsRequest = null
|
}
|
const stopAutoRefresh = () => {
|
if (autoRefreshTimer == 0) return
|
clearInterval(autoRefreshTimer)
|
autoRefreshTimer = 0
|
}
|
const startAutoRefresh = () => {
|
stopAutoRefresh()
|
if (talentId.value == '') return
|
autoRefreshTimer = setInterval(() => {
|
if (pageVisible) refreshRecords(false)
|
}, autoRefreshInterval)
|
}
|
|
const resetRecords = () => {
|
const requestVersion = listRequestVersion.value + 1
|
listRequestVersion.value = requestVersion
|
cancelActiveRequest()
|
currentPage.value = 1
|
initialLoading.value = true
|
initialError.value = false
|
loadMoreError.value = false
|
hasLoaded.value = false
|
loadingMore.value = false
|
refreshing.value = false
|
refreshInProgress = false
|
visibleRecords.value = []
|
hasMore.value = true
|
if (failInitialOnce.value) {
|
failInitialOnce.value = false
|
initialLoading.value = false
|
initialError.value = true
|
return
|
}
|
requestRecords(requestVersion, 1, pageSize, true)
|
}
|
const requestRecords = (requestVersion: number, page: number, size: number, initial: boolean) => {
|
cancelRecordsRequest = getUsageRecords(talentId.value, activeStatus.value, page, size, (data: any) => {
|
if (requestVersion != listRequestVersion.value) return
|
cancelRecordsRequest = null
|
const sourceRecords: any[] = data != null && data.records instanceof Array ? data.records as any[] : []
|
const records: any[] = []
|
for (let index = 0; index < sourceRecords.length; index++) records.push(mapRecord(sourceRecords[index]))
|
if (initial) visibleRecords.value = records
|
else {
|
const next = visibleRecords.value.slice()
|
for (let index = 0; index < records.length; index++) next.push(records[index])
|
visibleRecords.value = next
|
}
|
const total = data != null && data.total != null ? Number(data.total) : visibleRecords.value.length
|
currentPage.value = page
|
hasMore.value = visibleRecords.value.length < total
|
initialLoading.value = false
|
loadingMore.value = false
|
hasLoaded.value = true
|
}, (error: any) => {
|
if (requestVersion != listRequestVersion.value) return
|
cancelRecordsRequest = null
|
initialLoading.value = false
|
loadingMore.value = false
|
if (error != null && error.statusCode == 401) {
|
clearSession()
|
uni.reLaunch({ url: '/pages/login/login' })
|
return
|
}
|
if (initial) initialError.value = true
|
else loadMoreError.value = true
|
})
|
}
|
const refreshRecords = (fromPullDown: boolean) => {
|
if (talentId.value == '' || initialLoading.value || loadingMore.value || refreshInProgress) {
|
if (fromPullDown) refreshing.value = false
|
return
|
}
|
const requestVersion = listRequestVersion.value + 1
|
listRequestVersion.value = requestVersion
|
cancelActiveRequest()
|
refreshInProgress = true
|
refreshing.value = fromPullDown
|
loadMoreError.value = false
|
const refreshSize = Math.min(48, Math.max(pageSize, currentPage.value * pageSize))
|
cancelRecordsRequest = getUsageRecords(talentId.value, activeStatus.value, 1, refreshSize, (data: any) => {
|
if (requestVersion != listRequestVersion.value) return
|
cancelRecordsRequest = null
|
refreshInProgress = false
|
const sourceRecords: any[] = data != null && data.records instanceof Array ? data.records as any[] : []
|
const records: any[] = []
|
for (let index = 0; index < sourceRecords.length; index++) records.push(mapRecord(sourceRecords[index]))
|
visibleRecords.value = records
|
const total = data != null && data.total != null ? Number(data.total) : records.length
|
currentPage.value = Math.max(1, Math.ceil(records.length / pageSize))
|
hasMore.value = records.length < total
|
initialError.value = false
|
hasLoaded.value = true
|
refreshing.value = false
|
}, (error: any) => {
|
if (requestVersion != listRequestVersion.value) return
|
cancelRecordsRequest = null
|
refreshInProgress = false
|
refreshing.value = false
|
if (error != null && error.statusCode == 401) {
|
clearSession()
|
uni.reLaunch({ url: '/pages/login/login' })
|
return
|
}
|
if (fromPullDown) uni.showToast({ title: '刷新失败,请稍后重试', icon: 'none' })
|
})
|
}
|
const handlePullDownRefresh = () => refreshRecords(true)
|
const finishPullRefresh = () => { refreshing.value = false }
|
const retryInitialLoad = () => resetRecords()
|
const retryLoadMore = () => loadMoreRecords()
|
const changeTab = (name: string) => {
|
if (activeStatus.value == name) return
|
activeStatus.value = name
|
resetRecords()
|
}
|
const loadMoreRecords = () => {
|
if (initialLoading.value || loadingMore.value || refreshInProgress || !hasMore.value) return
|
const requestVersion = listRequestVersion.value
|
loadMoreError.value = false
|
loadingMore.value = true
|
if (failLoadMoreOnce.value) {
|
failLoadMoreOnce.value = false
|
loadingMore.value = false
|
loadMoreError.value = true
|
return
|
}
|
requestRecords(requestVersion, currentPage.value + 1, pageSize, false)
|
}
|
const goBack = () => {
|
const previewQuery = isPreviewMember.value ? '?preview=member&talentId=' + encodeURIComponent(talentId.value) : ''
|
safeBack('/pages/profile/profile' + previewQuery)
|
}
|
const openRecord = (item: any) => {
|
if (item.recordType == 'SERVICE_APPLICATION') {
|
uni.navigateTo({ url: '/pages/order-processing/order-processing?recordId=' + encodeURIComponent(item.id) + '&applicationNo=' + encodeURIComponent(item.businessId) + '&talentId=' + encodeURIComponent(talentId.value) })
|
return
|
}
|
if (item.statusCode == 'pending_payment') {
|
uni.navigateTo({ url: '/pages/pending-payment/pending-payment?recordId=' + encodeURIComponent(item.id) + '&orderId=' + encodeURIComponent(item.businessId) + '&talentId=' + encodeURIComponent(talentId.value) })
|
return
|
}
|
if (item.statusCode == 'pending_verification') {
|
uni.navigateTo({ url: '/pages/pending-use/pending-use?recordId=' + encodeURIComponent(item.id) + '&orderId=' + encodeURIComponent(item.businessId) + '&talentId=' + encodeURIComponent(talentId.value) })
|
return
|
}
|
if (item.statusCode == 'completed') {
|
uni.navigateTo({ url: '/pages/order-completed/order-completed?recordId=' + encodeURIComponent(item.id) + '&orderId=' + encodeURIComponent(item.businessId) + '&talentId=' + encodeURIComponent(talentId.value) })
|
return
|
}
|
if (item.statusCode == 'cancelled') {
|
uni.navigateTo({ url: '/pages/order-cancelled/order-cancelled?recordId=' + encodeURIComponent(item.id) + '&orderId=' + encodeURIComponent(item.businessId) + '&talentId=' + encodeURIComponent(talentId.value) })
|
return
|
}
|
if (item.statusCode == 'processing') {
|
uni.navigateTo({ url: '/pages/order-processing/order-processing?recordId=' + encodeURIComponent(item.id) })
|
return
|
}
|
if (item.statusCode == 'received') {
|
const detailName = item.detailName != null ? item.detailName as string : item.name as string
|
const claimId = item.targetDetailId != null ? item.targetDetailId as string : ''
|
const typeParam = item.targetDetailType == 'TICKET_PACKAGE_DETAIL' ? '&type=ticketPackage' : ''
|
uni.navigateTo({ url: '/pages/coupon-received/coupon-received?recordId=' + encodeURIComponent(item.id) + '&claimId=' + encodeURIComponent(claimId) + '&name=' + encodeURIComponent(detailName) + typeParam + '&talentId=' + encodeURIComponent(talentId.value) + '&merchantId=' + encodeURIComponent(item.merchantId) })
|
return
|
}
|
uni.showToast({ title: '记录详情后续接入', icon: 'none' })
|
}
|
</script>
|
|
<style>
|
.page { display: flex; width: 100%; height: 100vh; background-color: #F4F6F9; flex-direction: column; }
|
.page-header { position: relative; z-index: 10; flex-shrink: 0; background-color: #FFF0D7; }
|
.safe-space { height: 28px; }
|
.nav-row { display: flex; height: 32px; box-sizing: border-box; padding-left: 20rpx; padding-right: 10px; position: relative; flex-direction: row; align-items: center; justify-content: space-between; }
|
.back-button { display: flex; width: 70rpx; height: 64rpx; margin-left: -4rpx; justify-content: center; align-items: center; }
|
.back-button image { width: 36rpx; height: 36rpx; }
|
.back-button-active { opacity: .55; }
|
.nav-title { position: absolute; left: 50%; top: 50%; color: #161A21; font-size: 37rpx; line-height: 48rpx; font-weight: 700; transform: translate(-50%, -50%); white-space: nowrap; }
|
.capsule-space { width: 88px; height: 100%; flex-shrink: 0; }
|
.tabs { display: flex; height: 92rpx; padding: 8rpx 22rpx 0; box-sizing: border-box; flex-direction: row; justify-content: space-between; align-items: stretch; }
|
.tab { display: flex; min-width: 68rpx; position: relative; flex-direction: column; justify-content: center; align-items: center; }
|
.tab text { color: #7A818C; font-size: 25rpx; line-height: 38rpx; white-space: nowrap; }
|
.tab.active text { color: #252A32; font-weight: 700; }
|
.active-line { width: 43rpx; height: 6rpx; margin-top: 5rpx; border-radius: 3rpx; background-image: linear-gradient(90deg, #FF5D31 0%, #FF8B25 100%); opacity: 0; }
|
.tab.active .active-line { opacity: 1; }
|
.record-scroll { min-height: 0; flex: 1; background-color: #F4F6F9; }
|
.record-list { padding: 0 31rpx; }
|
.record-card { display: flex; height: 164rpx; margin-top: 26rpx; box-sizing: border-box; padding: 27rpx 31rpx; border-radius: 25rpx; background-color: #FFFFFF; flex-direction: row; align-items: center; box-shadow: 0 8rpx 22rpx rgba(72,82,96,.035); }
|
.record-card-active { opacity: .76; }
|
.record-icon { display: flex; width: 72rpx; height: 72rpx; flex-shrink: 0; border-radius: 17rpx; justify-content: center; align-items: center; }
|
.record-icon image { width: 38rpx; height: 38rpx; }
|
.icon-orange { background-color: #FFF2E8; }
|
.icon-green { background-color: #EAF9F1; }
|
.icon-blue { background-color: #EDF5FF; }
|
.icon-pink { background-color: #FFF0F7; }
|
.icon-purple { background-color: #F3F0FC; }
|
.icon-gray { background-color: #F0F2F5; }
|
.record-info { min-width: 0; flex: 1; margin-left: 27rpx; }
|
.record-name { display: block; color: #252B34; font-size: 29rpx; line-height: 40rpx; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.record-time { display: block; margin-top: 12rpx; color: #A0A8B4; font-size: 21rpx; line-height: 30rpx; }
|
.record-side { display: flex; height: 100%; margin-left: 18rpx; flex-shrink: 0; flex-direction: column; align-items: flex-end; justify-content: space-between; }
|
.status-pill { padding: 7rpx 16rpx; border-radius: 24rpx; font-size: 21rpx; line-height: 28rpx; white-space: nowrap; }
|
.status-orange { color: #FF8A37; background-color: #FFF4EA; }
|
.status-green { color: #35B875; background-color: #EAF9F1; }
|
.status-blue { color: #4E91FF; background-color: #EDF5FF; }
|
.status-gray { color: #9299A3; background-color: #F0F2F5; }
|
.record-amount { color: #222933; font-size: 27rpx; line-height: 34rpx; font-weight: 700; }
|
.empty-state { display: flex; height: 560rpx; flex-direction: column; justify-content: center; align-items: center; }
|
.empty-title { color: #8D96A3; font-size: 27rpx; }
|
.empty-desc { margin-top: 14rpx; color: #BBC1C9; font-size: 22rpx; }
|
.retry-text { margin-top: 22rpx; color: #F26A38; font-size: 24rpx; }
|
.loading-row { display: flex; height: 92rpx; flex-direction: row; justify-content: center; align-items: center; }
|
.loading-spinner { width: 28rpx; height: 28rpx; margin-right: 13rpx; box-sizing: border-box; border-width: 3rpx; border-style: solid; border-color: #FFD6C4; border-top-color: #FF6C2B; border-radius: 50%; animation: record-loading 720ms linear infinite; }
|
.loading-row text { color: #989DA5; font-size: 22rpx; }
|
.load-error text { color: #F26A38; }
|
.no-more { display: flex; height: 92rpx; flex-direction: row; justify-content: center; align-items: center; }
|
.no-more view { width: 70rpx; height: 1rpx; background-color: #DFE2E7; }
|
.no-more text { margin: 0 18rpx; color: #AEB4BC; font-size: 21rpx; }
|
@keyframes record-loading { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
.bottom-space { height: 38rpx; height: calc(38rpx + env(safe-area-inset-bottom)); }
|
</style>
|