qiangdanxitong/frontend/src/views/Notifications.vue

492 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<DefaultLayout>
<div class="notifications-page">
<div class="container">
<div class="header-actions">
<a-badge :count="unreadCount" :overflow-count="99">
<a-button @click="loadNotifications(true)">
<template #icon><Icon name="bell" size="sm" /></template>
未读消息
</a-button>
</a-badge>
<a-button @click="handleMarkAllRead" :disabled="unreadCount === 0">
<template #icon><Icon name="check" size="sm" /></template>
全部已读
</a-button>
</div>
<!-- 筛选标签 -->
<div class="filter-tabs">
<a-radio-group v-model:value="filterType" button-style="solid" @change="handleFilterChange">
<a-radio-button value="all">全部消息</a-radio-button>
<a-radio-button value="unread">未读消息</a-radio-button>
<a-radio-button value="system">系统通知</a-radio-button>
<a-radio-button value="task">任务消息</a-radio-button>
<a-radio-button value="score">积分消息</a-radio-button>
<a-radio-button value="review">评审消息</a-radio-button>
</a-radio-group>
</div>
<!-- 通知列表 -->
<a-spin :spinning="loading">
<div v-if="notifications.length > 0" class="notifications-list">
<div
v-for="notification in notifications"
:key="notification.id"
class="notification-item"
:class="{ 'unread': notification.isRead === 0 }"
@click="handleNotificationClick(notification)"
>
<!-- 左侧图标 -->
<div class="notification-icon" :class="getIconClass(notification)">
<Icon :name="getIconName(notification)" size="md" color="white" />
</div>
<!-- 中间内容 -->
<div class="notification-content">
<div class="notification-header">
<span class="notification-title">{{ notification.title }}</span>
<a-tag v-if="notification.isRead === 0" color="blue" size="small">未读</a-tag>
<a-tag :color="getPriorityColor(notification.priority)" size="small">
{{ getPriorityText(notification.priority) }}
</a-tag>
</div>
<div class="notification-body">{{ notification.content }}</div>
<div class="notification-footer">
<span class="notification-time">
<Icon name="clock" size="sm" />
{{ formatTime(notification.createTime) }}
</span>
<span v-if="notification.linkUrl" class="notification-link">
点击查看详情 →
</span>
</div>
</div>
<!-- 右侧操作 -->
<div class="notification-actions">
<a-button
v-if="notification.isRead === 0"
type="text"
size="small"
@click.stop="handleMarkRead(notification.id)"
>
<Icon name="check" size="sm" />
</a-button>
<a-popconfirm
title="确定删除这条通知?"
@confirm="handleDelete(notification.id)"
@click.stop
>
<a-button type="text" danger size="small" @click.stop>
<Icon name="delete" size="sm" />
</a-button>
</a-popconfirm>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
<a-empty description="暂无消息">
<template #image>
<Icon name="inbox" size="xl" color="#d9d9d9" />
</template>
</a-empty>
</div>
<!-- 分页 -->
<div v-if="total > 0" class="pagination">
<a-pagination
v-model:current="pageNum"
v-model:page-size="pageSize"
:total="total"
show-size-changer
show-quick-jumper
:show-total="total => `共 ${total} 条`"
@change="handlePageChange"
/>
</div>
</a-spin>
</div>
</div>
</DefaultLayout>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import { message } from 'ant-design-vue'
import Icon from '@/components/Icon.vue'
import {
getNotifications,
getUnreadCount,
markAsRead,
markAllAsRead,
deleteNotification,
type Notification
} from '@/api/notification'
import { useRouter } from 'vue-router'
const router = useRouter()
const loading = ref(false)
const notifications = ref<Notification[]>([])
const unreadCount = ref(0)
const total = ref(0)
const pageNum = ref(1)
const pageSize = ref(20)
const filterType = ref('all')
// 模拟当前用户ID(实际应该从用户状态管理获取)
const currentUserId = ref(1)
// 加载通知列表
const loadNotifications = async (unreadOnly = false) => {
loading.value = true
try {
const params: any = {
receiverId: currentUserId.value,
pageNum: pageNum.value,
pageSize: pageSize.value
}
if (unreadOnly || filterType.value === 'unread') {
params.unreadOnly = true
}
const result = await getNotifications(params)
// 根据类型筛选
let filteredRecords = result.records
if (filterType.value !== 'all' && filterType.value !== 'unread') {
filteredRecords = result.records.filter(n => n.notificationType === filterType.value)
}
notifications.value = filteredRecords
total.value = result.total
} catch (error) {
message.error('加载通知失败')
console.error(error)
} finally {
loading.value = false
}
}
// 加载未读数量
const loadUnreadCount = async () => {
try {
unreadCount.value = await getUnreadCount(currentUserId.value)
} catch (error) {
console.error('加载未读数量失败', error)
}
}
// 标记为已读
const handleMarkRead = async (id: number) => {
try {
await markAsRead(id, currentUserId.value)
message.success('已标记为已读')
await Promise.all([loadNotifications(), loadUnreadCount()])
} catch (error) {
message.error('操作失败')
}
}
// 全部标记为已读
const handleMarkAllRead = async () => {
try {
const count = await markAllAsRead(currentUserId.value)
message.success(`已标记 ${count} 条消息为已读`)
await Promise.all([loadNotifications(), loadUnreadCount()])
} catch (error) {
message.error('操作失败')
}
}
// 删除通知
const handleDelete = async (id: number) => {
try {
await deleteNotification(id, currentUserId.value)
message.success('删除成功')
await Promise.all([loadNotifications(), loadUnreadCount()])
} catch (error) {
message.error('删除失败')
}
}
// 点击通知
const handleNotificationClick = async (notification: Notification) => {
// 标记为已读
if (notification.isRead === 0) {
await markAsRead(notification.id, currentUserId.value)
loadUnreadCount()
}
// 跳转到相关页面
if (notification.linkUrl) {
router.push(notification.linkUrl)
}
}
// 筛选改变
const handleFilterChange = () => {
pageNum.value = 1
loadNotifications()
}
// 分页改变
const handlePageChange = () => {
loadNotifications()
}
// 获取图标名称
const getIconName = (notification: Notification) => {
switch (notification.notificationType) {
case 'system':
return 'info'
case 'task':
return 'document'
case 'score':
return 'trophy'
case 'review':
return 'star'
default:
return 'bell'
}
}
// 获取图标样式类
const getIconClass = (notification: Notification) => {
return `type-${notification.notificationType}`
}
// 获取优先级颜色
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'urgent':
return 'red'
case 'high':
return 'orange'
case 'normal':
return 'blue'
case 'low':
return 'default'
default:
return 'blue'
}
}
// 获取优先级文本
const getPriorityText = (priority: string) => {
switch (priority) {
case 'urgent':
return '紧急'
case 'high':
return '重要'
case 'normal':
return '普通'
case 'low':
return '低'
default:
return '普通'
}
}
// 格式化时间
const formatTime = (time: string) => {
const date = new Date(time)
const now = new Date()
const diff = now.getTime() - date.getTime()
const minute = 60 * 1000
const hour = 60 * minute
const day = 24 * hour
if (diff < minute) {
return '刚刚'
} else if (diff < hour) {
return `${Math.floor(diff / minute)}分钟前`
} else if (diff < day) {
return `${Math.floor(diff / hour)}小时前`
} else if (diff < 7 * day) {
return `${Math.floor(diff / day)}天前`
} else {
return date.toLocaleDateString('zh-CN')
}
}
onMounted(() => {
loadNotifications()
loadUnreadCount()
})
</script>
<style lang="scss" scoped>
.notifications-page {
min-height: 100vh;
padding: $space-xl 0;
background: $bg-primary;
}
.container {
max-width: $container-max-width;
margin: 0 auto;
padding: 0 $space-xl;
@media (max-width: $breakpoint-tablet) {
padding: 0 $space-lg;
}
@media (max-width: $breakpoint-mobile) {
padding: 0 $space-md;
}
}
.header-actions {
display: flex;
justify-content: flex-end;
gap: $space-md;
margin-bottom: $space-2xl;
}
.filter-tabs {
margin-bottom: $space-xl;
}
// 图表卡片 - Cursor 风格
.chart-card {
background: $bg-card;
border: 1px solid $border-primary;
border-radius: $radius-lg;
padding: $space-xl;
transition: all $transition-base $ease-out;
&:hover {
border-color: $border-color;
background: $bg-card-hover;
}
}
.notifications-list {
display: flex;
flex-direction: column;
gap: $space-md;
}
.notification-item {
background: $bg-card;
border: 1px solid $border-primary;
border-radius: $radius-lg;
padding: $space-lg;
display: flex;
gap: $space-lg;
transition: all $transition-base $ease-out;
cursor: pointer;
&:hover {
border-color: $border-color;
background: $bg-card-hover;
box-shadow: $shadow-base;
}
&.unread {
background: rgba($brand-primary, 0.05);
border-color: $brand-primary;
.notification-title {
font-weight: $font-weight-bold;
}
}
}
.notification-icon {
width: 48px;
height: 48px;
border-radius: $radius-md;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
&.type-system,
&.type-task,
&.type-score,
&.type-review {
background: rgba(#31f08b, 0.1);
border: 1px solid rgba(#31f08b, 0.3);
color: #31f08b;
}
}
.notification-content {
flex: 1;
min-width: 0;
}
.notification-header {
display: flex;
align-items: center;
gap: $space-sm;
margin-bottom: $space-sm;
.notification-title {
font-size: $font-size-lg;
color: $text-primary;
flex: 1;
}
}
.notification-body {
font-size: $font-size-base;
color: $text-secondary;
line-height: 1.6;
margin-bottom: $space-sm;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.notification-footer {
display: flex;
align-items: center;
justify-content: space-between;
font-size: $font-size-sm;
color: $text-tertiary;
.notification-time {
display: flex;
align-items: center;
gap: $space-xs;
}
.notification-link {
color: $primary-color;
font-weight: $font-weight-medium;
}
}
.notification-actions {
display: flex;
flex-direction: column;
gap: $space-xs;
flex-shrink: 0;
}
.empty-state {
padding: $space-3xl 0;
text-align: center;
}
.pagination {
margin-top: $space-2xl;
display: flex;
justify-content: center;
}
</style>