app2的被派单通知功能

pull/121/head
luoyuehang 4 months ago
parent 3330b5c061
commit 08ab1f457f

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

Loading…
Cancel
Save