文件

652 行
29 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>骑手状态监控</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<style>
.page-container {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
.monitor-header {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.stats-card {
flex: 1;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.stats-number {
font-size: 28px;
font-weight: bold;
color: #409EFF;
margin-bottom: 5px;
}
.stats-label {
color: #666;
font-size: 14px;
}
.monitor-content {
display: flex;
gap: 20px;
}
.left-panel {
flex: 2;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.panel-section {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
}
.section-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
.rider-map {
height: 400px;
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #666;
margin-bottom: 20px;
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 5px;
}
.status-online { background-color: #67C23A; }
.status-busy { background-color: #E6A23C; }
.status-offline { background-color: #F56C6C; }
.rider-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #f0f0f0;
}
.rider-info {
display: flex;
align-items: center;
}
.rider-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #409EFF;
color: white;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
font-size: 12px;
}
.real-time-data {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
}
.data-item {
text-align: center;
}
.data-value {
font-size: 20px;
font-weight: bold;
color: #409EFF;
}
.data-label {
font-size: 12px;
color: #666;
margin-top: 5px;
}
.search-bar {
padding: 15px;
border-bottom: 1px solid #f0f0f0;
}
.rider-list {
max-height: 500px;
overflow-y: auto;
}
.action-buttons {
margin-top: 10px;
}
</style>
</head>
<body>
<div id="app" class="page-container">
<!-- 监控头部统计 -->
<div class="monitor-header">
<div class="stats-card">
<div class="stats-number">{{ realTimeStats.online_riders || 0 }}</div>
<div class="stats-label">在线骑手</div>
</div>
<div class="stats-card">
<div class="stats-number">{{ realTimeStats.busy_riders || 0 }}</div>
<div class="stats-label">配送中</div>
</div>
<div class="stats-card">
<div class="stats-number">{{ realTimeStats.pending_orders || 0 }}</div>
<div class="stats-label">待配送订单</div>
</div>
<div class="stats-card">
<div class="stats-number">{{ realTimeStats.completed_orders || 0 }}</div>
<div class="stats-label">今日完成</div>
</div>
<div class="stats-card">
<div class="stats-number">{{ realTimeStats.avg_delivery_time || 0 }}</div>
<div class="stats-label">平均配送时长(分钟)</div>
</div>
</div>
<!-- 监控主体内容 -->
<div class="monitor-content">
<!-- 左侧地图和列表 -->
<div class="left-panel">
<!-- 地图区域 -->
<div style="padding: 20px;">
<div class="section-title">骑手分布地图</div>
<div class="rider-map">
实时骑手位置地图 ({{ mapData.length }} 个骑手在线)
</div>
</div>
<!-- 搜索栏 -->
<div class="search-bar">
<el-form :inline="true" :model="searchForm">
<el-form-item>
<el-input v-model="searchForm.keyword" placeholder="搜索骑手姓名/手机号" clearable style="width: 200px;"></el-input>
</el-form-item>
<el-form-item>
<el-select v-model="searchForm.status" placeholder="工作状态" clearable style="width: 120px;">
<el-option label="全部" value=""></el-option>
<el-option label="在线" value="online"></el-option>
<el-option label="配送中" value="busy"></el-option>
<el-option label="离线" value="offline"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select v-model="searchForm.area" placeholder="配送区域" clearable style="width: 120px;">
<el-option label="全部" value=""></el-option>
<el-option label="万达商圈" value="1"></el-option>
<el-option label="清华大学" value="2"></el-option>
<el-option label="中关村" value="3"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchRiders">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-button type="info" @click="exportData">导出</el-button>
</el-form-item>
</el-form>
</div>
<!-- 骑手列表 -->
<div class="rider-list">
<div v-for="rider in riderList" :key="rider.id" class="rider-item">
<div class="rider-info">
<div class="rider-avatar">{{ rider.name.charAt(0) }}</div>
<div>
<div style="font-weight: bold;">{{ rider.name }}</div>
<div style="font-size: 12px; color: #666;">{{ rider.phone }}</div>
</div>
</div>
<div style="text-align: center;">
<div>
<span :class="'status-indicator status-' + rider.status"></span>
{{ getStatusText(rider.status) }}
</div>
<div style="font-size: 12px; color: #666; margin-top: 2px;">
{{ rider.area_name }}
</div>
</div>
<div style="text-align: center;">
<div style="font-weight: bold; color: #409EFF;">{{ rider.today_orders }}</div>
<div style="font-size: 12px; color: #666;">今日订单</div>
</div>
<div style="text-align: center;">
<div style="font-weight: bold; color: #67C23A;">¥{{ rider.today_income }}</div>
<div style="font-size: 12px; color: #666;">今日收入</div>
</div>
<div>
<el-button size="mini" @click="viewRiderDetail(rider)">详情</el-button>
<el-button v-if="rider.status !== 'offline'" size="mini" type="warning" @click="forceOffline(rider)">强制下线</el-button>
<el-button size="mini" type="primary" @click="sendNotification(rider)">发送通知</el-button>
</div>
</div>
</div>
<!-- 分页 -->
<div style="padding: 20px; text-align: center;">
<el-pagination
@current-change="handleCurrentChange"
:current-page="pagination.page"
:page-size="pagination.pageSize"
layout="prev, pager, next"
:total="pagination.total">
</el-pagination>
</div>
</div>
<!-- 右侧面板 -->
<div class="right-panel">
<!-- 实时数据 -->
<div class="panel-section">
<div class="section-title">实时数据</div>
<div class="real-time-data">
<div class="data-item">
<div class="data-value">{{ realTimeStats.total_riders || 0 }}</div>
<div class="data-label">总骑手数</div>
</div>
<div class="data-item">
<div class="data-value">{{ realTimeStats.online_rate || 0 }}%</div>
<div class="data-label">在线率</div>
</div>
<div class="data-item">
<div class="data-value">{{ realTimeStats.avg_response_time || 0 }}s</div>
<div class="data-label">平均响应</div>
</div>
</div>
<el-button type="primary" size="small" @click="refreshData" style="width: 100%;">刷新数据</el-button>
</div>
<!-- 异常告警 -->
<div class="panel-section">
<div class="section-title">异常告警</div>
<div v-if="alerts.length === 0" style="text-align: center; color: #999; padding: 20px;">
暂无异常告警
</div>
<div v-else>
<div v-for="alert in alerts" :key="alert.id" style="margin-bottom: 10px; padding: 10px; background: #fff2f0; border-left: 3px solid #ff4d4f; border-radius: 4px;">
<div style="font-weight: bold; color: #ff4d4f;">{{ alert.title }}</div>
<div style="font-size: 12px; color: #666; margin-top: 5px;">{{ alert.content }}</div>
<div style="font-size: 12px; color: #999; margin-top: 5px;">{{ formatDateTime(alert.time) }}</div>
</div>
</div>
</div>
<!-- 快速操作 -->
<div class="panel-section">
<div class="section-title">快速操作</div>
<div class="action-buttons">
<el-button type="primary" size="small" @click="broadcastMessage" style="width: 100%; margin-bottom: 10px;">群发消息</el-button>
<el-button type="success" size="small" @click="optimizeDispatch" style="width: 100%; margin-bottom: 10px;">优化调度</el-button>
<el-button type="warning" size="small" @click="emergencyCall" style="width: 100%; margin-bottom: 10px;">紧急呼叫</el-button>
<el-button type="info" size="small" @click="generateReport" style="width: 100%;">生成报告</el-button>
</div>
</div>
<!-- 今日统计 -->
<div class="panel-section">
<div class="section-title">今日统计</div>
<div style="font-size: 14px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>订单总数:</span>
<span style="font-weight: bold; color: #409EFF;">{{ todayStats.total_orders || 0 }}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>完成订单:</span>
<span style="font-weight: bold; color: #67C23A;">{{ todayStats.completed_orders || 0 }}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>取消订单:</span>
<span style="font-weight: bold; color: #F56C6C;">{{ todayStats.cancelled_orders || 0 }}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>总收入:</span>
<span style="font-weight: bold; color: #E6A23C;">¥{{ todayStats.total_income || 0 }}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>完成率:</span>
<span style="font-weight: bold; color: #409EFF;">{{ todayStats.completion_rate || 0 }}%</span>
</div>
</div>
</div>
</div>
</div>
<!-- 骑手详情对话框 -->
<el-dialog title="骑手详情" :visible.sync="detailDialogVisible" width="800px">
<div v-if="currentRider">
<el-tabs v-model="activeTab">
<el-tab-pane label="基本信息" name="basic">
<el-descriptions :column="2" border>
<el-descriptions-item label="姓名">{{ currentRider.name }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ currentRider.phone }}</el-descriptions-item>
<el-descriptions-item label="工作状态">
<span :class="'status-indicator status-' + currentRider.status"></span>
{{ getStatusText(currentRider.status) }}
</el-descriptions-item>
<el-descriptions-item label="配送区域">{{ currentRider.area_name }}</el-descriptions-item>
<el-descriptions-item label="在线时长">{{ currentRider.online_duration }}</el-descriptions-item>
<el-descriptions-item label="当前位置">{{ currentRider.current_location }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="今日数据" name="today">
<div style="display: flex; gap: 20px;">
<div style="flex: 1;">
<h4>订单统计</h4>
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>接单数量:</span>
<span style="font-weight: bold;">{{ currentRider.today_orders }}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>完成数量:</span>
<span style="font-weight: bold; color: #67C23A;">{{ currentRider.completed_orders }}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>取消数量:</span>
<span style="font-weight: bold; color: #F56C6C;">{{ currentRider.cancelled_orders }}</span>
</div>
</div>
<div style="flex: 1;">
<h4>收入统计</h4>
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>配送费:</span>
<span style="font-weight: bold;">¥{{ currentRider.delivery_income }}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>奖励:</span>
<span style="font-weight: bold; color: #67C23A;">¥{{ currentRider.bonus_income }}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<span>总收入:</span>
<span style="font-weight: bold; color: #409EFF;">¥{{ currentRider.today_income }}</span>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="轨迹记录" name="track">
<div style="height: 300px; background: #f0f0f0; border: 1px solid #ddd; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: #666;">
骑手今日配送轨迹图
</div>
</el-tab-pane>
</el-tabs>
</div>
</el-dialog>
<!-- 发送通知对话框 -->
<el-dialog title="发送通知" :visible.sync="notificationDialogVisible" width="500px">
<el-form :model="notificationForm" :rules="notificationRules" ref="notificationForm" label-width="80px">
<el-form-item label="通知类型" prop="type">
<el-select v-model="notificationForm.type" placeholder="请选择" style="width: 100%;">
<el-option label="系统通知" value="system"></el-option>
<el-option label="订单提醒" value="order"></el-option>
<el-option label="紧急通知" value="urgent"></el-option>
</el-select>
</el-form-item>
<el-form-item label="通知内容" prop="content">
<el-input type="textarea" v-model="notificationForm.content" placeholder="请输入通知内容" :rows="4"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="notificationDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmSendNotification">发送</el-button>
</div>
</el-dialog>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
realTimeStats: {},
todayStats: {},
riderList: [],
mapData: [],
alerts: [],
searchForm: {
keyword: '',
status: '',
area: ''
},
pagination: {
page: 1,
pageSize: 10,
total: 0
},
detailDialogVisible: false,
notificationDialogVisible: false,
currentRider: null,
activeTab: 'basic',
notificationForm: {
type: '',
content: ''
},
notificationRules: {
type: [
{ required: true, message: '请选择通知类型', trigger: 'change' }
],
content: [
{ required: true, message: '请输入通知内容', trigger: 'blur' }
]
},
refreshTimer: null
}
},
mounted() {
this.loadData();
this.startAutoRefresh();
},
beforeDestroy() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
}
},
methods: {
loadData() {
this.loadRealTimeStats();
this.loadTodayStats();
this.loadRiderList();
this.loadAlerts();
},
loadRealTimeStats() {
// 模拟实时统计数据
this.realTimeStats = {
online_riders: 45,
busy_riders: 28,
pending_orders: 12,
completed_orders: 156,
avg_delivery_time: 23,
total_riders: 68,
online_rate: 66.2,
avg_response_time: 15
};
},
loadTodayStats() {
// 模拟今日统计数据
this.todayStats = {
total_orders: 245,
completed_orders: 198,
cancelled_orders: 12,
total_income: 15680,
completion_rate: 80.8
};
},
loadRiderList() {
// 模拟骑手列表数据
this.riderList = [
{
id: 1,
name: '张三',
phone: '13800138001',
status: 'online',
area_name: '万达商圈',
today_orders: 12,
today_income: 280,
online_duration: '6小时30分',
current_location: '北京市朝阳区万达广场',
completed_orders: 10,
cancelled_orders: 1,
delivery_income: 240,
bonus_income: 40
},
{
id: 2,
name: '李四',
phone: '13800138002',
status: 'busy',
area_name: '清华大学',
today_orders: 8,
today_income: 195,
online_duration: '5小时15分',
current_location: '北京市海淀区清华大学',
completed_orders: 7,
cancelled_orders: 0,
delivery_income: 175,
bonus_income: 20
},
{
id: 3,
name: '王五',
phone: '13800138003',
status: 'offline',
area_name: '中关村',
today_orders: 15,
today_income: 350,
online_duration: '8小时45分',
current_location: '北京市海淀区中关村',
completed_orders: 14,
cancelled_orders: 1,
delivery_income: 300,
bonus_income: 50
}
];
this.pagination.total = this.riderList.length;
},
loadAlerts() {
// 模拟告警数据
this.alerts = [
{
id: 1,
title: '骑手张三超时未接单',
content: '订单#12345已等待15分钟,建议重新分配',
time: new Date().getTime() - 300000
},
{
id: 2,
title: '万达商圈订单积压',
content: '当前有8个订单待配送,建议增加骑手',
time: new Date().getTime() - 600000
}
];
},
startAutoRefresh() {
// 每30秒自动刷新数据
this.refreshTimer = setInterval(() => {
this.loadRealTimeStats();
}, 30000);
},
refreshData() {
this.loadData();
this.$message.success('数据已刷新');
},
searchRiders() {
// 模拟搜索
this.loadRiderList();
},
resetSearch() {
this.searchForm = {
keyword: '',
status: '',
area: ''
};
this.loadRiderList();
},
handleCurrentChange(val) {
this.pagination.page = val;
this.loadRiderList();
},
viewRiderDetail(rider) {
this.currentRider = rider;
this.activeTab = 'basic';
this.detailDialogVisible = true;
},
forceOffline(rider) {
this.$confirm(`确定要强制 ${rider.name} 下线吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 模拟强制下线操作
rider.status = 'offline';
this.$message.success('操作成功');
});
},
sendNotification(rider) {
this.currentRider = rider;
this.notificationForm = {
type: '',
content: ''
};
this.notificationDialogVisible = true;
},
confirmSendNotification() {
this.$refs.notificationForm.validate((valid) => {
if (valid) {
// 模拟发送通知
this.$message.success('通知发送成功');
this.notificationDialogVisible = false;
}
});
},
broadcastMessage() {
this.$message.info('群发消息功能');
},
optimizeDispatch() {
this.$message.info('优化调度功能');
},
emergencyCall() {
this.$message.info('紧急呼叫功能');
},
generateReport() {
this.$message.info('生成报告功能');
},
exportData() {
this.$message.success('导出成功');
},
getStatusText(status) {
const statusMap = {
'online': '在线',
'busy': '配送中',
'offline': '离线'
};
return statusMap[status] || status;
},
formatDateTime(timestamp) {
if (!timestamp) return '';
return new Date(timestamp).toLocaleString('zh-CN');
}
}
});
</script>
</body>
</html>