Files
shanxia2.0/miniprogram/pages/pl/driver-plan/detail.vue
T
2026-09-17 09:10:10 +08:00

327 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="bx-page pl-driver-plan-page pl-driver-plan-detail">
<view class="topbar" :style="safeAreaStyle">
<view class="topbar-left">
<text class="topbar-back" @click="goBack">←</text>
<text class="topbar-title">司机回执</text>
</view>
<view class="topbar-right" />
</view>
<view v-if="loading" class="page-loading">加载中...</view>
<view v-else-if="errorMessage" class="empty-hint">{{ errorMessage }}</view>
<view v-else-if="page?.planVoided" class="empty-hint">运输计划已作废</view>
<view v-else-if="page?.planArchived" class="empty-hint">运输计划已归档完成</view>
<template v-else-if="page">
<view class="section-card">
<view class="kv-row">
<text class="kv-k">计划编号</text>
<text class="kv-v">{{ page.planNo || '—' }}</text>
</view>
<view class="kv-row">
<text class="kv-k">计划日期</text>
<text class="kv-v">{{ page.planDate || '—' }}</text>
</view>
<view class="kv-row">
<text class="kv-k">车牌</text>
<text class="kv-v">{{ page.licensePlate || '—' }}</text>
</view>
<view class="kv-row">
<text class="kv-k">司机</text>
<text class="kv-v">{{ page.driverName || '—' }} {{ page.driverPhone || '' }}</text>
</view>
</view>
<view
v-for="stop in stopStates"
:key="`${stop.stopType}-${stop.stopId}`"
class="section-card"
>
<view class="stop-title"
>{{ stop.label }} · {{ stop.customerName || '—'
}}<text v-if="stop.businessType" class="meta">({{ stop.businessType }})</text></view
>
<view class="kv-row">
<text class="kv-k">地址</text>
<view class="addr-row kv-v">
<text>{{ stop.address || '—' }}</text>
<text v-if="canNavigate(stop)" class="nav-btn" @click="openNavigation(stop)"
>导航</text
>
</view>
</view>
<view class="kv-row">
<text class="kv-k">联系方式</text>
<text class="kv-v">{{ stop.contact || '—' }}</text>
</view>
<view class="kv-row">
<text class="kv-k">注意事项</text>
<text class="kv-v">{{ stop.notes || '—' }}</text>
</view>
<view v-for="(item, idx) in stop.products || []" :key="item.stopProductId || idx" class="product-card">
<view class="product-name"
>{{ idx + 1 }}/{{ (stop.products || []).length }} {{ item.productName || '产品' }}</view
>
<view class="meta">计划:{{ formatPlanQty(item) }}</view>
<view class="meta">规格型号:{{ item.model || '—' }}</view>
<view class="meta">包装规格:{{ item.packageSpec || '—' }}</view>
<view v-if="stop.showProductQty && !readonly" class="actual-box">
<view class="actual-label">实际数量</view>
<view class="actual-row">
<input
class="actual-input"
type="digit"
v-model="item.actualQtyText"
placeholder="请输入"
/>
<text>{{ item.unit || '' }}</text>
</view>
</view>
</view>
<view class="attach-label">附件上传</view>
<view class="attach-row">
<image
v-for="(url, i) in stop.attachmentUrls"
:key="`${url}-${i}`"
class="attach-thumb"
:src="resolveFileUrl(url)"
mode="aspectFill"
@click="previewStopImages(stop, i)"
@longpress="removeAttachment(stop, i)"
/>
<view
v-if="!readonly && stop.attachmentUrls.length < 9"
class="attach-add"
@click="uploadForStop(stop)"
>+</view
>
</view>
<textarea
class="remark-input"
v-model="stop.remark"
:disabled="readonly"
maxlength="500"
placeholder="填写备注(可选)"
/>
</view>
</template>
<view v-if="page && !errorMessage && !loading" class="page-footer">
<view class="footer-btn footer-btn--ghost" @click="goList">运输记录</view>
<view
v-if="(page.stops || []).length && !readonly"
class="footer-btn footer-btn--primary"
@click="handleSave"
>{{ saving ? '保存中...' : '保存回执' }}</view
>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { PAGE_PATHS, TOKEN_KEY, resolveApiBase } from '@/config/api'
import { MINIO_BASE_URL } from '@/config/env'
import { driverPlanApi } from '@/api/pl'
import { getSafeAreaStyle } from '@/utils/safeArea'
import { previewImages } from '@/utils/upload'
const safeAreaStyle = getSafeAreaStyle()
const planId = ref('')
const page = ref(null)
const stopStates = ref([])
const loading = ref(true)
const saving = ref(false)
const errorMessage = ref('')
const readonly = computed(() => !!(page.value?.planVoided || page.value?.planArchived))
function formatPlanQty(item) {
const qty = item.planQty != null ? item.planQty : item.quantity
const unit = item.unit || ''
if (qty == null) return '—'
return unit ? `${qty} ${unit}` : String(qty)
}
function canNavigate(stop) {
return stop.lng != null && stop.lat != null
}
function openNavigation(stop) {
const lat = Number(stop.lat)
const lng = Number(stop.lng)
if (Number.isNaN(lat) || Number.isNaN(lng)) return
uni.openLocation({
latitude: lat,
longitude: lng,
name: stop.label || '目的地',
address: stop.address || '',
fail: () => uni.showToast({ title: '无法打开导航', icon: 'none' })
})
}
function resolveFileUrl(url) {
if (!url) return ''
if (/^https?:\/\//i.test(url)) return url
const base = String(MINIO_BASE_URL || '').replace(/\/$/, '')
return base ? `${base}/${String(url).replace(/^\//, '')}` : url
}
function previewStopImages(stop, index) {
const urls = (stop.attachmentUrls || []).map(resolveFileUrl)
previewImages(urls, urls[index])
}
function removeAttachment(stop, index) {
if (readonly.value) return
uni.showModal({
title: '删除附件',
content: '确定删除该附件吗?',
success: (res) => {
if (res.confirm) stop.attachmentUrls.splice(index, 1)
}
})
}
async function uploadForStop(stop) {
if (readonly.value) return
const remain = 9 - (stop.attachmentUrls?.length || 0)
if (remain <= 0) return
const choose = await new Promise((resolve) => {
uni.chooseImage({
count: remain,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: resolve,
fail: () => resolve(null)
})
})
if (!choose?.tempFilePaths?.length) return
const token = uni.getStorageSync(TOKEN_KEY) || ''
const uploadUrl = `${resolveApiBase()}/transport/driver-plan/${planId.value}/attachments`
uni.showLoading({ title: '上传中...' })
try {
for (const filePath of choose.tempFilePaths) {
const uploadRes = await new Promise((ok, fail) => {
uni.uploadFile({
url: uploadUrl,
filePath,
name: 'file',
header: { Authorization: token ? `Bearer ${token}` : '' },
formData: { folder: 'pl/transport/driver-feedback' },
success: ok,
fail
})
})
let body = {}
try {
body = JSON.parse(uploadRes.data || '{}')
} catch (e) {
body = {}
}
const data = body.data || {}
const url = data.url || data.fileUrl || data.file_url
if (body.code !== 200 && body.code !== 0) {
uni.showToast({ title: body.message || '上传失败', icon: 'none' })
break
}
if (!url) {
uni.showToast({ title: body.message || '上传失败', icon: 'none' })
break
}
stop.attachmentUrls.push(String(url))
}
} catch (e) {
uni.showToast({ title: '上传失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
async function loadDetail() {
if (!planId.value) {
errorMessage.value = '缺少计划 ID'
loading.value = false
return
}
loading.value = true
errorMessage.value = ''
try {
const data = await driverPlanApi.get(planId.value)
page.value = data
stopStates.value = (data.stops || []).map((s) => ({
...s,
remark: s.remark || '',
attachmentUrls: [...(s.attachmentUrls || [])],
products: (s.products || []).map((p) => ({
...p,
actualQtyText: p.actualQty != null ? String(p.actualQty) : ''
}))
}))
} catch (e) {
errorMessage.value = e?.message || '加载失败'
page.value = null
stopStates.value = []
} finally {
loading.value = false
}
}
async function handleSave() {
if (readonly.value || saving.value) return
if (!stopStates.value.length) return
saving.value = true
try {
for (const stop of stopStates.value) {
const products =
stop.showProductQty
? (stop.products || []).map((p) => ({
stopProductId: p.stopProductId,
actualQty: Number(p.actualQtyText || 0)
}))
: undefined
await driverPlanApi.saveDriverFeedback(planId.value, {
stopType: stop.stopType,
stopId: stop.stopId,
attachmentUrls: stop.attachmentUrls,
remark: (stop.remark || '').trim() || undefined,
products
})
}
uni.showToast({ title: '保存成功', icon: 'success' })
await loadDetail()
} catch (e) {
// dbRequest already toasted
} finally {
saving.value = false
}
}
function goList() {
uni.navigateBack({
fail: () => uni.redirectTo({ url: PAGE_PATHS.plDriverPlan })
})
}
function goBack() {
goList()
}
onLoad((q) => {
planId.value = (q && q.planId) || ''
loadDetail()
})
</script>
<style lang="scss">
@import '@/styles/bx-theme.scss';
@import '@/styles/pl-driver-plan.scss';
</style>