feat(gltTicketOrder): 添加配送订单时间追踪和配送员指派功能
- 在订单模型中新增派送时间、送达时间和签收时间字段 - 实现配送订单状态动态渲染,支持待配送、已派送、已送达、已签收状态 - 添加配送时间节点显示,包括下单时间、派送时间、送达时间和签收时间 - 实现配送员指派弹窗功能,支持选择新的配送人员 - 集成配送员列表查询和搜索过滤功能 - 优化配送信息展示布局,分离配送人员信息和联系信息显示 - 实现配送员指派的表单验证和提交逻辑
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: '状态',
|
||||||
|
|||||||
Reference in New Issue
Block a user