123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613 |
- <template>
- <div class="view-position-container">
- <s-header :name="$t('device.equipLocation')" :noback="false"></s-header>
- <!-- 地图加载状态 -->
- <div class="loading-overlay" v-if="loading">
- <div class="spinner"></div>
- <!-- <p>{{ $t("common.loadingMap") }}</p> -->
- <p>地图加载中</p>
- </div>
- <!-- 错误提示 -->
- <div class="error-message" v-if="error">
- <p>{{ error }}</p>
- <button class="retry-btn" @click="initMap">
- <!-- {{ $t("common.retry") }} -->
- 重试
- </button>
- </div>
- <!-- 地图容器 -->
- <div class="map-wrapper">
- <div ref="mapContainer" class="map"></div>
- </div>
- <!-- 位置信息卡片 -->
- <div class="location-info-card">
- <div class="card-header">
- <div class="device-name">{{ deviceName || position.fullName }}</div>
- </div>
- <div class="info-item">
- <!-- <span class="info-label">{{ $t("device.location") }}:</span> -->
- <span class="info-label">位置:</span>
- <span class="info-value">{{
- position.address || position.fullName
- }}</span>
- </div>
- <div class="info-item">
- <!-- <span class="info-label">{{ $t("device.coordinates") }}:</span> -->
- <span class="info-label">经纬度:</span>
- <span class="info-value"
- >{{ position.longitude }}, {{ position.latitude }}</span
- >
- </div>
- <div class="info-item">
- <span class="info-label">{{ $t("device.status") }}:</span>
- <span v-if="position.status === '1'" class="status-badge online">在线</span>
- <span v-else class="status-badge offline">离线</span>
- </div>
- </div>
- <!-- 地图操作按钮 -->
- <div class="map-controls">
- <!-- 定位按钮 -->
- <button
- class="map-btn locate-btn"
- @click="locateUser"
- title="定位到我的位置"
- >
- <svg width="20" height="20" viewBox="0 0 24 24">
- <path
- 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"
- fill="white"
- />
- </svg>
- </button>
- <!-- 放大按钮 -->
- <button class="map-btn zoom-in-btn" @click="zoomIn" title="放大">
- <svg width="20" height="20" viewBox="0 0 24 24">
- <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="white" />
- </svg>
- </button>
- <!-- 缩小按钮 -->
- <button class="map-btn zoom-out-btn" @click="zoomOut" title="缩小">
- <svg width="20" height="20" viewBox="0 0 24 24">
- <path d="M19 13H5v-2h14v2z" fill="white" />
- </svg>
- </button>
- </div>
- </div>
- </template>
- <script>
- import { defineComponent, ref, reactive, onMounted, toRefs } from "vue";
- import { Loader } from "@googlemaps/js-api-loader";
- import { useRoute } from "vue-router";
- import sHeader from "@/components/SimpleHeader";
- export default defineComponent({
- name: "GoogleMapView",
- components: {
- sHeader,
- },
- setup() {
- const route = useRoute();
- const position = reactive({
- latitude: null,
- longitude: null,
- fullName: "",
- address: "",
- status: route.query.status || 0, // 默认为离线状态
- ...route.query,
- });
- const state = reactive({
- loading: true,
- error: null,
- deviceName: route.query.name,
- map: null,
- marker: null,
- infoWindow: null,
- });
- const mapContainer = ref(null);
- // 设备定位图标 (Base64 编码)
- const deviceIconUrl =
- "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCAzMCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1IDBDNi43MTUgMCAwIDYuNzE1IDAgMTVjMCAxMCAxNSAyNSAxNSAyNXMxNS0xNSAxNS0yNWMwLTguMjg1LTYuNzE1LTE1LTE1LTE1eiIgZmlsbD0iIzQyODVGQiIvPgo8Y2lyY2xlIGN4PSIxNSIgY3k9IjE1IiByPSI2IiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjAgMTVoLTNMMTAgMjh2LThoLTN2LThoMTV2OHoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPg==";
- // 初始化地图
- const initMap = async () => {
- try {
- state.loading = true;
- state.error = null;
- // 确保位置参数有效
- if (!position.latitude || !position.longitude) {
- throw new Error("缺少位置信息参数");
- }
- const loader = new Loader({
- apiKey: "AIzaSyAqxnF8_35P_vlxVGxKhfL2lxFup-qZF6g", // 替换为实际API密钥
- version: "weekly",
- libraries: ["places"],
- });
- // 使用 loader 代替直接使用 google 对象
- const google = await loader.load();
- // 确保地图容器已渲染
- if (!mapContainer.value) {
- throw new Error("地图容器未找到");
- }
- // 创建地图实例
- state.map = new google.maps.Map(mapContainer.value, {
- center: {
- lat: parseFloat(position.latitude),
- lng: parseFloat(position.longitude),
- },
- zoom: 15,
- zoomControl: false,
- mapTypeControl: false,
- streetViewControl: false,
- fullscreenControl: false,
- styles: getMapStyle(),
- });
- // 创建设备图标对象
- const deviceIcon = {
- url: deviceIconUrl,
- scaledSize: new google.maps.Size(30, 40),
- origin: new google.maps.Point(0, 0),
- anchor: new google.maps.Point(15, 40),
- };
- // 创建标记
- state.marker = new google.maps.Marker({
- position: {
- lat: parseFloat(position.latitude),
- lng: parseFloat(position.longitude),
- },
- map: state.map,
- title: position.fullName,
- icon: deviceIcon,
- animation: google.maps.Animation.DROP,
- });
- // 创建信息窗口
- state.infoWindow = new google.maps.InfoWindow({
- content: `
- <div class="map-infowindow">
- <div><strong>${position.fullName}</strong></div>
- <div style="margin-top:5px">设备: ${state.deviceName}</div>
- <div style="margin-top:8px;color:#666;font-size:0.8rem">
- <div>经度: ${position.longitude}</div>
- <div>纬度: ${position.latitude}</div>
- </div>
- </div>
- `,
- });
- // 添加点击事件
- state.marker.addListener("click", () => {
- state.infoWindow.open(state.map, state.marker);
- });
- // 添加加载完成事件
- google.maps.event.addListenerOnce(state.map, "tilesloaded", () => {
- state.loading = false;
- });
- } catch (err) {
- console.error("地图加载失败:", err);
- state.error = "地图加载失败: " + err.message;
- state.loading = false;
- }
- };
- // 获取用户位置
- const locateUser = () => {
- if (navigator.geolocation) {
- navigator.geolocation.getCurrentPosition(
- (position) => {
- const userLocation = {
- lat: position.coords.latitude,
- lng: position.coords.longitude,
- };
- if (state.map) {
- state.map.setCenter(userLocation);
- // 添加用户位置标记
- const userIcon = {
- path: window.google.maps.SymbolPath.CIRCLE,
- fillColor: "#34a853",
- fillOpacity: 1,
- strokeColor: "#ffffff",
- strokeWeight: 2,
- scale: 8,
- };
- new window.google.maps.Marker({
- position: userLocation,
- map: state.map,
- icon: userIcon,
- title: "您的位置",
- });
- }
- },
- (error) => {
- console.error("获取位置失败:", error);
- state.error = "无法获取您的位置: " + error.message;
- },
- {
- enableHighAccuracy: true,
- timeout: 5000,
- maximumAge: 0,
- }
- );
- } else {
- state.error = "您的浏览器不支持地理位置功能";
- }
- };
- // 地图缩放控制
- const zoomIn = () => {
- if (state.map) {
- const currentZoom = state.map.getZoom();
- state.map.setZoom(currentZoom + 1);
- }
- };
- const zoomOut = () => {
- if (state.map) {
- const currentZoom = state.map.getZoom();
- state.map.setZoom(currentZoom - 1);
- }
- };
- // 地图样式配置
- const getMapStyle = () => {
- return [
- {
- featureType: "administrative",
- elementType: "labels.text.fill",
- stylers: [{ color: "#444444" }],
- },
- {
- featureType: "landscape",
- elementType: "all",
- stylers: [{ color: "#f2f2f2" }],
- },
- {
- featureType: "poi",
- elementType: "all",
- stylers: [{ visibility: "off" }],
- },
- {
- featureType: "road",
- elementType: "all",
- stylers: [{ saturation: -100 }, { lightness: 45 }],
- },
- {
- featureType: "road.highway",
- elementType: "all",
- stylers: [{ visibility: "simplified" }],
- },
- {
- featureType: "road.arterial",
- elementType: "labels.icon",
- stylers: [{ visibility: "off" }],
- },
- {
- featureType: "transit",
- elementType: "all",
- stylers: [{ visibility: "off" }],
- },
- {
- featureType: "water",
- elementType: "all",
- stylers: [{ color: "#c4e6f3" }, { visibility: "on" }],
- },
- ];
- };
- // 组件挂载时初始化地图
- onMounted(() => {
- // 确保有位置数据
- if (position.latitude && position.longitude) {
- initMap();
- } else {
- state.error = "设备位置信息缺失";
- state.loading = false;
- }
- });
- return {
- ...toRefs(state),
- position,
- mapContainer,
- initMap,
- locateUser,
- zoomIn,
- zoomOut,
- };
- },
- });
- </script>
- <style lang="less" scoped>
- .view-position-container {
- position: relative;
- height: 100vh;
- display: flex;
- flex-direction: column;
- background-color: #f5f7fa;
- overflow: hidden;
- .map-wrapper {
- flex: 1;
- position: relative;
- .map {
- width: 100%;
- height: 100%;
- }
- }
- .loading-overlay {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(255, 255, 255, 0.9);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- z-index: 100;
- .spinner {
- width: 50px;
- height: 50px;
- border: 4px solid rgba(66, 133, 244, 0.2);
- border-top: 4px solid #4285f4;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin-bottom: 15px;
- }
- p {
- color: #4285f4;
- font-weight: 500;
- }
- }
- .error-message {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- background-color: #ffebee;
- color: #c62828;
- padding: 20px;
- border-radius: 8px;
- text-align: center;
- z-index: 100;
- max-width: 80%;
- p {
- margin-bottom: 15px;
- }
- .retry-btn {
- background: #4285f4;
- color: white;
- border: none;
- padding: 8px 20px;
- border-radius: 4px;
- font-weight: 500;
- cursor: pointer;
- transition: background 0.3s;
- &:hover {
- background: #3367d6;
- }
- }
- }
- .location-info-card {
- position: absolute;
- bottom: 20px;
- left: 20px;
- right: 20px;
- background: rgba(255, 255, 255, 0.95);
- border-radius: 12px;
- padding: 15px;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
- backdrop-filter: blur(10px);
- z-index: 50;
- max-width: 500px;
- .card-header {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
- padding-bottom: 10px;
- border-bottom: 1px solid #eee;
- }
- .device-name {
- font-size: 1.1rem;
- font-weight: 600;
- color: #222;
- }
- .info-item {
- display: flex;
- align-items: center;
- margin-bottom: 8px;
- font-size: 0.9rem;
- .info-label {
- color: #666;
- width: 70px;
- flex-shrink: 0;
- }
- .info-value {
- color: #333;
- flex: 1;
- font-weight: 500;
- }
- .status-badge {
- padding: 2px 8px;
- border-radius: 10px;
- font-size: 14px;
- font-weight: 500;
- &.online {
- background-color: rgba(52, 168, 83, 0.15);
- color: #1d7e40;
- }
- &.offline {
- background-color: rgba(71, 55, 55, 0.15);
- color: #807575;
- }
- }
- }
- }
- .map-controls {
- position: absolute;
- top: 50px;
- right: 15px;
- display: flex;
- flex-direction: column;
- z-index: 50;
- gap: 10px;
- .zoom-controls {
- display: flex;
- flex-direction: column;
- gap: 10px;
- .map-btn {
- margin: 0;
- }
- }
- .map-btn {
- width: 42px;
- height: 42px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- font-size: 1.1rem;
- transition: all 0.3s ease;
- border: none;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
- &:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
- }
- i {
- color: white; // 确保图标是白色
- font-size: 16px;
- }
- // 定位按钮样式 - 蓝色
- &.locate-btn {
- background: #4285f4;
- &:hover {
- background: #3367d6;
- }
- }
- // 放大按钮样式 - 绿色
- &.zoom-in-btn {
- background: #34a853;
- &:hover {
- background: #2c9048;
- }
- }
- // 缩小按钮样式 - 红色
- &.zoom-out-btn {
- background: #ea4335;
- &:hover {
- background: #d33426;
- }
- }
- }
- }
- @media (max-width: 768px) {
- .location-info-card {
- left: 10px;
- right: 10px;
- width: 35%;
- .device-name {
- font-size: 15px;
- }
- .info-item {
- font-size: 0.85rem;
- }
- }
- .map-controls {
- top: 55px;
- right: 10px;
- .map-btn {
- width: 38px;
- height: 38px;
- font-size: 1rem;
- }
- }
- }
- }
- @keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
- }
- :deep(.gm-style .gm-style-iw) {
- padding: 12px !important;
- max-width: 250px !important;
- border-radius: 12px !important;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
- }
- :deep(.gm-style .gm-style-iw-c) {
- border-radius: 12px !important;
- }
- :deep(.map-infowindow) {
- font-size: 0.9rem;
- color: #333;
- strong {
- color: #4285f4;
- }
- }
- </style>
|