Files
track/frontend/src/pages/NotificationsPage.tsx

165 lines
5.4 KiB
TypeScript
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.

/** 消息通知 — 1:1 复刻 uni-app pages/notify/index.vue + 真实 API */
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import {
getNotifications,
markNotificationRead,
type NotificationItem,
} from "../services/notificationApi";
// ============================================================
// 类型常量
// ============================================================
const TYPE_CONFIG: Record<string, { icon: string; title: string }> = {
TRANSFER: { icon: "🟢", title: "新任务派发" },
REJECT: { icon: "🔴", title: "品质驳回提醒" },
};
function formatTime(t: string) {
if (!t) return "";
const d = new Date(t);
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
// ============================================================
// Page 组件
// ============================================================
export default function NotificationsPage() {
const { user } = useAuth();
const navigate = useNavigate();
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchNotifications();
}, [user]); // eslint-disable-line react-hooks/exhaustive-deps
async function fetchNotifications() {
const userId = user?.username || user?.id || "";
if (!userId) {
setLoading(false);
return;
}
setLoading(true);
try {
const res = await getNotifications(userId);
setNotifications(res.notifications || []);
} catch {
setNotifications([]);
} finally {
setLoading(false);
}
}
async function handleCardTap(item: NotificationItem) {
// 标记已读
if (!item.is_read) {
try {
await markNotificationRead(item.id);
setNotifications((prev) =>
prev.map((n) => (n.id === item.id ? { ...n, is_read: true } : n))
);
} catch {
// 静默
}
}
// 跳转详情页
if (item.task_id) {
navigate(`/scan?taskId=${item.task_id}`);
}
}
// ==========================================================
// 渲染
// ==========================================================
return (
<div className="flex min-h-full flex-col px-4 pt-safe pb-20">
{/* 标题区 — 与 uni-app 完全一致 */}
<div className="mb-5">
<h2 className="text-xl font-bold text-gray-800">消息通知</h2>
<p className="mt-1 text-[13px] text-gray-400">任务流转和系统通知</p>
</div>
{/* 加载中 */}
{loading && (
<div className="flex flex-1 items-center justify-center py-20 text-sm text-gray-400">
加载中...
</div>
)}
{/* 空状态 */}
{!loading && notifications.length === 0 && (
<div className="flex flex-1 flex-col items-center justify-center pb-24">
<span className="text-[64px] leading-none">🔔</span>
<span className="mt-3 text-sm text-gray-400">暂无新消息</span>
</div>
)}
{/* 通知列表 */}
{!loading && notifications.length > 0 && (
<div className="flex flex-col gap-2.5">
{notifications.map((item) => {
const cfg = TYPE_CONFIG[item.type] || {
icon: "📌",
title: "系统通知",
};
return (
<div
key={item.id}
onClick={() => handleCardTap(item)}
className={`flex cursor-pointer items-start gap-2.5 rounded-xl bg-white p-3.5 shadow-[0_1px_3px_rgba(0,0,0,0.06)] transition-all active:scale-[0.98] ${
!item.is_read
? "border-l-[3px] border-l-blue-600 shadow-[0_1px_6px_rgba(37,99,235,0.1)]"
: ""
}`}
>
{/* 左侧:未读红点 + 类型图标 */}
<div className="flex w-7 shrink-0 flex-col items-center gap-1">
{!item.is_read && (
<span className="h-2 w-2 rounded-full bg-red-500 shadow-[0_0_0_3px_rgba(239,68,68,0.15)]" />
)}
<span
className={`text-xl leading-none ${
item.is_read ? "opacity-50" : ""
}`}
>
{cfg.icon}
</span>
</div>
{/* 右侧:内容 */}
<div className="min-w-0 flex-1">
<div className="mb-1.5 flex items-center justify-between gap-2">
<span
className={`text-[15px] font-semibold ${
item.is_read ? "text-gray-600" : "font-bold text-gray-800"
}`}
>
{cfg.title}
</span>
<span className="shrink-0 text-[11px] text-gray-400">
{formatTime(item.created_at)}
</span>
</div>
<p className="text-[13px] leading-relaxed text-gray-500 break-all">
{item.content}
</p>
</div>
<span className="mt-1.5 shrink-0 text-xl text-gray-300">›</span>
</div>
);
})}
</div>
)}
</div>
);
}