- 在 GltTicketOrder 模型中新增 orderNo 和 orderStatus 字段 - 在 GltUserTicket 模型中新增 orderStatus 字段 - 在订单编辑页面添加订单编号输入框并调整表单初始化逻辑 - 移除图片上传相关功能代码 - 在订单列表页面添加订单编号显示和订单状态标签展示 - 实现订单状态的标签渲染和颜色区分 - 调整表格列配置,替换原有的状态列为订单状态列 - 在删除确认按钮上添加订单状态条件判断 - 启用表格行双击事件以支持快速编辑功能
541 lines
17 KiB
Vue
541 lines
17 KiB
Vue
<template>
|
||
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||
<ele-pro-table
|
||
ref="tableRef"
|
||
row-key="id"
|
||
:columns="columns"
|
||
:datasource="datasource"
|
||
:customRow="customRow"
|
||
tool-class="ele-toolbar-form"
|
||
class="sys-org-table"
|
||
>
|
||
<template #toolbar>
|
||
<search
|
||
@search="reload"
|
||
:selection="selection"
|
||
@add="openEdit"
|
||
@remove="removeBatch"
|
||
@batchMove="openMove"
|
||
/>
|
||
</template>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key ==='nickname'">
|
||
<a-space>
|
||
<a-avatar :src="record.avatar" />
|
||
<div class="flex flex-col">
|
||
<div class="font-bold">{{ record.nickname }}</div>
|
||
<div class="text-gray-400">订单编号:{{ record.orderNo }}</div>
|
||
<div class="text-gray-400">送货地址:{{ record.address }}</div>
|
||
<div class="text-blue-400">联系电话:{{ record.phone }}</div>
|
||
<div class="text-gray-400">买家留言:{{ record.buyerRemarks }}</div>
|
||
</div>
|
||
</a-space>
|
||
</template>
|
||
<template v-if="column.key === 'storeName'">
|
||
<a-space>
|
||
<div class="flex flex-col">
|
||
<div class="font-bold">{{ record.storeName }}</div>
|
||
<div class="text-gray-400">门店地址:{{ record.storeAddress }}</div>
|
||
<div class="text-blue-400">门店电话:{{ record.storePhone }}</div>
|
||
<div class="text-gray-400">仓库地址:{{ record.warehouseAddress }}</div>
|
||
</div>
|
||
</a-space>
|
||
</template>
|
||
<template v-if="column.key === 'riderName'">
|
||
<a-space>
|
||
<div class="flex flex-col">
|
||
<div class="text-gray-400">配送时间:{{ formatDay(record.sendTime) || '-'}}</div>
|
||
<div class="text-gray-400 flex justify-between" style="min-width: 224px">
|
||
配送人员:{{ 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-gray-400">留档图片:<a-image :src="record.sendEndImg" v-if="record.sendEndImg" :width="50" /></div>
|
||
</div>
|
||
</a-space>
|
||
</template>
|
||
<template v-if="column.key === 'image'">
|
||
<a-image :src="record.image" :width="50" />
|
||
</template>
|
||
<template v-if="column.key === 'status'">
|
||
<a-tag :color="getOrderStatus(record).color">
|
||
{{ getOrderStatus(record).label }}
|
||
</a-tag>
|
||
</template>
|
||
<template v-if="column.key === 'orderStatus'">
|
||
<a-space :size="6" wrap>
|
||
<!-- 订单状态 -->
|
||
<a-tag v-if="record.orderStatus === 0">未完成</a-tag>
|
||
<a-tag v-if="record.orderStatus === 1" color="green"
|
||
>已完成</a-tag
|
||
>
|
||
<a-tag v-if="record.orderStatus === 2">已关闭</a-tag>
|
||
<a-tag v-if="record.orderStatus === 3" color="red"
|
||
>关闭中</a-tag
|
||
>
|
||
<a-tag v-if="record.orderStatus === 4" color="red"
|
||
>退款申请中</a-tag
|
||
>
|
||
<a-tag v-if="record.orderStatus === 5" color="red"
|
||
>退款被拒绝</a-tag
|
||
>
|
||
<a-tag v-if="record.orderStatus === 6" color="orange"
|
||
>已退款</a-tag
|
||
>
|
||
<a-tag v-if="record.orderStatus === 7" color="pink"
|
||
>客户端申请退款</a-tag
|
||
>
|
||
</a-space>
|
||
</template>
|
||
<template v-if="column.key === 'totalNum'">
|
||
<div class="text-gray-800 text-3xl">{{ record.totalNum }}</div>
|
||
</template>
|
||
<template v-if="column.key ==='times'">
|
||
<a-space>
|
||
<div class="flex flex-col">
|
||
<div class="text-gray-400">
|
||
下单时间:{{ formatTime(getOrderTimes(record).orderTime) }}
|
||
</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>
|
||
</a-space>
|
||
</template>
|
||
<template v-if="column.key === 'action'">
|
||
<a-space>
|
||
<!-- <a @click="openEdit(record)">修改</a>-->
|
||
<!-- <a-divider type="vertical" />-->
|
||
<a-popconfirm
|
||
v-if="record.orderStatus == 6"
|
||
title="确定要删除此记录吗?"
|
||
@confirm="remove(record)"
|
||
>
|
||
<a class="ele-text-danger">删除</a>
|
||
</a-popconfirm>
|
||
</a-space>
|
||
</template>
|
||
</template>
|
||
</ele-pro-table>
|
||
</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" />
|
||
</a-page-header>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, createVNode, reactive, ref } from 'vue';
|
||
import { message, Modal } from 'ant-design-vue';
|
||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||
import type { EleProTable } from 'ele-admin-pro';
|
||
import { toDateString } from 'ele-admin-pro';
|
||
import type {
|
||
DatasourceFunction,
|
||
ColumnItem
|
||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||
import Search from './components/search.vue';
|
||
import {getPageTitle} from '@/utils/common';
|
||
import GltTicketOrderEdit from './components/gltTicketOrderEdit.vue';
|
||
import { pageGltTicketOrder, removeGltTicketOrder, removeBatchGltTicketOrder, getGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder';
|
||
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 selection = ref<GltTicketOrder[]>([]);
|
||
// 当前编辑数据
|
||
const current = ref<GltTicketOrder | null>(null);
|
||
// 是否显示编辑弹窗
|
||
const showEdit = ref(false);
|
||
// 是否显示批量移动弹窗
|
||
const showMove = ref(false);
|
||
// 加载状态
|
||
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.userId,
|
||
label,
|
||
disabled:
|
||
r.status === 0 ||
|
||
(currentRiderId != null && String(r.userId ?? '') === 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 formatDay = (value?: string) => {
|
||
return value ? toDateString(value, 'yyyy-MM-dd') : '-';
|
||
};
|
||
|
||
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 = ({
|
||
page,
|
||
limit,
|
||
where,
|
||
orders,
|
||
filters
|
||
}) => {
|
||
if (filters) {
|
||
where.status = filters.status;
|
||
}
|
||
return pageGltTicketOrder({
|
||
...where,
|
||
...orders,
|
||
page,
|
||
limit
|
||
});
|
||
};
|
||
|
||
// 完整的列配置(包含所有字段)
|
||
const columns = ref<ColumnItem[]>([
|
||
{
|
||
title: '票号',
|
||
dataIndex: 'userTicketId',
|
||
key: 'userTicketId',
|
||
width: 90
|
||
},
|
||
{
|
||
title: '订水用户',
|
||
dataIndex: 'nickname',
|
||
key: 'nickname'
|
||
},
|
||
// {
|
||
// title: '送货地址',
|
||
// dataIndex: 'address',
|
||
// key: 'address'
|
||
// },
|
||
{
|
||
title: '下单门店',
|
||
dataIndex: 'storeName',
|
||
key: 'storeName'
|
||
},
|
||
{
|
||
title: '配送信息',
|
||
dataIndex: 'riderName',
|
||
key: 'riderName'
|
||
},
|
||
{
|
||
title: '送水数量(桶)',
|
||
dataIndex: 'totalNum',
|
||
key: 'totalNum',
|
||
align: 'center',
|
||
width: 120
|
||
},
|
||
// {
|
||
// title: '买家留言',
|
||
// dataIndex: 'buyerRemarks',
|
||
// key: 'buyerRemarks'
|
||
// },
|
||
// {
|
||
// title: '用于统计',
|
||
// dataIndex: 'price',
|
||
// key: 'price'
|
||
// },
|
||
// {
|
||
// title: '备注',
|
||
// dataIndex: 'comments',
|
||
// key: 'comments',
|
||
// ellipsis: true
|
||
// },
|
||
{
|
||
title: '时间节点',
|
||
dataIndex: 'createTime',
|
||
key: 'times',
|
||
width: 260,
|
||
align: 'center'
|
||
},
|
||
{
|
||
title: '订单状态',
|
||
dataIndex: 'orderStatus',
|
||
key: 'orderStatus',
|
||
align: 'center',
|
||
width: 120
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 90,
|
||
fixed: 'right',
|
||
align: 'center',
|
||
hideInSetting: true
|
||
}
|
||
]);
|
||
|
||
/* 搜索 */
|
||
const reload = (where?: GltTicketOrderParam) => {
|
||
selection.value = [];
|
||
tableRef?.value?.reload({ where: where });
|
||
};
|
||
|
||
/* 打开编辑弹窗 */
|
||
const openEdit = (row?: GltTicketOrder) => {
|
||
current.value = row ?? null;
|
||
showEdit.value = true;
|
||
};
|
||
|
||
/* 打开批量移动弹窗 */
|
||
const openMove = () => {
|
||
showMove.value = true;
|
||
};
|
||
|
||
/* 删除单个 */
|
||
const remove = (row: GltTicketOrder) => {
|
||
const hide = message.loading('请求中..', 0);
|
||
removeGltTicketOrder(row.id)
|
||
.then((msg) => {
|
||
hide();
|
||
message.success(msg);
|
||
reload();
|
||
})
|
||
.catch((e) => {
|
||
hide();
|
||
message.error(e.message);
|
||
});
|
||
};
|
||
|
||
/* 批量删除 */
|
||
const removeBatch = () => {
|
||
if (!selection.value.length) {
|
||
message.error('请至少选择一条数据');
|
||
return;
|
||
}
|
||
Modal.confirm({
|
||
title: '提示',
|
||
content: '确定要删除选中的记录吗?',
|
||
icon: createVNode(ExclamationCircleOutlined),
|
||
maskClosable: true,
|
||
onOk: () => {
|
||
const hide = message.loading('请求中..', 0);
|
||
removeBatchGltTicketOrder(selection.value.map((d) => d.id))
|
||
.then((msg) => {
|
||
hide();
|
||
message.success(msg);
|
||
reload();
|
||
})
|
||
.catch((e) => {
|
||
hide();
|
||
message.error(e.message);
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
/* 查询 */
|
||
const query = () => {
|
||
loading.value = true;
|
||
};
|
||
|
||
/* 自定义行属性 */
|
||
const customRow = (record: GltTicketOrder) => {
|
||
return {
|
||
// 行点击事件
|
||
onClick: () => {
|
||
// console.log(record);
|
||
},
|
||
// 行双击事件
|
||
onDblclick: () => {
|
||
// openEdit(record);
|
||
}
|
||
};
|
||
};
|
||
query();
|
||
</script>
|
||
|
||
<script lang="ts">
|
||
export default {
|
||
name: 'GltTicketOrder'
|
||
};
|
||
</script>
|
||
|
||
<style lang="less" scoped></style>
|