app2的地图查看功能及app1的被派单通知功能 #121

Merged
hnu202326010125 merged 12 commits from luoyuehang_branch into develop 2 weeks ago

@ -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>

@ -81,6 +81,12 @@ const router = createRouter({
name: 'InspectionForm',
component: () => import('../views/InspectionForm.vue'),
meta: { requiresAuth: true }
},
{
path: '/notifications',
name: 'NotificationsPage',
component: () => import('../views/NotificationsPage.vue'),
meta: { requiresAuth: true }
}
]
})

@ -0,0 +1,40 @@
// src/services/notificationService.js
import api from './api'
export const notificationService = {
// 获取未读通知
async getUnreadNotifications(repairmanId) {
try {
const response = await api.get('/api/app/repairman/notification/unread', {
params: { repairmanId }
})
return response.data
} catch (error) {
throw error.response?.data || error.message
}
},
// 获取所有通知
async getAllNotifications(repairmanId) {
try {
const response = await api.get('/api/app/repairman/notification/all', {
params: { repairmanId }
})
return response.data
} catch (error) {
throw error.response?.data || error.message
}
},
// 标记通知为已读
async markNotificationAsRead(notificationId) {
try {
const response = await api.post('/api/app/repairman/notification/read', null, {
params: { notificationId }
})
return response.data
} catch (error) {
throw error.response?.data || error.message
}
}
}

@ -9,7 +9,12 @@
</div>
</div>
<div class="header-title">运维工作台</div>
<div class="header-right"></div>
<div class="header-right">
<div class="notification-icon" @click="goToNotifications">
<span>🔔</span>
<span v-if="unreadCount > 0" class="notification-badge">{{ unreadCount }}</span>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
@ -91,11 +96,28 @@
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { workOrderService } from '@/services/workOrderService'
import { notificationService } from '@/services/notificationService'
const authStore = useAuthStore()
const userInfo = authStore.getUserInfo()
const router = useRouter()
//
const unreadCount = ref(0)
//
const loadUnreadNotifications = async () => {
try {
const repairmanId = authStore.getRepairmanId
if (repairmanId) {
const response = await notificationService.getUnreadNotifications(repairmanId)
if (response.code === 200) {
unreadCount.value = response.data.length
}
}
} catch (error) {
console.error('获取未读通知失败:', error)
}
}
//
const processingOrders = ref([])
const loading = ref(false)
@ -195,9 +217,15 @@ const goToProfile = () => {
router.push('/profile')
}
//
const goToNotifications = () => {
router.push('/notifications')
}
//
onMounted(() => {
fetchProcessingOrders()
loadUnreadNotifications()
})
</script>
@ -435,4 +463,28 @@ onMounted(() => {
padding: 20px;
color: #666;
}
.notification-icon {
position: relative;
cursor: pointer;
font-size: 20px;
display: inline-block;
}
.notification-badge {
position: absolute;
top: -6px;
right: -8px;
background: #ff4d4f;
color: white;
font-size: 10px;
font-weight: 600;
border-radius: 50%;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
min-width: 18px;
}
</style>

@ -0,0 +1,459 @@
<!-- src/views/NotificationsPage.vue -->
<template>
<div class="notifications-page">
<!-- 顶部标题栏 -->
<div class="header">
<div class="header-left">
<span class="back-btn" @click="goBack"></span>
</div>
<div class="header-title">消息通知</div>
<div class="header-right">
<span class="mark-all-btn" @click="markAllAsRead"></span>
</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<div>加载中...</div>
</div>
<!-- 空状态 -->
<div v-else-if="notifications.length === 0" class="empty-state">
<div class="empty-icon">🔔</div>
<div class="empty-text">暂无通知</div>
</div>
<!-- 通知列表 -->
<div v-else class="notification-list">
<div
v-for="notification in notifications"
:key="notification.id"
:class="['notification-item', { 'unread': !notification.isRead }]"
@click="viewNotification(notification)"
>
<div class="notification-content">
<div class="notification-header">
<div class="notification-title">{{ getNotificationTitle(notification) }}</div>
<div class="notification-time">{{ formatTime(notification.createdTime) }}</div>
</div>
<div class="notification-body">
<div class="notification-text">{{ notification.content }}</div>
<div class="notification-status">
<span v-if="!notification.isRead" class="unread-dot"></span>
<span class="status-text">{{ notification.isRead ? '已读' : '未读' }}</span>
</div>
</div>
</div>
<div class="notification-actions" v-if="!notification.isRead">
<button class="mark-read-btn" @click.stop="markAsRead(notification.id)">
标记已读
</button>
</div>
</div>
</div>
</div>
<!-- 底部导航栏 -->
<div class="bottom-nav">
<div class="nav-item" @click="goToHome"></div>
<div class="nav-item" @click="goToInspection"></div>
<div class="nav-item" @click="goToWorkOrders"></div>
<div class="nav-item" @click="goToProfile"></div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { notificationService } from '@/services/notificationService'
const router = useRouter()
const authStore = useAuthStore()
//
const notifications = ref([])
const loading = ref(true)
//
const loadNotifications = async () => {
try {
const repairmanId = authStore.getRepairmanId
if (!repairmanId) {
console.error('未获取到维修人员ID')
return
}
const response = await notificationService.getAllNotifications(repairmanId)
if (response.code === 200) {
notifications.value = response.data.map(notification => ({
...notification,
isRead: notification.isRead || false
}))
} else {
console.error('获取通知失败:', response.message)
}
} catch (error) {
console.error('获取通知失败:', error)
alert('获取通知失败: ' + (error.message || '未知错误'))
} finally {
loading.value = false
}
}
//
const markAsRead = async (notificationId) => {
try {
await notificationService.markNotificationAsRead(notificationId)
//
const notification = notifications.value.find(n => n.id === notificationId)
if (notification) {
notification.isRead = true
}
} catch (error) {
console.error('标记已读失败:', error)
alert('标记已读失败: ' + (error.message || '未知错误'))
}
}
//
const markAllAsRead = async () => {
try {
const unreadNotifications = notifications.value.filter(n => !n.isRead)
if (unreadNotifications.length === 0) {
alert('没有未读通知')
return
}
for (const notification of unreadNotifications) {
await notificationService.markNotificationAsRead(notification.id)
notification.isRead = true
}
alert('已标记所有通知为已读')
} catch (error) {
console.error('标记所有已读失败:', error)
alert('标记失败: ' + (error.message || '未知错误'))
}
}
//
const getNotificationTitle = (notification) => {
switch (notification.type) {
case 'ORDER_ASSIGNED':
return '派单通知'
case 'SYSTEM':
return '系统通知'
case 'MAINTENANCE':
return '维护通知'
default:
return '通知'
}
}
//
const formatTime = (timeStr) => {
if (!timeStr) return '未知时间'
const date = new Date(timeStr)
const now = new Date()
const diffMs = now - date
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMins / 60)
const diffDays = Math.floor(diffHours / 24)
if (diffMins < 1) return '刚刚'
if (diffMins < 60) return `${diffMins}分钟前`
if (diffHours < 24) return `${diffHours}小时前`
return `${diffDays}天前`
}
//
const viewNotification = (notification) => {
//
if (notification.type === 'ORDER_ASSIGNED' && notification.orderId) {
router.push(`/work-orders/${notification.orderId}`)
//
if (!notification.isRead) {
markAsRead(notification.id)
}
} else {
//
if (!notification.isRead) {
markAsRead(notification.id)
}
//
alert(notification.content)
}
}
//
const goBack = () => {
router.back()
}
const goToHome = () => {
router.push('/home')
}
const goToInspection = () => {
router.push('/inspection')
}
const goToWorkOrders = () => {
router.push('/work-orders')
}
const goToProfile = () => {
router.push('/profile')
}
//
onMounted(() => {
loadNotifications()
})
</script>
<style scoped>
.notifications-page {
width: 100%;
height: 100%;
background: #f8f9fa;
display: flex;
flex-direction: column;
}
/* 顶部标题栏 */
.header {
background: white;
padding: 12px 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.header-left {
width: 80px;
}
.header-title {
font-size: 18px;
font-weight: 600;
color: #333;
text-align: center;
flex: 1;
}
.header-right {
width: 80px;
text-align: right;
}
.back-btn {
font-size: 14px;
color: #1890ff;
cursor: pointer;
transition: color 0.3s;
}
.back-btn:hover {
color: #096dd9;
}
.mark-all-btn {
font-size: 14px;
color: #1890ff;
cursor: pointer;
transition: color 0.3s;
}
.mark-all-btn:hover {
color: #096dd9;
}
/* 主要内容区域 */
.main-content {
flex: 1;
padding: 16px;
overflow-y: auto;
}
/* 加载状态 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
color: #ccc;
}
.empty-text {
font-size: 16px;
color: #999;
}
/* 通知列表 */
.notification-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.notification-item {
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s;
border-left: 3px solid #e8e8e8;
}
.notification-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.notification-item.unread {
border-left-color: #1890ff;
background: #f0f7ff;
}
.notification-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.notification-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.notification-title {
font-size: 15px;
font-weight: 600;
color: #333;
}
.notification-time {
font-size: 12px;
color: #999;
white-space: nowrap;
margin-left: 8px;
}
.notification-body {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.notification-text {
flex: 1;
font-size: 14px;
color: #666;
line-height: 1.4;
}
.notification-status {
display: flex;
align-items: center;
gap: 4px;
margin-left: 8px;
}
.unread-dot {
width: 8px;
height: 8px;
background: #ff4d4f;
border-radius: 50%;
}
.status-text {
font-size: 12px;
color: #999;
}
.notification-actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.mark-read-btn {
padding: 6px 12px;
background: #f0f7ff;
color: #1890ff;
border: 1px solid #1890ff;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.3s;
}
.mark-read-btn:hover {
background: #1890ff;
color: white;
}
/* 底部导航栏 */
.bottom-nav {
display: flex;
background: white;
border-top: 1px solid #e8e8e8;
padding: 8px 0;
}
.nav-item {
flex: 1;
text-align: center;
padding: 8px;
font-size: 12px;
color: #666;
cursor: pointer;
transition: color 0.3s;
}
.nav-item:hover {
color: #1890ff;
}
</style>

@ -1,3 +1,4 @@
<!-- src/views/ProfilePage.vue -->
<template>
<div class="profile-page">
<!-- 顶部标题栏 -->
@ -14,9 +15,9 @@
<div class="avatar-icon">👤</div>
</div>
<div class="user-info">
<div class="user-name">维修员 张三</div>
<div class="user-id">工号REP2023001</div>
<div class="user-area">负责片区教学楼A区图书馆宿舍C区</div>
<div class="user-name">{{ userInfo?.username || '未登录' }}</div>
<div class="user-id">工号{{ userInfo?.repairmanId || '未知' }}</div>
<div class="user-area">负责片区{{ userInfo?.areaId || '未分配' }}</div>
</div>
</div>
</div>
@ -26,19 +27,19 @@
<div class="section-title">本月工作统计</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-number">23</div>
<div class="stat-number">{{ stats.processedOrders }}</div>
<div class="stat-label">处理工单</div>
</div>
<div class="stat-item">
<div class="stat-number">45</div>
<div class="stat-number">{{ stats.inspections }}</div>
<div class="stat-label">设备巡检</div>
</div>
<div class="stat-item">
<div class="stat-number">98%</div>
<div class="stat-number">{{ stats.responseRate }}%</div>
<div class="stat-label">响应率</div>
</div>
<div class="stat-item">
<div class="stat-number">95%</div>
<div class="stat-number">{{ stats.completionRate }}%</div>
<div class="stat-label">完成率</div>
</div>
</div>
@ -126,18 +127,50 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { notificationService } from '@/services/notificationService'
const authStore = useAuthStore()
const router = useRouter()
//
const userInfo = ref(null)
//
const stats = ref({
processedOrders: 23,
inspections: 45,
responseRate: 98,
completionRate: 95
})
//
const unreadCount = ref(3)
const unreadCount = ref(0)
// 退
const showLogoutConfirm = ref(false)
//
const loadUserInfo = () => {
userInfo.value = authStore.getUserInfo()
}
//
const loadUnreadNotifications = async () => {
try {
const repairmanId = authStore.getRepairmanId
if (repairmanId) {
const response = await notificationService.getUnreadNotifications(repairmanId)
if (response.code === 200) {
unreadCount.value = response.data.length
}
}
} catch (error) {
console.error('获取未读通知失败:', error)
}
}
//
const goToHome = () => {
router.push('/home')
@ -154,8 +187,8 @@ const goToWorkOrders = () => {
const goToProfile = () => {
//
if (router.currentRoute.value.path === '/profile') {
//
console.log('刷新个人中心')
loadUserInfo()
loadUnreadNotifications()
}
}
@ -166,10 +199,7 @@ const goToSettings = () => {
}
const goToNotifications = () => {
console.log('跳转到消息通知')
alert('消息通知页面开发中')
//
// unreadCount.value = 0
router.push('/notifications')
}
const goToFeedback = () => {
@ -205,6 +235,12 @@ const confirmLogout = () => {
//
authStore.logout();
}
//
onMounted(() => {
loadUserInfo()
loadUnreadNotifications()
})
</script>
<style scoped>

@ -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