index1.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. <template>
  2. <div class="view-position-container">
  3. <s-header :name="$t('device.equipLocation')" :noback="false"></s-header>
  4. <!-- 地图加载状态 -->
  5. <div class="loading-overlay" v-if="loading">
  6. <div class="spinner"></div>
  7. <!-- <p>{{ $t("common.loadingMap") }}</p> -->
  8. <p>地图加载中</p>
  9. </div>
  10. <!-- 错误提示 -->
  11. <div class="error-message" v-if="error">
  12. <p>{{ error }}</p>
  13. <button class="retry-btn" @click="initMap">
  14. <!-- {{ $t("common.retry") }} -->
  15. 重试
  16. </button>
  17. </div>
  18. <!-- 地图容器 -->
  19. <div class="map-wrapper">
  20. <div ref="mapContainer" class="map"></div>
  21. </div>
  22. <!-- 位置信息卡片 -->
  23. <div class="location-info-card">
  24. <div class="card-header">
  25. <div class="device-name">{{ deviceName || position.fullName }}</div>
  26. </div>
  27. <div class="info-item">
  28. <!-- <span class="info-label">{{ $t("device.location") }}:</span> -->
  29. <span class="info-label">位置:</span>
  30. <span class="info-value">{{
  31. position.address || position.fullName
  32. }}</span>
  33. </div>
  34. <div class="info-item">
  35. <!-- <span class="info-label">{{ $t("device.coordinates") }}:</span> -->
  36. <span class="info-label">经纬度:</span>
  37. <span class="info-value"
  38. >{{ position.longitude }}, {{ position.latitude }}</span
  39. >
  40. </div>
  41. <div class="info-item">
  42. <span class="info-label">{{ $t("device.status") }}:</span>
  43. <span v-if="position.status === '1'" class="status-badge online">在线</span>
  44. <span v-else class="status-badge offline">离线</span>
  45. </div>
  46. </div>
  47. <!-- 地图操作按钮 -->
  48. <div class="map-controls">
  49. <!-- 定位按钮 -->
  50. <button
  51. class="map-btn locate-btn"
  52. @click="locateUser"
  53. title="定位到我的位置"
  54. >
  55. <svg width="20" height="20" viewBox="0 0 24 24">
  56. <path
  57. d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8.94 3c-.46-4.17-3.77-7.48-7.94-7.94V1h-2v2.06C6.83 3.52 3.52 6.83 3.06 11H1v2h2.06c.46 4.17 3.77 7.48 7.94 7.94V23h2v-2.06c4.17-.46 7.48-3.77 7.94-7.94H23v-2h-2.06zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"
  58. fill="white"
  59. />
  60. </svg>
  61. </button>
  62. <!-- 放大按钮 -->
  63. <button class="map-btn zoom-in-btn" @click="zoomIn" title="放大">
  64. <svg width="20" height="20" viewBox="0 0 24 24">
  65. <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="white" />
  66. </svg>
  67. </button>
  68. <!-- 缩小按钮 -->
  69. <button class="map-btn zoom-out-btn" @click="zoomOut" title="缩小">
  70. <svg width="20" height="20" viewBox="0 0 24 24">
  71. <path d="M19 13H5v-2h14v2z" fill="white" />
  72. </svg>
  73. </button>
  74. </div>
  75. </div>
  76. </template>
  77. <script>
  78. import { defineComponent, ref, reactive, onMounted, toRefs } from "vue";
  79. import { Loader } from "@googlemaps/js-api-loader";
  80. import { useRoute } from "vue-router";
  81. import sHeader from "@/components/SimpleHeader";
  82. export default defineComponent({
  83. name: "GoogleMapView",
  84. components: {
  85. sHeader,
  86. },
  87. setup() {
  88. const route = useRoute();
  89. const position = reactive({
  90. latitude: null,
  91. longitude: null,
  92. fullName: "",
  93. address: "",
  94. status: route.query.status || 0, // 默认为离线状态
  95. ...route.query,
  96. });
  97. const state = reactive({
  98. loading: true,
  99. error: null,
  100. deviceName: route.query.name,
  101. map: null,
  102. marker: null,
  103. infoWindow: null,
  104. });
  105. const mapContainer = ref(null);
  106. // 设备定位图标 (Base64 编码)
  107. const deviceIconUrl =
  108. "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCAzMCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1IDBDNi43MTUgMCAwIDYuNzE1IDAgMTVjMCAxMCAxNSAyNSAxNSAyNXMxNS0xNSAxNS0yNWMwLTguMjg1LTYuNzE1LTE1LTE1LTE1eiIgZmlsbD0iIzQyODVGQiIvPgo8Y2lyY2xlIGN4PSIxNSIgY3k9IjE1IiByPSI2IiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjAgMTVoLTNMMTAgMjh2LThoLTN2LThoMTV2OHoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPg==";
  109. // 初始化地图
  110. const initMap = async () => {
  111. try {
  112. state.loading = true;
  113. state.error = null;
  114. // 确保位置参数有效
  115. if (!position.latitude || !position.longitude) {
  116. throw new Error("缺少位置信息参数");
  117. }
  118. const loader = new Loader({
  119. apiKey: "AIzaSyAqxnF8_35P_vlxVGxKhfL2lxFup-qZF6g", // 替换为实际API密钥
  120. version: "weekly",
  121. libraries: ["places"],
  122. });
  123. // 使用 loader 代替直接使用 google 对象
  124. const google = await loader.load();
  125. // 确保地图容器已渲染
  126. if (!mapContainer.value) {
  127. throw new Error("地图容器未找到");
  128. }
  129. // 创建地图实例
  130. state.map = new google.maps.Map(mapContainer.value, {
  131. center: {
  132. lat: parseFloat(position.latitude),
  133. lng: parseFloat(position.longitude),
  134. },
  135. zoom: 15,
  136. zoomControl: false,
  137. mapTypeControl: false,
  138. streetViewControl: false,
  139. fullscreenControl: false,
  140. styles: getMapStyle(),
  141. });
  142. // 创建设备图标对象
  143. const deviceIcon = {
  144. url: deviceIconUrl,
  145. scaledSize: new google.maps.Size(30, 40),
  146. origin: new google.maps.Point(0, 0),
  147. anchor: new google.maps.Point(15, 40),
  148. };
  149. // 创建标记
  150. state.marker = new google.maps.Marker({
  151. position: {
  152. lat: parseFloat(position.latitude),
  153. lng: parseFloat(position.longitude),
  154. },
  155. map: state.map,
  156. title: position.fullName,
  157. icon: deviceIcon,
  158. animation: google.maps.Animation.DROP,
  159. });
  160. // 创建信息窗口
  161. state.infoWindow = new google.maps.InfoWindow({
  162. content: `
  163. <div class="map-infowindow">
  164. <div><strong>${position.fullName}</strong></div>
  165. <div style="margin-top:5px">设备: ${state.deviceName}</div>
  166. <div style="margin-top:8px;color:#666;font-size:0.8rem">
  167. <div>经度: ${position.longitude}</div>
  168. <div>纬度: ${position.latitude}</div>
  169. </div>
  170. </div>
  171. `,
  172. });
  173. // 添加点击事件
  174. state.marker.addListener("click", () => {
  175. state.infoWindow.open(state.map, state.marker);
  176. });
  177. // 添加加载完成事件
  178. google.maps.event.addListenerOnce(state.map, "tilesloaded", () => {
  179. state.loading = false;
  180. });
  181. } catch (err) {
  182. console.error("地图加载失败:", err);
  183. state.error = "地图加载失败: " + err.message;
  184. state.loading = false;
  185. }
  186. };
  187. // 获取用户位置
  188. const locateUser = () => {
  189. if (navigator.geolocation) {
  190. navigator.geolocation.getCurrentPosition(
  191. (position) => {
  192. const userLocation = {
  193. lat: position.coords.latitude,
  194. lng: position.coords.longitude,
  195. };
  196. if (state.map) {
  197. state.map.setCenter(userLocation);
  198. // 添加用户位置标记
  199. const userIcon = {
  200. path: window.google.maps.SymbolPath.CIRCLE,
  201. fillColor: "#34a853",
  202. fillOpacity: 1,
  203. strokeColor: "#ffffff",
  204. strokeWeight: 2,
  205. scale: 8,
  206. };
  207. new window.google.maps.Marker({
  208. position: userLocation,
  209. map: state.map,
  210. icon: userIcon,
  211. title: "您的位置",
  212. });
  213. }
  214. },
  215. (error) => {
  216. console.error("获取位置失败:", error);
  217. state.error = "无法获取您的位置: " + error.message;
  218. },
  219. {
  220. enableHighAccuracy: true,
  221. timeout: 5000,
  222. maximumAge: 0,
  223. }
  224. );
  225. } else {
  226. state.error = "您的浏览器不支持地理位置功能";
  227. }
  228. };
  229. // 地图缩放控制
  230. const zoomIn = () => {
  231. if (state.map) {
  232. const currentZoom = state.map.getZoom();
  233. state.map.setZoom(currentZoom + 1);
  234. }
  235. };
  236. const zoomOut = () => {
  237. if (state.map) {
  238. const currentZoom = state.map.getZoom();
  239. state.map.setZoom(currentZoom - 1);
  240. }
  241. };
  242. // 地图样式配置
  243. const getMapStyle = () => {
  244. return [
  245. {
  246. featureType: "administrative",
  247. elementType: "labels.text.fill",
  248. stylers: [{ color: "#444444" }],
  249. },
  250. {
  251. featureType: "landscape",
  252. elementType: "all",
  253. stylers: [{ color: "#f2f2f2" }],
  254. },
  255. {
  256. featureType: "poi",
  257. elementType: "all",
  258. stylers: [{ visibility: "off" }],
  259. },
  260. {
  261. featureType: "road",
  262. elementType: "all",
  263. stylers: [{ saturation: -100 }, { lightness: 45 }],
  264. },
  265. {
  266. featureType: "road.highway",
  267. elementType: "all",
  268. stylers: [{ visibility: "simplified" }],
  269. },
  270. {
  271. featureType: "road.arterial",
  272. elementType: "labels.icon",
  273. stylers: [{ visibility: "off" }],
  274. },
  275. {
  276. featureType: "transit",
  277. elementType: "all",
  278. stylers: [{ visibility: "off" }],
  279. },
  280. {
  281. featureType: "water",
  282. elementType: "all",
  283. stylers: [{ color: "#c4e6f3" }, { visibility: "on" }],
  284. },
  285. ];
  286. };
  287. // 组件挂载时初始化地图
  288. onMounted(() => {
  289. // 确保有位置数据
  290. if (position.latitude && position.longitude) {
  291. initMap();
  292. } else {
  293. state.error = "设备位置信息缺失";
  294. state.loading = false;
  295. }
  296. });
  297. return {
  298. ...toRefs(state),
  299. position,
  300. mapContainer,
  301. initMap,
  302. locateUser,
  303. zoomIn,
  304. zoomOut,
  305. };
  306. },
  307. });
  308. </script>
  309. <style lang="less" scoped>
  310. .view-position-container {
  311. position: relative;
  312. height: 100vh;
  313. display: flex;
  314. flex-direction: column;
  315. background-color: #f5f7fa;
  316. overflow: hidden;
  317. .map-wrapper {
  318. flex: 1;
  319. position: relative;
  320. .map {
  321. width: 100%;
  322. height: 100%;
  323. }
  324. }
  325. .loading-overlay {
  326. position: absolute;
  327. top: 0;
  328. left: 0;
  329. right: 0;
  330. bottom: 0;
  331. background: rgba(255, 255, 255, 0.9);
  332. display: flex;
  333. flex-direction: column;
  334. align-items: center;
  335. justify-content: center;
  336. z-index: 100;
  337. .spinner {
  338. width: 50px;
  339. height: 50px;
  340. border: 4px solid rgba(66, 133, 244, 0.2);
  341. border-top: 4px solid #4285f4;
  342. border-radius: 50%;
  343. animation: spin 1s linear infinite;
  344. margin-bottom: 15px;
  345. }
  346. p {
  347. color: #4285f4;
  348. font-weight: 500;
  349. }
  350. }
  351. .error-message {
  352. position: absolute;
  353. top: 50%;
  354. left: 50%;
  355. transform: translate(-50%, -50%);
  356. background-color: #ffebee;
  357. color: #c62828;
  358. padding: 20px;
  359. border-radius: 8px;
  360. text-align: center;
  361. z-index: 100;
  362. max-width: 80%;
  363. p {
  364. margin-bottom: 15px;
  365. }
  366. .retry-btn {
  367. background: #4285f4;
  368. color: white;
  369. border: none;
  370. padding: 8px 20px;
  371. border-radius: 4px;
  372. font-weight: 500;
  373. cursor: pointer;
  374. transition: background 0.3s;
  375. &:hover {
  376. background: #3367d6;
  377. }
  378. }
  379. }
  380. .location-info-card {
  381. position: absolute;
  382. bottom: 20px;
  383. left: 20px;
  384. right: 20px;
  385. background: rgba(255, 255, 255, 0.95);
  386. border-radius: 12px;
  387. padding: 15px;
  388. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  389. backdrop-filter: blur(10px);
  390. z-index: 50;
  391. max-width: 500px;
  392. .card-header {
  393. display: flex;
  394. align-items: center;
  395. margin-bottom: 12px;
  396. padding-bottom: 10px;
  397. border-bottom: 1px solid #eee;
  398. }
  399. .device-name {
  400. font-size: 1.1rem;
  401. font-weight: 600;
  402. color: #222;
  403. }
  404. .info-item {
  405. display: flex;
  406. align-items: center;
  407. margin-bottom: 8px;
  408. font-size: 0.9rem;
  409. .info-label {
  410. color: #666;
  411. width: 70px;
  412. flex-shrink: 0;
  413. }
  414. .info-value {
  415. color: #333;
  416. flex: 1;
  417. font-weight: 500;
  418. }
  419. .status-badge {
  420. padding: 2px 8px;
  421. border-radius: 10px;
  422. font-size: 14px;
  423. font-weight: 500;
  424. &.online {
  425. background-color: rgba(52, 168, 83, 0.15);
  426. color: #1d7e40;
  427. }
  428. &.offline {
  429. background-color: rgba(71, 55, 55, 0.15);
  430. color: #807575;
  431. }
  432. }
  433. }
  434. }
  435. .map-controls {
  436. position: absolute;
  437. top: 50px;
  438. right: 15px;
  439. display: flex;
  440. flex-direction: column;
  441. z-index: 50;
  442. gap: 10px;
  443. .zoom-controls {
  444. display: flex;
  445. flex-direction: column;
  446. gap: 10px;
  447. .map-btn {
  448. margin: 0;
  449. }
  450. }
  451. .map-btn {
  452. width: 42px;
  453. height: 42px;
  454. border-radius: 50%;
  455. display: flex;
  456. align-items: center;
  457. justify-content: center;
  458. cursor: pointer;
  459. font-size: 1.1rem;
  460. transition: all 0.3s ease;
  461. border: none;
  462. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  463. &:hover {
  464. transform: translateY(-2px);
  465. box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
  466. }
  467. i {
  468. color: white; // 确保图标是白色
  469. font-size: 16px;
  470. }
  471. // 定位按钮样式 - 蓝色
  472. &.locate-btn {
  473. background: #4285f4;
  474. &:hover {
  475. background: #3367d6;
  476. }
  477. }
  478. // 放大按钮样式 - 绿色
  479. &.zoom-in-btn {
  480. background: #34a853;
  481. &:hover {
  482. background: #2c9048;
  483. }
  484. }
  485. // 缩小按钮样式 - 红色
  486. &.zoom-out-btn {
  487. background: #ea4335;
  488. &:hover {
  489. background: #d33426;
  490. }
  491. }
  492. }
  493. }
  494. @media (max-width: 768px) {
  495. .location-info-card {
  496. left: 10px;
  497. right: 10px;
  498. width: 35%;
  499. .device-name {
  500. font-size: 15px;
  501. }
  502. .info-item {
  503. font-size: 0.85rem;
  504. }
  505. }
  506. .map-controls {
  507. top: 55px;
  508. right: 10px;
  509. .map-btn {
  510. width: 38px;
  511. height: 38px;
  512. font-size: 1rem;
  513. }
  514. }
  515. }
  516. }
  517. @keyframes spin {
  518. 0% {
  519. transform: rotate(0deg);
  520. }
  521. 100% {
  522. transform: rotate(360deg);
  523. }
  524. }
  525. :deep(.gm-style .gm-style-iw) {
  526. padding: 12px !important;
  527. max-width: 250px !important;
  528. border-radius: 12px !important;
  529. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
  530. }
  531. :deep(.gm-style .gm-style-iw-c) {
  532. border-radius: 12px !important;
  533. }
  534. :deep(.map-infowindow) {
  535. font-size: 0.9rem;
  536. color: #333;
  537. strong {
  538. color: #4285f4;
  539. }
  540. }
  541. </style>