Commit 24f8cd2dff6a9300dc428bf02e041cbfbb170d4e

Authored by 李宇
2 parents f566491d ee89710c

Merge branch 'master' of http://39.98.150.180/antissoft/lvqianmeiye_ERP

antis-ncc-admin/src/views/lqMdxx/Form.vue
1 -<template>  
2 - <el-dialog :title="!dataForm.id ? '新建' : isDetail ? '详情':'编辑'" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="600px">  
3 - <el-row :gutter="15" class="" >  
4 - <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="!!isDetail" :rules="rules">  
5 - <el-col :span="24" v-if="false" >  
6 - <el-form-item label="主键" prop="id">  
7 - <el-input v-model="dataForm.id" placeholder="请输入" clearable :style='{"width":"100%"}' >  
8 - </el-input>  
9 - </el-form-item>  
10 - </el-col>  
11 - <el-col :span="24">  
12 - <el-form-item label="门店编码" prop="mdbm">  
13 - <el-input v-model="dataForm.mdbm" placeholder="请输入" clearable :style='{"width":"100%"}' >  
14 - </el-input>  
15 - </el-form-item>  
16 - </el-col>  
17 - <el-col :span="24">  
18 - <el-form-item label="单据门店编号" prop="djmdbh">  
19 - <el-input v-model="dataForm.djmdbh" placeholder="请输入" clearable :style='{"width":"100%"}' >  
20 - </el-input>  
21 - </el-form-item>  
22 - </el-col>  
23 - <el-col :span="24">  
24 - <el-form-item label="单据门店" prop="djmd">  
25 - <el-input v-model="dataForm.djmd" placeholder="请输入" clearable :style='{"width":"100%"}' >  
26 - </el-input>  
27 - </el-form-item>  
28 - </el-col>  
29 - <el-col :span="24">  
30 - <el-form-item label="店名" prop="dm">  
31 - <el-input v-model="dataForm.dm" placeholder="请输入" clearable :style='{"width":"100%"}' >  
32 - </el-input>  
33 - </el-form-item>  
34 - </el-col>  
35 - <el-col :span="24">  
36 - <el-form-item label="城市" prop="cs">  
37 - <el-input v-model="dataForm.cs" placeholder="请输入" clearable :style='{"width":"100%"}' >  
38 - </el-input>  
39 - </el-form-item>  
40 - </el-col>  
41 - <el-col :span="24">  
42 - <el-form-item label="地址" prop="dz">  
43 - <el-input v-model="dataForm.dz" placeholder="请输入" clearable :style='{"width":"100%"}' >  
44 - </el-input>  
45 - </el-form-item>  
46 - </el-col>  
47 - <el-col :span="24">  
48 - <el-form-item label="姓名" prop="xm">  
49 - <el-input v-model="dataForm.xm" placeholder="请输入" clearable :style='{"width":"100%"}' >  
50 - </el-input>  
51 - </el-form-item>  
52 - </el-col>  
53 - <el-col :span="24">  
54 - <el-form-item label="电话号码" prop="dhhm">  
55 - <el-input v-model="dataForm.dhhm" placeholder="请输入" clearable :style='{"width":"100%"}' >  
56 - </el-input>  
57 - </el-form-item>  
58 - </el-col>  
59 - <el-col :span="24">  
60 - <el-form-item label="座机" prop="zj">  
61 - <el-input v-model="dataForm.zj" placeholder="请输入" clearable :style='{"width":"100%"}' >  
62 - </el-input>  
63 - </el-form-item>  
64 - </el-col>  
65 - <el-col :span="24">  
66 - <el-form-item label="开业时间" prop="kysj">  
67 - <el-date-picker v-model="dataForm.kysj" placeholder="请选择" clearable :style='{"width":"100%"}' type='date' format="yyyy-MM-dd" value-format="timestamp" >  
68 - </el-date-picker>  
69 - </el-form-item>  
70 - </el-col>  
71 - <el-col :span="24">  
72 - <el-form-item label="最新状态" prop="zxzt">  
73 - <el-select v-model="dataForm.zxzt" placeholder="请选择" clearable :style='{"width":"100%"}' >  
74 - <el-option v-for="(item, index) in zxztOptions" :key="index" :label="item.fullName" :value="item.id" ></el-option>  
75 - </el-select>  
76 - </el-form-item>  
77 - </el-col>  
78 - <el-col :span="24">  
79 - <el-form-item label="工商名称" prop="gsmc">  
80 - <el-input v-model="dataForm.gsmc" placeholder="请输入" clearable :style='{"width":"100%"}' >  
81 - </el-input>  
82 - </el-form-item>  
83 - </el-col>  
84 - <el-col :span="24">  
85 - <el-form-item label="法人" prop="fr">  
86 - <el-input v-model="dataForm.fr" placeholder="请输入" clearable :style='{"width":"100%"}' >  
87 - </el-input>  
88 - </el-form-item>  
89 - </el-col>  
90 - <el-col :span="24">  
91 - <el-form-item label="有无社保" prop="ywsb">  
92 - <el-select v-model="dataForm.ywsb" placeholder="请选择" clearable :style='{"width":"100%"}' >  
93 - <el-option v-for="(item, index) in ywsbOptions" :key="index" :label="item.fullName" :value="item.id" ></el-option>  
94 - </el-select>  
95 - </el-form-item>  
96 - </el-col>  
97 - <el-col :span="24">  
98 - <el-form-item label="在职人数" prop="zzrs">  
99 - <el-input-number v-model="dataForm.zzrs" :min="0" :step="1" :precision="0" placeholder="请输入" :style='{"width":"100%"}' />  
100 - </el-form-item>  
101 - </el-col>  
102 - <el-col :span="24">  
103 - <el-form-item label="门店类别" prop="storeCategory">  
104 - <el-select v-model="dataForm.storeCategory" placeholder="请选择" clearable :style='{"width":"100%"}' >  
105 - <el-option v-for="(item, index) in storeCategoryOptions" :key="index" :label="item.Name" :value="item.Value" ></el-option>  
106 - </el-select>  
107 - </el-form-item>  
108 - </el-col>  
109 - <el-col :span="24">  
110 - <el-form-item label="门店类型" prop="storeType">  
111 - <el-select v-model="dataForm.storeType" placeholder="请选择" clearable :style='{"width":"100%"}' >  
112 - <el-option v-for="(item, index) in storeTypeOptions" :key="index" :label="item.Name" :value="item.Value" ></el-option>  
113 - </el-select>  
114 - </el-form-item>  
115 - </el-col>  
116 - </el-form> 1 +<template>
  2 + <el-dialog :title="!dataForm.id ? '新建' : isDetail ? '详情' : '编辑'" :close-on-click-modal="false"
  3 + :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="720px">
  4 + <el-row :gutter="15">
  5 + <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right"
  6 + :disabled="!!isDetail" :rules="rules">
  7 + <!-- 基础信息字段 -->
  8 + <el-col :span="24" v-if="false">
  9 + <el-form-item label="主键" prop="id">
  10 + <el-input v-model="dataForm.id" />
  11 + </el-form-item>
  12 + </el-col>
  13 + <el-col :span="12">
  14 + <el-form-item label="门店编码" prop="mdbm">
  15 + <el-input v-model="dataForm.mdbm" placeholder="请输入" clearable />
  16 + </el-form-item>
  17 + </el-col>
  18 + <el-col :span="12">
  19 + <el-form-item label="店名" prop="dm">
  20 + <el-input v-model="dataForm.dm" placeholder="请输入" clearable />
  21 + </el-form-item>
  22 + </el-col>
  23 + <el-col :span="24">
  24 + <el-form-item label="地址" prop="dz">
  25 + <el-input v-model="dataForm.dz" placeholder="请输入完整地址" clearable>
  26 + <el-button slot="append" icon="el-icon-location-outline"
  27 + @click="handleOpenLocation">地图定位</el-button>
  28 + </el-input>
  29 + </el-form-item>
  30 + </el-col>
  31 + <el-col :span="12">
  32 + <el-form-item label="经度" prop="longitude">
  33 + <el-input v-model="dataForm.longitude" readonly placeholder="请通过地图定位选择" />
  34 + </el-form-item>
  35 + </el-col>
  36 + <el-col :span="12">
  37 + <el-form-item label="纬度" prop="latitude">
  38 + <el-input v-model="dataForm.latitude" readonly placeholder="请通过地图定位选择" />
  39 + </el-form-item>
  40 + </el-col>
  41 + <el-col :span="24">
  42 + <el-form-item label="电子围栏">
  43 + <div class="fence-status-bar">
  44 + <el-tag :type="dataForm.fencePolygons && dataForm.fencePolygons.length ? 'success' : 'info'"
  45 + size="medium">
  46 + {{ (dataForm.fencePolygons && dataForm.fencePolygons.length) ? '已设置围栏 (1块)' : '未设置围栏' }}
  47 + </el-tag>
  48 + <el-button type="primary" size="mini" icon="el-icon-edit" style="margin-left: 10px"
  49 + :disabled="!dataForm.longitude || !dataForm.latitude" @click="handleOpenFence">
  50 + 设置围栏
  51 + </el-button>
  52 + <span v-if="!dataForm.longitude" class="hint-text">(请先完成地图定位)</span>
  53 + </div>
  54 + </el-form-item>
  55 + </el-col>
  56 +
  57 + <!-- 其他业务字段 -->
  58 + <el-col :span="12">
  59 + <el-form-item label="城市" prop="cs">
  60 + <el-input v-model="dataForm.cs" placeholder="请输入" clearable />
  61 + </el-form-item>
  62 + </el-col>
  63 + <el-col :span="12">
  64 + <el-form-item label="最新状态" prop="zxzt">
  65 + <el-select v-model="dataForm.zxzt" placeholder="请选择" clearable :style='{ "width": "100%" }'>
  66 + <el-option v-for="(item, index) in zxztOptions" :key="index" :label="item.fullName"
  67 + :value="item.id"></el-option>
  68 + </el-select>
  69 + </el-form-item>
  70 + </el-col>
  71 + <el-col :span="12">
  72 + <el-form-item label="门店类别" prop="storeCategory">
  73 + <el-select v-model="dataForm.storeCategory" placeholder="请选择" clearable
  74 + :style='{ "width": "100%" }'>
  75 + <el-option v-for="(item, index) in storeCategoryOptions" :key="index" :label="item.Name"
  76 + :value="item.Value"></el-option>
  77 + </el-select>
  78 + </el-form-item>
  79 + </el-col>
  80 + <el-col :span="12">
  81 + <el-form-item label="门店类型" prop="storeType">
  82 + <el-select v-model="dataForm.storeType" placeholder="请选择" clearable
  83 + :style='{ "width": "100%" }'>
  84 + <el-option v-for="(item, index) in storeTypeOptions" :key="index" :label="item.Name"
  85 + :value="item.Value"></el-option>
  86 + </el-select>
  87 + </el-form-item>
  88 + </el-col>
  89 + <el-col :span="12">
  90 + <el-form-item label="姓名" prop="xm">
  91 + <el-input v-model="dataForm.xm" placeholder="请输入" clearable />
  92 + </el-form-item>
  93 + </el-col>
  94 + <el-col :span="12">
  95 + <el-form-item label="电话号码" prop="dhhm">
  96 + <el-input v-model="dataForm.dhhm" placeholder="请输入" clearable />
  97 + </el-form-item>
  98 + </el-col>
  99 + </el-form>
117 </el-row> 100 </el-row>
118 <span slot="footer" class="dialog-footer"> 101 <span slot="footer" class="dialog-footer">
119 <el-button @click="visible = false">取 消</el-button> 102 <el-button @click="visible = false">取 消</el-button>
120 <el-button type="primary" @click="dataFormSubmit()" v-if="!isDetail">确 定</el-button> 103 <el-button type="primary" @click="dataFormSubmit()" v-if="!isDetail">确 定</el-button>
121 </span> 104 </span>
  105 +
  106 + <!-- 定位弹窗 -->
  107 + <el-dialog title="门店地图定位" :visible.sync="locationVisible" width="800px" append-to-body class="map-dialog">
  108 + <div class="map-container-wrapper">
  109 + <div class="map-header-bar">
  110 + <span>在地图上点击以选择门店位置(仅支持单个标记)</span>
  111 + <div class="coordinate-info" v-if="tempMarker.lng">
  112 + 当前选择:{{ tempMarker.lng.toFixed(6) }}, {{ tempMarker.lat.toFixed(6) }}
  113 + </div>
  114 + </div>
  115 + <div id="location-map" class="map-canvas"></div>
  116 + </div>
  117 + <span slot="footer" class="dialog-footer">
  118 + <el-button @click="locationVisible = false">取 消</el-button>
  119 + <el-button type="primary" @click="confirmLocation" :disabled="!tempMarker.lng">确 定</el-button>
  120 + </span>
  121 + </el-dialog>
  122 +
  123 + <!-- 围栏设置弹窗 -->
  124 + <el-dialog title="设置电子围栏" :visible.sync="fenceVisible" width="1000px" append-to-body class="map-dialog">
  125 + <div class="fence-editor-layout">
  126 + <div class="map-side-panel">
  127 + <div class="panel-header">图形管理 ({{ fenceBuffer.length }})</div>
  128 + <div class="shape-list">
  129 + <div v-for="(shape, index) in fenceBuffer" :key="index" class="shape-item">
  130 + <i :class="getShapeIcon(shape.type)"></i>
  131 + <span class="shape-name">{{ getShapeName(shape.type) }} {{ index + 1 }}</span>
  132 + <el-button type="text" icon="el-icon-delete" class="delete-btn"
  133 + @click="removeBufferShape(index)"></el-button>
  134 + </div>
  135 + <div v-if="!fenceBuffer.length" class="empty-text">暂无图形,请在右侧绘制</div>
  136 + </div>
  137 + <div class="panel-footer">
  138 + <p class="warning-text" v-if="fenceBuffer.length > 1">
  139 + <i class="el-icon-warning"></i> 注意:最终只能保留1个围栏
  140 + </p>
  141 + </div>
  142 + </div>
  143 + <div class="map-main-area">
  144 + <div class="map-toolbar">
  145 + <div v-for="tool in fenceTools" :key="tool.id" class="tool-btn"
  146 + :class="{ active: activeFenceTool === tool.id }" @click="changeFenceTool(tool.id)">
  147 + <span class="tool-icon" :class="'tool-icon--' + tool.id"></span>
  148 + <span class="tool-label">{{ tool.name }}</span>
  149 + </div>
  150 + </div>
  151 + <div id="fence-map" class="map-canvas"></div>
  152 + </div>
  153 + </div>
  154 + <span slot="footer" class="dialog-footer">
  155 + <div class="footer-hint" v-if="fenceBuffer.length > 1">请删除多余图形,仅保留一个围栏后再保存</div>
  156 + <el-button @click="fenceVisible = false">取 消</el-button>
  157 + <el-button type="primary" @click="confirmFence" :disabled="fenceBuffer.length !== 1">确 定 保 存</el-button>
  158 + </span>
  159 + </el-dialog>
122 </el-dialog> 160 </el-dialog>
123 </template> 161 </template>
  162 +
124 <script> 163 <script>
125 - import request from '@/utils/request'  
126 - import { getDictionaryDataSelector } from '@/api/systemData/dictionary'  
127 - import { previewDataInterface } from '@/api/systemData/dataInterface'  
128 - export default {  
129 - components: {},  
130 - props: [],  
131 - data() {  
132 - return {  
133 - loading: false,  
134 - visible: false,  
135 - isDetail: false,  
136 - dataForm: {  
137 - id:'',  
138 - id:undefined,  
139 - mdbm:undefined,  
140 - djmdbh:undefined,  
141 - djmd:undefined,  
142 - dm:undefined,  
143 - cs:undefined,  
144 - dz:undefined,  
145 - xm:undefined,  
146 - dhhm:undefined,  
147 - zj:undefined,  
148 - kysj:undefined,  
149 - zxzt:undefined,  
150 - gsmc:undefined,  
151 - fr:undefined,  
152 - ywsb:undefined,  
153 - zzrs:0,  
154 - storeCategory:undefined,  
155 - storeType:undefined,  
156 - },  
157 - rules: {  
158 - },  
159 - zxztOptions:[{"fullName":"开店","id":"开店"},{"fullName":"闭店","id":"闭店"}],  
160 - ywsbOptions:[{"fullName":"有","id":"有"},{"fullName":"无","id":"无"}],  
161 - storeCategoryOptions:[],  
162 - storeTypeOptions:[],  
163 - } 164 +import request from '@/utils/request'
  165 +
  166 +export default {
  167 + data() {
  168 + return {
  169 + loading: false,
  170 + visible: false,
  171 + isDetail: false,
  172 + dataForm: {
  173 + id: undefined,
  174 + mdbm: undefined,
  175 + dm: undefined,
  176 + dz: undefined,
  177 + cs: undefined,
  178 + xm: undefined,
  179 + dhhm: undefined,
  180 + zxzt: undefined,
  181 + storeCategory: undefined,
  182 + storeType: undefined,
  183 + longitude: null,
  184 + latitude: null,
  185 + fencePolygons: [],
  186 + },
  187 + rules: {
  188 + mdbm: [{ required: true, message: '请输入门店编码', trigger: 'blur' }],
  189 + dm: [{ required: true, message: '请输入店名', trigger: 'blur' }],
  190 + dz: [{ required: true, message: '请输入地址', trigger: 'blur' }],
  191 + },
  192 + zxztOptions: [{ fullName: '开店', id: '开店' }, { fullName: '闭店', id: '闭店' }],
  193 + storeCategoryOptions: [],
  194 + storeTypeOptions: [],
  195 +
  196 + // 定位弹窗相关
  197 + locationVisible: false,
  198 + locationMap: null,
  199 + locationMarkerLayer: null,
  200 + tempMarker: { lng: null, lat: null },
  201 +
  202 + // 围栏弹窗相关
  203 + fenceVisible: false,
  204 + fenceMap: null,
  205 + fenceEditor: null,
  206 + fenceDrawLayers: {}, // 绘图图层(只保留多边形 / 圆形)
  207 + fenceDisplayLayer: null, // 展示层(显示 buffer 中的图形)
  208 + activeFenceTool: 'polygon',
  209 + fenceBuffer: [], // 临时存放绘制的多个图形:[{ id, type, points, geometry }]
  210 + // 目前仅支持多边形 / 圆形两个工具,矩形与椭圆功能取消
  211 + fenceTools: [
  212 + { id: 'polygon', name: '多边形' },
  213 + { id: 'circle', name: '圆形' }
  214 + ]
  215 + }
  216 + },
  217 + created() {
  218 + this.loadStoreCategoryOptions();
  219 + this.loadStoreTypeOptions();
  220 + },
  221 + methods: {
  222 + loadStoreCategoryOptions() {
  223 + request({ url: '/api/Extend/lqmdxx/Selector/StoreCategory', method: 'get' })
  224 + .then(res => { this.storeCategoryOptions = res.data || []; });
164 }, 225 },
165 - computed: {},  
166 - watch: {},  
167 - created() {  
168 - this.loadStoreCategoryOptions();  
169 - this.loadStoreTypeOptions(); 226 + loadStoreTypeOptions() {
  227 + request({ url: '/api/Extend/lqmdxx/Selector/StoreType', method: 'get' })
  228 + .then(res => { this.storeTypeOptions = res.data || []; });
170 }, 229 },
171 - mounted() {  
172 - },  
173 - methods: {  
174 - goBack() {  
175 - this.$emit('refresh')  
176 - },  
177 - // 加载门店类别选项  
178 - loadStoreCategoryOptions() {  
179 - request({  
180 - url: '/api/Extend/lqmdxx/Selector/StoreCategory',  
181 - method: 'get'  
182 - }).then(res => {  
183 - this.storeCategoryOptions = res.data || [];  
184 - }).catch(err => {  
185 - console.error('加载门店类别选项失败:', err);  
186 - this.storeCategoryOptions = []; 230 +
  231 + init(id, isDetail) {
  232 + this.dataForm.id = id || 0;
  233 + this.visible = true;
  234 + this.isDetail = isDetail || false;
  235 + this.$nextTick(() => {
  236 + this.$refs['elForm'].resetFields();
  237 + if (this.dataForm.id) {
  238 + request({ url: '/api/Extend/LqMdxx/' + this.dataForm.id, method: 'get' })
  239 + .then(res => {
  240 + this.dataForm = res.data;
  241 + });
  242 + }
  243 + })
  244 + },
  245 +
  246 + loadTMapScript() {
  247 + return new Promise((resolve, reject) => {
  248 + if (window.TMap) return resolve();
  249 + const script = document.createElement('script');
  250 + script.src = 'https://map.qq.com/api/gljs?v=1.exp&key=YRXBZ-NEV6T-K7SXH-VJPMF-G5IQF-F3FCJ&libraries=tools,geometry';
  251 + script.onload = resolve;
  252 + script.onerror = reject;
  253 + document.body.appendChild(script);
  254 + });
  255 + },
  256 +
  257 + // --- 门店定位逻辑 ---
  258 + handleOpenLocation() {
  259 + this.locationVisible = true;
  260 + this.tempMarker = { lng: this.dataForm.longitude, lat: this.dataForm.latitude };
  261 + this.$nextTick(() => {
  262 + this.initLocationMap();
  263 + });
  264 + },
  265 +
  266 + initLocationMap() {
  267 + this.loadTMapScript().then(() => {
  268 + const TMap = window.TMap;
  269 + const centerLat = this.tempMarker.lat || 30.656149; // 成都
  270 + const centerLng = this.tempMarker.lng || 104.065735;
  271 + const center = new TMap.LatLng(centerLat, centerLng);
  272 +
  273 + this.locationMap = new TMap.Map('location-map', { center, zoom: 14 });
  274 + this.locationMarkerLayer = new TMap.MultiMarker({ map: this.locationMap, geometries: [] });
  275 +
  276 + if (this.tempMarker.lng) {
  277 + this.locationMarkerLayer.setGeometries([{ id: 'm', position: new TMap.LatLng(this.tempMarker.lat, this.tempMarker.lng) }]);
  278 + }
  279 +
  280 + this.locationMap.on('click', (evt) => {
  281 + if (!evt.latLng) return;
  282 + const lat = evt.latLng.getLat();
  283 + const lng = evt.latLng.getLng();
  284 + this.tempMarker = { lng, lat };
  285 + this.locationMarkerLayer.setGeometries([{ id: 'm', position: evt.latLng }]);
187 }); 286 });
188 - },  
189 - // 加载门店类型选项  
190 - loadStoreTypeOptions() {  
191 - request({  
192 - url: '/api/Extend/lqmdxx/Selector/StoreType',  
193 - method: 'get'  
194 - }).then(res => {  
195 - this.storeTypeOptions = res.data || [];  
196 - }).catch(err => {  
197 - console.error('加载门店类型选项失败:', err);  
198 - this.storeTypeOptions = []; 287 + });
  288 + },
  289 +
  290 + confirmLocation() {
  291 + this.dataForm.longitude = this.tempMarker.lng;
  292 + this.dataForm.latitude = this.tempMarker.lat;
  293 + this.locationVisible = false;
  294 + },
  295 +
  296 + // --- 围栏设置逻辑 ---
  297 + handleOpenFence() {
  298 + if (!this.dataForm.longitude) return;
  299 + this.fenceVisible = true;
  300 + // 初始化 Buffer:历史上如果有多块,只保留第一块,保证始终至多一个围栏
  301 + const polygons = Array.isArray(this.dataForm.fencePolygons) ? this.dataForm.fencePolygons : [];
  302 + if (polygons.length > 0 && Array.isArray(polygons[0])) {
  303 + this.fenceBuffer = [{
  304 + id: `old-${Date.now()}`,
  305 + type: 'polygon',
  306 + points: polygons[0]
  307 + }];
  308 + } else {
  309 + this.fenceBuffer = [];
  310 + }
  311 + this.$nextTick(() => {
  312 + this.initFenceMap();
  313 + });
  314 + },
  315 +
  316 + initFenceMap() {
  317 + this.loadTMapScript().then(() => {
  318 + const TMap = window.TMap;
  319 + const center = new TMap.LatLng(this.dataForm.latitude, this.dataForm.longitude);
  320 + this.fenceMap = new TMap.Map('fence-map', { center, zoom: 16 });
  321 +
  322 + // 1. 门店位置固定标点
  323 + this.fenceStoreMarkerLayer = new TMap.MultiMarker({
  324 + map: this.fenceMap,
  325 + geometries: [{ id: 'store', position: center }]
199 }); 326 });
200 - },  
201 - init(id, isDetail) {  
202 - this.dataForm.id = id || 0;  
203 - this.visible = true;  
204 - this.isDetail = isDetail || false;  
205 - this.$nextTick(() => {  
206 - this.$refs['elForm'].resetFields();  
207 - if (this.dataForm.id) {  
208 - request({  
209 - url: '/api/Extend/LqMdxx/' + this.dataForm.id,  
210 - method: 'get'  
211 - }).then(res =>{  
212 - this.dataForm = res.data; 327 +
  328 + // 2. 初始化展示层
  329 + this.fenceDisplayLayer = new TMap.MultiPolygon({
  330 + map: this.fenceMap,
  331 + geometries: [],
  332 + styles: {
  333 + default: new TMap.PolygonStyle({
  334 + color: 'rgba(41,182,246,0.2)',
  335 + borderColor: 'rgba(41,182,246,0.9)',
  336 + borderWidth: 2
213 }) 337 })
214 } 338 }
  339 + });
  340 +
  341 + // 3. 构建临时绘制图层:
  342 + // - 一个多边形图层 polygonLayer
  343 + // - 一个圆形图层 circleLayer
  344 + const polygonLayer = new TMap.MultiPolygon({
  345 + map: this.fenceMap,
  346 + geometries: [],
  347 + styles: {
  348 + default: new TMap.PolygonStyle({
  349 + color: 'rgba(255,152,0,0.2)',
  350 + borderColor: '#FF9800',
  351 + borderWidth: 2
  352 + })
  353 + }
  354 + });
  355 + const circleLayer = new TMap.MultiCircle({
  356 + map: this.fenceMap,
  357 + geometries: [],
  358 + styles: {
  359 + default: new TMap.CircleStyle({
  360 + color: 'rgba(255,152,0,0.2)',
  361 + borderColor: '#FF9800',
  362 + borderWidth: 2
  363 + })
  364 + }
  365 + });
  366 + this.fenceDrawLayers = {
  367 + polygon: polygonLayer,
  368 + circle: circleLayer
  369 + };
  370 +
  371 + // 4. 初始化唯一编辑器(后续不再整体销毁,只清空内容)
  372 + this.fenceEditor = new TMap.tools.GeometryEditor({
  373 + map: this.fenceMap,
  374 + overlayList: [
  375 + { overlay: polygonLayer, id: 'polygon' },
  376 + { overlay: circleLayer, id: 'circle' }
  377 + ],
  378 + actionMode: TMap.tools.constants.EDITOR_ACTION.DRAW,
  379 + activeOverlayId: 'polygon',
  380 + snappable: true
  381 + });
  382 +
  383 + // 5. 监听绘制完毕事件:每次只保留当前绘制结果为唯一围栏
  384 + this.fenceEditor.on('draw_complete', (geometry) => {
  385 + const toolId = this.activeFenceTool;
  386 + const points = this._extractPoints(toolId, geometry);
  387 +
  388 + if (points && points.length >= 3) {
  389 + // 业务约束:无论之前画了什么,本次绘制即为“唯一围栏”
  390 + this.fenceBuffer = [{
  391 + id: `shape-${Date.now()}`,
  392 + type: toolId,
  393 + points: points
  394 + }];
  395 + this.refreshFenceDisplay();
  396 + }
  397 +
  398 + // 绘制完毕后:不销毁 Editor,只清空对应图层的临时几何
  399 + const drawLayer = this.fenceDrawLayers[toolId];
  400 + if (drawLayer && typeof drawLayer.setGeometries === 'function') {
  401 + drawLayer.setGeometries([]);
  402 + }
  403 +
  404 + // 延迟一帧重置绘制状态,避免与内部事件冲突
  405 + setTimeout(() => {
  406 + if (this.fenceEditor && window.TMap && window.TMap.tools && window.TMap.tools.constants) {
  407 + this.fenceEditor.setActiveOverlay(toolId);
  408 + this.fenceEditor.setActionMode(window.TMap.tools.constants.EDITOR_ACTION.DRAW);
  409 + }
  410 + }, 20);
  411 + });
  412 +
  413 + this.refreshFenceDisplay();
  414 + });
  415 + },
  416 +
  417 + changeFenceTool(id) {
  418 + this.activeFenceTool = id;
  419 + if (this.fenceEditor) {
  420 + // 清除可能画了一半的所有绘制层(去重后防止同一图层重复清理)
  421 + const uniqueLayers = Object.values(this.fenceDrawLayers).filter((layer, index, arr) => arr.indexOf(layer) === index);
  422 + uniqueLayers.forEach(layer => {
  423 + if (layer && typeof layer.setGeometries === 'function') {
  424 + layer.setGeometries([]);
  425 + }
  426 + });
  427 +
  428 + this.fenceEditor.setActiveOverlay(id);
  429 + this.fenceEditor.setActionMode(window.TMap.tools.constants.EDITOR_ACTION.DRAW);
  430 + }
  431 + },
  432 +
  433 + refreshFenceDisplay() {
  434 + if (!this.fenceDisplayLayer) return;
  435 + const TMap = window.TMap;
  436 + const geometries = this.fenceBuffer.map(item => {
  437 + const path = item.points.map(p => new TMap.LatLng(p.lat, p.lng));
  438 + // 闭合
  439 + if (path[0].getLat() !== path[path.length - 1].getLat() || path[0].getLng() !== path[path.length - 1].getLng()) {
  440 + path.push(path[0]);
  441 + }
  442 + return { id: item.id, paths: [path] };
  443 + });
  444 + this.fenceDisplayLayer.setGeometries(geometries);
  445 + },
  446 +
  447 + removeBufferShape(index) {
  448 + this.fenceBuffer.splice(index, 1);
  449 + this.refreshFenceDisplay();
  450 + },
  451 +
  452 + confirmFence() {
  453 + if (this.fenceBuffer.length !== 1) {
  454 + this.$message.warning('请确保最终只保留一个围栏');
  455 + return;
  456 + }
  457 + this.dataForm.fencePolygons = [this.fenceBuffer[0].points];
  458 + this.fenceVisible = false;
  459 + },
  460 +
  461 + // 将 GeometryEditor 返回的几何统一转换为点数组
  462 + _extractPoints(toolId, geometry) {
  463 + if (!geometry) return []
  464 +
  465 + // 通用:从 geometry 中尝试解析点数组(paths 或 path,一维或二维)
  466 + let pointsArray = []
  467 + if (Array.isArray(geometry.paths) && geometry.paths.length) {
  468 + const first = geometry.paths[0]
  469 + pointsArray = Array.isArray(first) ? first : geometry.paths
  470 + } else if (Array.isArray(geometry.path) && geometry.path.length) {
  471 + const first = geometry.path[0]
  472 + pointsArray = Array.isArray(first) ? first : geometry.path
  473 + }
  474 +
  475 + // 多边形 / 矩形 / 椭圆:统一按点数组处理
  476 + if (['polygon', 'rectangle', 'ellipse'].includes(toolId)) {
  477 + if (!Array.isArray(pointsArray) || !pointsArray.length) return []
  478 + return pointsArray.map(p => ({
  479 + lng: typeof p.getLng === 'function' ? p.getLng() : p.lng,
  480 + lat: typeof p.getLat === 'function' ? p.getLat() : p.lat
  481 + }))
  482 + }
  483 +
  484 + // 圆形:用中心 + 半径近似成 36 边多边形
  485 + if (toolId === 'circle' && geometry.center && geometry.radius) {
  486 + const center = geometry.center
  487 + const r = geometry.radius
  488 + const cLat = typeof center.getLat === 'function' ? center.getLat() : center.lat
  489 + const cLng = typeof center.getLng === 'function' ? center.getLng() : center.lng
  490 + const R = 6378137
  491 + const latRad = (cLat * Math.PI) / 180
  492 + return Array.from({ length: 36 }, (_, i) => {
  493 + const angle = (2 * Math.PI * i) / 36
  494 + return {
  495 + lat: cLat + (r * Math.cos(angle)) / R * (180 / Math.PI),
  496 + lng: cLng + (r * Math.sin(angle)) / (R * Math.cos(latRad)) * (180 / Math.PI)
  497 + }
215 }) 498 })
216 - },  
217 - dataFormSubmit() {  
218 - this.$refs['elForm'].validate((valid) => {  
219 - if (valid) {  
220 - if (!this.dataForm.id) {  
221 - request({  
222 - url: `/api/Extend/LqMdxx`,  
223 - method: 'post',  
224 - data: this.dataForm,  
225 - }).then((res) => {  
226 - this.$message({  
227 - message: res.msg,  
228 - type: 'success',  
229 - duration: 1000,  
230 - onClose: () => {  
231 - this.visible = false,  
232 - this.$emit('refresh', true)  
233 - }  
234 - })  
235 - })  
236 - } else {  
237 - request({  
238 - url: '/api/Extend/LqMdxx/' + this.dataForm.id,  
239 - method: 'PUT',  
240 - data: this.dataForm  
241 - }).then((res) => {  
242 - this.$message({  
243 - message: res.msg,  
244 - type: 'success',  
245 - duration: 1000,  
246 - onClose: () => {  
247 - this.visible = false  
248 - this.$emit('refresh', true)  
249 - }  
250 - })  
251 - })  
252 - }  
253 - }  
254 - })  
255 - }, 499 + }
  500 +
  501 + return []
  502 + },
  503 +
  504 + getShapeIcon(type) {
  505 + const icons = { polygon: 'el-icon-picture', circle: 'el-icon-loading', rectangle: 'el-icon-full-screen', ellipse: 'el-icon-help' };
  506 + return icons[type] || 'el-icon-info';
  507 + },
  508 +
  509 + getShapeName(type) {
  510 + const names = { polygon: '多边形', circle: '圆形', rectangle: '矩形', ellipse: '椭圆' };
  511 + return names[type] || '图形';
  512 + },
  513 +
  514 + dataFormSubmit() {
  515 + this.$refs['elForm'].validate((valid) => {
  516 + if (!valid) return;
  517 + const isNew = !this.dataForm.id;
  518 + request({
  519 + url: isNew ? '/api/Extend/LqMdxx' : `/api/Extend/LqMdxx/${this.dataForm.id}`,
  520 + method: isNew ? 'POST' : 'PUT',
  521 + data: this.dataForm
  522 + }).then((res) => {
  523 + this.$message({
  524 + message: res.msg,
  525 + type: 'success',
  526 + duration: 1000,
  527 + onClose: () => {
  528 + this.visible = false;
  529 + this.$emit('refresh', true);
  530 + }
  531 + })
  532 + })
  533 + })
256 } 534 }
257 } 535 }
  536 +}
258 </script> 537 </script>
  538 +
  539 +<style lang="scss" scoped>
  540 +.fence-status-bar {
  541 + display: flex;
  542 + align-items: center;
  543 + padding: 5px 0;
  544 +
  545 + .hint-text {
  546 + font-size: 12px;
  547 + color: #f56c6c;
  548 + margin-left: 8px;
  549 + }
  550 +}
  551 +
  552 +.map-dialog {
  553 + ::v-deep .el-dialog__body {
  554 + padding: 10px 20px;
  555 + }
  556 +}
  557 +
  558 +.map-container-wrapper {
  559 + .map-header-bar {
  560 + display: flex;
  561 + justify-content: space-between;
  562 + font-size: 13px;
  563 + color: #606266;
  564 + margin-bottom: 8px;
  565 +
  566 + .coordinate-info {
  567 + color: #409eff;
  568 + font-weight: bold;
  569 + }
  570 + }
  571 +}
  572 +
  573 +.map-canvas {
  574 + width: 100%;
  575 + height: 450px;
  576 + border-radius: 4px;
  577 + border: 1px solid #dcdfe6;
  578 +}
  579 +
  580 +.fence-editor-layout {
  581 + display: flex;
  582 + height: 500px;
  583 + gap: 15px;
  584 +
  585 + .map-side-panel {
  586 + width: 220px;
  587 + border: 1px solid #ebeef5;
  588 + border-radius: 4px;
  589 + display: flex;
  590 + flex-direction: column;
  591 +
  592 + .panel-header {
  593 + padding: 10px;
  594 + background: #f5f7fa;
  595 + border-bottom: 1px solid #ebeef5;
  596 + font-weight: bold;
  597 + font-size: 14px;
  598 + }
  599 +
  600 + .shape-list {
  601 + flex: 1;
  602 + overflow-y: auto;
  603 + padding: 10px;
  604 +
  605 + .shape-item {
  606 + display: flex;
  607 + align-items: center;
  608 + padding: 8px;
  609 + margin-bottom: 8px;
  610 + background: #fdfdfd;
  611 + border: 1px solid #f2f2f2;
  612 + border-radius: 4px;
  613 +
  614 + i {
  615 + margin-right: 8px;
  616 + color: #409eff;
  617 + }
  618 +
  619 + .shape-name {
  620 + flex: 1;
  621 + font-size: 12px;
  622 + }
  623 +
  624 + .delete-btn {
  625 + color: #f56c6c;
  626 + padding: 0;
  627 + }
  628 + }
  629 +
  630 + .empty-text {
  631 + text-align: center;
  632 + color: #909399;
  633 + font-size: 12px;
  634 + margin-top: 50px;
  635 + }
  636 + }
  637 +
  638 + .panel-footer {
  639 + padding: 10px;
  640 + border-top: 1px solid #ebeef5;
  641 +
  642 + .warning-text {
  643 + font-size: 11px;
  644 + color: #e6a23c;
  645 + margin: 0;
  646 + }
  647 + }
  648 + }
  649 +
  650 + .map-main-area {
  651 + flex: 1;
  652 + display: flex;
  653 + flex-direction: column;
  654 +
  655 + .map-toolbar {
  656 + display: flex;
  657 + padding: 0 0 10px 0;
  658 + gap: 8px;
  659 +
  660 + .tool-btn {
  661 + display: flex;
  662 + align-items: center;
  663 + padding: 5px 12px;
  664 + border: 1px solid #dcdfe6;
  665 + border-radius: 4px;
  666 + cursor: pointer;
  667 + background: #fff;
  668 + transition: all 0.2s;
  669 +
  670 + &:hover {
  671 + border-color: #409eff;
  672 + color: #409eff;
  673 + }
  674 +
  675 + &.active {
  676 + background: #ecf5ff;
  677 + border-color: #409eff;
  678 + color: #409eff;
  679 + }
  680 +
  681 + .tool-icon {
  682 + width: 16px;
  683 + height: 16px;
  684 + margin-right: 6px;
  685 + background-size: cover;
  686 + }
  687 +
  688 + .tool-label {
  689 + font-size: 12px;
  690 + }
  691 + }
  692 +
  693 + .tool-icon--polygon {
  694 + background-image: url('https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/polygon.png');
  695 + }
  696 +
  697 + .tool-icon--circle {
  698 + background-image: url('https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/circle.png');
  699 + }
  700 +
  701 + .tool-icon--rectangle {
  702 + background-image: url('https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/rectangle.png');
  703 + }
  704 +
  705 + .tool-icon--ellipse {
  706 + background-image: url('https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/ellipse.png');
  707 + }
  708 + }
  709 + }
  710 +}
  711 +
  712 +.footer-hint {
  713 + color: #f56c6c;
  714 + font-size: 12px;
  715 + margin-right: 20px;
  716 + display: inline-block;
  717 +}
  718 +</style>
259 \ No newline at end of file 719 \ No newline at end of file