This commit is contained in:
2026-11-03 11:43:56 +08:00
parent 5a1cde6c0f
commit 95fb17c5aa
11 changed files with 102 additions and 16 deletions
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, type FormInstance } from 'element-plus'
import { ElMessage, ElMessageBox, type FormInstance } from 'element-plus'
import TransportDemandForm from './TransportDemandForm.vue'
import OrderFundingStatus from './OrderFundingStatus.vue'
import { createTransportDemandForm, transportDemandError, buildTransportDemand, type TransportDemandForm as DemandFormModel } from '../transportDemandForm'
@@ -10,6 +10,7 @@ import QueryToolbar, { type QueryField, type QueryModel } from '@/components/com
import type { TableAction } from '@/components/common/tableActions'
import { useAuthStore } from '@/modules/auth/public'
import {
cancelTruckOrder,
createTransport,
getBusinessOrder,
listBusinessOrders,
@@ -108,7 +109,17 @@ const queryFields = computed<QueryField[]>(() => [
label: '状态',
type: 'select',
placeholder: '全部',
options: [
options: props.kind === 'trucking' ? [
{ label: '订单受理中', value: 'FUNDING_PENDING' },
{ label: '订单处理失败', value: 'FUNDING_FAILED' },
{ label: '待审核', value: 'PENDING_REVIEW' },
{ label: '待排车', value: 'PENDING_DISPATCH' },
{ label: '待取货', value: 'DISPATCHED' },
{ label: '运输中', value: 'IN_TRANSIT' },
{ label: '已完成', value: 'COMPLETED' },
{ label: '已拒绝', value: 'REJECTED' },
{ label: '已取消', value: 'CANCELLED' },
] : [
{ label: '订单受理中', value: 'FUNDING_PENDING' },
{ label: '订单处理失败', value: 'FUNDING_FAILED' },
{ label: '待审核', value: 'PENDING_REVIEW' },
@@ -155,6 +166,24 @@ const tableColumns = computed<DataTableColumn[]>(() => [
})),
])
const canCancelTruck = computed(() => props.kind === 'trucking' && detail.value?.summary.status === 'PENDING_REVIEW'
&& auth.hasPermission('merchant:cross-border:domestic-trucking:edit'))
/** 取消前核对客户意愿,失败保留订单并刷新服务端状态。 */
async function cancelTruck() {
if (!canCancelTruck.value || !detail.value || submitting.value) return
let reason: string
try {
const result = await ElMessageBox.prompt('请填写取消原因', '取消订单', { inputValidator: value => Boolean(value?.trim()) || '请填写取消原因', confirmButtonText: '确认取消', cancelButtonText: '保留订单', type: 'warning' })
reason = result.value.trim()
} catch { return }
submitting.value = true
try {
detail.value = await cancelTruckOrder(detail.value.summary.orderNo, { version: detail.value.summary.version, status: 'CANCELLED', idempotencyKey: idempotencyKey(), remark: reason })
ElMessage.success('订单已取消')
} catch (cause) { ElMessage.error(errorMessage(cause)); await loadDetail() }
finally { submitting.value = false }
}
const detailStatus = computed(() => detail.value ? statusLabel(detail.value.summary.status) : '—')
function emptyForm(): BusinessForm { return createTransportDemandForm() }
@@ -224,7 +253,8 @@ function getRowActions(candidate: object): TableAction[] {
}
function detailFieldClass(label: unknown) {
return ['备注', props.kind === 'trucking' ? '取货地址' : '起运地址', props.kind === 'trucking' ? '送达地址' : '目的地址'].includes(String(label)) ? 'is-wide' : ''
if (label === '备注') return 'is-full'
return [props.kind === 'trucking' ? '取货地址' : '起运地址', props.kind === 'trucking' ? '送达地址' : '目的地址'].includes(String(label)) ? 'is-wide' : ''
}
const listRouteName = computed(() => ({
@@ -295,7 +325,7 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
<h2>{{ configuration.title }}下单</h2>
</div>
<el-button v-if="kind === 'looseboard'" @click="returnToList">返回列表</el-button>
<el-button @click="returnToList">返回列表</el-button>
</header>
<template v-if="mode === 'list'">
@@ -315,13 +345,13 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
<section v-else-if="mode === 'detail'" class="business-detail-page" aria-label="订单详情">
<header class="business-detail-heading">
<div class="business-detail-heading-main"><el-button link type="primary" @click="returnToList">← 返回列表</el-button><div><h2>{{ detail?.summary.orderNo || '订单详情' }}</h2></div></div>
<div class="business-detail-heading-actions"><el-tag :type="statusTone(detail?.summary.status)" effect="light">{{ detailStatus }}</el-tag><el-button :disabled="loading" @click="loadDetail">刷新</el-button></div>
<div class="business-detail-heading-actions"><el-button v-if="canCancelTruck" type="danger" :loading="submitting" :disabled="loading" @click="cancelTruck">取消订单</el-button><el-tag :type="statusTone(detail?.summary.status)" effect="light">{{ detailStatus }}</el-tag><el-button :disabled="loading" @click="loadDetail">刷新</el-button></div>
</header>
<OrderFundingStatus v-if="kind === 'trucking' && detail" kind="trucking" :order-no="detail.summary.orderNo" :status="detail.summary.status" :can-retry="canCreate" @changed="loadDetail" />
<div v-if="error" class="business-detail-error" role="alert"><strong>{{ error }}</strong><el-button size="small" @click="loadDetail">重新加载</el-button></div>
<section v-else class="business-detail-section">
<h3>订单信息</h3>
<dl class="detail-grid" :class="{ 'trucking-detail-grid': kind === 'trucking' }">
<dl class="detail-grid" :class="{ 'trucking-detail-grid': kind === 'trucking', 'looseboard-detail-grid': kind === 'looseboard' }">
<div v-for="field in detailFields" :key="String(field[0])" :class="detailFieldClass(field[0])"><dt>{{ field[0] }}</dt><dd>{{ text(field[1]) }}</dd></div>
</dl>
</section>
@@ -374,6 +404,8 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
.detail-grid div { display: grid; min-width: 0; min-height: 34px; grid-template-columns: 76px minmax(0, 1fr); align-items: center; gap: 7px; padding: 3px 8px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.trucking-detail-grid div { grid-template-columns: 112px minmax(0, 1fr); }
.detail-grid div.is-wide { grid-column: span 2; }
.detail-grid div.is-full { grid-column: 1 / -1; }
.looseboard-detail-grid { grid-auto-flow: row dense; }
.detail-grid dt { color: var(--muted); font-size: 10px; white-space: nowrap; }
.arrangement-grid div { grid-template-columns: 124px minmax(0, 1fr); }
.arrangement-grid dt { white-space: normal; overflow-wrap: anywhere; }