文件

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>