文件
mashangyoujiu/Application/Retail/View/Xcx/visual_manager.html

1579 行
53 KiB
HTML
可执行文件

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<!--[1.8.50] 门店管理 - 可视化配送区域版本-->
<script charset="utf-8" type="text/javascript" src="https://map.qq.com/api/gljs?v=1.exp&key=BHHBZ-CVK3Q-PKW5B-BJBDV-PZILV-IIBJA&libraries=service,geometry"></script>
<script>
// 全局地图加载状态
window.mapLoadStatus = {
loaded: false,
error: false
};
// 监听地图API加载
window.addEventListener('load', function() {
setTimeout(function() {
if (typeof TMap !== 'undefined') {
window.mapLoadStatus.loaded = true;
console.log('腾讯地图API加载成功');
} else {
window.mapLoadStatus.error = true;
console.error('腾讯地图API加载失败');
}
}, 1000);
});
</script>
<style type="text/css">
.tips {
font-size: 13px;
color: #999;
margin-left: 20px;
}
.el-switch__label span[aria-hidden='true'] {
color: #999;
font-weight: normal;
}
.small-input {
width: 190px;
}
.mini-input {
width: 150px;
}
.inline-block {
display: inline-block;
margin-right: 50px;
}
.time-block {
margin-top: 8px;
}
.time-block:nth-child(1) {
margin-top: 0;
}
#storeManager .el-date-editor {
width: 130px;
}
#storeManager .el-form-item--small.el-form-item {
margin-bottom: 30px;
}
#storeManager .el-input-group__append {
padding: 0 7px;
}
/* 可视化配送区域样式 */
.delivery-area-container {
border: 1px solid #e4e7ed;
border-radius: 8px;
overflow: hidden;
background: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.delivery-map-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.delivery-map-title {
font-size: 16px;
font-weight: 600;
margin: 0;
}
.delivery-map-status {
font-size: 12px;
opacity: 0.9;
}
.delivery-map-wrapper {
position: relative;
background: #f8f9fa;
}
.delivery-map {
width: 100%;
height: 500px;
border: none;
display: block;
}
.map-toolbar {
position: absolute;
top: 15px;
left: 15px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 8px;
}
.map-tool-group {
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
padding: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
}
.map-tool-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: none;
border-radius: 6px;
background: transparent;
color: #606266;
cursor: pointer;
transition: all 0.3s ease;
font-size: 16px;
margin-bottom: 4px;
}
.map-tool-btn:last-child {
margin-bottom: 0;
}
.map-tool-btn:hover {
background: #f0f2f5;
color: #409eff;
transform: scale(1.05);
}
.map-tool-btn.active {
background: #409eff;
color: white;
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
}
.map-tool-btn.danger {
color: #f56c6c;
}
.map-tool-btn.danger:hover {
background: #fef0f0;
color: #f56c6c;
}
.drawing-tips {
position: absolute;
top: 15px;
right: 15px;
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
padding: 12px 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
max-width: 280px;
z-index: 1000;
}
.drawing-tips.hidden {
display: none;
}
.tips-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin: 0 0 8px 0;
display: flex;
align-items: center;
}
.tips-content {
font-size: 12px;
color: #606266;
line-height: 1.5;
margin: 0;
}
.area-management {
padding: 20px;
background: #fafbfc;
border-top: 1px solid #e4e7ed;
}
.area-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.area-list-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0;
}
.area-stats {
font-size: 12px;
color: #909399;
}
.area-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
max-height: 300px;
overflow-y: auto;
}
.area-item {
background: white;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 15px;
transition: all 0.3s ease;
position: relative;
cursor: pointer;
}
.area-item:hover {
border-color: #409eff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
transform: translateY(-2px);
}
.area-item.active {
border-color: #409eff;
background: #f0f9ff;
}
.area-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.area-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin: 0;
}
.area-actions {
display: flex;
gap: 5px;
}
.area-action-btn {
width: 24px;
height: 24px;
border: none;
border-radius: 4px;
background: transparent;
color: #909399;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.area-action-btn:hover {
background: #f0f2f5;
color: #409eff;
}
.area-action-btn.danger:hover {
background: #fef0f0;
color: #f56c6c;
}
.area-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
font-size: 12px;
color: #606266;
}
.area-info-item {
display: flex;
align-items: center;
gap: 5px;
}
.area-info-icon {
color: #909399;
}
.empty-area-list {
text-align: center;
padding: 40px 20px;
color: #909399;
}
.empty-area-icon {
font-size: 48px;
margin-bottom: 15px;
opacity: 0.5;
}
.search-controls {
position: absolute;
bottom: 15px;
left: 15px;
right: 15px;
z-index: 1000;
}
.search-input-wrapper {
display: flex;
gap: 10px;
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
padding: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
}
.search-input {
flex: 1;
border: 1px solid #e4e7ed;
border-radius: 6px;
padding: 8px 12px;
font-size: 14px;
outline: none;
transition: border-color 0.3s ease;
}
.search-input:focus {
border-color: #409eff;
}
.search-btn {
padding: 8px 16px;
background: #409eff;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s ease;
}
.search-btn:hover {
background: #66b1ff;
}
/* 响应式设计 */
@media (max-width: 768px) {
.delivery-map {
height: 400px;
}
.map-toolbar {
top: 10px;
left: 10px;
}
.drawing-tips {
top: 10px;
right: 10px;
max-width: 200px;
}
.area-list {
grid-template-columns: 1fr;
}
.search-controls {
bottom: 10px;
left: 10px;
right: 10px;
}
}
</style>
<div id="storeManager">
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="120px" size="small">
<!-- 基本设置 -->
<el-form-item label="门店状态">
<el-switch v-model="ruleForm.status" active-value="1" inactive-value="0">
</el-switch>
<span class="tips">关闭后,小程序商城将不显示该门店</span>
</el-form-item>
<el-form-item label="送货上门" prop="skip">
<el-switch v-model="ruleForm.skip" active-value="0" inactive-value="1">
</el-switch>
<span class="tips">开启后,用户可选择送货上门服务</span>
</el-form-item>
<el-form-item label="到店自提" prop="self_pick">
<el-switch v-model="ruleForm.self_pick" active-value="1" inactive-value="0">
</el-switch>
<span class="tips">开启后,用户可选择到店自提</span>
</el-form-item>
<!-- 可视化配送区域设置 -->
<el-form-item label="配送区域设置" prop="delivery_fences">
<div class="delivery-area-container">
<!-- 地图头部 -->
<div class="delivery-map-header">
<div>
<h3 class="delivery-map-title">
<i class="el-icon-location"></i>
可视化配送区域设置
</h3>
<div class="delivery-map-status">
<span v-if="!mapReady">地图加载中...</span>
<span v-else-if="isDrawing">绘制模式 - 点击地图添加区域边界点</span>
<span v-else>已设置 {{deliveryFences.length}} 个配送区域</span>
</div>
</div>
<div>
<el-button size="mini" type="primary" plain @click="toggleFullscreen">
<i class="el-icon-full-screen"></i>
</el-button>
</div>
</div>
<!-- 地图容器 -->
<div class="delivery-map-wrapper">
<div id="tencentMap" class="delivery-map"></div>
<!-- 地图工具栏 -->
<div class="map-toolbar">
<!-- 绘制工具组 -->
<div class="map-tool-group">
<div
class="map-tool-btn"
:class="{active: isDrawing}"
@click.stop.prevent="handleDrawingToggle"
title="绘制配送区域">
<i class="el-icon-edit"></i>
</div>
<div
class="map-tool-btn"
@click.stop.prevent="handleFinishDrawing"
v-if="isDrawing && currentPath.length >= 3"
title="完成当前区域绘制">
<i class="el-icon-check"></i>
</div>
<div
class="map-tool-btn danger"
@click.stop.prevent="handleCancelDrawing"
v-if="isDrawing"
title="取消绘制">
<i class="el-icon-close"></i>
</div>
</div>
<!-- 视图工具组 -->
<div class="map-tool-group">
<div
class="map-tool-btn"
@click.stop.prevent="centerToStore"
title="定位到门店">
<i class="el-icon-position"></i>
</div>
<div
class="map-tool-btn"
@click.stop.prevent="fitAllAreas"
v-if="deliveryFences.length > 0"
title="查看所有区域">
<i class="el-icon-zoom-in"></i>
</div>
</div>
<!-- 管理工具组 -->
<div class="map-tool-group" v-if="deliveryFences.length > 0">
<div
class="map-tool-btn"
@click.stop.prevent="exportFences"
title="导出配送区域">
<i class="el-icon-download"></i>
</div>
<div
class="map-tool-btn danger"
@click.stop.prevent="clearAllFences"
title="清空所有区域">
<i class="el-icon-delete"></i>
</div>
</div>
<!-- 导入工具 -->
<div class="map-tool-group">
<div
class="map-tool-btn"
@click.stop.prevent="importFences"
title="导入配送区域">
<i class="el-icon-upload2"></i>
</div>
</div>
</div>
<!-- 绘制提示 -->
<div class="drawing-tips" :class="{hidden: !isDrawing}">
<h4 class="tips-title">
<i class="el-icon-info"></i>
绘制提示
</h4>
<p class="tips-content">
• 点击地图添加区域边界点<br>
• 至少需要3个点形成区域<br>
• 双击地图完成绘制<br>
• 当前已添加 {{currentPath.length}} 个点
</p>
</div>
<!-- 地址搜索 -->
<div class="search-controls">
<div class="search-input-wrapper">
<input
type="text"
class="search-input"
v-model="searchAddress"
placeholder="输入地址快速定位..."
@keyup.enter="searchLocation">
<div class="search-btn" @click.stop.prevent="searchLocation">
<i class="el-icon-search"></i> 搜索
</div>
</div>
</div>
</div>
<!-- 区域管理面板 -->
<div class="area-management">
<div class="area-list-header">
<h4 class="area-list-title">配送区域管理</h4>
<div class="area-stats">
总计 {{deliveryFences.length}} 个区域
<span v-if="totalArea > 0">,覆盖面积约 {{totalArea.toFixed(2)}} 平方公里</span>
</div>
</div>
<!-- 区域列表 -->
<div class="area-list" v-if="deliveryFences.length > 0">
<div
v-for="(fence, index) in deliveryFences"
:key="fence.id"
class="area-item"
:class="{active: selectedAreaIndex === index}"
@click="selectArea(index)">
<div class="area-item-header">
<h5 class="area-name">{{fence.name || `配送区域 ${index + 1}`}}</h5>
<div class="area-actions">
<button
type="button"
class="area-action-btn"
@click.stop.prevent="locateFence(index)"
title="定位到此区域">
<i class="el-icon-location-outline"></i>
</button>
<button
type="button"
class="area-action-btn"
@click.stop.prevent="editAreaName(index)"
title="编辑区域名称">
<i class="el-icon-edit-outline"></i>
</button>
<button
type="button"
class="area-action-btn danger"
@click.stop.prevent="deleteFence(index)"
title="删除此区域">
<i class="el-icon-delete"></i>
</button>
</div>
</div>
<div class="area-info">
<div class="area-info-item">
<i class="el-icon-s-grid area-info-icon"></i>
<span>面积: {{fence.area || '计算中...'}} km²</span>
</div>
<div class="area-info-item">
<i class="el-icon-connection area-info-icon"></i>
<span>边界点: {{fence.points ? fence.points.length : 0}} 个</span>
</div>
<div class="area-info-item">
<i class="el-icon-time area-info-icon"></i>
<span>创建: {{formatDate(fence.createTime)}}</span>
</div>
<div class="area-info-item">
<i class="el-icon-circle-check area-info-icon"></i>
<span>状态:
<span style="color: #67c23a;">已启用</span>
</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div class="empty-area-list" v-else>
<div class="empty-area-icon">
<i class="el-icon-location-outline"></i>
</div>
<h4>暂无配送区域</h4>
<p>点击地图工具栏中的绘制按钮开始创建配送区域</p>
<el-button type="primary" size="small" @click="handleStartDrawing">
<i class="el-icon-plus"></i> 创建第一个配送区域
</el-button>
</div>
</div>
</div>
</el-form-item>
<!-- 其他设置 -->
<el-form-item label="门店地址" prop="address">
<el-input v-model="ruleForm.address" placeholder="请输入门店详细地址">
<el-button slot="append" @click="searchAddress">定位</el-button>
</el-input>
<span class="tips">用于地图定位和配送区域设置</span>
</el-form-item>
<el-form-item label="推广语" prop="tip">
<el-input v-model="ruleForm.tip"></el-input>
<span class="tips">小程序商城门店切换页面展示</span>
</el-form-item>
<el-form-item label="门店电话" prop="phone">
<el-input class="small-input" oninput="value=this.value.replace(/^\\D|[-]{0,2}/,'')"
v-model="ruleForm.phone" maxLength="11"></el-input>
<span class="tips">小程序商城门店联系电话</span>
</el-form-item>
<el-form-item label="起送价" prop="smmoney">
<el-input class="mini-input" v-model="ruleForm.smmoney">
<template slot="append">元</template>
</el-input>
</el-form-item>
<!-- 提交按钮 -->
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')" :loading="pageLoading">保存设置</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 导入对话框 -->
<el-dialog title="导入配送区域JSON" :visible.sync="importDialogVisible" width="600px">
<el-input type="textarea" :rows="10" v-model="importText" placeholder="粘贴之前导出的配送区域JSON"></el-input>
<span slot="footer" class="dialog-footer">
<el-button @click="importDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="confirmImport">确 定</el-button>
</span>
</el-dialog>
<script>
var storeManager = new Vue({
el: '#storeManager',
data() {
return {
pageLoading: false,
// 地图相关
map: null,
marker: null,
mapReady: false,
isDrawing: false,
currentPolygon: null,
currentPath: [],
drawingLine: null,
deliveryFences: [],
polygons: [],
importDialogVisible: false,
importText: '',
// 可视化功能属性
selectedAreaIndex: null,
searchAddress: '',
isFullscreen: false,
ruleForm: {
id: 0,
status: "0",
skip: "1",
self_pick: "1",
address: "",
longitude: 116.404,
latitude: 39.915,
tip: "",
phone: "",
smmoney: "",
delivery_fences: "",
},
rules: {
address: [
{ required: true, message: '请输入门店地址', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入门店电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
]
}
}
},
computed: {
// 计算总覆盖面积
totalArea() {
return this.deliveryFences.reduce((total, fence) => {
const area = parseFloat(fence.area) || 0;
return total + area;
}, 0);
}
},
mounted() {
this.waitForTMapLoad();
this.loadStoreData();
},
methods: {
// 地图初始化
waitForTMapLoad() {
let attempts = 0;
const maxAttempts = 40;
const checkTMap = () => {
attempts++;
console.log(`检查地图加载状态 (${attempts}/${maxAttempts})`);
if (window.mapLoadStatus.error) {
console.error('地图API加载失败');
this.showMapError();
return;
}
if (typeof TMap !== 'undefined') {
console.log('TMap已定义,开始初始化地图');
this.initMap();
return;
}
if (attempts < maxAttempts) {
setTimeout(checkTMap, 500);
} else {
console.error('地图API加载超时');
this.showMapError();
}
};
checkTMap();
},
initMap() {
try {
const mapContainer = document.getElementById('tencentMap');
if (!mapContainer) {
this.$message.error('地图容器未找到');
return;
}
mapContainer.innerHTML = '';
const center = new TMap.LatLng(this.ruleForm.latitude, this.ruleForm.longitude);
this.map = new TMap.Map(mapContainer, {
center: center,
zoom: 13,
viewMode: '2D'
});
this.map.on('tilesloaded', () => {
this.onMapReady();
});
this.map.on('click', (evt) => {
try {
if (this.isDrawing && evt && evt.latLng) {
this.addDrawingPoint(evt.latLng);
}
} catch (error) {
console.error('地图点击处理失败:', error);
}
});
this.map.on('dblclick', (evt) => {
try {
if (this.isDrawing && this.currentPath.length >= 3) {
evt.preventDefault && evt.preventDefault();
this.finishDrawing();
}
} catch (error) {
console.error('地图双击处理失败:', error);
}
});
setTimeout(() => {
if (this.map && !this.mapReady) {
this.onMapReady();
}
}, 5000);
} catch (error) {
console.error('地图初始化失败:', error);
this.showMapError();
}
},
onMapReady() {
if (this.mapReady) return;
this.mapReady = true;
this.createStoreMarker();
this.loadExistingFences();
this.$message.success('地图加载成功');
},
showMapError() {
const mapContainer = document.getElementById('tencentMap');
if (mapContainer) {
mapContainer.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; height: 100%; background: #f5f5f5; color: #666; flex-direction: column; border: 1px solid #ddd; border-radius: 4px;">
<i class="el-icon-warning" style="font-size: 48px; margin-bottom: 15px; color: #f56c6c;"></i>
<h4 style="margin: 0 0 10px 0;">地图加载失败</h4>
<p style="margin: 5px 0; font-size: 14px;">请检查网络连接或稍后重试</p>
<div style="margin-top: 15px;">
<button onclick="location.reload()" style="margin-right: 10px; padding: 8px 16px; background: #409eff; color: white; border: none; border-radius: 4px; cursor: pointer;">刷新页面</button>
<button onclick="storeManager.retryMapLoad()" style="padding: 8px 16px; background: #67c23a; color: white; border: none; border-radius: 4px; cursor: pointer;">重试加载</button>
</div>
</div>
`;
}
this.$message.error('地图加载失败,请检查网络连接');
},
retryMapLoad() {
this.mapReady = false;
this.waitForTMapLoad();
},
// 门店标记
createStoreMarker() {
try {
if (this.marker) {
this.marker.setMap(null);
this.marker = null;
}
const position = new TMap.LatLng(this.ruleForm.latitude, this.ruleForm.longitude);
if (TMap.Marker) {
this.marker = new TMap.Marker({
map: this.map,
position: position,
title: '门店位置'
});
} else if (TMap.MultiMarker) {
this.marker = new TMap.MultiMarker({
map: this.map,
styles: {
'marker': new TMap.MarkerStyle({
'width': 25,
'height': 35,
'anchor': { x: 16, y: 32 }
})
},
geometries: [{
id: 'store-marker',
styleId: 'marker',
position: position,
properties: { title: '门店位置' }
}]
});
}
} catch (error) {
console.error('创建门店标记失败:', error);
}
},
// 可视化配送区域方法
handleDrawingToggle() {
console.log('点击绘制按钮,当前状态:', this.isDrawing);
try {
if (this.isDrawing) {
this.handleCancelDrawing();
} else {
this.handleStartDrawing();
}
} catch (error) {
console.error('绘制切换失败:', error);
}
return false; // 确保不会导致页面刷新
},
toggleDrawing(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
if (this.isDrawing) {
this.cancelDrawing();
} else {
this.startDrawing();
}
},
handleStartDrawing() {
console.log('开始绘制');
if (!this.mapReady || !this.map) {
this.$message.error('地图未准备就绪,请稍后再试');
return;
}
this.resetDrawingState();
this.isDrawing = true;
this.selectedAreaIndex = null;
this.$message.info('请在地图上点击多个点来绘制配送区域,至少需要3个点');
},
handleFinishDrawing() {
console.log('完成绘制');
if (this.currentPath.length < 3) {
this.$message.warning('至少需要3个点才能形成配送区域');
return;
}
try {
const closedPath = this.ensureClosedPath([...this.currentPath]);
const area = this.calculatePolygonArea(closedPath);
const newFence = {
id: Date.now(),
name: `配送区域${this.deliveryFences.length + 1}`,
points: closedPath,
area: area.toFixed(2),
createTime: Date.now(),
status: 'active'
};
this.deliveryFences.push(newFence);
this.createFencePolygon(newFence, this.deliveryFences.length - 1);
this.resetDrawingState();
this.selectedAreaIndex = this.deliveryFences.length - 1;
this.$message.success(`配送区域 "${newFence.name}" 创建完成`);
this.updateDeliveryFencesData();
} catch (error) {
console.error('完成绘制失败:', error);
this.$message.error('创建配送区域失败,请重试');
}
},
handleCancelDrawing() {
console.log('取消绘制');
this.isDrawing = false;
this.resetDrawingState();
this.$message.info('已取消绘制');
},
startDrawing(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
if (!this.mapReady || !this.map) {
this.$message.error('地图未准备就绪,请稍后再试');
return false;
}
this.resetDrawingState();
this.isDrawing = true;
this.selectedAreaIndex = null;
this.$message.info('请在地图上点击多个点来绘制配送区域,至少需要3个点');
return false;
},
resetDrawingState() {
this.currentPath = [];
this.clearCurrentDrawing();
},
clearCurrentDrawing() {
if (this.currentPolygon) {
this.currentPolygon.setMap(null);
this.currentPolygon = null;
}
if (this.drawingLine) {
this.drawingLine.setMap(null);
this.drawingLine = null;
}
},
addDrawingPoint(latLng) {
this.currentPath.push({
lat: latLng.lat,
lng: latLng.lng
});
this.updateDrawingDisplay();
},
updateDrawingDisplay() {
if (this.currentPath.length < 2) return;
this.clearCurrentDrawing();
const latLngArray = this.currentPath.map(point => new TMap.LatLng(point.lat, point.lng));
try {
if (TMap.Polyline) {
this.drawingLine = new TMap.Polyline({
map: this.map,
path: latLngArray,
strokeColor: '#409eff',
strokeWeight: 3,
strokeDashArray: [10, 10]
});
}
} catch (error) {
console.error('更新绘制显示失败:', error);
}
},
finishDrawing(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
if (this.currentPath.length < 3) {
this.$message.warning('至少需要3个点才能形成配送区域');
return false;
}
try {
const closedPath = this.ensureClosedPath([...this.currentPath]);
const area = this.calculatePolygonArea(closedPath);
const newFence = {
id: Date.now(),
name: `配送区域${this.deliveryFences.length + 1}`,
points: closedPath,
area: area.toFixed(2),
createTime: Date.now(),
status: 'active'
};
this.deliveryFences.push(newFence);
this.createFencePolygon(newFence, this.deliveryFences.length - 1);
this.resetDrawingState();
// 自动选中新创建的区域
this.selectedAreaIndex = this.deliveryFences.length - 1;
this.$message.success(`配送区域 "${newFence.name}" 创建完成`);
this.updateDeliveryFencesData();
} catch (error) {
console.error('完成绘制失败:', error);
this.$message.error('创建配送区域失败,请重试');
}
return false;
},
cancelDrawing(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
this.isDrawing = false;
this.resetDrawingState();
this.$message.info('已取消绘制');
return false;
},
ensureClosedPath(points) {
const path = [...points];
const firstPoint = path[0];
const lastPoint = path[path.length - 1];
if (firstPoint.lat !== lastPoint.lat || firstPoint.lng !== lastPoint.lng) {
path.push({...firstPoint});
}
return path;
},
calculatePolygonArea(points) {
if (points.length < 3) return 0;
let area = 0;
const earthRadius = 6371000; // 地球半径(米)
for (let i = 0; i < points.length - 1; i++) {
const p1 = points[i];
const p2 = points[i + 1];
const lat1 = p1.lat * Math.PI / 180;
const lat2 = p2.lat * Math.PI / 180;
const lng1 = p1.lng * Math.PI / 180;
const lng2 = p2.lng * Math.PI / 180;
area += (lng2 - lng1) * (2 + Math.sin(lat1) + Math.sin(lat2));
}
area = Math.abs(area) * earthRadius * earthRadius / 2;
return area / 1000000; // 转换为平方公里
},
createFencePolygon(fence, index) {
try {
if (!fence || !fence.points || !Array.isArray(fence.points) || fence.points.length < 3) {
console.error(`围栏${index + 1}数据无效`);
return;
}
const validPoints = this.validateFencePoints(fence.points);
if (validPoints.length < 3) {
console.error(`围栏${index + 1}有效点数不足`);
return;
}
const closedPath = this.ensureClosedPath(validPoints);
const latLngArray = closedPath.map(point => new TMap.LatLng(point.lat, point.lng));
let polygon = null;
if (TMap.Polygon) {
polygon = new TMap.Polygon({
map: this.map,
paths: [latLngArray],
fillColor: 'rgba(16, 185, 129, 0.3)',
strokeColor: '#10b981',
strokeWeight: 2,
fillOpacity: 0.3
});
} else if (TMap.Polyline) {
polygon = new TMap.Polyline({
map: this.map,
path: latLngArray,
strokeColor: '#10b981',
strokeWeight: 3
});
}
if (polygon) {
this.polygons.push(polygon);
}
} catch (error) {
console.error(`创建围栏${index + 1}失败:`, error);
}
},
validateFencePoints(points) {
return points.filter(point =>
point &&
typeof point.lat === 'number' &&
typeof point.lng === 'number' &&
!isNaN(point.lat) && !isNaN(point.lng) &&
point.lat >= -90 && point.lat <= 90 &&
point.lng >= -180 && point.lng <= 180
);
},
// 可视化功能方法
toggleFullscreen() {
const container = document.querySelector('.delivery-area-container');
if (!document.fullscreenElement) {
container.requestFullscreen().catch(err => {
console.log('无法进入全屏模式:', err);
});
} else {
document.exitFullscreen();
}
},
centerToStore() {
if (this.map && this.ruleForm.latitude && this.ruleForm.longitude) {
const center = new TMap.LatLng(this.ruleForm.latitude, this.ruleForm.longitude);
this.map.setCenter(center);
this.map.setZoom(15);
this.$message.success('已定位到门店位置');
}
},
fitAllAreas() {
if (!this.map || this.deliveryFences.length === 0) return;
try {
let bounds = new TMap.LatLngBounds();
this.deliveryFences.forEach(fence => {
if (fence.points && fence.points.length > 0) {
fence.points.forEach(point => {
bounds.extend(new TMap.LatLng(point.lat, point.lng));
});
}
});
this.map.fitBounds(bounds, {
padding: 50
});
this.$message.success('已显示所有配送区域');
} catch (error) {
console.error('适配区域视图失败:', error);
}
},
selectArea(index) {
this.selectedAreaIndex = this.selectedAreaIndex === index ? null : index;
if (this.selectedAreaIndex !== null) {
this.locateFence(index);
}
},
locateFence(index) {
const fence = this.deliveryFences[index];
if (!fence || !fence.points || fence.points.length === 0) return;
try {
let bounds = new TMap.LatLngBounds();
fence.points.forEach(point => {
bounds.extend(new TMap.LatLng(point.lat, point.lng));
});
this.map.fitBounds(bounds, {
padding: 100
});
this.selectedAreaIndex = index;
this.$message.success(`已定位到配送区域 ${index + 1}`);
} catch (error) {
console.error('定位区域失败:', error);
}
},
editAreaName(index) {
const fence = this.deliveryFences[index];
const currentName = fence.name || `配送区域 ${index + 1}`;
this.$prompt('请输入新的区域名称', '编辑区域名称', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValue: currentName,
inputValidator: (value) => {
if (!value || value.trim().length === 0) {
return '区域名称不能为空';
}
if (value.length > 20) {
return '区域名称不能超过20个字符';
}
return true;
}
}).then(({ value }) => {
this.deliveryFences[index].name = value.trim();
this.updateDeliveryFencesData();
this.$message.success('区域名称已更新');
}).catch(() => {});
},
deleteFence(index) {
this.$confirm('确定要删除这个配送区域吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 删除地图上的多边形
if (this.polygons[index]) {
this.polygons[index].setMap(null);
this.polygons.splice(index, 1);
}
this.deliveryFences.splice(index, 1);
this.updateDeliveryFencesData();
this.selectedAreaIndex = null;
this.$message.success('配送区域已删除');
}).catch(() => {});
},
clearAllFences() {
this.$confirm('确定要清空所有配送区域吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.clearExistingPolygons();
this.deliveryFences = [];
this.selectedAreaIndex = null;
this.updateDeliveryFencesData();
this.$message.success('已清空所有配送区域');
}).catch(() => {});
},
clearExistingPolygons() {
this.polygons.forEach(polygon => {
if (polygon && polygon.setMap) {
polygon.setMap(null);
}
});
this.polygons = [];
},
searchLocation() {
if (!this.searchAddress || !this.searchAddress.trim()) {
this.$message.warning('请输入搜索地址');
return;
}
if (!this.map || typeof TMap === 'undefined' || !TMap.service || !TMap.service.Geocoder) {
this.$message.error('地图服务未加载');
return;
}
const geocoder = new TMap.service.Geocoder();
geocoder.getLocation({
address: this.searchAddress.trim()
}).then((result) => {
if (result.result && result.result.location) {
const location = result.result.location;
const newCenter = new TMap.LatLng(location.lat, location.lng);
this.map.setCenter(newCenter);
this.map.setZoom(16);
this.$message.success('地址定位成功');
} else {
this.$message.error('未找到该地址,请检查地址是否正确');
}
}).catch((error) => {
this.$message.error('地址搜索失败');
console.error('地址搜索错误:', error);
});
},
searchAddress() {
if (!this.ruleForm.address) {
this.$message.warning('请输入地址');
return;
}
if (!this.map || typeof TMap === 'undefined' || !TMap.service || !TMap.service.Geocoder) {
this.$message.error('地图服务未加载');
return;
}
const geocoder = new TMap.service.Geocoder();
geocoder.getLocation({
address: this.ruleForm.address
}).then((result) => {
if (result.result && result.result.location) {
const location = result.result.location;
this.ruleForm.longitude = location.lng;
this.ruleForm.latitude = location.lat;
const newCenter = new TMap.LatLng(location.lat, location.lng);
this.map.setCenter(newCenter);
this.createStoreMarker();
this.$message.success('地址定位成功');
} else {
this.$message.error('地址解析失败,请检查地址是否正确');
}
}).catch((error) => {
this.$message.error('地址搜索失败');
console.error('地址搜索错误:', error);
});
},
formatDate(timestamp) {
if (!timestamp) return '未知';
const date = new Date(timestamp);
return date.toLocaleDateString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
},
// 数据管理
updateDeliveryFencesData() {
this.ruleForm.delivery_fences = JSON.stringify(this.deliveryFences);
},
loadExistingFences() {
if (!this.ruleForm.delivery_fences) {
return;
}
try {
this.clearExistingPolygons();
const fencesData = JSON.parse(this.ruleForm.delivery_fences);
if (Array.isArray(fencesData) && fencesData.length > 0) {
this.deliveryFences = fencesData;
setTimeout(() => {
this.deliveryFences.forEach((fence, index) => {
this.createFencePolygon(fence, index);
});
if (this.deliveryFences.length > 0) {
this.$message.success(`已加载${this.deliveryFences.length}个配送区域`);
}
}, 500);
} else {
this.deliveryFences = [];
}
} catch (error) {
console.error('解析围栏数据失败:', error);
this.deliveryFences = [];
this.ruleForm.delivery_fences = '';
this.$message.warning('围栏数据格式错误,已清空');
}
},
// 导入导出
exportFences() {
if (this.deliveryFences.length === 0) {
this.$message.warning('暂无配送区域可导出');
return;
}
try {
const exportData = {
version: '1.0',
exportTime: new Date().toISOString(),
fences: this.deliveryFences
};
const text = JSON.stringify(exportData, null, 2);
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
this.$message.success('配送区域JSON已复制到剪贴板');
}).catch(() => {
this.downloadAsFile(text, 'delivery_fences.json');
});
} else {
this.downloadAsFile(text, 'delivery_fences.json');
}
} catch (error) {
this.$message.error('导出失败');
console.error('导出配送区域失败:', error);
}
},
downloadAsFile(content, filename) {
const blob = new Blob([content], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.$message.success('配送区域JSON文件已下载');
},
importFences() {
this.importDialogVisible = true;
this.importText = '';
},
confirmImport() {
if (!this.importText.trim()) {
this.$message.warning('请输入要导入的JSON数据');
return;
}
try {
const data = JSON.parse(this.importText);
let fences = [];
if (data.fences && Array.isArray(data.fences)) {
fences = data.fences;
} else if (Array.isArray(data)) {
fences = data;
} else {
throw new Error('数据格式不正确');
}
if (fences.length === 0) {
this.$message.warning('导入的数据中没有有效的配送区域');
return;
}
this.clearExistingPolygons();
this.deliveryFences = fences;
setTimeout(() => {
this.deliveryFences.forEach((fence, index) => {
this.createFencePolygon(fence, index);
});
}, 100);
this.updateDeliveryFencesData();
this.importDialogVisible = false;
this.$message.success(`成功导入${fences.length}个配送区域`);
} catch (error) {
this.$message.error('导入失败:JSON解析错误');
console.error('导入配送区域失败:', error);
}
},
// 表单操作
loadStoreData() {
// 这里应该从服务器加载门店数据
// 示例数据
this.ruleForm = {
id: 1,
status: "1",
skip: "0",
self_pick: "1",
address: "北京市朝阳区三里屯",
longitude: 116.404,
latitude: 39.915,
tip: "欢迎光临我们的门店",
phone: "13800138000",
smmoney: "30",
delivery_fences: "",
};
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.pageLoading = true;
// 这里应该提交到服务器
setTimeout(() => {
this.pageLoading = false;
this.$message.success('保存成功');
}, 1000);
} else {
this.$message.error('请检查表单填写');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
this.clearAllFences();
}
}
});
</script>