feat(pl): implement public driver transport history list page

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-21 11:29:10 +08:00
1 parent ea33e62eaa
commit d01603f709
1 file changed
+267 -5
@@ -1,19 +1,281 @@
<template>
<PublicMobileShell>
<van-nav-bar title="运输记录" left-arrow @click-left="goBack" />
<van-empty description="加载中…" />
<div class="public-pl-history-page">
<van-nav-bar
title="运输记录"
left-arrow
:border="false"
class="transport-nav-bar"
@click-left="goBack"
/>
<van-loading v-if="loading" vertical class="page-loading">加载中...</van-loading>
<van-empty v-else-if="errorMessage" :description="errorMessage" />
<template v-else>
<div class="filters">
<div class="chips">
<button
v-for="q in quickOptions"
:key="q.offset"
type="button"
class="chip"
:class="{ on: pickDate === q.ymd }"
@click="setQuick(q.offset)"
>
{{ q.label }}
</button>
</div>
<input v-model="pickDate" type="date" class="date-input" @change="load" />
<div class="hint">{{ filterHint }}</div>
</div>
<van-empty v-if="!records.length" description="暂无运输记录" />
<div
v-for="row in records"
:key="row.planId"
class="record-card"
role="button"
@click="openRecord(row)"
>
<div class="record-top">
<span class="plan-no">{{ row.planNo }}</span>
<span class="tag" :class="tagClass(row)">{{ row.statusLabel || '进行中' }}</span>
</div>
<div class="meta">
取货:{{ formatDay(row.pickupTime) }} · 送货:{{ formatDay(row.deliveryTime) }}
</div>
<div class="meta">
线路:{{ row.pickupAddress || '—' }} → {{ row.destinationAddress || '—' }}
</div>
</div>
</template>
</div>
</PublicMobileShell>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import { showFailToast } from 'vant'
import {
publicTransportApi,
type PublicTransportHistoryRecord
} from '@/api/pl/public-transport'
import PublicMobileShell from '@/layouts/PublicMobileShell.vue'
import 'vant/es/toast/style'
import '@/styles/public-mobile-vant.scss'
const route = useRoute()
const router = useRouter()
const token = computed(() => String(route.params.token || ''))
const loading = ref(true)
const errorMessage = ref('')
const records = ref<PublicTransportHistoryRecord[]>([])
const pickDate = ref(addDays(0))
const quickOptions = [
{ label: '今天', offset: 0, ymd: addDays(0) },
{ label: '明天', offset: 1, ymd: addDays(1) },
{ label: '后天', offset: 2, ymd: addDays(2) }
]
const filterHint = computed(() => `筛选日期:${pickDate.value}(取货或送货落在当天)`)
function formatYmd(d: Date) {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
function addDays(n: number) {
const d = new Date()
d.setHours(0, 0, 0, 0)
d.setDate(d.getDate() + n)
return formatYmd(d)
}
function formatDay(value?: string) {
if (!value) return '—'
const normalized = value.replace('T', ' ')
return normalized.length >= 10 ? normalized.slice(0, 10) : normalized
}
function tagClass(row: PublicTransportHistoryRecord) {
if (row.planVoided || row.statusLabel === '已作废') return 'tag--danger'
if (row.planArchived || row.statusLabel === '已归档') return 'tag--muted'
return 'tag--success'
}
function goBack() {
const token = String(route.params.token || '')
router.push({ name: 'PublicPlTransportDriver', params: { token } })
router.push({ name: 'PublicPlTransportDriver', params: { token: token.value } })
}
function setQuick(offset: number) {
pickDate.value = addDays(offset)
load()
}
function openRecord(row: PublicTransportHistoryRecord) {
const accessToken = (row.driverAccessToken || '').trim()
if (!accessToken) {
showFailToast('该记录暂无司机链接')
return
}
router.push({ name: 'PublicPlTransportDriver', params: { token: accessToken } })
}
async function load() {
loading.value = true
errorMessage.value = ''
try {
const data = await publicTransportApi.getHistory(token.value, pickDate.value)
records.value = data?.records || []
} catch (e: any) {
errorMessage.value = e?.message || '链接无效或已失效'
records.value = []
} finally {
loading.value = false
}
}
onMounted(load)
</script>
<style scoped lang="scss">
.page-loading {
display: flex;
justify-content: center;
padding: 48px 0;
}
.transport-nav-bar {
margin: -12px -16px 12px;
border-radius: 0 0 12px 12px;
overflow: hidden;
:deep(.van-nav-bar) {
background: #059669;
}
:deep(.van-nav-bar__title),
:deep(.van-nav-bar__arrow) {
color: #fff;
}
:deep(.van-nav-bar__title) {
font-weight: 600;
}
}
.filters {
margin-bottom: 12px;
padding: 12px 14px;
border-radius: 12px;
background: #fff;
border: 1px solid #e2e8f0;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.chip {
height: 30px;
padding: 0 12px;
border-radius: 999px;
border: 1px solid #d1fae5;
background: #ecfdf5;
color: #047857;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.chip.on {
background: #059669;
border-color: #059669;
color: #fff;
}
.date-input {
width: 100%;
height: 40px;
padding: 0 12px;
border: 1px solid #cbd5e1;
border-radius: 8px;
font-size: 14px;
color: #1e293b;
background: #fff;
box-sizing: border-box;
}
.hint {
margin-top: 8px;
font-size: 12px;
color: #64748b;
line-height: 1.4;
}
.record-card {
margin-bottom: 10px;
padding: 14px;
border-radius: 12px;
background: #fff;
border: 1px solid #e2e8f0;
cursor: pointer;
}
.record-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.plan-no {
font-size: 15px;
font-weight: 700;
color: #0f172a;
word-break: break-all;
}
.tag {
flex-shrink: 0;
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 999px;
line-height: 1.4;
}
.tag--success {
background: #059669;
color: #fff;
}
.tag--danger {
background: #dc2626;
color: #fff;
}
.tag--muted {
background: #94a3b8;
color: #fff;
}
.meta {
font-size: 13px;
color: #64748b;
line-height: 1.45;
word-break: break-all;
& + .meta {
margin-top: 4px;
}
}
</style>