2104 行
80 KiB
HTML
可执行文件
2104 行
80 KiB
HTML
可执行文件
<!--[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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
|
||
.fence-list {
|
||
overflow-y: auto;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
max-height: 200px;
|
||
}
|
||
|
||
.fence-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #f3f4f6;
|
||
}
|
||
|
||
.fence-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.fence-item-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.fence-item-name {
|
||
font-weight: 500;
|
||
color: #374151;
|
||
}
|
||
|
||
.fence-item-area {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.drawing-tip {
|
||
background: #fef3c7;
|
||
border: 1px solid #f59e0b;
|
||
border-radius: 4px;
|
||
padding: 8px 12px;
|
||
margin-bottom: 10px;
|
||
font-size: 13px;
|
||
color: #92400e;
|
||
}
|
||
</style>
|
||
|
||
<div id="storeManager" style="padding-bottom: 60px">
|
||
<el-collapse accordion>
|
||
<el-collapse-item>
|
||
<template slot="title">
|
||
<el-button type="text" size="mini">门店设置须知>>></el-button>
|
||
</template>
|
||
<div>1. 开启小程序商城后,才可以通过小程序进入门店</div>
|
||
<div>2. 电子围栏功能可以精确设置配送区域,支持多个不规则区域</div>
|
||
</el-collapse-item>
|
||
</el-collapse>
|
||
|
||
<el-form label-width="150px" :disabled="storeAuth.store_xcx_info==1?false:true" size="mini"
|
||
style="width: 762px" :model="ruleForm" :rules="rules" ref="ruleForm">
|
||
|
||
<el-form-item label="小程序商城" prop="status">
|
||
<el-switch v-model="ruleForm.status"
|
||
:active-text="ruleForm.status==0?'关闭':'开启'"
|
||
inactive-value="0"
|
||
active-value="1">
|
||
</el-switch>
|
||
<span class="tips">关闭则不在商城展示该门店</span>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="送货上门服务" prop="skip">
|
||
<el-switch v-model="ruleForm.skip"
|
||
:active-text="ruleForm.skip==1?'关闭':'开启'"
|
||
inactive-value="1"
|
||
active-value="0">
|
||
</el-switch>
|
||
<span class="tips">关闭用户在商城下单则不可以选择同城配送</span>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="默认配送时间" prop="send_delayed_time">
|
||
<el-input class="mini-input"
|
||
oninput="value=this.value.replace(/^[0]|\\D/,'')"
|
||
v-model="ruleForm.send_delayed_time" maxLength="8">
|
||
<template slot="append">分钟后</template>
|
||
</el-input>
|
||
<el-tooltip class="item" effect="dark" placement="top-start">
|
||
<div slot="content">
|
||
<p>根据下单时间往后推</p>
|
||
<p>拼团订单不会根据设置的时间设置</p>
|
||
<p>预售订单在预售产品发货期间内下单,将使用该设置</p>
|
||
</div>
|
||
<el-button icon="el-icon-question" type="text"></el-button>
|
||
</el-tooltip>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="到店自提功能" prop="self_pick">
|
||
<el-switch v-model="ruleForm.self_pick"
|
||
:active-text="ruleForm.self_pick==0?'关闭':'开启'"
|
||
inactive-value="1"
|
||
active-value="0">
|
||
</el-switch>
|
||
<span class="tips">关闭用户在商城下单则不可以选择到店自提</span>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="默认自提时间" prop="pickup_delayed_time">
|
||
<el-input class="mini-input"
|
||
oninput="value=this.value.replace(/^[0]|\\D/,'')"
|
||
v-model="ruleForm.pickup_delayed_time" maxLength="8">
|
||
<template slot="append">分钟后</template>
|
||
</el-input>
|
||
<el-tooltip class="item" effect="dark" placement="top-start">
|
||
<div slot="content">
|
||
<p>根据下单时间往后推</p>
|
||
<p>拼团订单不会根据设置的时间设置</p>
|
||
<p>预售订单在预售产品发货期间内下单,将使用该设置</p>
|
||
</div>
|
||
<el-button icon="el-icon-question" type="text"></el-button>
|
||
</el-tooltip>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="自动生成时间" prop="auto_set_time">
|
||
<el-switch v-model="ruleForm.auto_set_time"
|
||
:active-text="ruleForm.auto_set_time==1?'开启':'关闭'"
|
||
inactive-value="0"
|
||
active-value="1">
|
||
</el-switch>
|
||
<span class="tips">开启后用户在商城下单将会按照最近时间为客户生成自提/配送时间</span>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="快递发货功能" prop="express_status">
|
||
<el-switch v-model="ruleForm.express_status"
|
||
:active-text="ruleForm.express_status==1?'关闭':'开启'"
|
||
inactive-value="1"
|
||
active-value="2">
|
||
</el-switch>
|
||
<span class="tips">关闭后用户在商城下单则不可以选择快递发货</span>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="非营业时间下单" prop="business_non_order">
|
||
<el-switch v-model="ruleForm.business_non_order"
|
||
:active-text="ruleForm.business_non_order==2?'关闭':'开启'"
|
||
inactive-value="2"
|
||
active-value="1">
|
||
</el-switch>
|
||
<span class="tips">关闭后不在营业时间内则商城不可以下单</span>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="满额免配送费" prop="delivery_status">
|
||
<el-switch v-model="ruleForm.delivery_status"
|
||
:active-text="ruleForm.delivery_status==0?'关闭':'开启'"
|
||
inactive-value="0"
|
||
active-value="1">
|
||
</el-switch>
|
||
<span class="tips">开启后根据下面的金额设置启动免运费功能</span>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="免配送费金额" prop="delivery_cost">
|
||
<el-input-number class="small-input"
|
||
:precision="2"
|
||
:step="0.01"
|
||
:max="1000000"
|
||
:min="0.00"
|
||
v-model="ruleForm.delivery_cost"></el-input-number>
|
||
<span class="tips">达到该金额以后免配送费(同城、快递)</span>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="送货上门时间" prop="business">
|
||
<template v-for="(item,index) in ruleForm.business">
|
||
<div class="time-block">
|
||
<el-time-select
|
||
placeholder="开始时间"
|
||
v-model="item[0]"
|
||
value-format="HH:mm"
|
||
:picker-options="getDeliveryTime(index,1)">
|
||
</el-time-select>
|
||
<el-time-select
|
||
placeholder="结束时间"
|
||
v-model="item[1]"
|
||
value-format="HH:mm"
|
||
:picker-options="getDeliveryTime(index,2)">
|
||
</el-time-select>
|
||
<template v-if="index==0">
|
||
<el-button :disabled="ruleForm.business.length>=3?true:false"
|
||
size="mini"
|
||
@click="addBusiness()"
|
||
circle>
|
||
<i class="el-icon-plus"></i>
|
||
</el-button>
|
||
<span class="tips">送货上门服务的时间,最多可以设置3个时间段</span>
|
||
</template>
|
||
<el-button v-else
|
||
@click="spliceBusiness(index)"
|
||
size="mini"
|
||
circle>
|
||
<i class="el-icon-minus"></i>
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="营业时间" prop="business_time">
|
||
<template v-for="(item,index) in ruleForm.business_time">
|
||
<div class="time-block">
|
||
<el-time-select
|
||
placeholder="开始时间"
|
||
v-model="item[0]"
|
||
value-format="HH:mm"
|
||
:picker-options="getBusinessTime(index,1)">
|
||
</el-time-select>
|
||
<el-time-select
|
||
placeholder="结束时间"
|
||
v-model="item[1]"
|
||
value-format="HH:mm"
|
||
:picker-options="getBusinessTime(index,2)">
|
||
</el-time-select>
|
||
<template v-if="index==0">
|
||
<el-button :disabled="ruleForm.business_time.length>=3?true:false"
|
||
size="mini"
|
||
@click="addBusinessTime()"
|
||
circle>
|
||
<i class="el-icon-plus"></i>
|
||
</el-button>
|
||
<span class="tips">自提营业时间,最多可以设置3个时间段</span>
|
||
</template>
|
||
<el-button v-else
|
||
@click="spliceBusinessTime(index)"
|
||
size="mini"
|
||
circle>
|
||
<i class="el-icon-minus"></i>
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="原地址">
|
||
<el-input style="width: 500px" readonly v-model="before_address"></el-input>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="经纬度" prop="longitude">
|
||
<div class="inline-block">
|
||
<span>经度</span>
|
||
<el-input class="small-input" readonly v-model="ruleForm.longitude"></el-input>
|
||
</div>
|
||
<div class="inline-block">
|
||
<span>维度</span>
|
||
<el-input class="small-input" readonly v-model="ruleForm.latitude"></el-input>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="门店地址" prop="address">
|
||
<el-input style="width: 500px" v-model="ruleForm.address" placeholder="请输入门店地址"></el-input>
|
||
<el-button type="primary" size="mini" @click="searchAddress" style="margin-left: 10px;">搜索定位</el-button>
|
||
</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">
|
||
<button
|
||
class="map-tool-btn"
|
||
:class="{active: isDrawing}"
|
||
@click="toggleDrawing"
|
||
title="绘制配送区域">
|
||
<i class="el-icon-edit"></i>
|
||
</button>
|
||
<button
|
||
class="map-tool-btn"
|
||
@click="finishDrawing"
|
||
v-if="isDrawing && currentPath.length >= 3"
|
||
title="完成当前区域绘制">
|
||
<i class="el-icon-check"></i>
|
||
</button>
|
||
<button
|
||
class="map-tool-btn danger"
|
||
@click="cancelDrawing"
|
||
v-if="isDrawing"
|
||
title="取消绘制">
|
||
<i class="el-icon-close"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 视图工具组 -->
|
||
<div class="map-tool-group">
|
||
<button
|
||
class="map-tool-btn"
|
||
@click="centerToStore"
|
||
title="定位到门店">
|
||
<i class="el-icon-position"></i>
|
||
</button>
|
||
<button
|
||
class="map-tool-btn"
|
||
@click="fitAllAreas"
|
||
v-if="deliveryFences.length > 0"
|
||
title="查看所有区域">
|
||
<i class="el-icon-zoom-in"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 管理工具组 -->
|
||
<div class="map-tool-group" v-if="deliveryFences.length > 0">
|
||
<button
|
||
class="map-tool-btn"
|
||
@click="exportFences"
|
||
title="导出配送区域">
|
||
<i class="el-icon-download"></i>
|
||
</button>
|
||
<button
|
||
class="map-tool-btn danger"
|
||
@click="clearAllFences"
|
||
title="清空所有区域">
|
||
<i class="el-icon-delete"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 导入工具 -->
|
||
<div class="map-tool-group">
|
||
<button
|
||
class="map-tool-btn"
|
||
@click="importFences"
|
||
title="导入配送区域">
|
||
<i class="el-icon-upload2"></i>
|
||
</button>
|
||
</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">
|
||
<button class="search-btn" @click="searchLocation">
|
||
<i class="el-icon-search"></i> 搜索
|
||
</button>
|
||
</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
|
||
class="area-action-btn"
|
||
@click.stop="locateFence(index)"
|
||
title="定位到此区域">
|
||
<i class="el-icon-location-outline"></i>
|
||
</button>
|
||
<button
|
||
class="area-action-btn"
|
||
@click.stop="editAreaName(index)"
|
||
title="编辑区域名称">
|
||
<i class="el-icon-edit-outline"></i>
|
||
</button>
|
||
<button
|
||
class="area-action-btn danger"
|
||
@click.stop="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="startDrawing">
|
||
<i class="el-icon-plus"></i> 创建第一个配送区域
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</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>
|
||
|
||
<div v-if="storeAuth.store_xcx_info==1" class="footer-button-box">
|
||
<el-button type="primary" @click="submitForm('ruleForm')" size="medium">立即提交</el-button>
|
||
</div>
|
||
</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 type="text/javascript" src="/Public/js/calculate.js"></script>
|
||
<script type="text/javascript">
|
||
var storeManager = new Vue({
|
||
el: "#storeManager",
|
||
data() {
|
||
return {
|
||
operate: {
|
||
delivery_fee: false,
|
||
},
|
||
before_address: "",
|
||
pageLoading: false,
|
||
// 地图相关
|
||
map: null,
|
||
marker: null,
|
||
mapReady: false, // 地图是否准备就绪
|
||
isDrawing: false,
|
||
currentPolygon: null,
|
||
currentPath: [],
|
||
drawingLine: null, // 绘制时的线条
|
||
drawingManager: null, // 原生绘制管理器
|
||
deliveryFences: [], // 配送围栏数据
|
||
polygons: [], // 地图上的多边形对象
|
||
importDialogVisible: false,
|
||
importText: '',
|
||
editIndex: null,
|
||
|
||
// 可视化功能新增属性
|
||
selectedAreaIndex: null, // 当前选中的区域索引
|
||
searchAddress: '', // 搜索地址
|
||
totalArea: 0, // 总覆盖面积
|
||
isFullscreen: false, // 是否全屏模式
|
||
|
||
ruleForm: {
|
||
id: 0,
|
||
status: "0",
|
||
skip: "1",
|
||
send_delayed_time: "0",
|
||
pickup_delayed_time: "0",
|
||
auto_set_time: "1",
|
||
self_pick: "1",
|
||
express_status: "1",
|
||
business_non_order: "2",
|
||
delivery_status: "0",
|
||
delivery_cost: '',
|
||
business: [],
|
||
business_time: [],
|
||
address: "",
|
||
longitude: 116.404,
|
||
latitude: 39.915,
|
||
tip: "",
|
||
phone: "",
|
||
smmoney: "",
|
||
delivery_fences: "", // 电子围栏数据JSON字符串
|
||
},
|
||
rules: {
|
||
delivery_cost: [{message: '请设置金额', required: false, trigger: 'change'}, {
|
||
validator: (rule, value, callback) => {
|
||
var reg = /^[+]{0,1}(\d+)$|^[+]{0,1}(\d+\.\d{0,2})$/;
|
||
if (storeManager.ruleForm.delivery_status == 1 && value === '' || value === undefined) {
|
||
callback(new Error("请设置金额"));
|
||
} else if (storeManager.ruleForm.delivery_status == 1 && (reg.test(value) == false || value < 0.01)) {
|
||
callback(new Error("请设置正确的金额"));
|
||
} else {
|
||
callback();
|
||
}
|
||
}, trigger: 'change'
|
||
}],
|
||
business: [{message: '请选择配送时间段', required: true, trigger: 'change'}, {
|
||
validator: (rule, value, callback) => {
|
||
if (!value || !value.length) {
|
||
callback(new Error("请选择配送时间段"));
|
||
} else {
|
||
let max = '00:00';
|
||
for (let i = 0; i < value.length; i++) {
|
||
let key = i + 1;
|
||
if (!value[i] || !value[i][0] || !value[i][1]) {
|
||
callback(new Error("请完善第" + key + "组时间段"));
|
||
return;
|
||
}
|
||
if (value[i][0] == value[i][1]) {
|
||
callback(new Error("第" + key + "组时间段开始时间不得等于结束时间"));
|
||
return;
|
||
}
|
||
if (storeManager.getTimestamp(value[i][0]) <= storeManager.getTimestamp(max) && i != 0) {
|
||
callback(new Error("第" + key + "组时间段的开始时间不得小于等于上组时间段的结束时间"));
|
||
return;
|
||
}
|
||
max = value[i][1];
|
||
}
|
||
callback();
|
||
}
|
||
}, trigger: 'change'
|
||
}],
|
||
business_time: [{message: '请选择营业时间段', required: true, trigger: 'change'}, {
|
||
validator: (rule, value, callback) => {
|
||
if (!value || !value.length) {
|
||
callback(new Error("请选择营业时间段"));
|
||
} else {
|
||
let max = '00:00';
|
||
for (let i = 0; i < value.length; i++) {
|
||
let key = i + 1;
|
||
if (!value[i] || !value[i][0] || !value[i][1]) {
|
||
callback(new Error("请完善第" + key + "组时间段"));
|
||
return;
|
||
}
|
||
if (value[i][0] == value[i][1]) {
|
||
callback(new Error("第" + key + "组时间段开始时间不得等于结束时间"));
|
||
return;
|
||
}
|
||
if (storeManager.getTimestamp(value[i][0]) <= storeManager.getTimestamp(max) && i != 0) {
|
||
callback(new Error("第" + key + "组时间段的开始时间不得小于等于上组时间段的结束时间"));
|
||
return;
|
||
}
|
||
max = value[i][1];
|
||
}
|
||
callback();
|
||
}
|
||
}, trigger: 'change'
|
||
}],
|
||
smmoney: [{message: '请设置正确的起送价', required: false, trigger: 'change'}, {
|
||
validator: (rule, value, callback) => {
|
||
var reg = /^[+]{0,1}(\d+)$|^[+]{0,1}(\d+\.\d{0,2})$/;
|
||
if (value && (reg.test(value) == false || value < 0)) {
|
||
callback(new Error("请设置正确的起送价"));
|
||
} else {
|
||
callback();
|
||
}
|
||
}, trigger: 'change'
|
||
}],
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
// 计算总覆盖面积
|
||
totalArea() {
|
||
return this.deliveryFences.reduce((total, fence) => {
|
||
const area = parseFloat(fence.area) || 0;
|
||
return total + area;
|
||
}, 0);
|
||
}
|
||
},
|
||
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) => {
|
||
if (this.isDrawing) {
|
||
this.addDrawingPoint(evt.latLng);
|
||
}
|
||
});
|
||
|
||
this.map.on('dblclick', (evt) => {
|
||
if (this.isDrawing && this.currentPath.length >= 3) {
|
||
this.finishDrawing();
|
||
}
|
||
});
|
||
|
||
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);
|
||
}
|
||
},
|
||
|
||
updateStoreMarker(position) {
|
||
try {
|
||
if (this.marker && this.marker.setPosition) {
|
||
this.marker.setPosition(position);
|
||
} else {
|
||
this.createStoreMarker();
|
||
}
|
||
} catch (error) {
|
||
console.error('更新门店标记失败:', error);
|
||
this.createStoreMarker();
|
||
}
|
||
},
|
||
|
||
// 地址搜索方法
|
||
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.updateStoreMarker(newCenter);
|
||
|
||
this.$message.success('地址定位成功');
|
||
} else {
|
||
this.$message.error('地址解析失败,请检查地址是否正确');
|
||
}
|
||
}).catch((error) => {
|
||
this.$message.error('地址搜索失败');
|
||
console.error('地址搜索错误:', error);
|
||
});
|
||
},
|
||
|
||
// 可视化配送区域核心方法
|
||
toggleDrawing() {
|
||
if (this.isDrawing) {
|
||
this.cancelDrawing();
|
||
} else {
|
||
this.startDrawing();
|
||
}
|
||
},
|
||
|
||
startDrawing() {
|
||
if (!this.mapReady || !this.map) {
|
||
this.$message.error('地图未准备就绪,请稍后再试');
|
||
return;
|
||
}
|
||
|
||
if (!this.checkDrawingCapability()) {
|
||
this.$message.error('地图API不支持绘制功能,请检查网络连接后刷新页面');
|
||
return;
|
||
}
|
||
|
||
this.resetDrawingState();
|
||
this.isDrawing = true;
|
||
this.selectedAreaIndex = null;
|
||
this.$message.info('请在地图上点击多个点来绘制配送区域,至少需要3个点');
|
||
},
|
||
|
||
// 新增的可视化方法
|
||
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(() => {});
|
||
},
|
||
|
||
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);
|
||
});
|
||
},
|
||
|
||
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'
|
||
});
|
||
},
|
||
|
||
checkDrawingCapability() {
|
||
return typeof TMap !== 'undefined' &&
|
||
(TMap.Polygon || TMap.Polyline || TMap.MultiPolygon || TMap.MultiPolyline);
|
||
},
|
||
|
||
resetDrawingState() {
|
||
this.currentPath = [];
|
||
this.editIndex = null;
|
||
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();
|
||
|
||
if (this.currentPath.length >= 3) {
|
||
this.$message.success(`已添加第${this.currentPath.length}个点,可以完成绘制了`);
|
||
}
|
||
},
|
||
|
||
updateDrawingDisplay() {
|
||
try {
|
||
this.updateDrawingLine();
|
||
|
||
if (this.currentPath.length >= 3) {
|
||
this.updateDrawingPolygon();
|
||
}
|
||
} catch (error) {
|
||
console.error('更新绘制显示失败:', error);
|
||
}
|
||
},
|
||
|
||
updateDrawingLine() {
|
||
if (this.drawingLine) {
|
||
this.drawingLine.setMap(null);
|
||
this.drawingLine = null;
|
||
}
|
||
|
||
if (this.currentPath.length >= 2) {
|
||
const path = this.currentPath.map(p => new TMap.LatLng(p.lat, p.lng));
|
||
|
||
if (TMap.Polyline) {
|
||
this.drawingLine = new TMap.Polyline({
|
||
map: this.map,
|
||
path: path,
|
||
strokeColor: '#10b981',
|
||
strokeWeight: 3,
|
||
strokeStyle: 'solid'
|
||
});
|
||
}
|
||
}
|
||
},
|
||
|
||
updateDrawingPolygon() {
|
||
if (this.currentPolygon) {
|
||
this.currentPolygon.setMap(null);
|
||
this.currentPolygon = null;
|
||
}
|
||
|
||
const closedPath = [...this.currentPath];
|
||
const firstPoint = closedPath[0];
|
||
const lastPoint = closedPath[closedPath.length - 1];
|
||
|
||
if (firstPoint.lat !== lastPoint.lat || firstPoint.lng !== lastPoint.lng) {
|
||
closedPath.push({ lat: firstPoint.lat, lng: firstPoint.lng });
|
||
}
|
||
|
||
const latLngArray = closedPath.map(p => new TMap.LatLng(p.lat, p.lng));
|
||
|
||
if (TMap.Polygon) {
|
||
this.currentPolygon = 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) {
|
||
this.currentPolygon = new TMap.Polyline({
|
||
map: this.map,
|
||
path: latLngArray,
|
||
strokeColor: '#10b981',
|
||
strokeWeight: 3
|
||
});
|
||
}
|
||
},
|
||
|
||
finishDrawing() {
|
||
if (this.currentPath.length < 3) {
|
||
this.$message.warning('至少需要3个点才能形成有效区域');
|
||
return;
|
||
}
|
||
|
||
const fenceData = this.createFenceData();
|
||
this.saveFenceData(fenceData);
|
||
this.finalizeDrawing();
|
||
},
|
||
|
||
createFenceData() {
|
||
const closedPath = [...this.currentPath];
|
||
const firstPoint = closedPath[0];
|
||
const lastPoint = closedPath[closedPath.length - 1];
|
||
|
||
if (firstPoint.lat !== lastPoint.lat || firstPoint.lng !== lastPoint.lng) {
|
||
closedPath.push({ lat: firstPoint.lat, lng: firstPoint.lng });
|
||
}
|
||
|
||
const area = this.calculatePolygonArea(closedPath);
|
||
|
||
return {
|
||
id: Date.now(),
|
||
name: `配送区域${this.deliveryFences.length + 1}`,
|
||
points: closedPath,
|
||
area: area.toFixed(2)
|
||
};
|
||
},
|
||
|
||
saveFenceData(fenceData) {
|
||
if (this.editIndex !== null) {
|
||
// 编辑模式
|
||
if (this.deliveryFences[this.editIndex]) {
|
||
fenceData.id = this.deliveryFences[this.editIndex].id || fenceData.id;
|
||
fenceData.name = this.deliveryFences[this.editIndex].name || fenceData.name;
|
||
}
|
||
|
||
this.deliveryFences.splice(this.editIndex, 1, fenceData);
|
||
|
||
if (this.polygons[this.editIndex]) {
|
||
this.polygons[this.editIndex].setMap(null);
|
||
this.polygons.splice(this.editIndex, 1, this.currentPolygon);
|
||
} else {
|
||
this.polygons[this.editIndex] = this.currentPolygon;
|
||
}
|
||
} else {
|
||
// 新增模式
|
||
this.deliveryFences.push(fenceData);
|
||
if (this.currentPolygon) {
|
||
this.polygons.push(this.currentPolygon);
|
||
}
|
||
}
|
||
},
|
||
|
||
finalizeDrawing() {
|
||
// 清除绘制线条
|
||
if (this.drawingLine) {
|
||
this.drawingLine.setMap(null);
|
||
this.drawingLine = null;
|
||
}
|
||
|
||
// 重置状态
|
||
this.isDrawing = false;
|
||
this.currentPath = [];
|
||
this.currentPolygon = null;
|
||
this.editIndex = null;
|
||
|
||
this.$message.success('配送区域绘制完成');
|
||
this.updateDeliveryFencesData();
|
||
},
|
||
|
||
cancelDrawing() {
|
||
this.clearCurrentDrawing();
|
||
this.isDrawing = false;
|
||
this.currentPath = [];
|
||
this.editIndex = null;
|
||
this.$message.info('已取消绘制');
|
||
},
|
||
|
||
// 围栏管理方法
|
||
clearAllFences() {
|
||
this.$confirm('确定要清空所有配送区域吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
this.polygons.forEach(polygon => polygon.setMap(null));
|
||
this.deliveryFences = [];
|
||
this.polygons = [];
|
||
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.$message.success('配送区域已删除');
|
||
}).catch(() => {});
|
||
},
|
||
|
||
focusFence(index) {
|
||
const fence = this.deliveryFences[index];
|
||
if (fence && fence.points.length > 0) {
|
||
const center = this.calculateFenceCenter(fence.points);
|
||
this.map.setCenter(new TMap.LatLng(center.lat, center.lng));
|
||
this.map.setZoom(15);
|
||
}
|
||
},
|
||
|
||
calculateFenceCenter(points) {
|
||
let centerLat = 0, centerLng = 0;
|
||
points.forEach(point => {
|
||
centerLat += point.lat;
|
||
centerLng += point.lng;
|
||
});
|
||
return {
|
||
lat: centerLat / points.length,
|
||
lng: centerLng / points.length
|
||
};
|
||
},
|
||
|
||
editFence(index) {
|
||
try {
|
||
const fence = this.deliveryFences[index];
|
||
if (!fence || !Array.isArray(fence.points) || fence.points.length < 3) {
|
||
this.$message.error('该区域数据无效,无法编辑');
|
||
return;
|
||
}
|
||
|
||
if (this.isDrawing) {
|
||
this.cancelDrawing();
|
||
}
|
||
|
||
this.isDrawing = true;
|
||
this.editIndex = index;
|
||
|
||
// 复制点集并移除闭合尾点
|
||
const points = fence.points.slice();
|
||
if (points.length >= 2) {
|
||
const firstPoint = points[0];
|
||
const lastPoint = points[points.length - 1];
|
||
if (firstPoint.lat === lastPoint.lat && firstPoint.lng === lastPoint.lng) {
|
||
points.pop();
|
||
}
|
||
}
|
||
|
||
this.currentPath = points.map(p => ({
|
||
lat: parseFloat(p.lat),
|
||
lng: parseFloat(p.lng)
|
||
}));
|
||
|
||
this.clearCurrentDrawing();
|
||
this.updateDrawingDisplay();
|
||
this.focusFence(index);
|
||
|
||
this.$message.info('编辑模式:在地图上点击添加新点,双击或点击"完成绘制"保存修改');
|
||
} catch (error) {
|
||
console.error('进入编辑模式失败:', error);
|
||
this.$message.error('进入编辑模式失败');
|
||
this.isDrawing = false;
|
||
this.editIndex = null;
|
||
this.currentPath = [];
|
||
}
|
||
},
|
||
|
||
// 工具方法
|
||
calculatePolygonArea(path) {
|
||
if (path.length < 3) return 0;
|
||
|
||
let area = 0;
|
||
const earthRadius = 6371;
|
||
|
||
for (let i = 0; i < path.length; i++) {
|
||
const j = (i + 1) % path.length;
|
||
const lat1 = path[i].lat * Math.PI / 180;
|
||
const lat2 = path[j].lat * Math.PI / 180;
|
||
const lng1 = path[i].lng * Math.PI / 180;
|
||
const lng2 = path[j].lng * Math.PI / 180;
|
||
|
||
area += (lng2 - lng1) * (2 + Math.sin(lat1) + Math.sin(lat2));
|
||
}
|
||
|
||
area = Math.abs(area) * earthRadius * earthRadius / 2;
|
||
return area;
|
||
},
|
||
|
||
updateDeliveryFencesData() {
|
||
this.ruleForm.delivery_fences = JSON.stringify(this.deliveryFences);
|
||
},
|
||
|
||
// 数据加载方法
|
||
loadExistingFences() {
|
||
if (!this.ruleForm.delivery_fences) {
|
||
this.deliveryFences = [];
|
||
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('围栏数据格式错误,已清空');
|
||
}
|
||
},
|
||
|
||
clearExistingPolygons() {
|
||
this.polygons.forEach(polygon => {
|
||
if (polygon && polygon.setMap) {
|
||
polygon.setMap(null);
|
||
}
|
||
});
|
||
this.polygons = [];
|
||
},
|
||
|
||
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
|
||
);
|
||
},
|
||
|
||
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({ lat: firstPoint.lat, lng: firstPoint.lng });
|
||
}
|
||
|
||
return path;
|
||
},
|
||
|
||
// 导入导出方法
|
||
exportFences() {
|
||
try {
|
||
const text = JSON.stringify(this.deliveryFences, null, 2);
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
this.$message.success('配送区域JSON已复制到剪贴板');
|
||
}).catch(() => {
|
||
window.prompt('复制以下JSON:', text);
|
||
});
|
||
} else {
|
||
window.prompt('复制以下JSON:', text);
|
||
}
|
||
} catch (error) {
|
||
this.$message.error('导出失败');
|
||
console.error('导出配送区域失败:', error);
|
||
}
|
||
},
|
||
|
||
importFences() {
|
||
this.importText = '';
|
||
this.importDialogVisible = true;
|
||
},
|
||
|
||
confirmImport() {
|
||
try {
|
||
const data = JSON.parse(this.importText || '[]');
|
||
if (!Array.isArray(data)) {
|
||
this.$message.error('格式错误:应为数组');
|
||
return;
|
||
}
|
||
|
||
const valid = data.every(fence =>
|
||
fence && Array.isArray(fence.points) && fence.points.length >= 3
|
||
);
|
||
|
||
if (!valid) {
|
||
this.$message.error('数据不合法:每个区域至少包含3个点');
|
||
return;
|
||
}
|
||
|
||
this.deliveryFences = data;
|
||
this.updateDeliveryFencesData();
|
||
this.loadExistingFences();
|
||
this.importDialogVisible = false;
|
||
this.$message.success('导入成功');
|
||
} catch (error) {
|
||
this.$message.error('导入失败:JSON解析错误');
|
||
console.error('导入配送区域失败:', error);
|
||
}
|
||
},
|
||
|
||
// 原有方法保持不变
|
||
getTimestamp(time) {
|
||
let timestamp = Date.parse(new Date('1970-01-01 ' + time + ':00'));
|
||
return timestamp;
|
||
},
|
||
|
||
getDeliveryTime(index, type) {
|
||
if (type == 1) {
|
||
return {
|
||
start: '00:00',
|
||
step: '00:05',
|
||
end: this.ruleForm.business[index][1] ? this.ruleForm.business[index][1] : '23:59',
|
||
}
|
||
} else {
|
||
return {
|
||
start: this.ruleForm.business[index][0] ? this.ruleForm.business[index][0] : '00:00',
|
||
step: '00:05',
|
||
end: '23:59'
|
||
}
|
||
}
|
||
},
|
||
|
||
getBusinessTime(index, type) {
|
||
if (type == 1) {
|
||
return {
|
||
start: '00:00',
|
||
step: '00:05',
|
||
end: this.ruleForm.business_time[index][1] ? this.ruleForm.business_time[index][1] : '23:59',
|
||
}
|
||
} else {
|
||
return {
|
||
start: this.ruleForm.business_time[index][0] ? this.ruleForm.business_time[index][0] : '00:00',
|
||
step: '00:05',
|
||
end: '23:59'
|
||
}
|
||
}
|
||
},
|
||
|
||
addBusiness() {
|
||
this.ruleForm.business.push(['', '']);
|
||
},
|
||
|
||
spliceBusiness(index) {
|
||
this.ruleForm.business.splice(index, 1);
|
||
},
|
||
|
||
addBusinessTime() {
|
||
this.ruleForm.business_time.push(['', '']);
|
||
},
|
||
|
||
spliceBusinessTime(index) {
|
||
this.ruleForm.business_time.splice(index, 1);
|
||
},
|
||
|
||
// 导出配送区域JSON
|
||
exportFences() {
|
||
try {
|
||
const text = JSON.stringify(this.deliveryFences, null, 2);
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
this.$message.success('配送区域JSON已复制到剪贴板');
|
||
}).catch(() => {
|
||
window.prompt('复制以下JSON:', text);
|
||
});
|
||
} else {
|
||
window.prompt('复制以下JSON:', text);
|
||
}
|
||
} catch (e) {
|
||
this.$message.error('导出失败');
|
||
console.error('导出配送区域失败:', e);
|
||
}
|
||
},
|
||
|
||
// 导入配送区域JSON
|
||
importFences() {
|
||
this.importText = '';
|
||
this.importDialogVisible = true;
|
||
},
|
||
|
||
// 确认导入
|
||
confirmImport() {
|
||
try {
|
||
const data = JSON.parse(this.importText || '[]');
|
||
if (!Array.isArray(data)) {
|
||
this.$message.error('格式错误:应为数组');
|
||
return;
|
||
}
|
||
const valid = data.every(f => f && Array.isArray(f.points) && f.points.length >= 3);
|
||
if (!valid) {
|
||
this.$message.error('数据不合法:每个区域至少包含3个点');
|
||
return;
|
||
}
|
||
this.deliveryFences = data;
|
||
this.ruleForm.delivery_fences = JSON.stringify(this.deliveryFences);
|
||
this.loadExistingFences();
|
||
this.importDialogVisible = false;
|
||
this.$message.success('导入成功');
|
||
} catch (e) {
|
||
this.$message.error('导入失败:JSON解析错误');
|
||
console.error('导入配送区域失败:', e);
|
||
}
|
||
},
|
||
|
||
// 编辑已有配送区域
|
||
editFence(index) {
|
||
try {
|
||
const fence = this.deliveryFences[index];
|
||
if (!fence || !Array.isArray(fence.points) || fence.points.length < 3) {
|
||
this.$message.error('该区域数据无效,无法编辑');
|
||
return;
|
||
}
|
||
|
||
// 取消当前绘制(如果正在绘制)
|
||
if (this.isDrawing) {
|
||
this.cancelDrawing();
|
||
}
|
||
|
||
console.log(`开始编辑区域${index + 1}:`, fence);
|
||
|
||
this.isDrawing = true;
|
||
this.editIndex = index;
|
||
|
||
// 复制点集并移除闭合尾点(如果存在)
|
||
const pts = fence.points.slice();
|
||
if (pts.length >= 2) {
|
||
const fp = pts[0], lp = pts[pts.length - 1];
|
||
if (fp.lat === lp.lat && fp.lng === lp.lng) {
|
||
pts.pop();
|
||
console.log('移除闭合点,剩余点数:', pts.length);
|
||
}
|
||
}
|
||
|
||
// 载入路径(创建新的点对象避免引用问题)
|
||
this.currentPath = pts.map(p => ({
|
||
lat: parseFloat(p.lat),
|
||
lng: parseFloat(p.lng)
|
||
}));
|
||
|
||
console.log('编辑模式载入点数:', this.currentPath.length);
|
||
|
||
// 清理当前绘制对象
|
||
if (this.currentPolygon && this.currentPolygon.setMap) {
|
||
try { this.currentPolygon.setMap(null); } catch (e) {}
|
||
}
|
||
this.currentPolygon = null;
|
||
|
||
if (this.drawingLine && this.drawingLine.setMap) {
|
||
try { this.drawingLine.setMap(null); } catch (e) {}
|
||
}
|
||
this.drawingLine = null;
|
||
|
||
// 渲染编辑状态的显示
|
||
this.updateDrawingLines();
|
||
this.createPolygon();
|
||
|
||
// 将地图中心移动到该区域
|
||
this.focusFence(index);
|
||
|
||
this.$message.info('编辑模式:在地图上点击添加新点,双击或点击“完成绘制”保存修改');
|
||
} catch (e) {
|
||
console.error('进入编辑模式失败:', e);
|
||
this.$message.error('进入编辑模式失败');
|
||
// 重置状态
|
||
this.isDrawing = false;
|
||
this.editIndex = null;
|
||
this.currentPath = [];
|
||
}
|
||
},
|
||
|
||
submitForm(formName) {
|
||
this.$refs[formName].validate((valid) => {
|
||
if (valid) {
|
||
// 检查是否设置了配送区域
|
||
if (this.ruleForm.skip === "0" && this.deliveryFences.length === 0) {
|
||
this.$message.warning('开启送货上门服务时,请至少设置一个配送区域');
|
||
return;
|
||
}
|
||
|
||
let data = {...this.ruleForm};
|
||
this.sendRequest('Retail/Xcx/storeSet', {
|
||
data: data,
|
||
success: (res) => {
|
||
if (res.status == 1) {
|
||
this.$message.success(res.message);
|
||
this.before_address = this.ruleForm.address;
|
||
} else {
|
||
this.$message.error(res.message);
|
||
}
|
||
},
|
||
})
|
||
} else {
|
||
this.$message.warning('请检查必填项');
|
||
}
|
||
})
|
||
}
|
||
},
|
||
|
||
mounted() {
|
||
// 获取基本设置
|
||
$.ajax({
|
||
url: '/index.php/Retail/Xcx/manager',
|
||
type: 'post',
|
||
success: (res) => {
|
||
if (res.status == 1) {
|
||
this.ruleForm = res.data;
|
||
this.before_address = res.data.address;
|
||
|
||
// 等待DOM更新后初始化地图
|
||
this.$nextTick(() => {
|
||
// 等待腾讯地图API完全加载
|
||
this.waitForTMapLoad();
|
||
});
|
||
} else {
|
||
this.$message.error(res.msg)
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
created() {
|
||
// 获取操作权限
|
||
var auth_list = {
|
||
delivery_fee: '/index.php/Retail/Xcx/distribution'
|
||
};
|
||
$.ajax({
|
||
url: '/index.php/Retail/Component/checkOperateAuth',
|
||
type: "post",
|
||
data: {
|
||
auth_list: JSON.stringify(auth_list),
|
||
},
|
||
success: (res) => {
|
||
if (res.status == 1) {
|
||
this.operate = res.data;
|
||
}
|
||
}
|
||
})
|
||
}
|
||
})
|
||
</script> |