问题: 点击通知卡片跳转 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
162 lines
5.3 KiB
Vue
162 lines
5.3 KiB
Vue
<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>
|