608 lines
21 KiB
Vue
608 lines
21 KiB
Vue
<template>
|
||
<div class="space-y-6">
|
||
<!-- 页面头部 -->
|
||
<section class="page-header">
|
||
<div>
|
||
<p class="page-kicker">System Records</p>
|
||
<h2 class="page-title">系统记录</h2>
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<div class="page-meta" style="min-width: 120px;">
|
||
<p class="page-meta__label">记录总数</p>
|
||
<p class="page-meta__value">{{ total }}</p>
|
||
<p class="page-meta__subvalue">系统留痕</p>
|
||
</div>
|
||
<div class="page-meta" style="min-width: 120px;">
|
||
<p class="page-meta__label">待处理</p>
|
||
<p class="page-meta__value" :class="notificationStore.unreadCount > 0 ? 'text-status-warning' : ''">
|
||
{{ notificationStore.unreadCount }}
|
||
</p>
|
||
<p class="page-meta__subvalue">尚未查看记录</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 筛选 -->
|
||
<section class="card">
|
||
<div class="filter-bar">
|
||
<div class="filter-bar__inputs">
|
||
<div class="field-stack">
|
||
<label class="field-label">记录范围</label>
|
||
<div class="segmented-toggle">
|
||
<button
|
||
type="button"
|
||
class="segmented-toggle__item"
|
||
:class="{ 'is-active': statusFilter === 'all' }"
|
||
@click="switchFilter('all')"
|
||
>
|
||
全部记录
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="segmented-toggle__item"
|
||
:class="{ 'is-active': statusFilter === 'unread' }"
|
||
@click="switchFilter('unread')"
|
||
>
|
||
尚未查看
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filter-bar__actions">
|
||
<button class="btn-secondary text-xs sm:text-sm" :disabled="loading" @click="fetchNotifications">
|
||
{{ loading ? '刷新中...' : '刷新' }}
|
||
</button>
|
||
<button
|
||
class="btn-secondary text-xs sm:text-sm"
|
||
:disabled="markAllLoading || notificationStore.unreadCount === 0"
|
||
@click="handleMarkAllRead"
|
||
>
|
||
{{ markAllLoading ? '处理中...' : '全部标为已查看' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 消息列表 -->
|
||
<section class="space-y-4">
|
||
<article
|
||
v-for="item in notifications"
|
||
:key="item.id"
|
||
class="card"
|
||
:class="isNotificationAttentionRequired(item) ? 'border-status-warning/40' : 'border-border'"
|
||
>
|
||
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||
<div class="min-w-0 flex-1">
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<span class="badge" :class="getNotificationRecordBadge(item).badgeClass">
|
||
{{ getNotificationRecordBadge(item).label }}
|
||
</span>
|
||
<span class="text-xs text-text-tertiary">{{ formatDateTime(item.notification_created_at) }}</span>
|
||
<span v-if="!item.is_read" class="text-xs text-text-tertiary">尚未查看</span>
|
||
</div>
|
||
|
||
<h3 class="mt-3 text-lg font-semibold text-text-primary">{{ item.title }}</h3>
|
||
<p v-if="item.content" class="mt-3 whitespace-pre-line text-sm leading-6 text-text-secondary">
|
||
{{ item.content }}
|
||
</p>
|
||
|
||
<div
|
||
v-if="summaryPairs(item).length > 0"
|
||
class="mt-4 flex flex-wrap gap-2"
|
||
>
|
||
<span
|
||
v-for="pair in summaryPairs(item)"
|
||
:key="pair.label"
|
||
class="badge badge-neutral"
|
||
>
|
||
{{ pair.label }} {{ pair.count }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="hidden shrink-0 flex-wrap gap-3 md:flex lg:w-[220px] lg:flex-col">
|
||
<button
|
||
v-if="!item.is_read"
|
||
class="btn-secondary w-full text-sm"
|
||
@click="handleMarkRead(item)"
|
||
>
|
||
标为已查看
|
||
</button>
|
||
<button
|
||
v-if="item.action_url || item.related_sync_job_id"
|
||
class="btn-primary w-full text-sm"
|
||
@click="openAction(item)"
|
||
>
|
||
{{ getNotificationActionLabel(item) }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="flex gap-2 md:hidden">
|
||
<button
|
||
v-if="item.action_url || item.related_sync_job_id"
|
||
class="btn-primary flex-1"
|
||
@click="openAction(item)"
|
||
>
|
||
{{ getNotificationActionLabel(item) }}
|
||
</button>
|
||
<button
|
||
v-if="!item.is_read && !(item.action_url || item.related_sync_job_id)"
|
||
class="btn-secondary flex-1"
|
||
@click="handleMarkRead(item)"
|
||
>
|
||
标为已查看
|
||
</button>
|
||
<button
|
||
v-if="!item.is_read && (item.action_url || item.related_sync_job_id)"
|
||
class="btn-secondary"
|
||
@click="mobileActionNotification = item"
|
||
>
|
||
更多操作
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
|
||
<div v-if="!loading && notifications.length === 0" class="empty-state">
|
||
当前没有{{ statusFilter === 'unread' ? '尚未查看的' : '' }}系统记录。
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 分页 -->
|
||
<section v-if="total > 0" class="card">
|
||
<div class="pagination-bar">
|
||
<p class="text-sm text-text-tertiary">
|
||
第 {{ currentPage }} / {{ totalPages }} 页,共 {{ total }} 条系统记录
|
||
</p>
|
||
<div class="flex gap-2">
|
||
<button class="btn-secondary text-sm" :disabled="currentPage === 1 || loading" @click="handlePrevPage">
|
||
上一页
|
||
</button>
|
||
<button class="btn-secondary text-sm" :disabled="currentPage >= totalPages || loading" @click="handleNextPage">
|
||
下一页
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<MobileActionSheet
|
||
:open="Boolean(mobileActionNotification)"
|
||
title="记录操作"
|
||
:description="mobileActionNotification?.title || ''"
|
||
@close="mobileActionNotification = null"
|
||
>
|
||
<button type="button" class="btn-secondary w-full" @click="handleMobileMarkRead">标为已查看</button>
|
||
</MobileActionSheet>
|
||
|
||
<div v-if="syncDetailOpen" class="sheet-backdrop" @click.self="closeSyncDetail">
|
||
<div class="sheet-panel max-w-5xl">
|
||
<div class="form-panel__header">
|
||
<div>
|
||
<h3 class="form-panel__title">同步详情</h3>
|
||
<p class="form-panel__desc">
|
||
查看本次花名册同步的变更和资产处理结果。
|
||
</p>
|
||
</div>
|
||
<button type="button" class="btn-secondary text-sm" @click="closeSyncDetail">
|
||
关闭
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="syncDetailLoading" class="panel mt-4 text-sm text-text-tertiary">
|
||
正在读取同步详情...
|
||
</div>
|
||
|
||
<div v-else-if="selectedSyncJob" class="mt-4 space-y-4">
|
||
<div class="grid gap-3 md:grid-cols-4">
|
||
<div class="panel">
|
||
<p class="text-xs uppercase tracking-wider text-text-tertiary">执行状态</p>
|
||
<p class="mt-2 text-base font-semibold text-text-primary">
|
||
{{ getJobStatusLabel(selectedSyncJob.status) }}
|
||
</p>
|
||
</div>
|
||
<div class="panel">
|
||
<p class="text-xs uppercase tracking-wider text-text-tertiary">执行方式</p>
|
||
<p class="mt-2 text-base font-semibold text-text-primary">
|
||
{{ getTriggerModeLabel(selectedSyncJob.trigger_mode) }}
|
||
</p>
|
||
</div>
|
||
<div class="panel">
|
||
<p class="text-xs uppercase tracking-wider text-text-tertiary">执行人</p>
|
||
<p class="mt-2 text-base font-semibold text-text-primary">
|
||
{{ selectedSyncJob.operator || 'system-sync' }}
|
||
</p>
|
||
</div>
|
||
<div class="panel">
|
||
<p class="text-xs uppercase tracking-wider text-text-tertiary">变化数量</p>
|
||
<p class="mt-2 text-base font-semibold text-text-primary">
|
||
{{ selectedSyncJob.changes_count }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||
<p class="text-sm font-medium text-text-primary">同步摘要</p>
|
||
<span class="text-xs text-text-tertiary">{{ formatDateTime(selectedSyncJob.created_at) }}</span>
|
||
</div>
|
||
<p class="mt-3 whitespace-pre-line text-sm leading-6 text-text-secondary">
|
||
{{ selectedSyncJob.summary || '本次同步没有额外摘要。' }}
|
||
</p>
|
||
<p v-if="selectedSyncJob.error_message" class="mt-3 text-sm text-status-error">
|
||
错误信息:{{ selectedSyncJob.error_message }}
|
||
</p>
|
||
</div>
|
||
|
||
<div v-if="summaryPairs(selectedSyncJob).length > 0" class="flex flex-wrap gap-2">
|
||
<span
|
||
v-for="pair in summaryPairs(selectedSyncJob)"
|
||
:key="`detail-${pair.label}`"
|
||
class="badge badge-neutral"
|
||
>
|
||
{{ pair.label }} {{ pair.count }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="flex items-center justify-between gap-3">
|
||
<p class="text-sm font-medium text-text-primary">完整变更</p>
|
||
<span class="badge badge-neutral">{{ selectedSyncJob.changes.length }} 条</span>
|
||
</div>
|
||
|
||
<div v-if="selectedSyncJob.changes.length > 0" class="mt-4 space-y-3">
|
||
<div
|
||
v-for="change in selectedSyncJob.changes"
|
||
:key="change.id"
|
||
class="rounded-lg border border-border bg-bg-secondary px-4 py-3"
|
||
>
|
||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||
<div class="min-w-0 flex-1">
|
||
<p class="text-sm font-medium text-text-primary">{{ getChangeDisplayTitle(change) }}</p>
|
||
<p class="mt-2 whitespace-pre-line text-sm leading-6 text-text-secondary">
|
||
{{ change.detail || '无详细说明' }}
|
||
</p>
|
||
|
||
<div
|
||
v-if="getChangeAssets(change).length"
|
||
class="mt-4 rounded-xl border border-border bg-bg-primary/40 p-3"
|
||
>
|
||
<p class="text-xs font-medium text-text-tertiary">
|
||
{{ change.after_snapshot?.assetOutcome ? '涉及资产' : '同步时记录的资产' }}
|
||
</p>
|
||
<ul class="mt-3 flex flex-wrap gap-2">
|
||
<li
|
||
v-for="(asset, index) in getChangeAssets(change)"
|
||
:key="index"
|
||
class="flex max-w-full flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-border bg-bg-elevated px-3 py-2 text-xs [overflow-wrap:anywhere]"
|
||
>
|
||
<span class="font-semibold text-text-primary">{{ asset.equipmentCode }}</span>
|
||
<span class="text-text-secondary">{{ asset.equipmentName }}</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<span class="badge badge-neutral">
|
||
{{ CHANGE_TYPE_LABELS[change.change_type] || change.change_type }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="mt-3 flex flex-wrap items-center gap-2 text-xs text-text-tertiary">
|
||
<span class="badge" :class="getChangeProcessingBadgeClass(change.processing_status)">
|
||
{{ getChangeProcessingLabel(change.processing_status) }}
|
||
</span>
|
||
<span>处理人:{{ getChangeProcessorLabel(change) }}</span>
|
||
<span>处理时间:{{ change.processed_at ? formatDateTime(change.processed_at) : '未处理' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="empty-state mt-4">
|
||
本次同步没有变化明细。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="panel mt-4 text-sm text-text-tertiary">
|
||
暂时没有读取到这条消息对应的同步详情。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { formatDateTime } from '@/utils/format'
|
||
import type {
|
||
HrSyncBorrowedAssetPreview,
|
||
HrSyncChangeItem,
|
||
HrSyncChangeProcessingStatus,
|
||
HrSyncJobDetail,
|
||
HrSyncPersonAfterSnapshot,
|
||
NotificationItem
|
||
} from '@/types'
|
||
import {
|
||
getNotificationList,
|
||
markAllNotificationsAsRead,
|
||
markNotificationAsRead
|
||
} from '@/api/notifications'
|
||
import { getHrSyncJobDetail } from '@/api/sync'
|
||
import { useNotificationStore } from '@/stores/notifications'
|
||
import {
|
||
getNotificationActionLabel,
|
||
getNotificationRecordBadge,
|
||
isNotificationAttentionRequired
|
||
} from './notificationPresentation'
|
||
import MobileActionSheet from '@/components/MobileActionSheet.vue'
|
||
|
||
const notificationStore = useNotificationStore()
|
||
|
||
const notifications = ref<NotificationItem[]>([])
|
||
const total = ref(0)
|
||
const currentPage = ref(1)
|
||
const pageSize = 12
|
||
const statusFilter = ref<'all' | 'unread'>('all')
|
||
const loading = ref(false)
|
||
const markAllLoading = ref(false)
|
||
const mobileActionNotification = ref<NotificationItem | null>(null)
|
||
const syncDetailOpen = ref(false)
|
||
const syncDetailLoading = ref(false)
|
||
const selectedSyncJob = ref<HrSyncJobDetail | null>(null)
|
||
|
||
const CHANGE_TYPE_LABELS: Record<string, string> = {
|
||
person_created: '新人入职',
|
||
person_deactivated: '人员离职/停用',
|
||
person_reactivated: '重新启用',
|
||
person_main_department_changed: '主部门变动',
|
||
person_departments_changed: '关联部门变动',
|
||
person_position_changed: '岗位变动',
|
||
department_created: '新增部门',
|
||
department_renamed: '部门名称变动',
|
||
department_hierarchy_changed: '部门层级变动',
|
||
department_deactivated: '部门停用',
|
||
department_reactivated: '部门重新启用',
|
||
equipment_department_unresolved: '资产部门待处理',
|
||
person_assets_returned: '资产已自动入库',
|
||
person_assets_reassigned: '资产归属已更新',
|
||
person_assets_review: '资产待核实',
|
||
person_assets_retry: '资产自动处理待重试',
|
||
equipment_auto_return_failed: '离职自动入库未执行'
|
||
}
|
||
|
||
const CHANGE_PROCESSING_LABELS: Record<HrSyncChangeProcessingStatus, string> = {
|
||
pending: '待处理',
|
||
processed: '已处理',
|
||
ignored: '已忽略'
|
||
}
|
||
|
||
const CHANGE_PROCESSING_BADGES: Record<HrSyncChangeProcessingStatus, string> = {
|
||
pending: 'badge-warning',
|
||
processed: 'badge-success',
|
||
ignored: 'badge-neutral'
|
||
}
|
||
|
||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize)))
|
||
|
||
const normalizeChangeProcessingStatus = (
|
||
status?: HrSyncChangeProcessingStatus | string | null
|
||
): HrSyncChangeProcessingStatus => {
|
||
if (status === 'processed' || status === 'ignored') {
|
||
return status
|
||
}
|
||
return 'pending'
|
||
}
|
||
|
||
const getJobStatusLabel = (status?: string) => (status === 'failed' ? '失败' : '成功')
|
||
|
||
const getTriggerModeLabel = (mode?: string) => (mode === 'scheduler' ? '定时执行' : '手动执行')
|
||
|
||
const getChangeProcessingLabel = (status?: HrSyncChangeProcessingStatus | string | null) =>
|
||
CHANGE_PROCESSING_LABELS[normalizeChangeProcessingStatus(status)]
|
||
|
||
const getChangeProcessingBadgeClass = (status?: HrSyncChangeProcessingStatus | string | null) =>
|
||
CHANGE_PROCESSING_BADGES[normalizeChangeProcessingStatus(status)]
|
||
|
||
const getChangeProcessorLabel = (change: HrSyncChangeItem) => {
|
||
const processedBy = change.processed_by_name || change.processed_by
|
||
if (processedBy) return processedBy
|
||
if (change.processing_status === 'processed' &&
|
||
(change.change_type === 'person_assets_returned' || change.change_type === 'person_assets_reassigned')) return '系统'
|
||
return '未处理'
|
||
}
|
||
|
||
const getChangeDisplayTitle = (change: HrSyncChangeItem) => {
|
||
const personChangeTitleMap: Record<string, string> = {
|
||
person_created: '新人入职',
|
||
person_deactivated: '人员离职/停用',
|
||
person_reactivated: '人员重新启用',
|
||
person_main_department_changed: '主部门变动',
|
||
person_departments_changed: '关联部门变动',
|
||
person_position_changed: '岗位变动'
|
||
}
|
||
|
||
const titlePrefix = personChangeTitleMap[change.change_type]
|
||
if (!titlePrefix) {
|
||
return change.title
|
||
}
|
||
|
||
const afterSnapshot = change.after_snapshot as HrSyncPersonAfterSnapshot | null
|
||
const fallbackSnapshot = (change.before_snapshot || {}) as HrSyncPersonAfterSnapshot
|
||
const displayName =
|
||
afterSnapshot?.displayName ||
|
||
afterSnapshot?.nickname ||
|
||
afterSnapshot?.realName ||
|
||
fallbackSnapshot?.displayName ||
|
||
fallbackSnapshot?.nickname ||
|
||
fallbackSnapshot?.realName
|
||
|
||
return displayName ? `${titlePrefix}:${displayName}` : change.title
|
||
}
|
||
|
||
const extractDetailAssets = (detail?: string | null): HrSyncBorrowedAssetPreview[] => {
|
||
if (!detail) {
|
||
return []
|
||
}
|
||
|
||
const matched = detail.match(/名下(?:待处理)?资产:([^。]+)/)
|
||
if (!matched?.[1]) {
|
||
return []
|
||
}
|
||
|
||
return matched[1]
|
||
.split('、')
|
||
.map((rawItem) => {
|
||
const normalizedItem = rawItem.replace(/\s*等\s*\d+\s*项$/, '').trim()
|
||
const separatorIndex = normalizedItem.indexOf('/')
|
||
if (separatorIndex <= 0) {
|
||
return null
|
||
}
|
||
|
||
const equipmentCode = normalizedItem.slice(0, separatorIndex).trim()
|
||
const equipmentName = normalizedItem.slice(separatorIndex + 1).trim()
|
||
if (!equipmentCode || !equipmentName) {
|
||
return null
|
||
}
|
||
|
||
return {
|
||
equipmentCode,
|
||
equipmentName
|
||
}
|
||
})
|
||
.filter((asset): asset is HrSyncBorrowedAssetPreview => Boolean(asset))
|
||
}
|
||
|
||
const getChangeAssets = (change: HrSyncChangeItem): HrSyncBorrowedAssetPreview[] => {
|
||
const afterSnapshot = change.after_snapshot as HrSyncPersonAfterSnapshot | null
|
||
const assets = afterSnapshot?.assetOutcome?.assets ?? afterSnapshot?.assets
|
||
if (Array.isArray(assets) && assets.length > 0) {
|
||
return assets.filter(
|
||
(asset) => Boolean(asset?.equipmentCode) && Boolean(asset?.equipmentName)
|
||
)
|
||
}
|
||
|
||
return extractDetailAssets(change.detail)
|
||
}
|
||
|
||
const summaryPairs = (item: {
|
||
counts?: Record<string, number>
|
||
metadata?: {
|
||
counts?: Record<string, number>
|
||
} | null
|
||
}) =>
|
||
Object.entries(item.counts || item.metadata?.counts || {})
|
||
.filter(([, count]) => Number(count) > 0)
|
||
.map(([key, count]) => ({
|
||
label: CHANGE_TYPE_LABELS[key] || key,
|
||
count: Number(count)
|
||
}))
|
||
|
||
const fetchNotifications = async () => {
|
||
loading.value = true
|
||
try {
|
||
const response = await getNotificationList({
|
||
page: currentPage.value,
|
||
pageSize,
|
||
status: statusFilter.value
|
||
})
|
||
if (response.data) {
|
||
notifications.value = response.data.list
|
||
total.value = response.data.total
|
||
notificationStore.setUnreadCount(response.data.unread_count || 0)
|
||
}
|
||
} catch (error: any) {
|
||
alert(error?.message || '获取系统记录失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const switchFilter = (value: 'all' | 'unread') => {
|
||
statusFilter.value = value
|
||
currentPage.value = 1
|
||
fetchNotifications()
|
||
}
|
||
|
||
const handleMarkRead = async (item: NotificationItem) => {
|
||
try {
|
||
await markNotificationAsRead(item.id)
|
||
item.is_read = true
|
||
item.read_at = new Date().toISOString()
|
||
await notificationStore.fetchUnreadCount()
|
||
if (statusFilter.value === 'unread') {
|
||
fetchNotifications()
|
||
}
|
||
} catch (error: any) {
|
||
alert(error?.message || '标为已查看失败')
|
||
}
|
||
}
|
||
|
||
const handleMarkAllRead = async () => {
|
||
markAllLoading.value = true
|
||
try {
|
||
await markAllNotificationsAsRead()
|
||
await notificationStore.fetchUnreadCount()
|
||
await fetchNotifications()
|
||
} catch (error: any) {
|
||
alert(error?.message || '全部标为已查看失败')
|
||
} finally {
|
||
markAllLoading.value = false
|
||
}
|
||
}
|
||
|
||
const handleMobileMarkRead = async () => {
|
||
const item = mobileActionNotification.value
|
||
mobileActionNotification.value = null
|
||
if (item) await handleMarkRead(item)
|
||
}
|
||
|
||
const closeSyncDetail = () => {
|
||
syncDetailOpen.value = false
|
||
selectedSyncJob.value = null
|
||
}
|
||
|
||
const openSyncDetail = async (syncJobId: number) => {
|
||
syncDetailOpen.value = true
|
||
syncDetailLoading.value = true
|
||
|
||
try {
|
||
const response = await getHrSyncJobDetail(syncJobId)
|
||
selectedSyncJob.value = response.data || null
|
||
} catch (error: any) {
|
||
selectedSyncJob.value = null
|
||
alert(error?.message || '读取同步详情失败')
|
||
} finally {
|
||
syncDetailLoading.value = false
|
||
}
|
||
}
|
||
|
||
const openAction = async (item: NotificationItem) => {
|
||
if (!item.is_read) {
|
||
await handleMarkRead(item)
|
||
}
|
||
|
||
if (item.related_sync_job_id) {
|
||
await openSyncDetail(item.related_sync_job_id)
|
||
return
|
||
}
|
||
|
||
window.location.href = item.action_url || '/settings?tab=sync'
|
||
}
|
||
|
||
const handlePrevPage = () => {
|
||
if (currentPage.value <= 1) return
|
||
currentPage.value -= 1
|
||
fetchNotifications()
|
||
}
|
||
|
||
const handleNextPage = () => {
|
||
if (currentPage.value >= totalPages.value) return
|
||
currentPage.value += 1
|
||
fetchNotifications()
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await notificationStore.fetchUnreadCount()
|
||
await fetchNotifications()
|
||
})
|
||
</script>
|