selectTdMap.vue 18 KB

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