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 100 </el-row>
118 101 <span slot="footer" class="dialog-footer">
119 102 <el-button @click="visible = false">取 消</el-button>
120 103 <el-button type="primary" @click="dataFormSubmit()" v-if="!isDetail">确 定</el-button>
121 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 160 </el-dialog>
123 161 </template>
  162 +
124 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 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 719 \ No newline at end of file
... ...