Files
track/track-uniapp/src/pages/notify/index.vue
duxingchen b731144a5a fix: 移动端通知已读状态无法持久化
根因:
  handleCardTap 中 try { await markNotificationRead(); item.is_read = true } catch {}
  静默吞噬了 API 错误。PUT /notifications/{id}/read 调用失败时,本地 is_read
  仍被设为 true,红点消失。下次进入页面重新拉取,服务端仍为未读状态。

修复:
  - API 失败时保留 is_read = false,不更新本地状态
  - 弹出Toast提示用户下拉刷新重试
  - 失败时 return 阻止跳转,避免用户误以为已处理
2026-08-12 12:15:36 +08:00

169 lines
5.5 KiB
Vue
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.

<template>
<view class="page">
<view class="header">
<text class="title">消息通知</text>
<text class="subtitle">任务流转和系统通知</text>
</view>
<!-- 加载中 -->
<view v-if="loading" class="center">加载中...</view>
<!-- 空状态 -->
<view v-else-if="notifications.length === 0" class="empty">
<text class="empty-icon">🔔</text>
<text class="empty-text">暂无新消息</text>
</view>
<!-- 通知列表 -->
<view v-else class="list">
<view
v-for="item in notifications"
:key="item.id"
:class="['card', item.is_read ? '' : 'card-unread']"
@tap="handleCardTap(item)"
>
<view class="card-left">
<view v-if="!item.is_read" class="unread-dot" />
<text :class="['type-icon', item.is_read ? 'type-icon-read' : '']">
{{ typeIcon(item.type) }}
</text>
</view>
<view class="card-body">
<view class="card-top">
<text :class="['card-title', item.is_read ? '' : 'card-title-bold']">
{{ typeTitle(item.type) }}
</text>
<text class="card-time">{{ formatTime(item.created_at) }}</text>
</view>
<text class="card-content">{{ item.content }}</text>
</view>
<text class="card-arrow">›</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { getNotifications, markNotificationRead } from "../../api/notification";
const TYPE_CONFIG = {
TRANSFER: { icon: "🟢", title: "新任务派发" },
REJECT: { icon: "🔴", title: "品质驳回提醒" },
COMMENT: { icon: "💬", title: "收到新留言" },
};
const notifications = ref([]);
const loading = ref(true);
let currentUser = null;
onShow(() => {
loadUser();
setTimeout(() => {
if (!currentUser) loadUser();
fetchNotifications();
}, 200);
});
function loadUser() {
try {
let user = uni.getStorageSync("user");
if (typeof user === "string" && user) {
try { user = JSON.parse(user); } catch (e) { user = null; }
}
if (user && typeof user === "object") currentUser = user;
} catch {}
}
async function fetchNotifications() {
const userId = currentUser?.username || currentUser?.id || "";
if (!userId) { loading.value = false; return; }
loading.value = true;
try {
const res = await getNotifications(userId);
notifications.value = res.notifications || [];
} catch {
notifications.value = [];
} finally {
loading.value = false;
}
}
function typeIcon(type) { return (TYPE_CONFIG[type] || { icon: "📌" }).icon; }
function typeTitle(type) { return (TYPE_CONFIG[type] || { title: "系统通知" }).title; }
function formatTime(t) {
if (!t) return "";
const d = new Date(t);
const pad = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
async function handleCardTap(item) {
if (!item.is_read) {
try {
await markNotificationRead(item.id);
item.is_read = true;
} catch {
uni.showToast({ title: "标记已读失败,请下拉刷新重试", icon: "none", duration: 2000 });
return;
}
}
// 🚀 优先使用 product_serial_number,兜底从 content 中解析
let sn = item.product_serial_number || "";
if (!sn && item.content) {
const match = item.content.match(/\[([A-Za-z0-9]{8,16})\]/);
if (match) sn = match[1];
}
if (sn) {
uni.navigateTo({ url: `/pages/scan/detail?serial=${sn}` });
} else if (item.task_id) {
uni.navigateTo({ url: `/pages/scan/detail?taskId=${item.task_id}` });
}
}
</script>
<style scoped>
.page { min-height: 100vh; padding: 16px; padding-bottom: 80px; background: #f3f4f6; }
.header { margin-bottom: 20px; }
.title { font-size: 20px; font-weight: 700; color: #1f2937; display: block; }
.subtitle { font-size: 13px; color: #9ca3af; margin-top: 4px; display: block; }
.center { text-align: center; padding: 80px 0; color: #9ca3af; font-size: 14px; }
.empty { display: flex; flex-direction: column; align-items: center; padding-top: 80px; }
.empty-icon { font-size: 64px; margin-bottom: 12px; }
.empty-text { font-size: 14px; color: #9ca3af; }
.list { display: flex; flex-direction: column; gap: 10px; }
.card {
display: flex; align-items: flex-start; gap: 10px;
background: #fff; border-radius: 12px; padding: 14px 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
position: relative; transition: all 0.2s;
}
.card:active { transform: scale(0.98); }
.card-unread {
box-shadow: 0 1px 6px rgba(37,99,235,0.1);
border-left: 3px solid #2563eb;
}
.card-left { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 28px; flex-shrink: 0; }
.unread-dot {
width: 8px; height: 8px; border-radius: 50%;
background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,0.15);
}
.type-icon { font-size: 20px; line-height: 1; }
.type-icon-read { opacity: 0.5; }
.card-body { flex: 1; min-width: 0; }
.card-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
.card-title { font-size: 15px; font-weight: 600; color: #374151; }
.card-title-bold { color: #1f2937; font-weight: 700; }
.card-time { font-size: 11px; color: #9ca3af; flex-shrink: 0; }
.card-content { font-size: 13px; color: #6b7280; line-height: 1.5; display: block; word-break: break-all; }
.card-arrow { font-size: 20px; color: #d1d5db; margin-top: 6px; flex-shrink: 0; }
</style>