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
This commit is contained in:
@ -4,9 +4,12 @@ import uuid
|
|||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
from sqlalchemy import select, func
|
from sqlalchemy import select, func
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.models.notification import Notification
|
from app.models.notification import Notification
|
||||||
|
from app.models.task import Task
|
||||||
|
from app.models.product import Product
|
||||||
from app.schemas.notification import NotificationResponse, NotificationListResponse
|
from app.schemas.notification import NotificationResponse, NotificationListResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/notifications", tags=["消息通知"])
|
router = APIRouter(prefix="/notifications", tags=["消息通知"])
|
||||||
@ -46,10 +49,28 @@ async def list_notifications(
|
|||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
notifications = result.scalars().all()
|
notifications = result.scalars().all()
|
||||||
|
|
||||||
|
# 🚀 批量查询关联的 product_serial_number
|
||||||
|
task_ids = [n.task_id for n in notifications if n.task_id]
|
||||||
|
serial_map: dict[uuid.UUID, str] = {}
|
||||||
|
if task_ids:
|
||||||
|
task_result = await db.execute(
|
||||||
|
select(Task.id, Product.serial_number)
|
||||||
|
.join(Product, Task.product_id == Product.id)
|
||||||
|
.where(Task.id.in_(task_ids))
|
||||||
|
)
|
||||||
|
for row in task_result:
|
||||||
|
serial_map[row[0]] = row[1]
|
||||||
|
|
||||||
|
# 组装响应
|
||||||
|
response_list: list[NotificationResponse] = []
|
||||||
|
for n in notifications:
|
||||||
|
resp = NotificationResponse.model_validate(n)
|
||||||
|
if n.task_id and n.task_id in serial_map:
|
||||||
|
resp.product_serial_number = serial_map[n.task_id]
|
||||||
|
response_list.append(resp)
|
||||||
|
|
||||||
return NotificationListResponse(
|
return NotificationListResponse(
|
||||||
notifications=[
|
notifications=response_list,
|
||||||
NotificationResponse.model_validate(n) for n in notifications
|
|
||||||
],
|
|
||||||
total=total,
|
total=total,
|
||||||
unread_count=unread_count,
|
unread_count=unread_count,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -13,6 +13,7 @@ class NotificationResponse(BaseModel):
|
|||||||
content: str
|
content: str
|
||||||
type: str
|
type: str
|
||||||
task_id: uuid.UUID | None = None
|
task_id: uuid.UUID | None = None
|
||||||
|
product_serial_number: str | None = None
|
||||||
is_read: bool
|
is_read: bool
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
|
|||||||
@ -103,7 +103,15 @@ async function handleCardTap(item) {
|
|||||||
if (!item.is_read) {
|
if (!item.is_read) {
|
||||||
try { await markNotificationRead(item.id); item.is_read = true; } catch {}
|
try { await markNotificationRead(item.id); item.is_read = true; } catch {}
|
||||||
}
|
}
|
||||||
if (item.task_id) {
|
// 🚀 优先使用 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}` });
|
uni.navigateTo({ url: `/pages/scan/detail?taskId=${item.task_id}` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -246,7 +246,7 @@ export default {
|
|||||||
},
|
},
|
||||||
msgUnreadCount() { if (!this.lastMsgSeenAt) return this.messages.length; return this.messages.filter(m => m.created_at > this.lastMsgSeenAt).length; },
|
msgUnreadCount() { if (!this.lastMsgSeenAt) return this.messages.length; return this.messages.filter(m => m.created_at > this.lastMsgSeenAt).length; },
|
||||||
},
|
},
|
||||||
onLoad(options) { this.loadUsers(); this.loadCurrentUser(); const sn = options.serial || ""; if (sn) this.doQuery(sn); },
|
onLoad(options) { this.loadUsers(); this.loadCurrentUser(); const sn = options.serial || ""; if (sn) { this.doQuery(sn); return; } /* 🚀 兜底: 从 taskId 反查 product */ const tid = options.taskId || ""; if (tid) this.doQueryByTask(tid); },
|
||||||
// 🚀 onShow 生命周期:每次页面显示时刷新留言板(解决从聊天室退回不更新问题)
|
// 🚀 onShow 生命周期:每次页面显示时刷新留言板(解决从聊天室退回不更新问题)
|
||||||
onShow() { if (this.product?.id) { this.fetchMessages(); } },
|
onShow() { if (this.product?.id) { this.fetchMessages(); } },
|
||||||
methods: {
|
methods: {
|
||||||
@ -256,6 +256,8 @@ export default {
|
|||||||
statusColor(s) { switch (s) { case "PENDING": return "s-yellow"; case "WIP": return "s-blue"; case "COMPLETED": return "s-green"; case "REJECTED": return "s-red"; default: return "s-gray"; } },
|
statusColor(s) { switch (s) { case "PENDING": return "s-yellow"; case "WIP": return "s-blue"; case "COMPLETED": return "s-green"; case "REJECTED": return "s-red"; default: return "s-gray"; } },
|
||||||
|
|
||||||
async doQuery(sn) { this.loading = true; this.error = ""; try { this.product = await get(`/products/scan/${sn}`); this.$nextTick(() => { this.currentMode = 'workspace'; this.autoLockTaskId = this.findMyImmersiveTask() || ''; if (!this.product.task_tree || !this.product.task_tree.length) { this.openCreateFirstTask(); } }); this.fetchMessages(); } catch (e) { this.error = e?.data?.detail || "查询失败"; } finally { this.loading = false; } },
|
async doQuery(sn) { this.loading = true; this.error = ""; try { this.product = await get(`/products/scan/${sn}`); this.$nextTick(() => { this.currentMode = 'workspace'; this.autoLockTaskId = this.findMyImmersiveTask() || ''; if (!this.product.task_tree || !this.product.task_tree.length) { this.openCreateFirstTask(); } }); this.fetchMessages(); } catch (e) { this.error = e?.data?.detail || "查询失败"; } finally { this.loading = false; } },
|
||||||
|
// 🚀 从 taskId 反查 product_serial → 再 doQuery
|
||||||
|
async doQueryByTask(tid) { try { const task = await get(`/tasks/${tid}`); const sn = task?.product_sn || ""; if (sn) { this.doQuery(sn); } else { this.error = "未找到关联产品"; this.loading = false; } } catch { this.error = "任务查询失败"; this.loading = false; } },
|
||||||
findMyImmersiveTask() {
|
findMyImmersiveTask() {
|
||||||
// 🚀 扫描用户的 WIP/PENDING 任务,自动沉浸锁定
|
// 🚀 扫描用户的 WIP/PENDING 任务,自动沉浸锁定
|
||||||
if (!this.product || !this.product.task_tree) return null;
|
if (!this.product || !this.product.task_tree) return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user