feat(gltTicketOrder): 添加配送订单时间追踪和配送员指派功能

- 在订单模型中新增派送时间、送达时间和签收时间字段
- 实现配送订单状态动态渲染,支持待配送、已派送、已送达、已签收状态
- 添加配送时间节点显示,包括下单时间、派送时间、送达时间和签收时间
- 实现配送员指派弹窗功能,支持选择新的配送人员
- 集成配送员列表查询和搜索过滤功能
- 优化配送信息展示布局,分离配送人员信息和联系信息显示
- 实现配送员指派的表单验证和提交逻辑
This commit is contained in:
2026-02-06 20:32:43 +08:00
parent 4b93b4db48
commit 633109e67e
2 changed files with 224 additions and 18 deletions

View File

@@ -50,6 +50,12 @@ export interface GltTicketOrder {
tenantId?: number; tenantId?: number;
// 创建时间 // 创建时间
createTime?: string; createTime?: string;
// 派送时间(后端可能返回该字段用于派单/出库时间)
sendTime?: string;
// 送达时间
arriveTime?: string;
// 签收时间
signTime?: string;
// 修改时间 // 修改时间
updateTime?: string; updateTime?: string;
} }

View File

@@ -44,8 +44,16 @@
<template v-if="column.key === 'riderName'"> <template v-if="column.key === 'riderName'">
<a-space> <a-space>
<div class="flex flex-col"> <div class="flex flex-col">
<div v-if="record.sendTime">配送时间{{ record.sendTime || '-' }}</div> <div class="text-gray-400 flex justify-between">
<div class="text-gray-400 flex justify-between">配送人员{{ record.riderName || '-' }} <a-tag color="blue" class="cursor-pointer">指派</a-tag></div> 配送人员{{ record.riderName || '-' }}
<a-tag
color="blue"
class="cursor-pointer"
@click.stop="openAssign(record)"
>
指派
</a-tag>
</div>
<div class="text-blue-400">联系电话{{ record.riderPhone || '-' }}</div> <div class="text-blue-400">联系电话{{ record.riderPhone || '-' }}</div>
</div> </div>
</a-space> </a-space>
@@ -54,18 +62,25 @@
<a-image :src="record.image" :width="50" /> <a-image :src="record.image" :width="50" />
</template> </template>
<template v-if="column.key === 'status'"> <template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">待配送</a-tag> <a-tag :color="getOrderStatus(record).color">
<a-tag v-if="record.status === 1" color="red">已派送</a-tag> {{ getOrderStatus(record).label }}
<a-tag v-if="record.status === 2" color="red">已送达</a-tag> </a-tag>
<a-tag v-if="record.status === 3" color="red">已签收</a-tag>
</template> </template>
<template v-if="column.key ==='createTime'"> <template v-if="column.key ==='times'">
<a-space> <a-space>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="text-gray-400">下单时间{{ record.createTime }}</div> <div class="text-gray-400">
<div class="text-gray-400">派送时间{{ record.updateTime }}</div> 下单时间{{ formatTime(getOrderTimes(record).orderTime) }}
<div class="text-gray-400">送达时间{{ record.createTime }}</div> </div>
<div class="text-gray-400">签收时间{{ record.updateTime }}</div> <div class="text-gray-400">
派送时间{{ formatTime(getOrderTimes(record).sendTime) }}
</div>
<div class="text-gray-400">
送达时间{{ formatTime(getOrderTimes(record).arriveTime) }}
</div>
<div class="text-gray-400">
签收时间{{ formatTime(getOrderTimes(record).signTime) }}
</div>
</div> </div>
</a-space> </a-space>
</template> </template>
@@ -85,13 +100,53 @@
</ele-pro-table> </ele-pro-table>
</a-card> </a-card>
<!-- 指派弹窗 -->
<a-modal
v-model:visible="assignVisible"
title="指派配送人员"
:confirm-loading="assignConfirmLoading"
:maskClosable="false"
width="520px"
@ok="submitAssign"
@cancel="closeAssign"
>
<a-form
ref="assignFormRef"
:model="assignForm"
:rules="assignRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="当前配送员">
<span>
{{ assignRow?.riderName || '-' }}
<span v-if="assignRow?.riderPhone" class="text-gray-400">
{{ assignRow?.riderPhone }}
</span>
</span>
</a-form-item>
<a-form-item label="新配送员" name="riderId">
<a-select
v-model:value="assignForm.riderId"
placeholder="请选择配送人员"
show-search
allow-clear
:loading="riderLoading"
option-filter-prop="label"
:filter-option="filterRiderOption"
:options="riderOptions"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 编辑弹窗 --> <!-- 编辑弹窗 -->
<GltTicketOrderEdit v-model:visible="showEdit" :data="current" @done="reload" /> <GltTicketOrderEdit v-model:visible="showEdit" :data="current" @done="reload" />
</a-page-header> </a-page-header>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { createVNode, ref } from 'vue'; import { computed, createVNode, reactive, ref } from 'vue';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'; import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro'; import type { EleProTable } from 'ele-admin-pro';
@@ -103,8 +158,11 @@
import Search from './components/search.vue'; import Search from './components/search.vue';
import {getPageTitle} from '@/utils/common'; import {getPageTitle} from '@/utils/common';
import GltTicketOrderEdit from './components/gltTicketOrderEdit.vue'; import GltTicketOrderEdit from './components/gltTicketOrderEdit.vue';
import { pageGltTicketOrder, removeGltTicketOrder, removeBatchGltTicketOrder } from '@/api/glt/gltTicketOrder'; import { pageGltTicketOrder, removeGltTicketOrder, removeBatchGltTicketOrder, getGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder';
import type { GltTicketOrder, GltTicketOrderParam } from '@/api/glt/gltTicketOrder/model'; import type { GltTicketOrder, GltTicketOrderParam } from '@/api/glt/gltTicketOrder/model';
import { listShopStoreRider } from '@/api/shop/shopStoreRider';
import type { ShopStoreRider } from '@/api/shop/shopStoreRider/model';
import type { FormInstance } from 'ant-design-vue/es/form';
// 表格实例 // 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null); const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
@@ -120,6 +178,149 @@
// 加载状态 // 加载状态
const loading = ref(true); const loading = ref(true);
/* 指派配送员 */
const assignVisible = ref(false);
const assignConfirmLoading = ref(false);
const riderLoading = ref(false);
const riderList = ref<ShopStoreRider[]>([]);
const assignRow = ref<GltTicketOrder | null>(null);
const assignFormRef = ref<FormInstance | null>(null);
const assignForm = reactive<{ riderId: string | number | undefined }>({
riderId: undefined
});
const assignRules = reactive({
riderId: [{ required: true, message: '请选择配送人员', trigger: 'change' }]
});
const filterRiderOption = (input: string, option: any) => {
const label = String(option?.label ?? '');
return label.toLowerCase().includes(input.toLowerCase());
};
const riderOptions = computed(() => {
const currentRiderId = assignRow.value?.riderId;
return (riderList.value || []).map((r) => {
const label = `${r.realName || '-'}${r.mobile ? `${r.mobile}` : ''}`;
return {
value: r.id,
label,
disabled:
r.status === 0 ||
(currentRiderId != null && String(r.id ?? '') === String(currentRiderId))
};
});
});
const loadRiders = async (row: GltTicketOrder) => {
riderLoading.value = true;
try {
// 优先按配送点/门店过滤;若后端不支持或字段不匹配,可回退到全量列表
const dealerId = (row as any).dealerId ?? row.storeId;
const data = await listShopStoreRider(
dealerId != null ? ({ dealerId } as any) : undefined
);
riderList.value = data || [];
if (!riderList.value.length) {
riderList.value = (await listShopStoreRider()) || [];
}
} catch (e: any) {
message.error(e.message);
riderList.value = [];
} finally {
riderLoading.value = false;
}
};
const openAssign = (row: GltTicketOrder) => {
assignRow.value = row;
assignForm.riderId = undefined;
assignVisible.value = true;
loadRiders(row);
};
const closeAssign = () => {
assignVisible.value = false;
assignRow.value = null;
assignForm.riderId = undefined;
};
const submitAssign = async () => {
if (!assignFormRef.value || !assignRow.value?.id) {
return;
}
try {
await assignFormRef.value.validate();
const nextRiderId = assignForm.riderId;
if (nextRiderId == null) {
return;
}
if (String(nextRiderId) === String(assignRow.value.riderId ?? '')) {
message.error('请选择其他配送人员');
return;
}
assignConfirmLoading.value = true;
// 先取详情再更新,避免后端 PUT 语义为“全量覆盖”导致字段被清空
const detail = await getGltTicketOrder(assignRow.value.id);
const normalizedRiderId =
typeof nextRiderId === 'string'
? Number.isNaN(Number(nextRiderId))
? nextRiderId
: Number(nextRiderId)
: nextRiderId;
const payload: GltTicketOrder = {
...(detail as any),
id: assignRow.value.id,
riderId: normalizedRiderId as any
};
const msg = await updateGltTicketOrder(payload);
message.success(msg);
closeAssign();
reload();
} catch (e: any) {
// 表单校验失败时 e 可能不是 Error
if (e?.message) {
message.error(e.message);
}
} finally {
assignConfirmLoading.value = false;
}
};
/* 状态&时间:按你的规则派生(配送时间不为空=已派送、送达时间不为空=已送达、签收时间不为空=已签收) */
const pickTime = (record: any, keys: string[]) => {
for (const k of keys) {
const v = record?.[k];
if (v) return v as string;
}
return undefined;
};
const getOrderTimes = (record: any) => {
return {
orderTime: pickTime(record, ['createTime', 'orderTime']),
sendTime: pickTime(record, ['sendTime', 'dispatchTime']),
arriveTime: pickTime(record, ['arriveTime', 'deliveredTime']),
signTime: pickTime(record, ['signTime', 'signedTime', 'receiveTime'])
};
};
const formatTime = (value?: string) => {
return value ? toDateString(value, 'yyyy-MM-dd HH:mm:ss') : '-';
};
const getOrderStatus = (record: any) => {
const { sendTime, arriveTime, signTime } = getOrderTimes(record);
if (signTime) return { value: 3, label: '已签收', color: 'red' };
if (arriveTime) return { value: 2, label: '已送达', color: 'orange' };
if (sendTime) return { value: 1, label: '已派送', color: 'blue' };
// 兼容后端只返回 status、不返回时间的情况
if (record?.status === 3) return { value: 3, label: '已签收', color: 'red' };
if (record?.status === 2) return { value: 2, label: '已送达', color: 'orange' };
if (record?.status === 1) return { value: 1, label: '已派送', color: 'blue' };
return { value: 0, label: '待配送', color: 'green' };
};
// 表格数据源 // 表格数据源
const datasource: DatasourceFunction = ({ const datasource: DatasourceFunction = ({
page, page,
@@ -191,12 +392,11 @@
// ellipsis: true // ellipsis: true
// }, // },
{ {
title: '下单时间', title: '时间节点',
dataIndex: 'createTime', dataIndex: 'createTime',
key: 'createTime', key: 'times',
width: 250, width: 260,
align: 'center', align: 'center'
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
}, },
{ {
title: '状态', title: '状态',