selectTdMap.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  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>
  11. </view>
  12. <!-- 地图容器 -->
  13. <view id="mapContainer" class="map-container"></view>
  14. <!-- 搜索功能区域 -->
  15. <view class="search-panel" v-if="showSearch">
  16. <view class="search-input-group">
  17. <input class="search-input" v-model="searchKeyword" placeholder="输入关键词搜索地点" @confirm="handleSearch"
  18. @input="onSearchInput" />
  19. <button class="search-btn" @click="handleSearch">搜索</button>
  20. </view>
  21. <view class="search-results" v-if="searchResults.length > 0">
  22. <view class="result-header">
  23. <text>搜索结果 ({{ searchResults.length }} 个)</text>
  24. <text class="clear-results" @click="clearSearchResults">清除</text>
  25. </view>
  26. <scroll-view class="results-list" scroll-y>
  27. <view v-for="(result, index) in searchResults" :key="index"
  28. class="result-item"
  29. @click="selectSearchResult(result)"
  30. :class="{'selected': isSelected(result)}">
  31. <view class="left">
  32. <view class="result-name">{{ result.name }}</view>
  33. <view class="result-address">{{ result.address }}</view>
  34. </view>
  35. <view class="manage" @click.stop="confirmResult(result)">确定</view>
  36. </view>
  37. </scroll-view>
  38. </view>
  39. </view>
  40. </view>
  41. </template>
  42. <script>
  43. export default {
  44. props: {
  45. initialPosition: {
  46. type: Object,
  47. default: () => ({
  48. lnglat: null,
  49. address: ''
  50. })
  51. },
  52. allowSelection: {
  53. type: Boolean,
  54. default: true
  55. },
  56. showCoordinates: {
  57. type: Boolean,
  58. default: true
  59. },
  60. showSearch: {
  61. type: Boolean,
  62. default: true
  63. }
  64. },
  65. data() {
  66. return {
  67. tk: '9bcf63358817bfb878e8236de8bf1423',
  68. searchKeyword: '',
  69. searchResults: [],
  70. selectedResult: null,
  71. renderjsReady: false
  72. };
  73. },
  74. mounted() {
  75. console.log('组件mounted');
  76. // 等待renderjs初始化
  77. setTimeout(() => {
  78. this.waitForRenderjs();
  79. }, 1000);
  80. },
  81. methods: {
  82. // 等待renderjs就绪
  83. waitForRenderjs() {
  84. if (this.renderjsReady) return;
  85. const checkInterval = setInterval(() => {
  86. if (this.$refs && this.$refs.renderjsMap) {
  87. clearInterval(checkInterval);
  88. this.renderjsReady = true;
  89. console.log('renderjs就绪');
  90. // 如果有初始位置,设置地图中心
  91. if (this.initialPosition && this.isValidPosition(this.initialPosition)) {
  92. this.setMapCenterFromProps();
  93. }
  94. }
  95. }, 100);
  96. // 10秒后超时
  97. setTimeout(() => {
  98. clearInterval(checkInterval);
  99. console.warn('等待renderjs超时');
  100. }, 10000);
  101. },
  102. // 从props设置地图中心
  103. setMapCenterFromProps() {
  104. const mapData = {
  105. lng: this.initialPosition.lng ||
  106. (this.initialPosition.lnglat && this.initialPosition.lnglat.lng) ||
  107. (this.initialPosition.lnglat && this.initialPosition.lnglat.longitude),
  108. lat: this.initialPosition.lat ||
  109. (this.initialPosition.lnglat && this.initialPosition.lnglat.lat) ||
  110. (this.initialPosition.lnglat && this.initialPosition.lnglat.latitude),
  111. address: this.initialPosition.address || ''
  112. };
  113. this.callRenderjsMethod('updateMapWithMarker', mapData);
  114. },
  115. // 调用renderjs方法
  116. callRenderjsMethod(methodName, data) {
  117. if (!this.renderjsReady) {
  118. console.warn('renderjs未就绪,延迟执行');
  119. setTimeout(() => {
  120. this.callRenderjsMethod(methodName, data);
  121. }, 300);
  122. return;
  123. }
  124. // 直接调用renderjs模块的方法
  125. try {
  126. // 方法1: 通过模块名调用
  127. if (this.$refs.renderjsMap) {
  128. this.$refs.renderjsMap.callMethod(methodName, data);
  129. return;
  130. }
  131. // 方法2: 使用uni的callMethod
  132. const ownerInstance = this.$ownerInstance;
  133. if (ownerInstance && ownerInstance.callMethod) {
  134. ownerInstance.callMethod('callRenderjsMethod', {
  135. method: methodName,
  136. data: data
  137. });
  138. return;
  139. }
  140. console.error('无法调用renderjs方法');
  141. } catch (error) {
  142. console.error(`调用renderjs方法 ${methodName} 失败:`, error);
  143. // 重试
  144. setTimeout(() => {
  145. if (this.renderjsReady) {
  146. this.callRenderjsMethod(methodName, data);
  147. }
  148. }, 500);
  149. }
  150. },
  151. // 选择搜索结果
  152. selectSearchResult(result) {
  153. console.log('选择搜索结果:', result);
  154. // 设置为当前选中
  155. this.selectedResult = result;
  156. // 提取经纬度信息
  157. const coords = this.extractCoordinates(result);
  158. if (!coords) {
  159. uni.showToast({
  160. title: '无法获取位置坐标',
  161. icon: 'none'
  162. });
  163. console.error('无法解析坐标:', result);
  164. return;
  165. }
  166. const { lng, lat } = coords;
  167. // 创建地图数据
  168. const mapData = {
  169. lng: lng,
  170. lat: lat,
  171. address: result.address || result.name,
  172. name: result.name,
  173. fromSearch: true
  174. };
  175. // 更新地图并添加标记
  176. this.callRenderjsMethod('updateMapWithMarker', mapData);
  177. console.log('地图已定位到:', lng, lat, result.name);
  178. },
  179. // 确定并提交结果(选中+提交一步完成)
  180. confirmResult(result) {
  181. if (result) {
  182. this.selectedResult = result;
  183. this.submit(result);
  184. return;
  185. }
  186. },
  187. // 提交选中的结果
  188. submitSelectedResult() {
  189. this.confirmResult(this.selectedResult);
  190. },
  191. // 判断结果是否被选中
  192. isSelected(result) {
  193. if (!result || !this.selectedResult) return false;
  194. // 提取当前结果的坐标
  195. const resultCoords = this.extractCoordinates(result);
  196. const selectedCoords = this.extractCoordinates(this.selectedResult);
  197. if (!resultCoords || !selectedCoords) return false;
  198. // 使用更精确的比较方式
  199. return Math.abs(resultCoords.lng - selectedCoords.lng) < 0.000001 &&
  200. Math.abs(resultCoords.lat - selectedCoords.lat) < 0.000001;
  201. },
  202. // 提取坐标信息
  203. extractCoordinates(item) {
  204. let lng, lat;
  205. if (item.lonlat) {
  206. const coords = item.lonlat.split(',');
  207. if (coords.length >= 2) {
  208. lng = parseFloat(coords[0]);
  209. lat = parseFloat(coords[1]);
  210. }
  211. } else if (item.longitude && item.latitude) {
  212. lng = item.longitude;
  213. lat = item.latitude;
  214. } else if (item.lng && item.lat) {
  215. lng = item.lng;
  216. lat = item.lat;
  217. }
  218. // 验证坐标有效性
  219. if (lng !== undefined && lat !== undefined &&
  220. !isNaN(lng) && !isNaN(lat) &&
  221. lng >= -180 && lng <= 180 &&
  222. lat >= -90 && lat <= 90) {
  223. return { lng, lat };
  224. }
  225. console.warn('无法提取有效坐标:', item);
  226. return null;
  227. },
  228. // 提交选择
  229. submit(item) {
  230. const coords = this.extractCoordinates(item);
  231. if (!coords) {
  232. uni.showToast({
  233. title: '无法获取位置坐标',
  234. icon: 'none'
  235. });
  236. return;
  237. }
  238. const params = {
  239. lng: coords.lng,
  240. lat: coords.lat,
  241. address: item.name,
  242. name: item.name
  243. };
  244. console.log('提交参数:', params);
  245. uni.setStorageSync('address', params);
  246. uni.navigateBack({
  247. delta: 1
  248. });
  249. },
  250. back() {
  251. uni.navigateBack();
  252. },
  253. isValidPosition(position) {
  254. const lng = position.lng ||
  255. (position.lnglat && (position.lnglat.lng || position.lnglat.longitude));
  256. const lat = position.lat ||
  257. (position.lnglat && (position.lnglat.lat || position.lnglat.latitude));
  258. return lng && lat && !isNaN(parseFloat(lng)) && !isNaN(parseFloat(lat));
  259. },
  260. handleSearch() {
  261. if (!this.searchKeyword.trim()) {
  262. uni.showToast({
  263. title: '请输入搜索关键词',
  264. icon: 'none'
  265. });
  266. return;
  267. }
  268. this.searchByRestAPI(this.searchKeyword);
  269. },
  270. searchByRestAPI(keyword) {
  271. let cityCode = uni.getStorageSync('cityCode') || '140100';
  272. let postStr = {
  273. "keyWord": keyword,
  274. "queryType": 12,
  275. "start": 0,
  276. "count": 10,
  277. "specify": "156" + cityCode
  278. };
  279. uni.request({
  280. url: `https://api.tianditu.gov.cn/v2/search?postStr=${encodeURIComponent(JSON.stringify(postStr))}&type=query&tk=${this.tk}`,
  281. success: (res) => {
  282. if (res.statusCode == 200 && res.data) {
  283. this.searchResults = res.data.pois.map(item => ({
  284. ...item,
  285. location: item.lonlat
  286. }));
  287. } else {
  288. uni.showToast({
  289. title: '搜索失败,请重试',
  290. icon: 'none'
  291. });
  292. }
  293. },
  294. fail: (error) => {
  295. console.error('搜索请求失败:', error);
  296. uni.showToast({
  297. title: '网络错误,请重试',
  298. icon: 'none'
  299. });
  300. }
  301. });
  302. },
  303. onSearchInput(e) {
  304. this.searchKeyword = e.detail.value;
  305. },
  306. clearSearchResults() {
  307. this.searchResults = [];
  308. this.searchKeyword = '';
  309. this.selectedResult = null;
  310. }
  311. }
  312. };
  313. </script>
  314. <!-- 使用module属性定义renderjs模块 -->
  315. <script module="renderjsMap" lang="renderjs">
  316. let mapInstance = null;
  317. let markerInstance = null;
  318. let labelInstance = null;
  319. export default {
  320. data() {
  321. return {
  322. mapLoaded: false,
  323. lastDataHash: ''
  324. };
  325. },
  326. methods: {
  327. // Vue组件调用的方法
  328. updateMapWithMarker(mapData) {
  329. console.log('【RenderJS】updateMapWithMarker 被调用', mapData);
  330. if (!this.mapLoaded || !mapInstance) {
  331. console.warn('地图未加载,延迟执行');
  332. setTimeout(() => {
  333. this.updateMapWithMarker(mapData);
  334. }, 300);
  335. return;
  336. }
  337. // 验证数据
  338. const dataHash = JSON.stringify(mapData);
  339. if (dataHash === this.lastDataHash) {
  340. console.log('数据未变化,跳过更新');
  341. return;
  342. }
  343. this.lastDataHash = dataHash;
  344. // 验证坐标
  345. const lng = Number(mapData.lng);
  346. const lat = Number(mapData.lat);
  347. const address = mapData.address || mapData.name || '未知位置';
  348. if (isNaN(lng) || isNaN(lat)) {
  349. console.error('坐标无效:', mapData);
  350. return;
  351. }
  352. // 移动地图中心
  353. const targetLngLat = new window.T.LngLat(lng, lat);
  354. mapInstance.panTo(targetLngLat);
  355. // 设置缩放级别
  356. if (mapData.fromSearch) {
  357. mapInstance.setZoom(15);
  358. }
  359. // 创建标记
  360. this.createMarker(targetLngLat, address, mapData.name || '');
  361. },
  362. clearMarkers() {
  363. if (markerInstance && mapInstance) {
  364. mapInstance.removeOverLay(markerInstance);
  365. markerInstance = null;
  366. }
  367. if (labelInstance && mapInstance) {
  368. mapInstance.removeOverLay(labelInstance);
  369. labelInstance = null;
  370. }
  371. },
  372. createMarker(lngLat, address, name) {
  373. try {
  374. const T = window.T;
  375. // 清除旧标记
  376. this.clearMarkers();
  377. // 创建自定义图标
  378. const iconUrl = this.generateMarkerIcon('#ff0000');
  379. const icon = new T.Icon({
  380. iconUrl: iconUrl,
  381. iconSize: new T.Point(30, 30),
  382. iconAnchor: new T.Point(15, 30)
  383. });
  384. // 创建标记
  385. markerInstance = new T.Marker(lngLat, {
  386. icon: icon,
  387. title: name || address
  388. });
  389. // 创建标签
  390. labelInstance = new T.Label({
  391. text: address,
  392. position: lngLat,
  393. style: {
  394. color: '#333',
  395. backgroundColor: 'rgba(255, 255, 255, 0.9)',
  396. borderColor: '#ff0000',
  397. borderWidth: 1,
  398. borderRadius: 4,
  399. fontSize: '14px',
  400. fontWeight: '500',
  401. padding: '6px 10px'
  402. },
  403. offset: new T.Point(0, -40)
  404. });
  405. // 添加到地图
  406. mapInstance.addOverLay(markerInstance);
  407. mapInstance.addOverLay(labelInstance);
  408. // 标记点击事件
  409. markerInstance.addEventListener('click', () => {
  410. this.sendToVue('showLocationInfo', {
  411. lng: lngLat.getLng().toFixed(6),
  412. lat: lngLat.getLat().toFixed(6),
  413. address: address,
  414. name: name
  415. });
  416. });
  417. console.log('标记创建成功');
  418. } catch (error) {
  419. console.error('创建标记失败:', error);
  420. }
  421. },
  422. generateMarkerIcon(color) {
  423. const size = 30;
  424. const canvas = document.createElement('canvas');
  425. canvas.width = size;
  426. canvas.height = size;
  427. const ctx = canvas.getContext('2d');
  428. ctx.beginPath();
  429. ctx.arc(size/2, size/2, size/2 - 1, 0, Math.PI * 2);
  430. ctx.fillStyle = '#ffffff';
  431. ctx.fill();
  432. ctx.beginPath();
  433. ctx.arc(size/2, size/2, size/2 - 4, 0, Math.PI * 2);
  434. ctx.fillStyle = color;
  435. ctx.fill();
  436. return canvas.toDataURL();
  437. },
  438. // 初始化地图
  439. async initMap() {
  440. try {
  441. console.log('【RenderJS】开始初始化地图');
  442. // 加载API
  443. await this.loadTiandituAPI();
  444. // 获取容器
  445. const container = document.getElementById('mapContainer');
  446. if (!container) {
  447. throw new Error('地图容器不存在');
  448. }
  449. // 创建地图实例
  450. const T = window.T;
  451. mapInstance = new T.Map(container);
  452. // 设置默认中心点
  453. const center = new T.LngLat(112.55, 37.87);
  454. mapInstance.centerAndZoom(center, 12);
  455. // 添加图层
  456. const vecLayer = new T.TileLayer(
  457. `https://t{s}.tianditu.gov.cn/vec_c/wmts?tk=9bcf63358817bfb878e8236de8bf1423`, {
  458. subdomains: ['0', '1', '2', '3', '4', '5', '6', '7']
  459. }
  460. );
  461. const cvaLayer = new T.TileLayer(
  462. `https://t{s}.tianditu.gov.cn/cva_c/wmts?tk=9bcf63358817bfb878e8236de8bf1423`, {
  463. subdomains: ['0', '1', '2', '3', '4', '5', '6', '7']
  464. }
  465. );
  466. mapInstance.addLayer(vecLayer);
  467. mapInstance.addLayer(cvaLayer);
  468. mapInstance.addControl(new T.Control.Zoom());
  469. // 添加点击事件
  470. mapInstance.addEventListener('click', (e) => {
  471. this.handleMapClick(e);
  472. });
  473. this.mapLoaded = true;
  474. console.log('【RenderJS】地图初始化完成');
  475. } catch (error) {
  476. console.error('【RenderJS】地图初始化失败:', error);
  477. }
  478. },
  479. // 加载天地图API
  480. loadTiandituAPI() {
  481. return new Promise((resolve, reject) => {
  482. if (window.T && window.T.Map) {
  483. resolve();
  484. return;
  485. }
  486. const script = document.createElement('script');
  487. script.src = 'https://api.tianditu.gov.cn/api?v=4.0&tk=9bcf63358817bfb878e8236de8bf1423';
  488. script.onload = () => {
  489. // 等待T对象初始化
  490. const checkInterval = setInterval(() => {
  491. if (window.T && window.T.Map) {
  492. clearInterval(checkInterval);
  493. resolve();
  494. }
  495. }, 100);
  496. setTimeout(() => {
  497. clearInterval(checkInterval);
  498. reject(new Error('T对象初始化超时'));
  499. }, 5000);
  500. };
  501. script.onerror = reject;
  502. document.head.appendChild(script);
  503. });
  504. },
  505. handleMapClick(e) {
  506. if (!this.mapLoaded) return;
  507. const lng = e.lnglat.lng;
  508. const lat = e.lnglat.lat;
  509. const mapData = {
  510. lng: lng,
  511. lat: lat,
  512. address: '点击位置'
  513. };
  514. this.updateMapWithMarker(mapData);
  515. // 通知Vue组件
  516. this.sendToVue('onMapClick', {
  517. lng: lng,
  518. lat: lat
  519. });
  520. },
  521. // 向Vue发送消息
  522. sendToVue(method, data) {
  523. try {
  524. if (this.$ownerInstance && this.$ownerInstance.callMethod) {
  525. this.$ownerInstance.callMethod(method, data);
  526. }
  527. } catch (error) {
  528. console.error('发送消息到Vue失败:', error);
  529. }
  530. },
  531. cleanupMap() {
  532. this.clearMarkers();
  533. if (mapInstance) {
  534. try {
  535. mapInstance.destroy();
  536. } catch (e) {}
  537. mapInstance = null;
  538. }
  539. this.mapLoaded = false;
  540. }
  541. },
  542. mounted() {
  543. console.log('【RenderJS】模块挂载');
  544. // 延迟初始化
  545. setTimeout(() => {
  546. this.initMap();
  547. }, 500);
  548. },
  549. beforeDestroy() {
  550. console.log('【RenderJS】模块销毁');
  551. this.cleanupMap();
  552. }
  553. };
  554. </script>
  555. <style scoped lang="scss">
  556. .top {
  557. width: 100%;
  558. height: var(--status-bar-height);
  559. background: linear-gradient(87deg, #FFFFFF 0%, #A3CDFF 100%), linear-gradient(360deg, #FFFFFF 0%, rgba(255, 255, 255, 0) 100%);
  560. }
  561. .head {
  562. width: 100%;
  563. background: #ffffff;
  564. .handle {
  565. height: 112rpx;
  566. display: flex;
  567. justify-content: space-between;
  568. align-items: center;
  569. padding: 0px 24rpx;
  570. position: relative;
  571. .head_left {
  572. font-size: 40rpx;
  573. }
  574. .center {
  575. position: absolute;
  576. left: 50%;
  577. top: 50%;
  578. transform: translate(-50%, -50%);
  579. font-size: 30rpx;
  580. font-weight: 600;
  581. }
  582. }
  583. }
  584. .container {
  585. width: 100%;
  586. height: 100vh;
  587. display: flex;
  588. flex-direction: column;
  589. position: relative;
  590. }
  591. .map-container {
  592. width: 100%;
  593. height: 300px;
  594. background-color: #f8f8f8;
  595. position: relative;
  596. }
  597. .search-panel {
  598. margin-top: 20rpx;
  599. padding: 20rpx;
  600. background-color: #f8f9fa;
  601. border-radius: 8rpx;
  602. flex: 1;
  603. display: flex;
  604. flex-direction: column;
  605. }
  606. .search-input-group {
  607. display: flex;
  608. gap: 10rpx;
  609. margin-bottom: 20rpx;
  610. }
  611. .search-input {
  612. flex: 1;
  613. height: 70rpx;
  614. padding: 0 20rpx;
  615. border: 1px solid #ddd;
  616. border-radius: 8rpx;
  617. background-color: white;
  618. font-size: 28rpx;
  619. }
  620. .search-btn {
  621. height: 70rpx;
  622. padding: 0 20rpx;
  623. background-color: #1890ff;
  624. color: white;
  625. border-radius: 8rpx;
  626. display: flex;
  627. align-items: center;
  628. justify-content: center;
  629. font-size: 28rpx;
  630. border: none;
  631. }
  632. .search-results {
  633. border: 1px solid #e8e8e8;
  634. border-radius: 8rpx;
  635. flex: 1;
  636. background-color: white;
  637. }
  638. .result-header {
  639. display: flex;
  640. justify-content: space-between;
  641. align-items: center;
  642. padding: 20rpx;
  643. border-bottom: 1px solid #f0f0f0;
  644. font-weight: bold;
  645. font-size: 28rpx;
  646. }
  647. .clear-results {
  648. color: #1890ff;
  649. font-size: 24rpx;
  650. }
  651. .results-list {
  652. max-height: 700rpx;
  653. }
  654. .result-item {
  655. padding: 20rpx;
  656. border-bottom: 1px solid #f5f5f5;
  657. cursor: pointer;
  658. display: flex;
  659. justify-content: space-between;
  660. align-items: center;
  661. &.selected {
  662. background-color: #e6f7ff;
  663. border-left: 4rpx solid #1890ff;
  664. }
  665. .left {
  666. flex: 1;
  667. margin-right: 20rpx;
  668. }
  669. .manage {
  670. width: 100rpx;
  671. height: 50rpx;
  672. display: flex;
  673. align-items: center;
  674. justify-content: center;
  675. background-color: #1890ff;
  676. border-radius: 10rpx;
  677. color: #FFFFFF;
  678. font-size: 24rpx;
  679. }
  680. }
  681. .result-item.selected .manage {
  682. background-color: #52c41a;
  683. }
  684. .result-name {
  685. font-weight: bold;
  686. margin-bottom: 5rpx;
  687. color: #333;
  688. font-size: 28rpx;
  689. }
  690. .result-address {
  691. font-size: 24rpx;
  692. color: #666;
  693. }
  694. </style>