217 lines
6.2 KiB
Vue
217 lines
6.2 KiB
Vue
<script setup>
|
|
import { ref, onMounted, watch } from "vue";
|
|
import Dialog from "primevue/dialog";
|
|
import Button from "primevue/button";
|
|
import { userApi } from "../../api/user";
|
|
|
|
const currentTab = ref("all");
|
|
const dialogVisible = ref(false);
|
|
const selectedNotification = ref(null);
|
|
const loading = ref(false);
|
|
const page = ref(1);
|
|
|
|
const tabs = ref([
|
|
{ label: "全部", value: "all", count: 0 },
|
|
{ label: "系统通知", value: "system", count: 0 },
|
|
{ label: "订单通知", value: "order", count: 0 },
|
|
{ label: "审核通知", value: "audit", count: 0 },
|
|
{ label: "互动消息", value: "interaction", count: 0 },
|
|
]);
|
|
|
|
const notifications = ref([]);
|
|
|
|
const fetchNotifications = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const res = await userApi.getNotifications(currentTab.value, page.value);
|
|
notifications.value = res.items || [];
|
|
} catch (e) {
|
|
console.error("Fetch notifications failed:", e);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
onMounted(fetchNotifications);
|
|
|
|
watch(currentTab, () => {
|
|
page.value = 1;
|
|
fetchNotifications();
|
|
});
|
|
|
|
const getIconStyle = (type) => {
|
|
switch (type) {
|
|
case "system":
|
|
return { bg: "bg-blue-50", color: "text-blue-600", icon: "pi-megaphone" };
|
|
case "order":
|
|
return {
|
|
bg: "bg-green-50",
|
|
color: "text-green-600",
|
|
icon: "pi-shopping-bag",
|
|
};
|
|
case "audit":
|
|
return {
|
|
bg: "bg-orange-50",
|
|
color: "text-orange-600",
|
|
icon: "pi-file-edit",
|
|
};
|
|
case "interaction":
|
|
return {
|
|
bg: "bg-purple-50",
|
|
color: "text-purple-600",
|
|
icon: "pi-comments",
|
|
};
|
|
default:
|
|
return { bg: "bg-slate-100", color: "text-slate-500", icon: "pi-bell" };
|
|
}
|
|
};
|
|
|
|
const handleNotificationClick = async (item) => {
|
|
if (!item.read) {
|
|
try {
|
|
await userApi.markNotificationRead(item.id);
|
|
item.read = true;
|
|
} catch (e) {
|
|
console.error("Mark read failed:", e);
|
|
}
|
|
}
|
|
|
|
if (item.type === "system") {
|
|
selectedNotification.value = item;
|
|
dialogVisible.value = true;
|
|
}
|
|
};
|
|
|
|
const handleMarkAllRead = async () => {
|
|
try {
|
|
await userApi.markAllNotificationsRead();
|
|
notifications.value.forEach((n) => (n.read = true));
|
|
tabs.value.forEach((t) => (t.count = 0));
|
|
} catch (e) {
|
|
console.error("Mark all read failed:", e);
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="bg-white rounded-xl shadow-sm border border-slate-100 min-h-[600px]"
|
|
>
|
|
<!-- Header & Tabs -->
|
|
<div
|
|
class="px-6 pt-6 border-b border-slate-100 flex items-center justify-between"
|
|
>
|
|
<div class="flex items-center gap-8">
|
|
<button
|
|
v-for="tab in tabs"
|
|
:key="tab.value"
|
|
@click="currentTab = tab.value"
|
|
class="pb-4 text-base font-bold transition-colors border-b-2 cursor-pointer focus:outline-none relative"
|
|
:class="
|
|
currentTab === tab.value
|
|
? 'text-primary-600 border-primary-600'
|
|
: 'text-slate-500 border-transparent hover:text-slate-700'
|
|
"
|
|
>
|
|
{{ tab.label }}
|
|
<span
|
|
v-if="tab.count > 0"
|
|
class="absolute -top-1 -right-4 min-w-[1.25rem] h-5 px-1.5 bg-red-500 text-white text-[10px] rounded-full flex items-center justify-center"
|
|
>{{ tab.count }}</span
|
|
>
|
|
</button>
|
|
</div>
|
|
<button
|
|
@click="handleMarkAllRead"
|
|
class="mb-4 text-base font-medium text-slate-500 hover:text-primary-600 cursor-pointer flex items-center gap-1"
|
|
>
|
|
<i class="pi pi-check-circle"></i> 全部已读
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Notification List -->
|
|
<div class="p-0">
|
|
<div v-if="notifications.length > 0">
|
|
<div
|
|
v-for="item in notifications"
|
|
:key="item.id"
|
|
@click="handleNotificationClick(item)"
|
|
class="flex items-start gap-4 p-5 border-b border-slate-50 hover:bg-slate-50 transition-colors cursor-pointer group"
|
|
:class="{ 'bg-blue-50/30': !item.read }"
|
|
>
|
|
<!-- Icon -->
|
|
<div
|
|
class="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0"
|
|
:class="getIconStyle(item.type).bg"
|
|
>
|
|
<i
|
|
class="pi"
|
|
:class="[
|
|
getIconStyle(item.type).icon,
|
|
getIconStyle(item.type).color,
|
|
]"
|
|
></i>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<h3
|
|
class="font-bold text-slate-900 text-lg group-hover:text-primary-600 transition-colors flex items-center gap-2"
|
|
>
|
|
<span
|
|
v-if="!item.read"
|
|
class="w-2 h-2 bg-blue-600 rounded-full inline-block"
|
|
></span>
|
|
{{ item.title }}
|
|
</h3>
|
|
<span class="text-sm text-slate-400 whitespace-nowrap">{{
|
|
item.time
|
|
}}</span>
|
|
</div>
|
|
<p class="text-base text-slate-600 line-clamp-2">
|
|
{{ item.content }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else class="text-center py-20">
|
|
<div
|
|
class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-slate-50 mb-4"
|
|
>
|
|
<i class="pi pi-bell-slash text-2xl text-slate-300"></i>
|
|
</div>
|
|
<p class="text-slate-500 text-lg">暂无消息通知</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Message Modal -->
|
|
<Dialog
|
|
v-model:visible="dialogVisible"
|
|
modal
|
|
:header="selectedNotification?.title"
|
|
:style="{ width: '50rem' }"
|
|
:breakpoints="{ '960px': '75vw', '641px': '90vw' }"
|
|
>
|
|
<div class="p-4">
|
|
<div class="text-slate-500 text-sm mb-4">
|
|
{{ selectedNotification?.time }}
|
|
</div>
|
|
<div class="text-slate-700 leading-relaxed whitespace-pre-wrap">
|
|
{{ selectedNotification?.content }}
|
|
</div>
|
|
</div>
|
|
<template #footer>
|
|
<Button
|
|
label="关闭"
|
|
icon="pi pi-check"
|
|
@click="dialogVisible = false"
|
|
autofocus
|
|
/>
|
|
</template>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|