176 lines
3.8 KiB
Vue
176 lines
3.8 KiB
Vue
<template>
|
|
<div class="page-container">
|
|
<div class="page-title">消息通知</div>
|
|
|
|
<el-card shadow="never">
|
|
<template #header>
|
|
<div class="card-head">
|
|
<span>共 {{ items.length }} 条通知</span>
|
|
<el-button type="primary" size="small" plain @click="readAll">全部标记已读</el-button>
|
|
</div>
|
|
</template>
|
|
|
|
<el-empty v-if="!loading && items.length === 0" description="暂无通知" />
|
|
|
|
<div v-else v-loading="loading">
|
|
<div
|
|
v-for="n in items"
|
|
:key="n.id"
|
|
class="notice-item"
|
|
:class="{ unread: !n.read }"
|
|
@click="openNotice(n)"
|
|
>
|
|
<div class="notice-dot" :class="{ dot: !n.read }" />
|
|
<div class="notice-main">
|
|
<div class="notice-title">
|
|
<span class="title">{{ n.title }}</span>
|
|
<el-tag size="small" effect="plain" :type="typeTag(n.type)">{{ typeLabel(n.type) }}</el-tag>
|
|
<span class="time">{{ fmtTime(n.created_at) }}</span>
|
|
</div>
|
|
<div class="content">{{ n.content }}</div>
|
|
</div>
|
|
<el-button v-if="!n.read" type="primary" link size="small" @click.stop="markOne(n)">标为已读</el-button>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { myNotifications, markRead, markAllRead } from '@/api/notification'
|
|
import { useNotificationStore } from '@/store/notification'
|
|
|
|
const router = useRouter()
|
|
const notifStore = useNotificationStore()
|
|
const loading = ref(false)
|
|
const items = ref([])
|
|
|
|
function fmtTime(t) {
|
|
if (!t) return '—'
|
|
return String(t).replace('T', ' ').slice(0, 16)
|
|
}
|
|
|
|
function typeLabel(t) {
|
|
return {
|
|
exam_publish: '考试发布',
|
|
exam_pass: '考试通过',
|
|
position_set: '岗位设置',
|
|
worker_task_assign: '事项指派',
|
|
worker_artifact_review: '交付待确认',
|
|
worker_artifact_approved: '交付已确认',
|
|
worker_artifact_published: '交付已发布',
|
|
}[t] || '通知'
|
|
}
|
|
|
|
function typeTag(t) {
|
|
return {
|
|
exam_publish: 'warning',
|
|
exam_pass: 'success',
|
|
position_set: 'primary',
|
|
worker_task_assign: 'primary',
|
|
worker_artifact_review: 'warning',
|
|
worker_artifact_approved: 'success',
|
|
worker_artifact_published: 'success',
|
|
}[t] || 'info'
|
|
}
|
|
|
|
function openNotice(n) {
|
|
if (!n.read) markOne(n)
|
|
if (n.link) router.push(n.link)
|
|
}
|
|
|
|
async function markOne(n) {
|
|
try {
|
|
await markRead(n.id)
|
|
n.read = true
|
|
notifStore.refresh()
|
|
} catch (e) {
|
|
// 忽略
|
|
}
|
|
}
|
|
|
|
async function readAll() {
|
|
try {
|
|
await markAllRead()
|
|
items.value.forEach((n) => (n.read = true))
|
|
notifStore.setUnread(0)
|
|
} catch (e) {
|
|
// 拦截器已提示
|
|
}
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
try {
|
|
const res = await myNotifications()
|
|
items.value = res.data || []
|
|
} catch (e) {
|
|
items.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.card-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.notice-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
padding: 14px 8px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
cursor: pointer;
|
|
}
|
|
.notice-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.notice-item:hover {
|
|
background: #f8f9fb;
|
|
}
|
|
.notice-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
margin-top: 6px;
|
|
flex: none;
|
|
}
|
|
.notice-dot.dot {
|
|
background: #f56c6c;
|
|
}
|
|
.notice-main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.notice-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.title {
|
|
font-weight: 600;
|
|
color: #303133;
|
|
}
|
|
.notice-item.unread .title {
|
|
color: #303133;
|
|
}
|
|
.time {
|
|
margin-left: auto;
|
|
font-size: 12px;
|
|
color: #c0c4cc;
|
|
}
|
|
.content {
|
|
font-size: 13px;
|
|
color: #606266;
|
|
margin-top: 4px;
|
|
}
|
|
</style>
|