selectTdMap.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  1. <template>
  2. <view class="container">
  3. <view class="top"></view>
  4. <view class="head">
  5. <view class="handle">
  6. <view class="head_left">
  7. <span class="cuIcon-back" @click="back"></span>
  8. </view>
  9. <span class="center">商品管理</span>
  10. <view class="head_right">
  11. <!-- <span class="cuIcon-search" @click="goSearch"></span> -->
  12. <view class="manage" @click="confirm">
  13. 确定
  14. </view>
  15. </view>
  16. </view>
  17. </view>
  18. <!-- 地图容器 - 必须设置明确高度 -->
  19. <view id="mapContainer" class="map-container"></view>
  20. <!-- 搜索功能区域 -->
  21. <view class="search-panel" v-if="showSearch">
  22. <view class="search-input-group">
  23. <input class="search-input" v-model="searchKeyword" placeholder="输入关键词搜索地点" @confirm="handleSearch" />
  24. <button class="search-btn" @click="handleSearch">搜索</button>
  25. </view>
  26. <!-- 搜索结果列表 -->
  27. <view class="search-results" v-if="searchResults.length > 0">
  28. <view class="result-header">
  29. <text>搜索结果 ({{ searchResults.length }} 个)</text>
  30. <text class="clear-results" @click="clearSearchResults">清除</text>
  31. </view>
  32. <scroll-view class="results-list" scroll-y>
  33. <view v-for="(result, index) in searchResults" :key="index" class="result-item"
  34. @click="selectSearchResult(result)">
  35. <view class="result-name">{{ result.name }}</view>
  36. <view class="result-address">{{ result.address }}</view>
  37. <view class="result-coord">经度:{{ result.lng.toFixed(6) }} 纬度:{{ result.lat.toFixed(6) }}</view>
  38. </view>
  39. </scroll-view>
  40. </view>
  41. </view>
  42. </view>
  43. </template>
  44. <script>
  45. export default {
  46. props: {
  47. initialPosition: {
  48. type: Object,
  49. default: () => ({
  50. lnglat: null,
  51. address: ''
  52. })
  53. },
  54. allowSelection: {
  55. type: Boolean,
  56. default: true
  57. },
  58. showCoordinates: {
  59. type: Boolean,
  60. default: true
  61. },
  62. showSearch: {
  63. type: Boolean,
  64. default: true
  65. }
  66. },
  67. data() {
  68. return {
  69. map: null,
  70. geocoder: null,
  71. localSearch: null,
  72. tk: '92c4ddd919f4e41a30e765680e3a29b7',
  73. longitude: '未选择',
  74. latitude: '未选择',
  75. address: '未获取',
  76. markers: [],
  77. labels: [],
  78. infoWindows: [],
  79. mapInitialized: false,
  80. searchKeyword: '',
  81. searchResults: [],
  82. currentInfoWindow: null,
  83. lng: '',
  84. lat: '',
  85. addressname: '',
  86. selectedPosition: null,
  87. backurl: ''
  88. };
  89. },
  90. watch: {
  91. initialPosition: {
  92. immediate: true,
  93. handler(newVal) {
  94. if (this.mapInitialized && newVal && this.isValidPosition(newVal)) {
  95. this.processPosition(newVal);
  96. }
  97. }
  98. },
  99. allowSelection(newVal) {
  100. if (this.map) {
  101. if (newVal) {
  102. this.enableMapSelection();
  103. } else {
  104. this.disableMapSelection();
  105. }
  106. }
  107. }
  108. },
  109. mounted() {
  110. // 确保在H5环境下运行
  111. // #ifdef H5
  112. this.$nextTick(() => {
  113. setTimeout(() => {
  114. this.initMap();
  115. }, 300);
  116. });
  117. // #endif
  118. // 非H5环境提示
  119. // #ifndef H5
  120. console.warn("天地图组件仅支持H5环境");
  121. uni.showToast({
  122. title: '地图功能仅在H5环境可用',
  123. icon: 'none'
  124. });
  125. // #endif
  126. },
  127. beforeUnmount() {
  128. this.cleanupMap();
  129. },
  130. onLoad(option) {
  131. console.log(option)
  132. this.backurl = option.url
  133. },
  134. methods: {
  135. confirm() {
  136. console.log('确认选择位置:', this.lng, this.lat, this.addressname);
  137. if (!this.lng || !this.lat) {
  138. uni.showToast({
  139. title: '请先选择位置',
  140. icon: 'none'
  141. });
  142. return;
  143. }
  144. const params = {
  145. lng: this.lng,
  146. lat: this.lat,
  147. address: this.addressname
  148. };
  149. uni.setStorageSync('address', params);
  150. uni.navigateBack({
  151. delta: 1 // 返回的页面数,默认为1
  152. })
  153. // 使用encodeURIComponent编码参数
  154. // const queryString = Object.keys(params)
  155. // .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
  156. // .join('&');
  157. // // 拼接完整URL
  158. // const url = `${this.backurl}?${queryString}`;
  159. // uni.navigateTo({
  160. // url: url
  161. // })
  162. },
  163. back() {
  164. uni.navigateBack()
  165. },
  166. isValidPosition(position) {
  167. const lng = position.lng ||
  168. (position.lnglat && (position.lnglat.lng || position.lnglat.longitude));
  169. const lat = position.lat ||
  170. (position.lnglat && (position.lnglat.lat || position.lnglat.latitude));
  171. return lng && lat && !isNaN(parseFloat(lng)) && !isNaN(parseFloat(lat));
  172. },
  173. processPosition(position) {
  174. const lng = position.lng ||
  175. (position.lnglat && (position.lnglat.lng || position.lnglat.longitude));
  176. const lat = position.lat ||
  177. (position.lnglat && (position.lnglat.lat || position.lnglat.latitude));
  178. if (lng && lat) {
  179. this.setMapCenter(parseFloat(lng), parseFloat(lat));
  180. this.reverseGeocode(parseFloat(lng), parseFloat(lat));
  181. }
  182. },
  183. initMap() {
  184. if (typeof T === 'undefined') {
  185. this.loadTiandituAPI().then(() => {
  186. this.createMap();
  187. }).catch(err => {
  188. console.error("天地图加载失败:", err);
  189. uni.showToast({
  190. title: '地图加载失败',
  191. icon: 'none'
  192. });
  193. });
  194. } else {
  195. this.createMap();
  196. }
  197. },
  198. loadTiandituAPI() {
  199. return new Promise((resolve, reject) => {
  200. if (window.T) return resolve();
  201. const script = document.createElement('script');
  202. script.src = `https://api.tianditu.gov.cn/api?v=4.0&tk=${this.tk}`;
  203. script.onload = resolve;
  204. script.onerror = () => reject(new Error('天地图脚本加载失败'));
  205. document.head.appendChild(script);
  206. });
  207. },
  208. createMap() {
  209. try {
  210. this.map = new T.Map('mapContainer');
  211. let centerLng = 112.55;
  212. let centerLat = 37.87;
  213. if (this.initialPosition && this.isValidPosition(this.initialPosition)) {
  214. const lng = this.initialPosition.lng ||
  215. (this.initialPosition.lnglat && (this.initialPosition.lnglat.lng || this.initialPosition.lnglat
  216. .longitude));
  217. const lat = this.initialPosition.lat ||
  218. (this.initialPosition.lnglat && (this.initialPosition.lnglat.lat || this.initialPosition.lnglat.latitude));
  219. centerLng = parseFloat(lng);
  220. centerLat = parseFloat(lat);
  221. this.longitude = centerLng.toFixed(6);
  222. this.latitude = centerLat.toFixed(6);
  223. this.address = '正在查询地址...';
  224. }
  225. this.map.centerAndZoom(new T.LngLat(centerLng, centerLat), 12);
  226. this.geocoder = new T.Geocoder();
  227. // 初始化本地搜索
  228. const searchConfig = {
  229. pageCapacity: 10,
  230. onSearchComplete: this.handleSearchComplete.bind(this)
  231. };
  232. this.localSearch = new T.LocalSearch(this.map, searchConfig);
  233. const vecLayer = new T.TileLayer(
  234. `https://t{s}.tianditu.gov.cn/vec_c/wmts?tk=${this.tk}`, {
  235. subdomains: ['0', '1', '2', '3', '4', '5', '6', '7']
  236. }
  237. );
  238. const cvaLayer = new T.TileLayer(
  239. `https://t{s}.tianditu.gov.cn/cva_c/wmts?tk=${this.tk}`, {
  240. subdomains: ['0', '1', '2', '3', '4', '5', '6', '7']
  241. }
  242. );
  243. this.map.addLayer(vecLayer);
  244. this.map.addLayer(cvaLayer);
  245. this.map.addControl(new T.Control.Zoom());
  246. if (this.allowSelection) this.enableMapSelection();
  247. if (this.initialPosition && this.isValidPosition(this.initialPosition)) {
  248. const lng = this.initialPosition.lng ||
  249. (this.initialPosition.lnglat && (this.initialPosition.lnglat.lng || this.initialPosition.lnglat
  250. .longitude));
  251. const lat = this.initialPosition.lat ||
  252. (this.initialPosition.lnglat && (this.initialPosition.lnglat.lat || this.initialPosition.lnglat.latitude));
  253. this.addMarker(parseFloat(lng), parseFloat(lat));
  254. this.reverseGeocode(parseFloat(lng), parseFloat(lat));
  255. }
  256. this.mapInitialized = true;
  257. } catch (e) {
  258. console.error('地图初始化错误:', e);
  259. }
  260. },
  261. // 搜索相关方法
  262. handleSearch() {
  263. if (!this.searchKeyword.trim()) {
  264. uni.showToast({
  265. title: '请输入搜索关键词',
  266. icon: 'none'
  267. });
  268. return;
  269. }
  270. this.localSearch.search(this.searchKeyword);
  271. },
  272. handleSearchComplete(result) {
  273. this.clearSearchMarkers();
  274. this.searchResults = [];
  275. if (result.getResultType() === 1) {
  276. const pois = result.getPois();
  277. if (pois && pois.length > 0) {
  278. this.searchResults = pois.map(poi => ({
  279. name: poi.name,
  280. address: poi.address,
  281. lng: parseFloat(poi.lonlat.split(',')[0]),
  282. lat: parseFloat(poi.lonlat.split(',')[1])
  283. }));
  284. // 在地图上显示搜索结果标记
  285. pois.forEach(poi => {
  286. const lng = parseFloat(poi.lonlat.split(',')[0]);
  287. const lat = parseFloat(poi.lonlat.split(',')[1]);
  288. this.addSearchMarker(lng, lat, poi.name, poi.address);
  289. });
  290. // 调整地图视图以显示所有结果
  291. const points = pois.map(poi => {
  292. const lnglat = poi.lonlat.split(',');
  293. return new T.LngLat(parseFloat(lnglat[0]), parseFloat(lnglat[1]));
  294. });
  295. this.map.setViewport(points);
  296. } else {
  297. uni.showToast({
  298. title: '未找到相关结果',
  299. icon: 'none'
  300. });
  301. }
  302. }
  303. },
  304. selectSearchResult(result) {
  305. this.setMapCenter(result.lng, result.lat);
  306. this.address = result.address;
  307. this.lng = result.lng;
  308. this.lat = result.lat;
  309. this.addressname = result.name;
  310. console.log('选择搜索结果:', this.lng, this.lat, this.addressname);
  311. this.updateLabelContent();
  312. this.notifyParent();
  313. // 关闭其他信息窗口,打开当前结果的信息窗口
  314. this.closeAllInfoWindows();
  315. this.openInfoWindow(result.lng, result.lat, result.name, result.address);
  316. },
  317. clearSearchResults() {
  318. this.searchResults = [];
  319. this.searchKeyword = '';
  320. this.clearSearchMarkers();
  321. },
  322. // 地图交互方法
  323. enableMapSelection() {
  324. this.map.on('click', this.handleMapClick);
  325. },
  326. disableMapSelection() {
  327. this.map.off('click', this.handleMapClick);
  328. },
  329. setMapCenter(lng, lat) {
  330. this.longitude = parseFloat(lng).toFixed(6);
  331. this.latitude = parseFloat(lat).toFixed(6);
  332. this.clearMarkers();
  333. this.addMarker(lng, lat);
  334. this.map.panTo(new T.LngLat(lng, lat));
  335. this.notifyParent();
  336. },
  337. reverseGeocode(lng, lat) {
  338. if (!this.geocoder) return;
  339. this.address = '正在查询...';
  340. const point = new T.LngLat(lng, lat);
  341. this.geocoder.getLocation(point, (result) => {
  342. if (result.getStatus() === 0) {
  343. this.address = result.getAddress();
  344. this.addressname = this.address;
  345. } else {
  346. this.address = '查询失败';
  347. }
  348. this.updateLabelContent();
  349. this.notifyParent();
  350. });
  351. },
  352. handleMapClick(e) {
  353. if (!this.allowSelection) return;
  354. this.setMapCenter(e.lnglat.lng, e.lnglat.lat);
  355. this.reverseGeocode(e.lnglat.lng, e.lnglat.lat);
  356. // 点击地图时也显示信息窗口
  357. this.openInfoWindow(e.lnglat.lng, e.lnglat.lat, '选中位置', this.address);
  358. },
  359. notifyParent() {
  360. this.$emit('update-location', {
  361. longitude: this.longitude,
  362. latitude: this.latitude,
  363. address: this.address
  364. });
  365. },
  366. // 标记和信息窗口管理
  367. addMarker(lng, lat) {
  368. try {
  369. const point = new T.LngLat(lng, lat);
  370. const markerIcon = new T.Icon({
  371. iconUrl: 'data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="%231296db" stroke="%23ffffff" stroke-width="2"/></svg>',
  372. iconSize: new T.Point(24, 24),
  373. iconAnchor: new T.Point(12, 24)
  374. });
  375. const marker = new T.Marker(point, {
  376. icon: markerIcon
  377. });
  378. // 为标记添加点击事件,显示信息窗口
  379. marker.addEventListener('click', () => {
  380. this.openInfoWindow(lng, lat, '选中位置', this.address);
  381. });
  382. this.map.addOverLay(marker);
  383. this.markers.push(marker);
  384. this.addLabel(lng, lat, this.address);
  385. } catch (error) {
  386. console.error('添加标记失败:', error);
  387. }
  388. },
  389. addSearchMarker(lng, lat, name, address) {
  390. try {
  391. const point = new T.LngLat(lng, lat);
  392. const markerIcon = new T.Icon({
  393. iconUrl: 'data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="%23ff6b6b" stroke="%23ffffff" stroke-width="2"/></svg>',
  394. iconSize: new T.Point(20, 20),
  395. iconAnchor: new T.Point(10, 20)
  396. });
  397. const marker = new T.Marker(point, {
  398. icon: markerIcon
  399. });
  400. // 为搜索结果的标记添加点击事件
  401. marker.addEventListener('click', () => {
  402. this.openInfoWindow(lng, lat, name, address);
  403. });
  404. this.map.addOverLay(marker);
  405. this.markers.push(marker);
  406. } catch (error) {
  407. console.error('添加搜索标记失败:', error);
  408. }
  409. },
  410. openInfoWindow(lng, lat, title, content) {
  411. this.closeAllInfoWindows();
  412. const point = new T.LngLat(lng, lat);
  413. const infoContent = `
  414. <div style="min-width: 250px; padding: 10px;">
  415. <div style="font-weight: bold; margin-bottom: 8px; color: #1890ff;">${title}</div>
  416. <div style="margin-bottom: 5px; color: #666;">${content}</div>
  417. <div style="font-size: 12px; color: #999;">
  418. 经度: ${lng.toFixed(6)}<br/>
  419. 纬度: ${lat.toFixed(6)}
  420. </div>
  421. </div>
  422. `;
  423. const infoWindow = new T.InfoWindow(infoContent, {
  424. autoPan: true,
  425. width: 280,
  426. height: 'auto'
  427. });
  428. this.map.openInfoWindow(infoWindow, point);
  429. this.currentInfoWindow = infoWindow;
  430. },
  431. closeAllInfoWindows() {
  432. if (this.currentInfoWindow) {
  433. this.map.closeInfoWindow();
  434. this.currentInfoWindow = null;
  435. }
  436. },
  437. addLabel(lng, lat, content) {
  438. try {
  439. const point = new T.LngLat(lng, lat);
  440. const label = new T.Label({
  441. text: content || '未知地址',
  442. position: point,
  443. offset: new T.Point(0, -35)
  444. });
  445. label.setStyle({
  446. color: '#333',
  447. backgroundColor: 'rgba(255, 255, 255, 0.95)',
  448. border: '1px solid #ddd',
  449. borderRadius: '4px',
  450. padding: '6px 12px',
  451. fontSize: '12px',
  452. fontWeight: 'normal',
  453. boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
  454. maxWidth: '200px',
  455. textAlign: 'center',
  456. whiteSpace: 'nowrap',
  457. overflow: 'hidden',
  458. textOverflow: 'ellipsis'
  459. });
  460. this.map.addOverLay(label);
  461. this.labels.push(label);
  462. } catch (error) {
  463. console.error('添加标签失败:', error);
  464. }
  465. },
  466. updateLabelContent() {
  467. if (this.labels.length > 0) {
  468. const lastLabel = this.labels[this.labels.length - 1];
  469. lastLabel.setText(this.address);
  470. }
  471. },
  472. clearMarkers() {
  473. this.markers.forEach(marker => {
  474. try {
  475. this.map.removeOverLay(marker);
  476. } catch (error) {
  477. console.error('移除标记失败:', error);
  478. }
  479. });
  480. this.markers = [];
  481. this.labels.forEach(label => {
  482. try {
  483. this.map.removeOverLay(label);
  484. } catch (error) {
  485. console.error('移除标签失败:', error);
  486. }
  487. });
  488. this.labels = [];
  489. this.closeAllInfoWindows();
  490. },
  491. clearSearchMarkers() {
  492. // 清除搜索相关的标记,保留主标记
  493. const mainMarkers = this.markers.filter(marker => {
  494. // 这里可以根据标记的特定属性来区分主标记和搜索标记
  495. // 简化处理:保留最后一个标记(通常是用户选择的主标记)
  496. return this.markers.indexOf(marker) === this.markers.length - 1;
  497. });
  498. this.markers = mainMarkers;
  499. },
  500. cleanupMap() {
  501. if (this.map) {
  502. this.clearMarkers();
  503. this.map.destroy();
  504. this.map = null;
  505. }
  506. }
  507. }
  508. };
  509. </script>
  510. <style scoped lang="scss">
  511. .top {
  512. width: 100%;
  513. height: var(--status-bar-height);
  514. background: linear-gradient(87deg, #FFFFFF 0%, #A3CDFF 100%), linear-gradient(360deg, #FFFFFF 0%, rgba(255, 255, 255, 0) 100%);
  515. }
  516. .head {
  517. width: 100%;
  518. background: #ffffff;
  519. .handle {
  520. height: 112rpx;
  521. display: flex;
  522. justify-content: space-between;
  523. align-items: center;
  524. padding: 0px 24rpx;
  525. position: relative;
  526. .head_left {
  527. font-size: 40rpx;
  528. }
  529. .center {
  530. position: absolute;
  531. left: 50%;
  532. top: 50%;
  533. transform: translate(-50%, -50%);
  534. font-size: 30rpx;
  535. font-weight: 600;
  536. }
  537. .head_right {
  538. display: flex;
  539. justify-content: space-between;
  540. align-items: center;
  541. span {
  542. width: 40rpx;
  543. height: 40rpx;
  544. display: flex;
  545. font-size: 28rpx;
  546. justify-content: center;
  547. align-items: center;
  548. }
  549. }
  550. }
  551. }
  552. .container {
  553. width: 100%;
  554. height: 100vh;
  555. display: flex;
  556. flex-direction: column;
  557. position: relative;
  558. z-index: 0;
  559. }
  560. .map-container {
  561. width: 100%;
  562. height: 600rpx;
  563. border: 1px solid #eee;
  564. background-color: #f8f8f8;
  565. }
  566. /* 搜索面板样式 */
  567. .search-panel {
  568. margin-top: 20rpx;
  569. padding: 20rpx;
  570. background-color: #f8f9fa;
  571. border-radius: 8rpx;
  572. flex: 1;
  573. display: flex;
  574. flex-direction: column;
  575. }
  576. .search-input-group {
  577. display: flex;
  578. gap: 10rpx;
  579. margin-bottom: 20rpx;
  580. }
  581. .search-input {
  582. flex: 1;
  583. height: 70rpx;
  584. padding: 0 20rpx;
  585. border: 1px solid #ddd;
  586. border-radius: 8rpx;
  587. background-color: white;
  588. }
  589. .search-btn {
  590. height: 70rpx;
  591. background-color: #1890ff;
  592. color: white;
  593. border-radius: 8rpx;
  594. display: flex;
  595. align-items: center;
  596. justify-content: center;
  597. }
  598. .search-results {
  599. border: 1px solid #e8e8e8;
  600. border-radius: 8rpx;
  601. flex: 1;
  602. background-color: white;
  603. }
  604. .result-header {
  605. display: flex;
  606. justify-content: space-between;
  607. align-items: center;
  608. padding: 20rpx;
  609. border-bottom: 1px solid #f0f0f0;
  610. font-weight: bold;
  611. }
  612. .clear-results {
  613. color: #1890ff;
  614. font-size: 12px;
  615. }
  616. .results-list {
  617. max-height: 700rpx;
  618. }
  619. .result-item {
  620. padding: 20rpx;
  621. border-bottom: 1px solid #f5f5f5;
  622. cursor: pointer;
  623. }
  624. .result-item:last-child {
  625. border-bottom: none;
  626. }
  627. .result-item:hover {
  628. background-color: #f8f9fa;
  629. }
  630. .result-name {
  631. font-weight: bold;
  632. margin-bottom: 5rpx;
  633. color: #333;
  634. }
  635. .result-address {
  636. font-size: 12px;
  637. color: #666;
  638. margin-bottom: 5rpx;
  639. }
  640. .result-coord {
  641. font-size: 11px;
  642. color: #999;
  643. }
  644. ::v-deep .tdt-bottom {
  645. z-index: 0;
  646. }
  647. ::v-deep .tdt-top {
  648. z-index: 0;
  649. }
  650. ::v-deep .tdt-bottom,
  651. .tdt-control {
  652. z-index: 0;
  653. }
  654. </style>