559 行
25 KiB
HTML
559 行
25 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;
|
|
}
|
|
.stats-cards {
|
|
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);
|
|
}
|
|
.stats-number {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: #409EFF;
|
|
}
|
|
.stats-label {
|
|
color: #666;
|
|
margin-top: 5px;
|
|
}
|
|
.search-form {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
.table-container {
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
.area-map {
|
|
height: 300px;
|
|
background: #f0f0f0;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #666;
|
|
}
|
|
.rider-tag {
|
|
margin: 2px;
|
|
}
|
|
.coordinate-input {
|
|
width: 120px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app" class="page-container">
|
|
<div class="stats-cards">
|
|
<div class="stats-card">
|
|
<div class="stats-number">{{ stats.total_areas || 0 }}</div>
|
|
<div class="stats-label">配送区域总数</div>
|
|
</div>
|
|
<div class="stats-card">
|
|
<div class="stats-number">{{ stats.active_areas || 0 }}</div>
|
|
<div class="stats-label">活跃区域</div>
|
|
</div>
|
|
<div class="stats-card">
|
|
<div class="stats-number">{{ stats.total_riders || 0 }}</div>
|
|
<div class="stats-label">分配骑手数</div>
|
|
</div>
|
|
<div class="stats-card">
|
|
<div class="stats-number">{{ stats.avg_delivery_time || 0 }}</div>
|
|
<div class="stats-label">平均配送时长(分钟)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="search-form">
|
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
|
<el-form-item label="关键词">
|
|
<el-input v-model="searchForm.keyword" placeholder="区域名称" clearable style="width: 200px;"></el-input>
|
|
</el-form-item>
|
|
<el-form-item label="状态">
|
|
<el-select v-model="searchForm.status" placeholder="请选择" clearable style="width: 120px;">
|
|
<el-option label="全部" value=""></el-option>
|
|
<el-option label="启用" value="1"></el-option>
|
|
<el-option label="禁用" value="0"></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="区域类型">
|
|
<el-select v-model="searchForm.area_type" placeholder="请选择" clearable style="width: 120px;">
|
|
<el-option label="全部" value=""></el-option>
|
|
<el-option label="商圈" value="business"></el-option>
|
|
<el-option label="住宅区" value="residential"></el-option>
|
|
<el-option label="学校" value="school"></el-option>
|
|
<el-option label="医院" value="hospital"></el-option>
|
|
<el-option label="其他" value="other"></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" @click="loadData">查询</el-button>
|
|
<el-button @click="resetSearch">重置</el-button>
|
|
<el-button type="success" @click="addArea">新增区域</el-button>
|
|
<el-button type="info" @click="viewHeatmap">热力图</el-button>
|
|
<el-button type="warning" @click="exportData">导出数据</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<el-table :data="tableData" v-loading="loading" style="width: 100%">
|
|
<el-table-column prop="area_name" label="区域名称" width="150"></el-table-column>
|
|
<el-table-column prop="area_type" label="区域类型" width="100">
|
|
<template slot-scope="scope">
|
|
{{ getAreaTypeText(scope.row.area_type) }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="center_address" label="中心地址" width="200" show-overflow-tooltip></el-table-column>
|
|
<el-table-column prop="delivery_radius" label="配送半径(km)" width="120"></el-table-column>
|
|
<el-table-column prop="rider_count" label="分配骑手" width="100">
|
|
<template slot-scope="scope">
|
|
<el-tag type="info" size="small">{{ scope.row.rider_count }}人</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="order_count" label="订单数量" width="100">
|
|
<template slot-scope="scope">
|
|
<span style="color: #409EFF;">{{ scope.row.order_count }}</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="avg_delivery_time" label="平均配送时长" width="120">
|
|
<template slot-scope="scope">
|
|
{{ scope.row.avg_delivery_time }}分钟
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="status" label="状态" width="80">
|
|
<template slot-scope="scope">
|
|
<el-switch
|
|
v-model="scope.row.status"
|
|
:active-value="1"
|
|
:inactive-value="0"
|
|
@change="toggleStatus(scope.row)">
|
|
</el-switch>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="create_time" label="创建时间" width="180">
|
|
<template slot-scope="scope">
|
|
{{ formatDateTime(scope.row.create_time) }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="250" fixed="right">
|
|
<template slot-scope="scope">
|
|
<el-button size="mini" @click="viewDetail(scope.row)">详情</el-button>
|
|
<el-button size="mini" type="primary" @click="editArea(scope.row)">编辑</el-button>
|
|
<el-button size="mini" type="success" @click="manageRiders(scope.row)">分配骑手</el-button>
|
|
<el-button size="mini" type="danger" @click="deleteArea(scope.row)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<div style="padding: 20px; text-align: right;">
|
|
<el-pagination
|
|
@size-change="handleSizeChange"
|
|
@current-change="handleCurrentChange"
|
|
:current-page="pagination.page"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
:page-size="pagination.pageSize"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
:total="pagination.total">
|
|
</el-pagination>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 区域详情对话框 -->
|
|
<el-dialog title="区域详情" :visible.sync="detailDialogVisible" width="800px">
|
|
<div v-if="currentArea">
|
|
<el-descriptions :column="2" border>
|
|
<el-descriptions-item label="区域名称">{{ currentArea.area_name }}</el-descriptions-item>
|
|
<el-descriptions-item label="区域类型">{{ getAreaTypeText(currentArea.area_type) }}</el-descriptions-item>
|
|
<el-descriptions-item label="中心地址">{{ currentArea.center_address }}</el-descriptions-item>
|
|
<el-descriptions-item label="配送半径">{{ currentArea.delivery_radius }}km</el-descriptions-item>
|
|
<el-descriptions-item label="分配骑手">{{ currentArea.rider_count }}人</el-descriptions-item>
|
|
<el-descriptions-item label="订单数量">{{ currentArea.order_count }}</el-descriptions-item>
|
|
<el-descriptions-item label="平均配送时长">{{ currentArea.avg_delivery_time }}分钟</el-descriptions-item>
|
|
<el-descriptions-item label="状态">
|
|
<el-tag :type="currentArea.status ? 'success' : 'danger'">
|
|
{{ currentArea.status ? '启用' : '禁用' }}
|
|
</el-tag>
|
|
</el-descriptions-item>
|
|
</el-descriptions>
|
|
|
|
<h4 style="margin-top: 20px;">区域地图</h4>
|
|
<div class="area-map">
|
|
区域地图显示区域 (经度: {{ currentArea.center_longitude }}, 纬度: {{ currentArea.center_latitude }})
|
|
</div>
|
|
|
|
<h4 style="margin-top: 20px;">分配骑手</h4>
|
|
<div v-if="currentArea.riders && currentArea.riders.length > 0">
|
|
<el-tag
|
|
v-for="rider in currentArea.riders"
|
|
:key="rider.id"
|
|
class="rider-tag"
|
|
type="info">
|
|
{{ rider.name }} ({{ rider.phone }})
|
|
</el-tag>
|
|
</div>
|
|
<div v-else style="color: #999;">暂无分配骑手</div>
|
|
|
|
<div v-if="currentArea.description" style="margin-top: 20px;">
|
|
<h4>区域描述</h4>
|
|
<p>{{ currentArea.description }}</p>
|
|
</div>
|
|
</div>
|
|
</el-dialog>
|
|
|
|
<!-- 新增/编辑区域对话框 -->
|
|
<el-dialog :title="areaForm.id ? '编辑区域' : '新增区域'" :visible.sync="areaDialogVisible" width="600px">
|
|
<el-form :model="areaForm" :rules="areaRules" ref="areaForm" label-width="120px">
|
|
<el-form-item label="区域名称" prop="area_name">
|
|
<el-input v-model="areaForm.area_name" placeholder="请输入区域名称"></el-input>
|
|
</el-form-item>
|
|
<el-form-item label="区域类型" prop="area_type">
|
|
<el-select v-model="areaForm.area_type" placeholder="请选择区域类型" style="width: 100%;">
|
|
<el-option label="商圈" value="business"></el-option>
|
|
<el-option label="住宅区" value="residential"></el-option>
|
|
<el-option label="学校" value="school"></el-option>
|
|
<el-option label="医院" value="hospital"></el-option>
|
|
<el-option label="其他" value="other"></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="中心地址" prop="center_address">
|
|
<el-input v-model="areaForm.center_address" placeholder="请输入中心地址"></el-input>
|
|
</el-form-item>
|
|
<el-form-item label="中心坐标">
|
|
<el-input v-model="areaForm.center_longitude" placeholder="经度" class="coordinate-input"></el-input>
|
|
<span style="margin: 0 10px;">,</span>
|
|
<el-input v-model="areaForm.center_latitude" placeholder="纬度" class="coordinate-input"></el-input>
|
|
</el-form-item>
|
|
<el-form-item label="配送半径" prop="delivery_radius">
|
|
<el-input-number v-model="areaForm.delivery_radius" :min="0.1" :max="50" :step="0.1" style="width: 100%;"></el-input-number>
|
|
<span style="margin-left: 10px;">km</span>
|
|
</el-form-item>
|
|
<el-form-item label="区域描述">
|
|
<el-input type="textarea" v-model="areaForm.description" placeholder="请输入区域描述" :rows="3"></el-input>
|
|
</el-form-item>
|
|
<el-form-item label="状态">
|
|
<el-switch v-model="areaForm.status" :active-value="1" :inactive-value="0"></el-switch>
|
|
</el-form-item>
|
|
</el-form>
|
|
<div slot="footer" class="dialog-footer">
|
|
<el-button @click="areaDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="saveArea">保存</el-button>
|
|
</div>
|
|
</el-dialog>
|
|
|
|
<!-- 分配骑手对话框 -->
|
|
<el-dialog title="分配骑手" :visible.sync="riderDialogVisible" width="600px">
|
|
<div v-if="currentArea">
|
|
<h4>区域:{{ currentArea.area_name }}</h4>
|
|
|
|
<el-form :inline="true" style="margin-bottom: 20px;">
|
|
<el-form-item label="搜索骑手">
|
|
<el-input v-model="riderSearchKeyword" placeholder="姓名/手机号" clearable style="width: 200px;"></el-input>
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" @click="searchRiders">搜索</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
|
|
<div style="display: flex; gap: 20px;">
|
|
<div style="flex: 1;">
|
|
<h5>可分配骑手</h5>
|
|
<el-transfer
|
|
v-model="selectedRiderIds"
|
|
:data="availableRiders"
|
|
:titles="['可分配', '已分配']"
|
|
:button-texts="['移除', '分配']"
|
|
:format="{
|
|
noChecked: '${total}',
|
|
hasChecked: '${checked}/${total}'
|
|
}"
|
|
@change="handleRiderChange">
|
|
</el-transfer>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div slot="footer" class="dialog-footer">
|
|
<el-button @click="riderDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="saveRiderAssignment">保存</el-button>
|
|
</div>
|
|
</el-dialog>
|
|
|
|
<!-- 热力图对话框 -->
|
|
<el-dialog title="订单热力图" :visible.sync="heatmapDialogVisible" width="800px">
|
|
<div class="area-map" style="height: 500px;">
|
|
订单分布热力图显示区域
|
|
</div>
|
|
</el-dialog>
|
|
</div>
|
|
|
|
<script>
|
|
new Vue({
|
|
el: '#app',
|
|
data() {
|
|
return {
|
|
loading: false,
|
|
tableData: [],
|
|
stats: {},
|
|
searchForm: {
|
|
keyword: '',
|
|
status: '',
|
|
area_type: ''
|
|
},
|
|
pagination: {
|
|
page: 1,
|
|
pageSize: 20,
|
|
total: 0
|
|
},
|
|
detailDialogVisible: false,
|
|
areaDialogVisible: false,
|
|
riderDialogVisible: false,
|
|
heatmapDialogVisible: false,
|
|
currentArea: null,
|
|
areaForm: {
|
|
id: null,
|
|
area_name: '',
|
|
area_type: '',
|
|
center_address: '',
|
|
center_longitude: '',
|
|
center_latitude: '',
|
|
delivery_radius: 3,
|
|
description: '',
|
|
status: 1
|
|
},
|
|
areaRules: {
|
|
area_name: [
|
|
{ required: true, message: '请输入区域名称', trigger: 'blur' }
|
|
],
|
|
area_type: [
|
|
{ required: true, message: '请选择区域类型', trigger: 'change' }
|
|
],
|
|
center_address: [
|
|
{ required: true, message: '请输入中心地址', trigger: 'blur' }
|
|
],
|
|
delivery_radius: [
|
|
{ required: true, message: '请输入配送半径', trigger: 'blur' }
|
|
]
|
|
},
|
|
availableRiders: [],
|
|
selectedRiderIds: [],
|
|
riderSearchKeyword: ''
|
|
}
|
|
},
|
|
mounted() {
|
|
this.loadData();
|
|
this.loadStats();
|
|
},
|
|
methods: {
|
|
loadData() {
|
|
this.loading = true;
|
|
const params = {
|
|
page: this.pagination.page,
|
|
pageSize: this.pagination.pageSize,
|
|
...this.searchForm
|
|
};
|
|
|
|
// 模拟API调用
|
|
setTimeout(() => {
|
|
this.tableData = [
|
|
{
|
|
id: 1,
|
|
area_name: '万达商圈',
|
|
area_type: 'business',
|
|
center_address: '北京市朝阳区万达广场',
|
|
center_longitude: '116.404',
|
|
center_latitude: '39.915',
|
|
delivery_radius: 3.5,
|
|
rider_count: 12,
|
|
order_count: 156,
|
|
avg_delivery_time: 25,
|
|
status: 1,
|
|
create_time: '2024-01-15 10:00:00',
|
|
description: '繁华商业区,订单量大',
|
|
riders: [
|
|
{ id: 1, name: '张三', phone: '13800138001' },
|
|
{ id: 2, name: '李四', phone: '13800138002' }
|
|
]
|
|
},
|
|
{
|
|
id: 2,
|
|
area_name: '清华大学',
|
|
area_type: 'school',
|
|
center_address: '北京市海淀区清华大学',
|
|
center_longitude: '116.326',
|
|
center_latitude: '40.003',
|
|
delivery_radius: 2.0,
|
|
rider_count: 8,
|
|
order_count: 89,
|
|
avg_delivery_time: 18,
|
|
status: 1,
|
|
create_time: '2024-01-20 14:30:00',
|
|
description: '学校区域,学生订单较多',
|
|
riders: [
|
|
{ id: 3, name: '王五', phone: '13800138003' }
|
|
]
|
|
}
|
|
];
|
|
this.pagination.total = 2;
|
|
this.loading = false;
|
|
}, 500);
|
|
},
|
|
loadStats() {
|
|
// 模拟统计数据
|
|
this.stats = {
|
|
total_areas: 25,
|
|
active_areas: 22,
|
|
total_riders: 156,
|
|
avg_delivery_time: 23
|
|
};
|
|
},
|
|
handleSizeChange(val) {
|
|
this.pagination.pageSize = val;
|
|
this.loadData();
|
|
},
|
|
handleCurrentChange(val) {
|
|
this.pagination.page = val;
|
|
this.loadData();
|
|
},
|
|
resetSearch() {
|
|
this.searchForm = {
|
|
keyword: '',
|
|
status: '',
|
|
area_type: ''
|
|
};
|
|
this.pagination.page = 1;
|
|
this.loadData();
|
|
},
|
|
addArea() {
|
|
this.areaForm = {
|
|
id: null,
|
|
area_name: '',
|
|
area_type: '',
|
|
center_address: '',
|
|
center_longitude: '',
|
|
center_latitude: '',
|
|
delivery_radius: 3,
|
|
description: '',
|
|
status: 1
|
|
};
|
|
this.areaDialogVisible = true;
|
|
},
|
|
editArea(row) {
|
|
this.areaForm = {
|
|
id: row.id,
|
|
area_name: row.area_name,
|
|
area_type: row.area_type,
|
|
center_address: row.center_address,
|
|
center_longitude: row.center_longitude,
|
|
center_latitude: row.center_latitude,
|
|
delivery_radius: row.delivery_radius,
|
|
description: row.description || '',
|
|
status: row.status
|
|
};
|
|
this.areaDialogVisible = true;
|
|
},
|
|
saveArea() {
|
|
this.$refs.areaForm.validate((valid) => {
|
|
if (valid) {
|
|
// 模拟保存操作
|
|
this.$message.success('保存成功');
|
|
this.areaDialogVisible = false;
|
|
this.loadData();
|
|
}
|
|
});
|
|
},
|
|
deleteArea(row) {
|
|
this.$confirm('确定要删除该区域吗?', '提示', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}).then(() => {
|
|
// 模拟删除操作
|
|
this.$message.success('删除成功');
|
|
this.loadData();
|
|
});
|
|
},
|
|
toggleStatus(row) {
|
|
// 模拟状态切换
|
|
this.$message.success('状态更新成功');
|
|
},
|
|
viewDetail(row) {
|
|
this.currentArea = row;
|
|
this.detailDialogVisible = true;
|
|
},
|
|
manageRiders(row) {
|
|
this.currentArea = row;
|
|
this.loadAvailableRiders();
|
|
this.selectedRiderIds = row.riders ? row.riders.map(r => r.id) : [];
|
|
this.riderDialogVisible = true;
|
|
},
|
|
loadAvailableRiders() {
|
|
// 模拟加载可分配骑手
|
|
this.availableRiders = [
|
|
{ key: 1, label: '张三 (13800138001)' },
|
|
{ key: 2, label: '李四 (13800138002)' },
|
|
{ key: 3, label: '王五 (13800138003)' },
|
|
{ key: 4, label: '赵六 (13800138004)' },
|
|
{ key: 5, label: '钱七 (13800138005)' }
|
|
];
|
|
},
|
|
searchRiders() {
|
|
// 模拟搜索骑手
|
|
this.loadAvailableRiders();
|
|
},
|
|
handleRiderChange(value, direction, movedKeys) {
|
|
console.log('骑手分配变更:', value, direction, movedKeys);
|
|
},
|
|
saveRiderAssignment() {
|
|
// 模拟保存骑手分配
|
|
this.$message.success('骑手分配保存成功');
|
|
this.riderDialogVisible = false;
|
|
this.loadData();
|
|
},
|
|
viewHeatmap() {
|
|
this.heatmapDialogVisible = true;
|
|
},
|
|
exportData() {
|
|
// 模拟导出操作
|
|
this.$message.success('导出成功');
|
|
},
|
|
getAreaTypeText(type) {
|
|
const typeMap = {
|
|
'business': '商圈',
|
|
'residential': '住宅区',
|
|
'school': '学校',
|
|
'hospital': '医院',
|
|
'other': '其他'
|
|
};
|
|
return typeMap[type] || type;
|
|
},
|
|
formatDateTime(timestamp) {
|
|
if (!timestamp) return '';
|
|
return new Date(timestamp).toLocaleString('zh-CN');
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |