feat: Web端401队列拦截器 + 消息/任务/个人中心重写 + TabBar红点 + Admin页面完善
This commit is contained in:
@ -1,29 +1,164 @@
|
||||
/** 消息 — 通知推送 */
|
||||
export default function NotificationsPage() {
|
||||
return (
|
||||
<div className="flex min-h-full flex-col px-4 pt-safe">
|
||||
<h2 className="text-xl font-bold text-gray-800">消息通知</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">任务流转和系统通知</p>
|
||||
/** 消息通知 — 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";
|
||||
|
||||
{/* 占位空状态 */}
|
||||
<div className="mt-12 flex flex-1 flex-col items-center justify-center">
|
||||
<div className="flex h-24 w-24 items-center justify-center rounded-full bg-gray-100">
|
||||
<svg
|
||||
className="h-10 w-10 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-gray-400">暂无新消息</p>
|
||||
// ============================================================
|
||||
// 类型常量
|
||||
// ============================================================
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user