功能(提醒):统一项目提醒中心与桌面通知链路
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled

增加通用提醒状态、数据库迁移和定时同步,覆盖风险时效、文件回执、项目里程碑、访视窗口与协作申请。

新增网页端和桌面端提醒中心、真实投递诊断与固定隐私通知正文,并补齐登录来源聚合、测试和说明文档。
This commit is contained in:
Cheng Zhou
2026-07-16 16:50:34 +08:00
parent 88bd0c5942
commit 3e77127687
50 changed files with 2894 additions and 247 deletions
@@ -1,6 +1,10 @@
import { computed, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { listGeneralNotifications, markGeneralNotificationRead } from "../api/notifications";
import {
listGeneralNotifications,
markAllGeneralNotificationsRead,
markGeneralNotificationRead,
} from "../api/notifications";
import { useStudyStore } from "../store/study";
import type { GeneralNotificationFeed, GeneralNotificationItem } from "../types/notifications";
@@ -32,7 +36,7 @@ export const useProjectNotifications = () => {
const route = useRoute();
const router = useRouter();
const headerRemindersLoading = ref(false);
const feed = ref<GeneralNotificationFeed>({ unread_count: 0, items: [] });
const feed = ref<GeneralNotificationFeed>({ unread_count: 0, total_count: 0, items: [] });
let requestId = 0;
let pollTimer: number | undefined;
@@ -40,12 +44,12 @@ export const useProjectNotifications = () => {
const studyId = study.currentStudy?.id;
const currentRequest = ++requestId;
if (!studyId || route.path.startsWith("/admin")) {
feed.value = { unread_count: 0, items: [] };
feed.value = { unread_count: 0, total_count: 0, items: [] };
return;
}
headerRemindersLoading.value = true;
try {
const { data } = await listGeneralNotifications(studyId, 10);
const { data } = await listGeneralNotifications(studyId, { limit: 10 });
if (currentRequest === requestId && study.currentStudy?.id === studyId) feed.value = data;
} catch {
// 短时网络异常时保留当前内存中的提醒,避免角标在轮询失败时闪烁消失。
@@ -67,6 +71,17 @@ export const useProjectNotifications = () => {
const headerReminderBadgeValue = computed(() => headerReminderTotal.value > 99 ? "99+" : headerReminderTotal.value);
const handleReminderCommand = async (command: string) => {
if (command === "notification:center") {
await router.push("/project/notifications");
return;
}
if (command === "notification:read-all") {
const studyId = study.currentStudy?.id;
if (!studyId || feed.value.unread_count <= 0) return;
await markAllGeneralNotificationsRead(studyId);
await loadHeaderReminders();
return;
}
if (!command.startsWith("notification:")) return;
const notificationId = command.slice("notification:".length);
const item = feed.value.items.find((candidate) => candidate.id === notificationId);
@@ -75,10 +90,15 @@ export const useProjectNotifications = () => {
if (!item.read_at) {
item.read_at = new Date().toISOString();
feed.value.unread_count = Math.max(0, feed.value.unread_count - 1);
await markGeneralNotificationRead(studyId, item.id).catch(() => {
const response = await markGeneralNotificationRead(studyId, item.id).catch(() => {
item.read_at = null;
feed.value.unread_count += 1;
return null;
});
if (response?.data.resolved_at) {
feed.value.items = feed.value.items.filter((candidate) => candidate.id !== item.id);
feed.value.total_count = Math.max(0, feed.value.total_count - 1);
}
}
if (item.action_path?.startsWith("/")) await router.push(item.action_path);
};