599 行
22 KiB
HTML
599 行
22 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<title>订单管理</title>
|
|
<link rel="stylesheet" type="text/css" href="__PUBLIC__/css/menu.css?v=1.0.5"/>
|
|
<link rel="stylesheet" type="text/css" href="__PUBLIC__/css/minimal-admin.css?v=1.0.0"/>
|
|
<link rel="stylesheet" type="text/css" href="__PUBLIC__/Retail/css/myscroll.css?v=1.0.2"/>
|
|
<link rel="stylesheet" type="text/css" href="__PUBLIC__/layui-v2.4.3/layui/css/layui.css"/>
|
|
<link rel="stylesheet" type="text/css" href="__PUBLIC__/Retail/css/home.css"/>
|
|
<link rel="stylesheet" type="text/css" href="__PUBLIC__/Retail/css/goodsList.css?v=1.0.2"/>
|
|
<link rel="stylesheet" type="text/css" href="__PUBLIC__/vue/element-ui.css?v=2.13.2">
|
|
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<script type="text/javascript" charset="utf-8" src="__PUBLIC__/vue/js/vue2.5.16.js"></script>
|
|
<script type="text/javascript" charset="utf-8" src="__PUBLIC__/vue/element-ui.js?v=2.13.2"></script>
|
|
<script type="text/javascript" charset="utf-8" src="__PUBLIC__/js/common.js?v=1.0.2"></script>
|
|
<script type="text/javascript" charset="utf-8" src="__PUBLIC__/jquery/js/jquery-3.2.1.min.js"></script>
|
|
<script type="text/javascript" charset="utf-8" src="__PUBLIC__/layui-v2.4.3/layui/layui.js"></script>
|
|
</head>
|
|
<body>
|
|
<div id="RiderOrderManagement" v-cloak>
|
|
<div class="goodsBox">
|
|
<div class="goods_title">
|
|
<div class="goods_title_inner">
|
|
<i class="fas fa-clipboard-list" style="margin-right: 8px; color: #409EFF;"></i>
|
|
订单管理
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 统计卡片区域 -->
|
|
<div class="order-stats-container" style="padding: 20px; background: #f5f7fa;">
|
|
<el-row :gutter="20">
|
|
<el-col :span="6">
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
|
<i class="fas fa-list-alt"></i>
|
|
</div>
|
|
<div class="stat-content">
|
|
<div class="stat-number">{{ statistics.totalOrders }}</div>
|
|
<div class="stat-label">总订单数</div>
|
|
</div>
|
|
</div>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
|
<i class="fas fa-clock"></i>
|
|
</div>
|
|
<div class="stat-content">
|
|
<div class="stat-number">{{ statistics.pendingOrders }}</div>
|
|
<div class="stat-label">待配送</div>
|
|
</div>
|
|
</div>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
|
|
<i class="fas fa-shipping-fast"></i>
|
|
</div>
|
|
<div class="stat-content">
|
|
<div class="stat-number">{{ statistics.deliveringOrders }}</div>
|
|
<div class="stat-label">配送中</div>
|
|
</div>
|
|
</div>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
|
|
<i class="fas fa-check-circle"></i>
|
|
</div>
|
|
<div class="stat-content">
|
|
<div class="stat-number">{{ statistics.completedOrders }}</div>
|
|
<div class="stat-label">已完成</div>
|
|
</div>
|
|
</div>
|
|
</el-col>
|
|
</el-row>
|
|
</div>
|
|
|
|
<!-- 主要内容区域 -->
|
|
<div style="padding: 20px; background: white; margin: 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);">
|
|
<!-- 工具栏 -->
|
|
<div class="toolbar" style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
|
|
<div class="toolbar-left" style="display: flex; align-items: center;">
|
|
<el-input
|
|
v-model="searchKeyword"
|
|
placeholder="搜索订单号或收货人"
|
|
prefix-icon="el-icon-search"
|
|
style="width: 300px; margin-right: 15px;"
|
|
@input="handleSearch">
|
|
</el-input>
|
|
<el-select v-model="statusFilter" placeholder="订单状态" style="width: 120px; margin-right: 15px;" @change="handleFilter">
|
|
<el-option label="全部状态" value=""></el-option>
|
|
<el-option label="待分配" value="pending"></el-option>
|
|
<el-option label="已接单" value="accepted"></el-option>
|
|
<el-option label="取货中" value="picking_up"></el-option>
|
|
<el-option label="配送中" value="delivering"></el-option>
|
|
<el-option label="已完成" value="completed"></el-option>
|
|
<el-option label="已取消" value="cancelled"></el-option>
|
|
</el-select>
|
|
<el-date-picker
|
|
v-model="dateRange"
|
|
type="daterange"
|
|
range-separator="至"
|
|
start-placeholder="开始日期"
|
|
end-placeholder="结束日期"
|
|
style="width: 240px; margin-right: 15px;"
|
|
@change="handleFilter">
|
|
</el-date-picker>
|
|
</div>
|
|
<div class="toolbar-right">
|
|
<el-button type="success" icon="el-icon-refresh" @click="refreshData">刷新</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 订单列表表格 -->
|
|
<el-table
|
|
:data="filteredOrders"
|
|
v-loading="loading"
|
|
element-loading-text="加载中..."
|
|
style="width: 100%"
|
|
:header-cell-style="{background:'#f5f7fa',color:'#606266'}"
|
|
@selection-change="handleSelectionChange">
|
|
|
|
<el-table-column type="selection" width="55"></el-table-column>
|
|
|
|
<el-table-column prop="order_no" label="订单号" width="160">
|
|
<template slot-scope="scope">
|
|
<el-button type="text" @click="viewOrderDetail(scope.row)">{{ scope.row.order_no }}</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column label="状态" width="100" align="center">
|
|
<template slot-scope="scope">
|
|
<el-tag :type="getStatusTagType(scope.row.status)" size="small">
|
|
{{ getStatusText(scope.row.status) }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column label="骑手" width="120">
|
|
<template slot-scope="scope">
|
|
<div v-if="scope.row.rider">
|
|
<el-avatar :size="30" :src="scope.row.rider.avatar" icon="el-icon-user-solid"></el-avatar>
|
|
<span style="margin-left: 8px;">{{ scope.row.rider.name }}</span>
|
|
</div>
|
|
<span v-else style="color: #999;">未分配</span>
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column label="商户信息" width="200">
|
|
<template slot-scope="scope">
|
|
<div>
|
|
<div style="font-weight: bold;">{{ scope.row.merchant.name }}</div>
|
|
<div style="color: #666; font-size: 12px;">{{ scope.row.merchant.address }}</div>
|
|
<div style="color: #666; font-size: 12px;">{{ scope.row.merchant.phone }}</div>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column label="收货信息" width="200">
|
|
<template slot-scope="scope">
|
|
<div>
|
|
<div style="font-weight: bold;">{{ scope.row.customer.name }}</div>
|
|
<div style="color: #666; font-size: 12px;">{{ scope.row.customer.address }}</div>
|
|
<div style="color: #666; font-size: 12px;">{{ scope.row.customer.phone }}</div>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column label="配送费" width="100" align="center">
|
|
<template slot-scope="scope">
|
|
<span style="color: #67C23A; font-weight: bold;">¥{{ scope.row.deliveryFee }}</span>
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column label="距离" width="80" align="center">
|
|
<template slot-scope="scope">
|
|
{{ scope.row.distance }}km
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column label="下单时间" width="160">
|
|
<template slot-scope="scope">
|
|
{{ formatDate(scope.row.createdAt) }}
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column label="操作" width="200" align="center" fixed="right">
|
|
<template slot-scope="scope">
|
|
<el-link icon="el-icon-view" @click="viewOrderDetail(scope.row)">详情</el-link>
|
|
<el-link v-if="scope.row.status === 'pending'" icon="el-icon-user" @click="assignRider(scope.row)">分配</el-link>
|
|
<el-link v-if="['pending', 'accepted'].includes(scope.row.status)" icon="el-icon-close" @click="cancelOrder(scope.row)">取消</el-link>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<!-- 分页 -->
|
|
<div style="margin-top: 20px; text-align: center;">
|
|
<el-pagination
|
|
@size-change="handleSizeChange"
|
|
@current-change="handleCurrentChange"
|
|
:current-page="currentPage"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
:page-size="pageSize"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
:total="+totalCount">
|
|
</el-pagination>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<style>
|
|
/* 统计卡片样式 */
|
|
.stat-card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
display: flex;
|
|
align-items: center;
|
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
}
|
|
|
|
.stat-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.stat-icon {
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 15px;
|
|
}
|
|
|
|
.stat-icon i {
|
|
font-size: 24px;
|
|
color: white;
|
|
}
|
|
|
|
.stat-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 28px;
|
|
font-weight: bold;
|
|
color: #2c3e50;
|
|
line-height: 1;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 14px;
|
|
color: #7f8c8d;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
/* 响应式设计 */
|
|
.stat-card {
|
|
text-align: center;
|
|
padding: 20px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: #409eff;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 14px;
|
|
color: #666;
|
|
}
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
.order-stats-container .el-col {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.toolbar {
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
|
|
.toolbar-left, .toolbar-right {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
}
|
|
}
|
|
|
|
.el-date-editor .el-range__icon, .el-date-editor .el-range__close-icon {
|
|
line-height: 14px !important;
|
|
}
|
|
.el-date-editor .el-range-separator {
|
|
line-height: 16px !important;
|
|
width: 13% !important;
|
|
}
|
|
.el-input--prefix .el-input__inner {
|
|
padding-left: 30px !important;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
new Vue({
|
|
el: '#RiderOrderManagement',
|
|
data() {
|
|
return {
|
|
loading: false,
|
|
assigning: false,
|
|
searchKeyword: '',
|
|
statusFilter: '',
|
|
dateRange: [],
|
|
currentPage: 1,
|
|
pageSize: 20,
|
|
totalCount: 0,
|
|
selectedOrders: [],
|
|
showAssignRiderDialog: false,
|
|
showDetailDialog: false,
|
|
selectedOrder: null,
|
|
|
|
// 统计数据
|
|
statistics: {
|
|
totalOrders: 0,
|
|
pendingOrders: 0,
|
|
deliveringOrders: 0,
|
|
completedOrders: 0
|
|
},
|
|
|
|
// 订单列表数据
|
|
orders: [],
|
|
|
|
// 分配表单
|
|
assignForm: {
|
|
riderId: '',
|
|
remark: ''
|
|
},
|
|
|
|
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
filteredOrders() {
|
|
// 服务端分页返回当前页数据,直接展示
|
|
return this.orders;
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
// 加载统计数据
|
|
async loadStatistics() {
|
|
try {
|
|
const res = await fetch('/Retail/RiderOrder/getStatistics', { method: 'POST' });
|
|
const data = await res.json();
|
|
if (data.code === 1 && data.data) {
|
|
this.statistics.totalOrders = data.data.totalOrders || 0;
|
|
this.statistics.pendingOrders = data.data.pendingOrders || 0;
|
|
this.statistics.deliveringOrders = data.data.deliveringOrders || 0;
|
|
this.statistics.completedOrders = data.data.completedOrders || 0;
|
|
}
|
|
} catch (e) {
|
|
console.error('加载统计失败', e);
|
|
}
|
|
},
|
|
|
|
|
|
// 加载订单列表(服务端筛选)
|
|
async loadOrders() {
|
|
this.loading = true;
|
|
try {
|
|
const params = {
|
|
keyword: this.searchKeyword,
|
|
status: this.statusFilter,
|
|
startDate: this.dateRange && this.dateRange.length === 2 ? this.formatDateParam(this.dateRange[0]) : '',
|
|
endDate: this.dateRange && this.dateRange.length === 2 ? this.formatDateParam(this.dateRange[1]) : '',
|
|
page: this.currentPage,
|
|
pageSize: this.pageSize
|
|
};
|
|
const res = await fetch('/Retail/RiderOrder/getOrderList', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams(params).toString()
|
|
});
|
|
const data = await res.json();
|
|
if (data.code === 1 && data.data) {
|
|
this.totalCount = data.data.totalCount || 0;
|
|
this.orders = (data.data.list || []).map(o => ({
|
|
id: o.id,
|
|
order_no: o.order_no,
|
|
status: o.status,
|
|
rider: o.rider ? {
|
|
id: o.rider.id,
|
|
name: o.rider.name,
|
|
phone: o.rider.phone,
|
|
avatar: o.rider.avatar || '',
|
|
rating: o.rider.rating ? Number(o.rider.rating) : 0
|
|
} : null,
|
|
merchant: o.merchant || { name: '', address: '', phone: '' },
|
|
customer: o.customer || { name: '', address: '', phone: '' },
|
|
deliveryFee: o.delivery_fee ? Number(o.delivery_fee) : (o.deliveryFee || 0),
|
|
distance: o.distance ? Number(o.distance) : 0,
|
|
estimatedTime: o.estimated_time ? Number(o.estimated_time) : 0,
|
|
createdAt: o.created_at || o.createdAt,
|
|
items: o.items || []
|
|
}));
|
|
} else {
|
|
this.$message.error(data.msg || '加载订单失败');
|
|
}
|
|
} catch (e) {
|
|
console.error('加载订单失败', e);
|
|
this.$message.error('网络错误');
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
// 状态相关方法
|
|
getStatusTagType(status) {
|
|
const types = {
|
|
'pending': 'warning',
|
|
'accepted': 'primary',
|
|
'picking_up': 'primary',
|
|
'delivering': 'success',
|
|
'completed': 'success',
|
|
'cancelled': 'danger'
|
|
};
|
|
return types[status] || 'info';
|
|
},
|
|
|
|
getStatusText(status) {
|
|
const texts = {
|
|
'pending': '待分配',
|
|
'accepted': '已接单',
|
|
'picking_up': '取货中',
|
|
'delivering': '配送中',
|
|
'completed': '已完成',
|
|
'cancelled': '已取消'
|
|
};
|
|
return texts[status] || '未知';
|
|
},
|
|
|
|
// 格式化日期
|
|
formatDate(date) {
|
|
return new Date(date).toLocaleString();
|
|
},
|
|
|
|
// 搜索和筛选
|
|
handleSearch() {
|
|
this.currentPage = 1;
|
|
this.loadOrders();
|
|
},
|
|
|
|
handleFilter() {
|
|
this.currentPage = 1;
|
|
this.loadOrders();
|
|
},
|
|
|
|
// 分页
|
|
handleSizeChange(val) {
|
|
this.pageSize = val;
|
|
this.currentPage = 1;
|
|
this.loadOrders();
|
|
},
|
|
|
|
handleCurrentChange(val) {
|
|
this.currentPage = val;
|
|
this.loadOrders();
|
|
},
|
|
|
|
// 选择
|
|
handleSelectionChange(val) {
|
|
this.selectedOrders = val;
|
|
},
|
|
|
|
// 订单操作
|
|
viewOrderDetail(order) {
|
|
layer.open({
|
|
type: 2,
|
|
title: '订单详情',
|
|
content: '/Retail/RiderOrder/detail.html?id=' + order.id,
|
|
area: ['calc(100% - 240px)', 'calc(100% - 66px)'],
|
|
offset: 'rb'
|
|
});
|
|
},
|
|
|
|
assignRider(order) {
|
|
layer.open({
|
|
type: 2,
|
|
title: '分配骑手',
|
|
content: '/Retail/RiderOrder/assign.html?id=' + order.id,
|
|
area: ["500px", '320px'],
|
|
btnAlign: 'c',
|
|
});
|
|
},
|
|
|
|
async confirmAssign() {
|
|
if (!this.assignForm.riderId) {
|
|
this.$message.warning('请选择骑手');
|
|
return;
|
|
}
|
|
this.assigning = true;
|
|
try {
|
|
const res = await fetch('/Retail/RiderOrder/assignRider', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
orderId: this.selectedOrder.id,
|
|
riderId: this.assignForm.riderId,
|
|
remark: this.assignForm.remark
|
|
}).toString()
|
|
});
|
|
const data = await res.json();
|
|
if (data.code === 1) {
|
|
this.$message.success('分配成功');
|
|
this.closeAssignLayer();
|
|
this.loadOrders();
|
|
} else {
|
|
this.$message.error(data.msg || '分配失败');
|
|
}
|
|
} catch (e) {
|
|
console.error('分配失败', e);
|
|
this.$message.error('网络错误');
|
|
} finally {
|
|
this.assigning = false;
|
|
}
|
|
},
|
|
|
|
cancelOrder(order) {
|
|
this.$confirm('确定要取消这个订单吗?', '提示', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}).then(async () => {
|
|
try {
|
|
const res = await fetch('/Retail/RiderOrder/cancelOrder', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({ orderId: order.id, reason: '用户取消' }).toString()
|
|
});
|
|
const data = await res.json();
|
|
if (data.code === 1) {
|
|
this.$message.success('订单已取消');
|
|
this.loadOrders();
|
|
} else {
|
|
this.$message.error(data.msg || '取消失败');
|
|
}
|
|
} catch (e) {
|
|
console.error('取消失败', e);
|
|
this.$message.error('网络错误');
|
|
}
|
|
}).catch(() => {
|
|
this.$message.info('已取消操作');
|
|
});
|
|
},
|
|
|
|
// 其他操作
|
|
refreshData() {
|
|
this.loadStatistics();
|
|
this.loadOrders();
|
|
},
|
|
|
|
// 获取订单详情
|
|
// 详情改为 iframe 打开,不在当前页加载数据
|
|
|
|
|
|
|
|
// 工具
|
|
formatDateParam(date) {
|
|
const d = new Date(date);
|
|
const pad = (n) => (n < 10 ? '0' + n : n);
|
|
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
}
|
|
},
|
|
|
|
mounted() {
|
|
this.refreshData();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |