longz
2026-07-08 700f9d0da314a4ea701fe9f68d305b67d72ec5fb
yuyueMiniProgram/src/pages/reviewList/index.vue
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { fetchMyApprovalHistory, fetchMyApprovalTodos } from '@/api/appointment'
import type { MiniAppointmentListItem, MiniApprovalTodoItem } from '@/types/appointment'
import { fetchMyApprovalAll, fetchMyApprovalHistory, fetchMyApprovalTodos } from '@/api/appointment'
import type { MiniAppointmentListItem, MiniApprovalAllItem, MiniApprovalTodoItem } from '@/types/appointment'
import { normalizeVisitReasonText } from '@/utils/visitReason'
definePage({
  name: 'reviewList',
@@ -14,10 +15,13 @@
  key: string
  status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'CANCELLED' | 'OTHER'
  statusText: string
  name: string
  visitorText: string
  plateText: string
  nodeText: string
  orderNo: string
  time: string
  reasonText: string
  reasonOtherText: string
  createTime: string
  taskId?: number
  taskUuid?: string
@@ -25,17 +29,27 @@
  orderUuid: string
}
const router = useRouter()
const sysInfo = uni.getSystemInfoSync()
const statusBarHeight = sysInfo.statusBarHeight || 0
const navBarHeight = 44
const fixedHeaderHeightPx = computed(() =>
  statusBarHeight + navBarHeight + uni.upx2px(36) + uni.upx2px(220) + uni.upx2px(24) + uni.upx2px(24),
)
const searchKeyword = ref('')
const dateValue = ref('')
const showDatePicker = ref(false)
const activeTab = ref(0)
const loading = ref(false)
const loadingMore = ref(false)
const records = ref<ReviewRecord[]>([])
const page = ref(1)
const pageSize = 20
const hasMore = ref(true)
const refreshing = ref(false)
const scrollIntoViewId = ref('')
const listRenderKey = ref(0)
const scrollLoadLocked = ref(false)
const tabs = [
  { title: '待审核' },
@@ -47,8 +61,27 @@
const dateColumns = ref<string[][]>([[], [], []])
const datePickerIndex = ref([0, 0, 0])
function backOrHome() {
  const pages = getCurrentPages()
  if (pages.length > 1) {
    uni.navigateBack({
      delta: 1,
      fail: () => {
        uni.switchTab({ url: '/pages/index/index' })
      },
    })
    return
  }
  uni.switchTab({
    url: '/pages/index/index',
    fail: () => {
      uni.reLaunch({ url: '/pages/index/index' })
    },
  })
}
function handleBack() {
  router.back()
  backOrHome()
}
function getCurrentDate() {
@@ -79,6 +112,15 @@
    months.findIndex(item => item === selectedMonth.toString()),
    days.findIndex(item => item === selectedDay.toString()),
  ]
}
function formatVisitReasonDisplay(reasonText?: string | null, reasonCode?: string | null) {
  const normalizedText = normalizeVisitReasonText(reasonText)
  const code = (reasonCode || '').trim().toUpperCase()
  if (code === 'OTHER' || normalizedText === '其他') {
    return '其他'
  }
  return normalizedText || code || '-'
}
function handleDateChange(e: any) {
@@ -115,10 +157,13 @@
    key: `todo-${item.taskId}`,
    status: 'PENDING',
    statusText: '待审核',
    name: item.visitedUserName,
    visitorText: item.visitorDisplayText || '-',
    plateText: item.plateNoText || '',
    nodeText: item.nodeName || '待审核',
    orderNo: item.orderNo,
    time: `${item.visitStartDate || '-'}${item.visitEndDate && item.visitEndDate !== item.visitStartDate ? ` 至 ${item.visitEndDate}` : ''}`,
    time: normalizeVisitTimeText(item.visitTimeText || `${item.visitStartDate || '-'}${item.visitEndDate && item.visitEndDate !== item.visitStartDate ? ` 至 ${item.visitEndDate}` : ''}`),
    reasonText: formatVisitReasonDisplay(item.visitReasonText, item.visitReasonCode),
    reasonOtherText: (item.visitReasonOther || '').trim() || '-',
    createTime: item.createTime,
    taskId: item.taskId,
    taskUuid: item.taskUuid,
@@ -151,13 +196,36 @@
    key: `history-${item.id}`,
    status,
    statusText,
    name: item.visitedUserName,
    visitorText: item.visitorDisplayText || '-',
    plateText: item.plateNoText || '',
    nodeText,
    orderNo: item.orderNo,
    time: item.visitTimeText,
    time: normalizeVisitTimeText(item.visitTimeText),
    reasonText: formatVisitReasonDisplay(item.visitReasonText, item.visitReasonCode),
    reasonOtherText: (item.visitReasonOther || '').trim() || '-',
    createTime: item.createTime,
    orderId: item.id,
    orderUuid: item.uuid,
  }
}
function mapAll(item: MiniApprovalAllItem): ReviewRecord {
  return {
    key: item.taskId ? `todo-${item.taskId}` : `history-${item.orderId}`,
    status: (item.status as ReviewRecord['status']) || 'OTHER',
    statusText: item.statusText || '已处理',
    visitorText: item.visitorDisplayText || '-',
    plateText: item.plateNoText || '',
    nodeText: item.nodeText || (item.statusText || '已处理'),
    orderNo: item.orderNo,
    time: normalizeVisitTimeText(item.visitTimeText || '-'),
    reasonText: formatVisitReasonDisplay(item.visitReasonText, item.visitReasonCode),
    reasonOtherText: (item.visitReasonOther || '').trim() || '-',
    createTime: item.createTime,
    taskId: item.taskId || undefined,
    taskUuid: item.taskUuid || undefined,
    orderId: item.orderId,
    orderUuid: item.orderUuid,
  }
}
@@ -172,22 +240,36 @@
  return value.replace('T', ' ').slice(0, 16)
}
function normalizeVisitTimeText(value?: string | null) {
  const text = (value || '').trim()
  if (!text) {
    return '-'
  }
  return text
    .replace(' 全天', ' 00:00-23:59')
    .replace(' 上午', ' 00:00-11:59')
    .replace(' 下午', ' 12:00-23:59')
}
async function loadRecords() {
  if (loading.value) {
  if (loading.value || loadingMore.value) {
    return
  }
  loading.value = true
  try {
    const targetPage = 1
    const params = {
      page: 1,
      size: 50,
      visitedUserNameKeyword: searchKeyword.value.trim() || undefined,
      page: targetPage,
      size: pageSize,
      visitorNameKeyword: searchKeyword.value.trim() || undefined,
      visitDate: dateValue.value || undefined,
    }
    if (activeTab.value === 0) {
      const todoResult = await fetchMyApprovalTodos(params)
      records.value = todoResult.records.map(mapTodo)
      page.value = targetPage
      hasMore.value = todoResult.page < todoResult.pages
    }
    else if (activeTab.value === 1) {
      const historyResult = await fetchMyApprovalHistory({
@@ -195,6 +277,8 @@
        status: 'REJECTED',
      })
      records.value = historyResult.records.map(mapHistory)
      page.value = targetPage
      hasMore.value = historyResult.page < historyResult.pages
    }
    else if (activeTab.value === 2) {
      const historyResult = await fetchMyApprovalHistory({
@@ -202,16 +286,14 @@
        status: 'APPROVED',
      })
      records.value = historyResult.records.map(mapHistory)
      page.value = targetPage
      hasMore.value = historyResult.page < historyResult.pages
    }
    else {
      const [todoResult, historyResult] = await Promise.all([
        fetchMyApprovalTodos(params),
        fetchMyApprovalHistory(params),
      ])
      records.value = sortRecords([
        ...todoResult.records.map(mapTodo),
        ...historyResult.records.map(mapHistory),
      ])
      const allResult = await fetchMyApprovalAll(params)
      records.value = allResult.records.map(mapAll)
      page.value = targetPage
      hasMore.value = allResult.page < allResult.pages
    }
  }
  catch (error) {
@@ -225,8 +307,92 @@
  }
}
async function loadMoreRecords() {
  if (loading.value || loadingMore.value || !hasMore.value) {
    return
  }
  loadingMore.value = true
  const nextPage = page.value + 1
  try {
    const params = {
      page: nextPage,
      size: pageSize,
      visitorNameKeyword: searchKeyword.value.trim() || undefined,
      visitDate: dateValue.value || undefined,
    }
    if (activeTab.value === 0) {
      const todoResult = await fetchMyApprovalTodos(params)
      records.value = [...records.value, ...todoResult.records.map(mapTodo)]
      page.value = nextPage
      hasMore.value = todoResult.page < todoResult.pages
      return
    }
    if (activeTab.value === 1) {
      const historyResult = await fetchMyApprovalHistory({
        ...params,
        status: 'REJECTED',
      })
      records.value = [...records.value, ...historyResult.records.map(mapHistory)]
      page.value = nextPage
      hasMore.value = historyResult.page < historyResult.pages
      return
    }
    if (activeTab.value === 2) {
      const historyResult = await fetchMyApprovalHistory({
        ...params,
        status: 'APPROVED',
      })
      records.value = [...records.value, ...historyResult.records.map(mapHistory)]
      page.value = nextPage
      hasMore.value = historyResult.page < historyResult.pages
      return
    }
    const allResult = await fetchMyApprovalAll(params)
    records.value = [...records.value, ...allResult.records.map(mapAll)]
    page.value = nextPage
    hasMore.value = allResult.page < allResult.pages
  }
  catch (error) {
    uni.showToast({
      title: error instanceof Error ? error.message : '加载更多失败',
      icon: 'none',
    })
  }
  finally {
    loadingMore.value = false
    scrollLoadLocked.value = false
  }
}
function handleListScroll(e: any) {
  const detail = e?.detail
  if (!detail || scrollLoadLocked.value || loading.value || loadingMore.value || !hasMore.value) {
    return
  }
  const scrollTop = Number(detail.scrollTop || 0)
  const clientHeight = Number(detail.clientHeight || 0)
  const scrollHeight = Number(detail.scrollHeight || 0)
  if (scrollHeight <= 0 || clientHeight <= 0) {
    return
  }
  const remain = scrollHeight - (scrollTop + clientHeight)
  if (remain <= 120) {
    scrollLoadLocked.value = true
    void loadMoreRecords()
  }
}
function handleTabClick(index: number) {
  activeTab.value = index
  listRenderKey.value += 1
  scrollIntoViewId.value = ''
  nextTick(() => {
    scrollIntoViewId.value = 'review-list-top-anchor'
  })
  void loadRecords()
}
@@ -249,18 +415,28 @@
onShow(() => {
  void loadRecords()
})
function handleScrollRefresh() {
  if (refreshing.value) {
    return
  }
  refreshing.value = true
  void loadRecords().finally(() => {
    refreshing.value = false
  })
}
</script>
<template>
  <view class="min-h-screen bg-[#F6FBFF] w-full relative box-border flex flex-col">
  <view class="h-screen bg-[#F6FBFF] w-full relative box-border flex flex-col overflow-hidden">
    <view class="fixed left-0 top-0 z-0 h-[500rpx] w-full" style="background: linear-gradient(180deg, #5398FF 0%, #A9D0FF 50%, #F6FBFF 100%);" />
    <view class="fixed top-0 left-0 w-full z-50 flex flex-col">
      <view class="absolute left-0 top-0 z-[-1] w-full overflow-hidden" :style="{ height: (statusBarHeight + navBarHeight + uni.upx2px(36) + uni.upx2px(220) + uni.upx2px(24)) + 'px' }">
    <view class="fixed top-0 left-0 w-full z-50 flex flex-col pointer-events-none">
      <view class="absolute left-0 top-0 z-[-1] w-full overflow-hidden" :style="{ height: `${fixedHeaderHeightPx}px` }">
        <view class="w-full h-[500rpx] absolute top-0 left-0" style="background: linear-gradient(180deg, #5398FF 0%, #A9D0FF 50%, #F6FBFF 100%);" />
      </view>
      <view class="relative w-full shrink-0" :style="{ height: (statusBarHeight + navBarHeight) + 'px' }">
      <view class="relative w-full shrink-0 pointer-events-auto" :style="{ height: (statusBarHeight + navBarHeight) + 'px' }">
        <view class="flex items-center justify-between w-full absolute left-0" :style="{ top: statusBarHeight + 'px', height: navBarHeight + 'px' }">
          <view class="flex h-full w-[88rpx] items-center justify-center relative z-10" @click="handleBack">
            <wd-icon name="arrow-left" size="40rpx" color="#ffffff" />
@@ -272,7 +448,7 @@
        </view>
      </view>
      <view class="px-[24rpx] pt-[36rpx] pb-[24rpx] w-full box-border">
      <view class="px-[24rpx] pt-[36rpx] pb-[24rpx] w-full box-border pointer-events-auto">
        <view class="bg-white rounded-[16rpx] search-card shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] w-full shrink-0 flex flex-col">
          <view class="flex h-[88rpx] px-[16rpx] mt-[16rpx]">
            <view v-for="(tab, index) in tabs" :key="tab.title" class="flex-1 flex flex-col items-center justify-center relative" @click="handleTabClick(index)">
@@ -290,7 +466,7 @@
              <input
                v-model="searchKeyword"
                type="text"
                placeholder="搜索被访人姓名"
                placeholder="搜索访客姓名"
                placeholder-class="text-[#B3B3B3] text-[26rpx]"
                class="w-full text-[26rpx] text-[#333333] h-full"
                confirm-type="search"
@@ -312,11 +488,24 @@
      </view>
    </view>
    <view :style="{ height: (statusBarHeight + navBarHeight + uni.upx2px(36) + uni.upx2px(220) + uni.upx2px(24)) + 'px' }" class="shrink-0" />
    <view :style="{ height: `${fixedHeaderHeightPx}px` }" class="shrink-0" />
    <view class="relative z-10 px-[24rpx] flex-1 flex flex-col min-h-0 box-border">
      <scroll-view scroll-y class="flex-1 min-h-0" :show-scrollbar="false">
      <scroll-view
        :key="listRenderKey"
        scroll-y
        class="flex-1 min-h-0"
        :show-scrollbar="false"
        :scroll-into-view="scrollIntoViewId"
        :lower-threshold="120"
        refresher-enabled
        :refresher-triggered="refreshing"
        @refresherrefresh="handleScrollRefresh"
        @scroll="handleListScroll"
        @scrolltolower="loadMoreRecords"
      >
        <view class="pb-[24rpx]">
          <view id="review-list-top-anchor" class="h-[2rpx]" />
          <view
            v-for="item in records"
            :key="item.key"
@@ -345,33 +534,33 @@
              <view class="flex flex-col px-[40rpx] pt-[8rpx]">
                <view class="flex items-center h-[80rpx]">
                  <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">被访人:</text>
                  <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">访客姓名:</text>
                  <view class="flex-1 h-full flex items-center border-b1">
                    <text class="text-[#666666] text-[28rpx]">{{ item.name || '-' }}</text>
                    <text class="text-[#666666] text-[28rpx]">{{ item.visitorText || '-' }}</text>
                  </view>
                </view>
                <view v-if="item.plateText" class="flex items-center h-[80rpx]">
                  <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">访客车辆:</text>
                  <view class="flex-1 h-full flex items-center border-b1">
                    <text class="text-[#666666] text-[28rpx]">{{ item.plateText }}</text>
                  </view>
                </view>
                <view class="flex items-center h-[80rpx]">
                  <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">单号:</text>
                  <view class="flex-1 h-full flex items-center border-b1">
                    <text class="text-[#666666] text-[28rpx]">{{ item.orderNo }}</text>
                  </view>
                </view>
                <view class="flex items-center h-[80rpx]">
                  <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">拜访时间:</text>
                  <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">来访时间:</text>
                  <view class="flex-1 h-full flex items-center border-b1">
                    <text class="text-[#666666] text-[28rpx]">{{ item.time }}</text>
                  </view>
                </view>
                <view class="flex items-center h-[80rpx]">
                  <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">节点:</text>
                  <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">来访事由:</text>
                  <view class="flex-1 h-full flex items-center border-b1">
                    <text class="text-[#666666] text-[28rpx]">{{ item.nodeText }}</text>
                    <text class="text-[#666666] text-[28rpx]">{{ item.reasonText }}</text>
                  </view>
                </view>
                <view class="flex items-center h-[80rpx]">
                  <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">提交时间:</text>
                  <view class="flex-1 h-full flex items-center">
                    <text class="text-[#666666] text-[28rpx]">{{ formatDateTime(item.createTime) }}</text>
                <view class="flex items-center min-h-[80rpx]">
                  <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">事由说明:</text>
                  <view class="flex-1 min-h-[80rpx] flex items-center border-b1 py-[16rpx] box-border">
                    <text class="text-[#666666] text-[28rpx] line-clamp-2 break-all">{{ item.reasonOtherText }}</text>
                  </view>
                </view>
              </view>
@@ -384,6 +573,12 @@
          <view v-else-if="records.length === 0" class="text-center text-[#999999] text-[24rpx] mt-[32rpx] mb-[32rpx]">
            暂无待办记录
          </view>
          <view v-else-if="loadingMore" class="text-center text-[#999999] text-[24rpx] mt-[32rpx] mb-[32rpx]">
            加载更多中...
          </view>
          <view v-else-if="hasMore" class="text-center text-[#999999] text-[24rpx] mt-[32rpx] mb-[32rpx]">
            上拉加载更多
          </view>
          <view v-else class="text-center text-[#999999] text-[24rpx] mt-[32rpx] mb-[32rpx]">
            -最多显示3个月数据-
          </view>