文件

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>