|
|
@@ -0,0 +1,935 @@
|
|
|
+<template>
|
|
|
+ <view class="points-package-record">
|
|
|
+ <!-- 顶部筛选栏 -->
|
|
|
+ <view class="record-header">
|
|
|
+ <view class="record-tabs">
|
|
|
+ <view
|
|
|
+ v-for="tab in tabs"
|
|
|
+ :key="tab.value"
|
|
|
+ class="record-tabs__item"
|
|
|
+ :class="{ 'record-tabs__item--active': params.activeTab === tab.value }"
|
|
|
+ @click="handleTabChange(tab.value)"
|
|
|
+ >
|
|
|
+ {{ tab.label }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 月份筛选 -->
|
|
|
+ <view class="record-date">
|
|
|
+ <view class="record-date__divider" />
|
|
|
+
|
|
|
+ <view v-if="!selectedDate" class="record-date__inner" @click="openDatePicker">
|
|
|
+ <text class="record-date__text">筛选</text>
|
|
|
+ <wd-icon name="caret-down" size="28rpx" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-else class="record-date__inner" @click="clearDatePicker">
|
|
|
+ <text class="record-date__text">{{ selectedDateText }}</text>
|
|
|
+ <wd-icon name="close-circle" size="28rpx" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="record-content">
|
|
|
+ <!-- 顶部提示 -->
|
|
|
+ <view class="record-notice">
|
|
|
+ <wd-notice-bar text="任务包即将完成时,请不要做积分较高的任务,避免浪费积分哦~" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 空状态 -->
|
|
|
+ <view v-if="showEmpty" class="record-empty">
|
|
|
+ <wd-empty icon="no-content" tip="暂时还没有任务记录哦~" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 列表 -->
|
|
|
+ <view v-else class="record-list">
|
|
|
+ <view
|
|
|
+ v-for="item in recordList"
|
|
|
+ :key="item.statusId || item.id"
|
|
|
+ class="record-card"
|
|
|
+ :class="{ 'record-card--disabled': isDisabledPackage(item) }"
|
|
|
+ @click="handleOpenDetail(item)"
|
|
|
+ >
|
|
|
+ <!-- 卡片头部 -->
|
|
|
+ <view class="record-card__header">
|
|
|
+ <view class="record-card__title-wrap">
|
|
|
+ <view class="record-card__title">
|
|
|
+ {{ item.scorePackageName || '-' }}
|
|
|
+
|
|
|
+ <wd-tag
|
|
|
+ v-if="getPackageTypeTag(item.packageType1, isDisabledPackage(item))"
|
|
|
+ custom-class="record-card__tag"
|
|
|
+ plain
|
|
|
+ :color="getPackageTypeTag(item.packageType1, isDisabledPackage(item))?.color"
|
|
|
+ :bg-color="getPackageTypeTag(item.packageType1, isDisabledPackage(item))?.bgColor"
|
|
|
+ >
|
|
|
+ {{ getPackageTypeTag(item.packageType1, isDisabledPackage(item))?.label }}
|
|
|
+ </wd-tag>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="item.packageSn" class="record-card__sn">
|
|
|
+ 编号:{{ item.packageSn }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="record-card__status">
|
|
|
+ <view class="record-card__dot" />
|
|
|
+ <text>{{ getPackageStatusText(item.packageStatus) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 卡片主体 -->
|
|
|
+ <view class="record-card__body">
|
|
|
+ <view class="record-card__info">
|
|
|
+ <text>发包方:{{ item.packageEntName || '-' }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="record-card__meta">
|
|
|
+ <text>发包时间:{{ formatDate(item.createTime) }}</text>
|
|
|
+ <text>接包时间:{{ formatDate(item.receiveTime) }}</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 进度信息 -->
|
|
|
+ <view class="record-card__progress">
|
|
|
+ <view class="record-card__progress-main">
|
|
|
+ <view class="record-card__progress-top">
|
|
|
+ <text class="record-card__progress-label">完成进度</text>
|
|
|
+ <text
|
|
|
+ class="record-card__progress-value"
|
|
|
+ :class="{ 'record-card__progress-value--disabled': isDisabledPackage(item) }"
|
|
|
+ >
|
|
|
+ {{ getProgressText(item) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <wd-progress
|
|
|
+ :percentage="getProgressPercentage(item)"
|
|
|
+ :stroke-width="8"
|
|
|
+ :color="isDisabledPackage(item) ? '#d5d5d5' : '#3fa3f1'"
|
|
|
+ hide-text
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 结算信息 -->
|
|
|
+ <view v-if="isSettledPackage(item)" class="record-card__settle">
|
|
|
+ <view class="record-card__settle-item">结算状态:已结算</view>
|
|
|
+ <view class="record-card__settle-item"
|
|
|
+ >税后收入:{{ formatAmount(item.userRealAmount) }}</view
|
|
|
+ >
|
|
|
+ <view class="record-card__settle-item"
|
|
|
+ >已交税金:{{ formatAmount(item.actualTax) }}</view
|
|
|
+ >
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 待审批状态下支持撤回 -->
|
|
|
+ <view v-if="showRevokeButton" class="record-card__actions" @click.stop>
|
|
|
+ <view class="record-card__action-tip">
|
|
|
+ <wd-icon name="warning" size="24rpx" />
|
|
|
+ <text>当前申请待审批,可撤回后重新申领</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <wd-button
|
|
|
+ size="small"
|
|
|
+ round
|
|
|
+ custom-class="record-card__revoke"
|
|
|
+ @click.stop="handleRevoke(item)"
|
|
|
+ >
|
|
|
+ 撤回申领
|
|
|
+ </wd-button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 过期标识 -->
|
|
|
+ <view v-if="String(item.packageStatus) === '2'" class="record-card__expired">
|
|
|
+ <image src="/static/images/common/expired.png" mode="heightFix" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 底部加载状态 -->
|
|
|
+ <view class="record-load-status">
|
|
|
+ <text v-if="loading">加载中...</text>
|
|
|
+ <text v-else-if="finished">没有更多了</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 月份选择器 -->
|
|
|
+ <wd-datetime-picker
|
|
|
+ v-model:visible="datetimePickerShow"
|
|
|
+ v-model="datetimePickerValue"
|
|
|
+ type="year-month"
|
|
|
+ @confirm="handleDateConfirm"
|
|
|
+ />
|
|
|
+
|
|
|
+ <wd-toast />
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, reactive, ref } from 'vue'
|
|
|
+
|
|
|
+import { onLoad, onReachBottom } from '@dcloudio/uni-app'
|
|
|
+import { useToast } from '@wot-ui/ui'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+
|
|
|
+import { getDictTypeApi } from '@/services/modules/common'
|
|
|
+import {
|
|
|
+ getUserScorePackageListApi,
|
|
|
+ pkgWithdrawApi,
|
|
|
+} from '@/services/modules/pointsPackageRecord/index'
|
|
|
+import type { ScorePackageRecordItem } from '@/services/modules/pointsPackageRecord/type'
|
|
|
+
|
|
|
+const toast = useToast()
|
|
|
+
|
|
|
+/**
|
|
|
+ * 页面入口类型
|
|
|
+ * on-the-way:从工作台「进行中」进入
|
|
|
+ * wait-approve:从工作台「待审批」进入
|
|
|
+ */
|
|
|
+type PageEntryType = 'on-the-way' | 'wait-approve' | ''
|
|
|
+
|
|
|
+/**
|
|
|
+ * 积分包记录 Tab
|
|
|
+ * 这里的值需要和后端 activeId 入参保持一致
|
|
|
+ */
|
|
|
+enum RecordTab {
|
|
|
+ All = '1',
|
|
|
+ Processing = '2',
|
|
|
+ Approving = '3',
|
|
|
+ Finished = '4',
|
|
|
+}
|
|
|
+
|
|
|
+type Nullable<T> = T | null | undefined
|
|
|
+
|
|
|
+interface DictItem {
|
|
|
+ label: string
|
|
|
+ value: string | number
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 列表分页响应结构
|
|
|
+ */
|
|
|
+interface ListResponse {
|
|
|
+ records?: ScorePackageRecordItem[]
|
|
|
+ total?: number
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 页面 Tab 配置
|
|
|
+ */
|
|
|
+const tabs = [
|
|
|
+ { label: '全部', value: RecordTab.All },
|
|
|
+ { label: '进行中', value: RecordTab.Processing },
|
|
|
+ { label: '待审批', value: RecordTab.Approving },
|
|
|
+ { label: '已完成', value: RecordTab.Finished },
|
|
|
+] as const
|
|
|
+
|
|
|
+/**
|
|
|
+ * 工作台入口 type 与页面 Tab 的映射
|
|
|
+ */
|
|
|
+const entryTypeTabMap: Record<Exclude<PageEntryType, ''>, RecordTab> = {
|
|
|
+ 'on-the-way': RecordTab.Processing,
|
|
|
+ 'wait-approve': RecordTab.Approving,
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 查询参数
|
|
|
+ */
|
|
|
+const params = reactive({
|
|
|
+ activeTab: RecordTab.All,
|
|
|
+ current: 1,
|
|
|
+ size: 10,
|
|
|
+ selectedYear: '',
|
|
|
+ selectedMonth: '',
|
|
|
+})
|
|
|
+
|
|
|
+/**
|
|
|
+ * 页面状态
|
|
|
+ */
|
|
|
+const openType = ref<PageEntryType>('')
|
|
|
+const loading = ref(false)
|
|
|
+const finished = ref(false)
|
|
|
+const recordList = ref<ScorePackageRecordItem[]>([])
|
|
|
+const packageStatusMap = ref<DictItem[]>([])
|
|
|
+
|
|
|
+/**
|
|
|
+ * 请求标识,用于规避快速切换筛选时旧请求覆盖新数据
|
|
|
+ */
|
|
|
+let requestId = 0
|
|
|
+
|
|
|
+/**
|
|
|
+ * 当前选中的月份,格式:YYYY-MM
|
|
|
+ */
|
|
|
+const selectedDate = computed(() => {
|
|
|
+ const { selectedYear, selectedMonth } = params
|
|
|
+ return selectedYear && selectedMonth ? `${selectedYear}-${selectedMonth}` : ''
|
|
|
+})
|
|
|
+
|
|
|
+/**
|
|
|
+ * 月份筛选展示文案
|
|
|
+ */
|
|
|
+const selectedDateText = computed(() => {
|
|
|
+ const { selectedYear, selectedMonth } = params
|
|
|
+ return selectedYear && selectedMonth ? `${selectedYear}年${selectedMonth}月` : '筛选'
|
|
|
+})
|
|
|
+
|
|
|
+/**
|
|
|
+ * 是否展示空状态
|
|
|
+ */
|
|
|
+const showEmpty = computed(() => !loading.value && recordList.value.length === 0)
|
|
|
+
|
|
|
+/**
|
|
|
+ * 只有待审批 Tab 展示撤回按钮
|
|
|
+ */
|
|
|
+const showRevokeButton = computed(() => params.activeTab === RecordTab.Approving)
|
|
|
+
|
|
|
+/**
|
|
|
+ * 日期选择器状态
|
|
|
+ */
|
|
|
+const datetimePickerShow = ref(false)
|
|
|
+const datetimePickerValue = ref(dayjs().startOf('month').valueOf())
|
|
|
+
|
|
|
+/**
|
|
|
+ * 重置列表分页
|
|
|
+ */
|
|
|
+const resetList = () => {
|
|
|
+ params.current = 1
|
|
|
+ finished.value = false
|
|
|
+ recordList.value = []
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 刷新列表
|
|
|
+ */
|
|
|
+const refreshRecordList = () => {
|
|
|
+ resetList()
|
|
|
+ fetchRecordList()
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取积分包状态字典
|
|
|
+ */
|
|
|
+const fetchPackageStatus = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getDictTypeApi('package_new_status')
|
|
|
+ packageStatusMap.value = res?.data ?? []
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取积分包状态字典失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取积分包记录列表
|
|
|
+ */
|
|
|
+const fetchRecordList = async () => {
|
|
|
+ if (loading.value || finished.value) return
|
|
|
+
|
|
|
+ loading.value = true
|
|
|
+ const currentRequestId = ++requestId
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await getUserScorePackageListApi({
|
|
|
+ current: params.current,
|
|
|
+ size: params.size,
|
|
|
+ selType: 2,
|
|
|
+ selDate: selectedDate.value,
|
|
|
+ packageName: '',
|
|
|
+ activeId: params.activeTab,
|
|
|
+ })
|
|
|
+
|
|
|
+ if (currentRequestId !== requestId) return
|
|
|
+
|
|
|
+ const data = (res?.data ?? {}) as ListResponse
|
|
|
+ const records = data.records ?? []
|
|
|
+ const total = data.total ?? 0
|
|
|
+
|
|
|
+ recordList.value = params.current === 1 ? records : recordList.value.concat(records)
|
|
|
+ finished.value = recordList.value.length >= total || records.length < params.size
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取积分包记录失败:', error)
|
|
|
+ toast.error('获取列表失败')
|
|
|
+ } finally {
|
|
|
+ if (currentRequestId === requestId) {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 加载下一页
|
|
|
+ */
|
|
|
+const loadMoreRecordList = () => {
|
|
|
+ if (loading.value || finished.value) return
|
|
|
+
|
|
|
+ params.current += 1
|
|
|
+ fetchRecordList()
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 切换 Tab
|
|
|
+ */
|
|
|
+const handleTabChange = (value: RecordTab) => {
|
|
|
+ if (params.activeTab === value) return
|
|
|
+
|
|
|
+ params.activeTab = value
|
|
|
+ refreshRecordList()
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 打开月份筛选
|
|
|
+ */
|
|
|
+const openDatePicker = () => {
|
|
|
+ datetimePickerShow.value = true
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 确认月份筛选
|
|
|
+ */
|
|
|
+const handleDateConfirm = (event: { value: string | number | Date }) => {
|
|
|
+ const date = dayjs(event.value)
|
|
|
+
|
|
|
+ params.selectedYear = date.format('YYYY')
|
|
|
+ params.selectedMonth = date.format('MM')
|
|
|
+
|
|
|
+ refreshRecordList()
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 清空月份筛选
|
|
|
+ */
|
|
|
+const clearDatePicker = () => {
|
|
|
+ datetimePickerValue.value = dayjs().startOf('month').valueOf()
|
|
|
+ params.selectedYear = ''
|
|
|
+ params.selectedMonth = ''
|
|
|
+
|
|
|
+ refreshRecordList()
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 点击撤回申领
|
|
|
+ */
|
|
|
+const handleRevoke = (item: ScorePackageRecordItem) => {
|
|
|
+ uni.showModal({
|
|
|
+ title: '提示',
|
|
|
+ content: '请确认是否撤回积分包申领?撤回后可重新发起申请。',
|
|
|
+ cancelText: '取消',
|
|
|
+ confirmText: '撤回',
|
|
|
+ success: (res) => {
|
|
|
+ if (!res.confirm) return
|
|
|
+ revokeApply(item)
|
|
|
+ },
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 提交撤回申领
|
|
|
+ */
|
|
|
+const revokeApply = async (item: ScorePackageRecordItem) => {
|
|
|
+ if (!item.statusId) {
|
|
|
+ toast.warning('缺少申领记录ID')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await pkgWithdrawApi({
|
|
|
+ packageStatusId: item.statusId,
|
|
|
+ })
|
|
|
+ console.log('res', res)
|
|
|
+
|
|
|
+ if (res.code === 0) {
|
|
|
+ toast.success('撤回成功')
|
|
|
+ refreshRecordList()
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('撤回申领失败:', error)
|
|
|
+ toast.error(getErrorMessage(error, '撤回失败'))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 打开详情页
|
|
|
+ */
|
|
|
+const handleOpenDetail = (item: ScorePackageRecordItem) => {
|
|
|
+ uni.navigateTo({
|
|
|
+ url: `/pages-task/task-package/detail?id=${item.id}`,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取积分包状态文案
|
|
|
+ */
|
|
|
+const getPackageStatusText = (status: Nullable<string | number>) => {
|
|
|
+ const statusItem = packageStatusMap.value.find((item) => String(item.value) === String(status))
|
|
|
+ return statusItem?.label || '-'
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取积分包类型标签
|
|
|
+ */
|
|
|
+const getPackageTypeTag = (type: Nullable<string | number>, disabled = false) => {
|
|
|
+ const disabledTag = {
|
|
|
+ label: getPackageTypeText(type),
|
|
|
+ color: '#999999',
|
|
|
+ bgColor: '#f5f5f5',
|
|
|
+ }
|
|
|
+
|
|
|
+ const tagMap = {
|
|
|
+ '1': { label: '积分值', color: '#3fa3f1', bgColor: 'rgba(63, 163, 241, 0.1)' },
|
|
|
+ '6': { label: '积分值', color: '#3fa3f1', bgColor: 'rgba(63, 163, 241, 0.1)' },
|
|
|
+ '2': { label: '任务量', color: '#4cd964', bgColor: 'rgba(76, 217, 100, 0.1)' },
|
|
|
+ '3': { label: '通用', color: '#3fa3f1', bgColor: 'rgba(63, 163, 241, 0.1)' },
|
|
|
+ '4': { label: '特定', color: '#ffbe1c', bgColor: 'rgba(255, 190, 28, 0.1)' },
|
|
|
+ } as const
|
|
|
+
|
|
|
+ const key = String(type) as keyof typeof tagMap
|
|
|
+
|
|
|
+ if (!type || !tagMap[key]) return undefined
|
|
|
+ return disabled ? disabledTag : tagMap[key]
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取积分包类型文案
|
|
|
+ */
|
|
|
+const getPackageTypeText = (type: Nullable<string | number>) => {
|
|
|
+ const textMap = {
|
|
|
+ '1': '积分值',
|
|
|
+ '6': '积分值',
|
|
|
+ '2': '任务量',
|
|
|
+ '3': '通用',
|
|
|
+ '4': '特定',
|
|
|
+ } as const
|
|
|
+
|
|
|
+ return textMap[String(type) as keyof typeof textMap] || ''
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 是否为不可继续追加任务的积分包
|
|
|
+ */
|
|
|
+const isDisabledPackage = (item: ScorePackageRecordItem) => {
|
|
|
+ return String(item.taskAddFlag) === '0'
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 是否为积分值类型积分包
|
|
|
+ */
|
|
|
+const isScorePackage = (item: ScorePackageRecordItem) => {
|
|
|
+ return ['1', '6'].includes(String(item.packageType1))
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 是否已结算
|
|
|
+ */
|
|
|
+const isSettledPackage = (item: ScorePackageRecordItem) => {
|
|
|
+ return String(item.packageStatus) === '5' && String(item.settleNoteStatus) === '1'
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取进度百分比
|
|
|
+ */
|
|
|
+const getProgressPercentage = (item: ScorePackageRecordItem) => {
|
|
|
+ if (isDisabledPackage(item)) return 100
|
|
|
+
|
|
|
+ const progress = Number(item.speedProgress ?? 0)
|
|
|
+ if (Number.isNaN(progress)) return 0
|
|
|
+
|
|
|
+ return Math.max(0, Math.min(progress, 100))
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取进度展示文案
|
|
|
+ */
|
|
|
+const getProgressText = (item: ScorePackageRecordItem) => {
|
|
|
+ if (isScorePackage(item)) {
|
|
|
+ return `${formatAmount(item.finishScore)}/${formatAmount(item.score)}`
|
|
|
+ }
|
|
|
+
|
|
|
+ const finishedNum = String(item.taskAddFlag) === '1' ? item.taskFinishNum : item.taskNum2
|
|
|
+ return `${formatAmount(finishedNum)}/${formatAmount(item.taskNum2)}`
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 格式化日期
|
|
|
+ */
|
|
|
+const formatDate = (value: Nullable<string>) => {
|
|
|
+ if (!value) return '-'
|
|
|
+
|
|
|
+ const date = dayjs(value)
|
|
|
+ return date.isValid() ? date.format('YYYY-MM-DD') : '-'
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 格式化金额 / 数值
|
|
|
+ */
|
|
|
+const formatAmount = (value: Nullable<string | number>) => {
|
|
|
+ return value === null || value === undefined || value === '' ? '-' : value
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取错误文案
|
|
|
+ */
|
|
|
+const getErrorMessage = (error: unknown, fallback: string) => {
|
|
|
+ if (error instanceof Error) return error.message || fallback
|
|
|
+ return fallback
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 根据入口参数设置默认 Tab
|
|
|
+ */
|
|
|
+const setDefaultTabByEntryType = (type: PageEntryType) => {
|
|
|
+ if (!type) return
|
|
|
+
|
|
|
+ const tab = entryTypeTabMap[type as Exclude<PageEntryType, ''>]
|
|
|
+ if (tab) {
|
|
|
+ params.activeTab = tab
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onReachBottom(loadMoreRecordList)
|
|
|
+
|
|
|
+onLoad((query) => {
|
|
|
+ const type = String(query?.type || '') as PageEntryType
|
|
|
+
|
|
|
+ openType.value = type
|
|
|
+ setDefaultTabByEntryType(type)
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 兼容旧入口:如果仍然传 page,则 page 优先级高于 type
|
|
|
+ */
|
|
|
+ if (query?.page) {
|
|
|
+ const page = String(query.page)
|
|
|
+
|
|
|
+ if (Object.values(RecordTab).includes(page as RecordTab)) {
|
|
|
+ params.activeTab = page as RecordTab
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fetchRecordList()
|
|
|
+ fetchPackageStatus()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.points-package-record {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: linear-gradient(180deg, #f3f8ff 0%, #fafafa 360rpx, #fafafa 100%);
|
|
|
+
|
|
|
+ .record-header {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ z-index: 99;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ height: 96rpx;
|
|
|
+ padding: 0 20rpx;
|
|
|
+ background: rgba(255, 255, 255, 0.96);
|
|
|
+ box-shadow: 0 8rpx 24rpx rgba(31, 91, 150, 0.06);
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-tabs {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-tabs__item {
|
|
|
+ position: relative;
|
|
|
+ flex: 1;
|
|
|
+ height: 100%;
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 400;
|
|
|
+ line-height: 96rpx;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-tabs__item--active {
|
|
|
+ color: #1f2937;
|
|
|
+ font-weight: 700;
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 10rpx;
|
|
|
+ left: 50%;
|
|
|
+ width: 44rpx;
|
|
|
+ height: 8rpx;
|
|
|
+ background: linear-gradient(90deg, #3fa3f1 0%, #68c7ff 100%);
|
|
|
+ border-radius: 999rpx;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ content: '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-date {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 240rpx;
|
|
|
+ height: 100%;
|
|
|
+ color: #333333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-date__divider {
|
|
|
+ width: 2rpx;
|
|
|
+ height: 40rpx;
|
|
|
+ margin-right: 24rpx;
|
|
|
+ background: #ebebeb;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-date__inner {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-width: 170rpx;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-date__text {
|
|
|
+ margin-right: 10rpx;
|
|
|
+ color: #333333;
|
|
|
+ font-size: 28rpx;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-content {
|
|
|
+ padding: 120rpx 28rpx 28rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-notice {
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ box-shadow: 0 8rpx 22rpx rgba(255, 142, 40, 0.08);
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-empty {
|
|
|
+ padding-top: 220rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card {
|
|
|
+ position: relative;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #ffffff;
|
|
|
+ border: 2rpx solid rgba(63, 163, 241, 0.08);
|
|
|
+ border-radius: 24rpx;
|
|
|
+ box-shadow: 0 12rpx 32rpx rgba(31, 91, 150, 0.08);
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__header {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 18rpx;
|
|
|
+ padding: 26rpx 26rpx 24rpx;
|
|
|
+ background: linear-gradient(135deg, #eaf6ff 0%, #f5fbff 100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__title-wrap {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__title {
|
|
|
+ color: #1f2937;
|
|
|
+ font-size: 34rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 48rpx;
|
|
|
+ word-break: break-all;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__sn {
|
|
|
+ margin-top: 8rpx;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 22rpx;
|
|
|
+ line-height: 32rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.record-card__tag) {
|
|
|
+ display: inline-flex;
|
|
|
+ height: 34rpx;
|
|
|
+ margin-left: 10rpx;
|
|
|
+ padding: 0 10rpx;
|
|
|
+ font-size: 22rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ line-height: 32rpx;
|
|
|
+ vertical-align: 4rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__status {
|
|
|
+ display: flex;
|
|
|
+ flex-shrink: 0;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 48rpx;
|
|
|
+ padding: 0 14rpx;
|
|
|
+ color: #3fa3f1;
|
|
|
+ font-size: 26rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ background: rgba(63, 163, 241, 0.1);
|
|
|
+ border-radius: 999rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__dot {
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 10rpx;
|
|
|
+ height: 10rpx;
|
|
|
+ margin-right: 8rpx;
|
|
|
+ background: #3fa3f1;
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__body {
|
|
|
+ padding: 24rpx 26rpx 26rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__info,
|
|
|
+ .record-card__meta {
|
|
|
+ color: #8a94a6;
|
|
|
+ font-size: 24rpx;
|
|
|
+ line-height: 40rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__meta {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 0 28rpx;
|
|
|
+ margin-top: 4rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__progress {
|
|
|
+ margin-top: 20rpx;
|
|
|
+ padding: 20rpx;
|
|
|
+ background: #f8fbff;
|
|
|
+ border-radius: 18rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__progress-main {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__progress-top {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 14rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__progress-label {
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 24rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__progress-value {
|
|
|
+ color: #1f2937;
|
|
|
+ font-size: 26rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__progress-value--disabled {
|
|
|
+ color: #9ca3af;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__settle {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 10rpx 16rpx;
|
|
|
+ margin-top: 18rpx;
|
|
|
+ padding: 18rpx;
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 24rpx;
|
|
|
+ line-height: 36rpx;
|
|
|
+ background: #f9fafb;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__settle-item {
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 18rpx;
|
|
|
+ margin-top: 20rpx;
|
|
|
+ padding: 18rpx 18rpx 18rpx 20rpx;
|
|
|
+ background: linear-gradient(135deg, #f8fbff 0%, #eef7ff 100%);
|
|
|
+ border: 2rpx solid rgba(63, 163, 241, 0.12);
|
|
|
+ border-radius: 18rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__action-tip {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ align-items: center;
|
|
|
+ min-width: 0;
|
|
|
+ color: #7b8794;
|
|
|
+ font-size: 22rpx;
|
|
|
+ line-height: 32rpx;
|
|
|
+
|
|
|
+ text {
|
|
|
+ margin-left: 6rpx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.record-card__revoke) {
|
|
|
+ flex-shrink: 0;
|
|
|
+ min-width: 148rpx;
|
|
|
+ height: 58rpx;
|
|
|
+ padding: 0 26rpx;
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 26rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ background: linear-gradient(135deg, #3fa3f1 0%, #61bef8 100%);
|
|
|
+ border: 0;
|
|
|
+ box-shadow: 0 8rpx 18rpx rgba(63, 163, 241, 0.24);
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__expired {
|
|
|
+ z-index: 2;
|
|
|
+ position: absolute;
|
|
|
+ right: 30rpx;
|
|
|
+ bottom: 25rpx;
|
|
|
+ width: 150rpx;
|
|
|
+ height: 150rpx;
|
|
|
+
|
|
|
+ image {
|
|
|
+ width: 150rpx;
|
|
|
+ height: 150rpx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card--disabled {
|
|
|
+ border-color: rgba(156, 163, 175, 0.1);
|
|
|
+ box-shadow: 0 10rpx 28rpx rgba(31, 41, 55, 0.05);
|
|
|
+
|
|
|
+ .record-card__header {
|
|
|
+ background: linear-gradient(135deg, #f3f4f6 0%, #fafafa 100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__title,
|
|
|
+ .record-card__status {
|
|
|
+ color: #9ca3af;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__status {
|
|
|
+ background: #f3f4f6;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-card__dot {
|
|
|
+ background: #9ca3af;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-load-status {
|
|
|
+ padding: 12rpx 0 32rpx;
|
|
|
+ color: #9ca3af;
|
|
|
+ font-size: 24rpx;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|