165 lines
5.4 KiB
TypeScript
165 lines
5.4 KiB
TypeScript
/** 消息通知 — 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>
|
||
);
|
||
}
|