app2的地图查看功能

pull/121/head
luoyuehang 3 weeks ago
parent 4d2a7e66a0
commit 40950edd4a

@ -0,0 +1 @@
VITE_API_BASE_URL=http://localhost:8080

@ -0,0 +1,5 @@
# 开发环境
NODE_ENV=development
VITE_AMAP_KEY=7e03ef3b43a8cdbb62e3038fc727e035
VITE_API_BASE_URL=http://localhost:8080
VITE_DEBUG=true

@ -8,6 +8,7 @@
"name": "app2",
"version": "0.0.0",
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"pinia": "^3.0.4",
"vue": "^3.5.25",
"vue-router": "^4.6.3"
@ -31,6 +32,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@amap/amap-jsapi-loader": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/@amap/amap-jsapi-loader/-/amap-jsapi-loader-1.0.1.tgz",
"integrity": "sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw==",
"license": "MIT"
},
"node_modules/@asamuzakjp/css-color": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-4.1.0.tgz",

@ -13,6 +13,7 @@
"test:unit": "vitest"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"pinia": "^3.0.4",
"vue": "^3.5.25",
"vue-router": "^4.6.3"

@ -75,5 +75,49 @@ export const deviceService = {
console.error(`获取终端 ${terminalId} 实时数据失败:`, error)
throw error.response?.data || error.message
}
},
// 在 deviceService.js 中添加
async getTerminalLocations() {
try {
const response = await api.get('/api/student/terminal/location/all')
return response.data
} catch (error) {
console.error('获取设备位置失败:', error)
// 返回模拟数据作为后备
return {
code: 200,
message: '使用模拟数据',
data: [
{
terminalId: 'TERM001',
terminalName: '教学楼饮水机',
longitude: 112.938,
latitude: 28.165,
installLocation: '教学楼1F大厅',
deviceStatus: 'active',
isAvailable: true
},
{
terminalId: 'TERM002',
terminalName: '学生公寓饮水机',
longitude: 112.940,
latitude: 28.167,
installLocation: '天马学生公寓1F',
deviceStatus: 'active',
isAvailable: true
},
{
terminalId: 'TERM003',
terminalName: '图书馆饮水机',
longitude: 112.937,
latitude: 28.164,
installLocation: '图书馆2F',
deviceStatus: 'offline',
isAvailable: false
}
]
}
}
}
}

@ -0,0 +1,41 @@
// src/services/mapService.js
import api from './api'
export const mapService = {
// 获取所有设备位置
async getTerminalLocations() {
try {
const response = await api.get('/api/student/terminal/location/all')
return response.data
} catch (error) {
console.error('获取设备位置失败:', error)
throw error.response?.data || error.message
}
},
// 获取可用设备位置
async getAvailableTerminalLocations() {
try {
const response = await api.get('/api/student/terminal/location/available')
return response.data
} catch (error) {
console.error('获取可用设备位置失败:', error)
throw error.response?.data || error.message
}
},
// 根据坐标获取附近设备
async getNearbyTerminals(lng, lat, radius = 1000) {
try {
const response = await api.post('/api/student/terminal/nearby', {
longitude: lng,
latitude: lat,
radius: radius
})
return response.data
} catch (error) {
console.error('获取附近设备失败:', error)
throw error.response?.data || error.message
}
}
}

@ -7,127 +7,13 @@
<!-- 地图容器 -->
<div class="map-container" @click="hideAllPopups">
<!-- 地图模拟背景 -->
<div class="map-background">
<!-- 地图网格线 -->
<div class="map-grid">
<!-- 横向网格线 -->
<div class="grid-line horizontal" v-for="i in 8" :key="'h' + i"
:style="{ top: `${i * 12}%` }"></div>
<!-- 纵向网格线 -->
<div class="grid-line vertical" v-for="i in 6" :key="'v' + i"
:style="{ left: `${i * 16}%` }"></div>
</div>
<!-- 校园建筑标记 -->
<div class="campus-building building-1" :style="{ top: '30%', left: '30%' }">
<div class="building-icon">🏫</div>
<div class="building-name">教学楼</div>
</div>
<div class="campus-building building-2" :style="{ top: '60%', left: '60%' }">
<div class="building-icon">🏢</div>
<div class="building-name">学生公寓</div>
</div>
<div class="campus-building building-3" :style="{ top: '45%', left: '20%' }">
<div class="building-icon">📚</div>
<div class="building-name">图书馆</div>
</div>
<div class="campus-building building-4" :style="{ top: '70%', left: '40%' }">
<div class="building-icon">🍽</div>
<div class="building-name">食堂</div>
</div>
<div class="campus-building building-5" :style="{ top: '20%', left: '50%' }">
<div class="building-icon"></div>
<div class="building-name">体育馆</div>
</div>
<div class="campus-building building-6" :style="{ top: '40%', left: '70%' }">
<div class="building-icon">🔬</div>
<div class="building-name">实验室</div>
</div>
<!-- 道路 -->
<div class="road road-1" style="top: 40%; left: 20%; width: 60%; height: 6px;"></div>
<div class="road road-2" style="top: 60%; left: 30%; width: 6px; height: 40%;"></div>
<div class="road road-3" style="top: 30%; left: 50%; width: 6px; height: 40%;"></div>
<!-- 草地/绿化带 -->
<div class="green-area area-1" style="top: 25%; left: 10%; width: 15%; height: 20%;"></div>
<div class="green-area area-2" style="top: 65%; left: 75%; width: 15%; height: 20%;"></div>
<!-- 饮水机标记1教学楼 -->
<div
class="water-marker marker-1"
:class="{ active: selectedMarker === 'TERM001' }"
@click.stop="showMarkerInfo('TERM001')"
style="top: 35%; left: 45%;"
>
<div class="marker-content">
<!-- SVG水滴形状 -->
<svg class="marker-svg" viewBox="0 0 22 32">
<path
d="M22 11.2C22 19.2 15.4 24 11 32C6.6 24 0 19.2 0 11.2C0 7.04 2.2 0 11 0C19.8 0 22 7.04 22 11.2Z"
fill="#04d919"
stroke="white"
stroke-width="2"
/>
</svg>
<div class="marker-pulse"></div>
</div>
<div class="marker-label">TERM001</div>
</div>
<!-- 饮水机标记2学生公寓 -->
<div
class="water-marker marker-2"
:class="{ active: selectedMarker === 'TERM002' }"
@click.stop="showMarkerInfo('TERM002')"
style="top: 65%; left: 55%;"
>
<div class="marker-content">
<svg class="marker-svg" viewBox="0 0 22 32">
<path
d="M22 11.2C22 19.2 15.4 24 11 32C6.6 24 0 19.2 0 11.2C0 7.04 2.2 0 11 0C19.8 0 22 7.04 22 11.2Z"
fill="#04d919"
stroke="white"
stroke-width="2"
/>
</svg>
<div class="marker-pulse"></div>
</div>
<div class="marker-label">TERM002</div>
</div>
<!-- 饮水机标记3图书馆 -->
<div
class="water-marker marker-3"
:class="{ active: selectedMarker === 'TERM003' }"
@click.stop="showMarkerInfo('TERM003')"
style="top: 50%; left: 25%;"
>
<div class="marker-content">
<svg class="marker-svg" viewBox="0 0 22 32">
<path
d="M22 11.2C22 19.2 15.4 24 11 32C6.6 24 0 19.2 0 11.2C0 7.04 2.2 0 11 0C19.8 0 22 7.04 22 11.2Z"
fill="#aaaaaa"
stroke="white"
stroke-width="2"
/>
</svg>
<div class="marker-pulse"></div>
</div>
<div class="marker-label">TERM003</div>
</div>
<!-- 真实地图容器 -->
<div id="mapContainer" class="real-map-container" style="width: 100%; height: 100%;"></div>
<!-- 当前位置标记 -->
<div class="current-location-marker" style="top: 50%; left: 50%;">
<div class="location-icon">📍</div>
<div class="location-label">我的位置</div>
</div>
<!-- 如果地图加载失败显示模拟地图 -->
<div v-if="!mapLoaded && !isMapLoading" class="map-background">
<!-- 保持原有的模拟地图内容不变 -->
...
</div>
<!-- 地图控制按钮 -->
@ -456,10 +342,224 @@ import { ref, reactive, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { deviceService } from '@/services/deviceService'
import { useUserStore } from '@/stores/user'
import AMapLoader from '@amap/amap-jsapi-loader'
const router = useRouter()
const userStore = useUserStore()
const mapInstance = ref(null)
const mapLoaded = ref(false)
const isMapLoading = ref(false)
//
// HomePage.vue
const mapConfig = reactive({
center: [112.9375, 28.1655],
zoom: 16,
key: import.meta.env.VITE_AMAP_KEY || '', // 使
viewMode: '3D'
})
//
const fetchDeviceLocations = async () => {
try {
//
//
const response = await deviceService.getTerminalLocations()
if (response.code === 200 && response.data) {
return response.data
}
return []
} catch (error) {
console.error('获取设备位置失败:', error)
return []
}
}
//
const initMap = async () => {
if (isMapLoading.value) return
isMapLoading.value = true
try {
//
const AMap = await AMapLoader.load({
key: mapConfig.key,
version: '2.0',
plugins: [
'AMap.Marker',
'AMap.Geolocation',
'AMap.ToolBar',
'AMap.Scale',
'AMap.ControlBar'
]
})
//
mapInstance.value = new AMap.Map('mapContainer', {
zoom: mapConfig.zoom,
center: mapConfig.center,
viewMode: mapConfig.viewMode
})
//
mapInstance.value.addControl(new AMap.ToolBar())
mapInstance.value.addControl(new AMap.Scale())
mapInstance.value.addControl(new AMap.ControlBar())
//
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true,
timeout: 10000,
buttonOffset: new AMap.Pixel(10, 20),
zoomToAccuracy: true,
buttonPosition: 'RB'
})
mapInstance.value.addControl(geolocation)
//
geolocation.getCurrentPosition((status, result) => {
if (status === 'complete') {
console.log('定位成功:', result)
} else {
console.log('定位失败:', result)
}
})
//
const devices = await fetchDeviceLocations()
addDeviceMarkers(devices)
mapLoaded.value = true
console.log('地图初始化成功')
} catch (error) {
console.error('地图初始化失败:', error)
// 使
useMockMap()
} finally {
isMapLoading.value = false
}
}
//
const addDeviceMarkers = (devices) => {
if (!mapInstance.value || !devices.length) return
devices.forEach(device => {
//
const marker = new AMap.Marker({
position: [device.longitude, device.latitude],
title: device.terminalName,
content: createMarkerContent(device),
offset: new AMap.Pixel(-12, -32) //
})
//
marker.on('click', () => {
showMarkerInfo(device.terminalId)
})
marker.setMap(mapInstance.value)
//
if (!markers[device.terminalId]) {
markers[device.terminalId] = {}
}
markers[device.terminalId].marker = marker
})
}
//
const createMarkerContent = (device) => {
const isOnline = device.deviceStatus === 'active'
const color = isOnline ? '#04d919' : '#aaaaaa'
return `
<div class="custom-marker" style="cursor: pointer;">
<svg viewBox="0 0 22 32" width="24" height="32">
<path d="M22 11.2C22 19.2 15.4 24 11 32C6.6 24 0 19.2 0 11.2C0 7.04 2.2 0 11 0C19.8 0 22 7.04 22 11.2Z"
fill="${color}" stroke="white" stroke-width="2"/>
</svg>
<div style="position: absolute; top: -20px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.7); color: white; padding: 2px 6px; border-radius: 10px;
font-size: 10px; white-space: nowrap; display: none;">
${device.terminalId}
</div>
</div>
`
}
//
const useMockMap = () => {
console.log('使用模拟地图')
//
}
// onMounted
onMounted(() => {
//
Object.keys(markers).forEach(terminalId => {
fetchDeviceInfo(terminalId)
})
//
setTimeout(() => {
initMap()
}, 500)
})
//
const centerMap = () => {
if (mapInstance.value) {
// 使
const geolocation = new AMap.Geolocation()
geolocation.getCurrentPosition((status, result) => {
if (status === 'complete') {
mapInstance.value.setCenter([result.position.lng, result.position.lat])
}
})
}
}
const zoomIn = () => {
if (mapInstance.value) {
mapInstance.value.setZoom(mapInstance.value.getZoom() + 1)
}
}
const zoomOut = () => {
if (mapInstance.value) {
mapInstance.value.setZoom(mapInstance.value.getZoom() - 1)
}
}
//
const showMarkerInfo = async (markerId) => {
if (!mapInstance.value) {
// 使
await originalShowMarkerInfo(markerId)
return
}
currentMarker.value = markerId
selectedMarker.value = markerId
//
if (markers[markerId].status === 'loading') {
await fetchDeviceInfo(markerId)
}
//
const device = markers[markerId]
if (device && device.marker) {
mapInstance.value.setCenter(device.marker.getPosition())
mapInstance.value.setZoom(18)
}
showDevicePopup.value = true
}
//
const markerConfigs = {
TERM001: {
@ -547,6 +647,7 @@ const currentMarker = ref('')
const selectedMarker = ref('')
const isLoading = ref(false)
//
//
const fetchDeviceInfo = async (terminalId) => {
try {
@ -558,9 +659,10 @@ const fetchDeviceInfo = async (terminalId) => {
const marker = markers[terminalId]
if (marker) {
//
marker.status = data.status === 'active' ? 'online' : 'offline'
marker.statusText = data.status === 'active' ? '在线' : '离线'
// 'active' 'online'
const isActive = data.status === 'active' || data.status === 'online'
marker.status = isActive ? 'online' : 'offline'
marker.statusText = isActive ? '在线' : '离线'
marker.quality = data.waterQuality || '--'
//
@ -573,7 +675,7 @@ const fetchDeviceInfo = async (terminalId) => {
marker.recordTime = data.updateTime || '--'
//
updateMarkerColor(terminalId, data.status)
updateMarkerColor(terminalId, isActive ? 'active' : 'inactive')
}
} else {
console.error(`获取设备 ${terminalId} 信息失败:`, result.message)
@ -587,6 +689,7 @@ const fetchDeviceInfo = async (terminalId) => {
}
}
//
const fetchWaterQualityInfo = async (deviceId) => {
try {
@ -609,18 +712,7 @@ const updateMarkerColor = (terminalId, status) => {
}
}
//
const showMarkerInfo = async (markerId) => {
currentMarker.value = markerId
selectedMarker.value = markerId
//
if (markers[markerId].status === 'loading') {
await fetchDeviceInfo(markerId)
}
showDevicePopup.value = true
}
//
const hidePopup = () => {
@ -683,18 +775,7 @@ const openMap = (mapType) => {
// window.location.href = `amapuri://route/...`
}
//
const centerMap = () => {
alert('定位到当前位置')
}
const zoomIn = () => {
alert('放大地图')
}
const zoomOut = () => {
alert('缩小地图')
}
//
const goToPage = (page) => {
@ -1532,4 +1613,67 @@ onMounted(() => {
font-size: 14px;
color: #666;
}
/* 地图容器调整 */
.map-container {
flex: 1;
position: relative;
overflow: hidden;
background: #e8f4fc;
}
.real-map-container {
width: 100%;
height: 100%;
}
/* 模拟地图背景(只有在真实地图加载失败时才显示) */
.map-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #a8d8ff 0%, #e3f2fd 100%);
overflow: hidden;
display: none; /* 默认隐藏 */
}
/* 当真实地图加载失败时显示模拟地图 */
.map-container:not(.map-loaded) .map-background {
display: block;
}
/* 地图控制按钮保持原有样式 */
.map-controls {
position: absolute;
bottom: 100px;
right: 16px;
display: flex;
flex-direction: column;
gap: 12px;
z-index: 1000; /* 确保在地图上层 */
}
/* 自定义标记样式 */
.custom-marker {
position: relative;
}
.custom-marker:hover div {
display: block !important;
}
/* 响应式调整 */
@media (max-width: 420px) {
.home-page {
width: 100%;
height: 100vh;
}
.map-controls {
bottom: 70px;
right: 12px;
}
}
</style>

@ -104,9 +104,14 @@
<span class="order-location">{{ getOrderLocation(order.deviceId) }}</span>
<span class="order-time">{{ formatDate(order.completedTime) }}</span>
</div>
<div class="order-status">
<span class="status-badge" :class="getStatusClass(order.status)">
{{ getStatusText(order.status) }}
</span>
</div>
</div>
<button class="order-btn completed">
已完成
{{ getStatusText(order.status) }}
</button>
</div>
@ -138,7 +143,7 @@
<div class="bottom-nav">
<div class="nav-item" @click="goToHome"></div>
<div class="nav-item" @click="goToInspection"></div>
<div class="nav-item active" @click="goToWorkOrders"></div>
<div class="nav-item" @click="goToWorkOrders"></div>
<div class="nav-item" @click="goToProfile"></div>
</div>
</div>
@ -264,6 +269,24 @@ const formatDate = (dateStr) => {
return new Date(dateStr).toLocaleDateString('zh-CN')
}
//
const getStatusText = (status) => {
const statusMap = {
completed: '已完成',
reviewing: '待审核'
}
return statusMap[status] || status
}
//
const getStatusClass = (status) => {
const classMap = {
completed: 'completed',
reviewing: 'reviewing'
}
return classMap[status] || 'completed'
}
//
const loadOrders = async () => {
loading.value = true
@ -276,8 +299,9 @@ const loadOrders = async () => {
processingOrders.value = allOrders.value.filter(order =>
order.status === 'processing'
)
//
completedOrders.value = allOrders.value.filter(order =>
order.status === 'completed'
order.status === 'completed' || order.status === 'reviewing'
)
//
@ -502,6 +526,18 @@ onMounted(() => {
border: 1px solid #b7eb8f;
}
.status-badge.completed {
background: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.status-badge.reviewing {
background: #fff7e6;
color: #fa8c16;
border: 1px solid #ffd591;
}
/* 工单按钮 */
.order-btn {
padding: 6px 16px;

Loading…
Cancel
Save