Files
track/track-uniapp/src/pages/notify/index.vue
duxingchen 40a2d87345 fix: 消息通知页不能点进详情 — 补全 product_serial_number
问题: 点击通知卡片跳转 detail?taskId=xxx,但 detail.vue onLoad 只认 serial 参数导致空白页

修复:
1. 后端 NotificationResponse 新增 product_serial_number 字段
2. 后端 notifications API 联表 tasks+products 批量填充 serial
3. 前端 notify/index.vue handleCardTap 优先用 product_serial_number 跳转,兜底从 content 正则解析
4. 前端 detail.vue onLoad 新增 taskId 兜底 → doQueryByTask 反查 product_sn
2026-08-11 15:56:25 +08:00

162 lines
5.3 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: "品质驳回提醒" },
};
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 {}
}
// 🚀 优先使用 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>