feat: Web端401队列拦截器 + 消息/任务/个人中心重写 + TabBar红点 + Admin页面完善

This commit is contained in:
2026-08-07 11:44:12 +08:00
parent b71c5a2d07
commit 721cfe1504
25 changed files with 1730 additions and 435 deletions

View File

@ -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>
);
}