2026-08-07 11:44:12 +08:00
|
|
|
|
/** 消息通知 — 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 组件
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
2026-08-04 17:10:01 +08:00
|
|
|
|
export default function NotificationsPage() {
|
2026-08-07 11:44:12 +08:00
|
|
|
|
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}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==========================================================
|
|
|
|
|
|
// 渲染
|
|
|
|
|
|
// ==========================================================
|
|
|
|
|
|
|
2026-08-04 17:10:01 +08:00
|
|
|
|
return (
|
2026-08-07 11:44:12 +08:00
|
|
|
|
<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>
|
2026-08-04 17:10:01 +08:00
|
|
|
|
</div>
|
2026-08-07 11:44:12 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 加载中 */}
|
|
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
2026-08-04 17:10:01 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|