merge account p7 notification intensity
# Conflicts: # docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md
This commit is contained in:
commit
77fa288a81
|
|
@ -879,7 +879,7 @@ Shell / Integration owner 负责:
|
|||
|
||||
- `cd frontend && npm run test -- src/features/account-settings/accountSettings.test.ts` 通过:`11 passed`。
|
||||
- `cd frontend && npm run typecheck` 通过。
|
||||
- `cd frontend && npm run build` 待本包最终验证记录。
|
||||
- `cd frontend && npm run build` 通过;Vite 仍提示既有 manual chunk circular warning(`ai-steward / sstc / charts / governance` chunk 环),构建成功。
|
||||
|
||||
### 2026-04-26 / big-module-integration-p6 / done
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,15 @@ import { getAuthToken } from "@/lib/api";
|
|||
export type AccountLanguage = "zh-CN" | "en-US";
|
||||
export type AccountAnswerDepth = "concise" | "balanced" | "deep";
|
||||
export type AccountEvidenceDisplay = "summary" | "inline" | "full";
|
||||
export type AccountReminderIntensity = "silent" | "normal" | "prominent";
|
||||
export type AccountNotificationEvidenceDisplay = "compact" | "expanded";
|
||||
export type AccountNotificationKey = "pendingConfirmation" | "executionWriteback" | "releaseGate" | "failureAlert";
|
||||
export type AccountSettingsSaveScope = "local" | "account";
|
||||
export type AccountReminderConsumptionEvent =
|
||||
| "change_review_high_risk"
|
||||
| "iteration_draft_ready"
|
||||
| "release_blocked"
|
||||
| "execution_stalled";
|
||||
export type AccountPostReleaseNotificationEvent =
|
||||
| "feedback_received"
|
||||
| "revenue_signal"
|
||||
|
|
@ -35,6 +42,8 @@ export interface AccountNotificationPreference {
|
|||
enabled: boolean;
|
||||
desktop: boolean;
|
||||
emailDigest: boolean;
|
||||
reminderIntensity?: AccountReminderIntensity;
|
||||
evidenceDisplay?: AccountNotificationEvidenceDisplay;
|
||||
}
|
||||
|
||||
export type AccountNotificationSettings = Record<AccountNotificationKey, AccountNotificationPreference>;
|
||||
|
|
@ -47,20 +56,46 @@ export interface AccountNotificationConsumption {
|
|||
productArea: string;
|
||||
trigger: string;
|
||||
enabled: boolean;
|
||||
reminderIntensity: AccountReminderIntensity;
|
||||
evidenceDisplay: AccountNotificationEvidenceDisplay;
|
||||
showInAppReminder: boolean;
|
||||
showProminentReminder: boolean;
|
||||
desktopPreference: boolean;
|
||||
emailDigestPreference: boolean;
|
||||
statusLabel: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface AccountReminderConsumptionContract {
|
||||
event: AccountReminderConsumptionEvent;
|
||||
notificationKey: AccountNotificationKey;
|
||||
surface: Exclude<AccountNotificationConsumptionSurface, "system_failure">;
|
||||
productArea: string;
|
||||
title: string;
|
||||
reason: string;
|
||||
reminderIntensity: AccountReminderIntensity;
|
||||
evidenceDisplay: AccountNotificationEvidenceDisplay;
|
||||
showInAppReminder: boolean;
|
||||
showProminentReminder: boolean;
|
||||
desktopPreference: boolean;
|
||||
emailDigestPreference: boolean;
|
||||
notificationTransport: "in_app_only";
|
||||
evidencePolicy: "preserve_business_evidence";
|
||||
auditPolicy: "preserve_audit_state";
|
||||
mutedDetail: string;
|
||||
}
|
||||
|
||||
export interface AccountReleaseGateNotificationPreference {
|
||||
event: AccountReleaseNotificationEvent;
|
||||
notificationKey: AccountNotificationKey;
|
||||
surface: "release_growth" | "system_failure";
|
||||
inAppEnabled: boolean;
|
||||
reminderIntensity: AccountReminderIntensity;
|
||||
evidenceDisplay: AccountNotificationEvidenceDisplay;
|
||||
showProminentReminder: boolean;
|
||||
desktopPreference: boolean;
|
||||
emailDigestPreference: boolean;
|
||||
notificationTransport: "in_app_only";
|
||||
title: string;
|
||||
reason: string;
|
||||
}
|
||||
|
|
@ -72,8 +107,12 @@ export interface AccountPostReleaseNotificationPreference {
|
|||
spaceReadable: boolean;
|
||||
inAppEnabled: boolean;
|
||||
showInAppReminder: boolean;
|
||||
reminderIntensity: AccountReminderIntensity;
|
||||
evidenceDisplay: AccountNotificationEvidenceDisplay;
|
||||
showProminentReminder: boolean;
|
||||
desktopPreference: boolean;
|
||||
emailDigestPreference: boolean;
|
||||
notificationTransport: "in_app_only";
|
||||
title: string;
|
||||
reason: string;
|
||||
evidencePolicy: "preserve_business_evidence";
|
||||
|
|
@ -156,6 +195,36 @@ const accountNotificationConsumptionMeta: Record<
|
|||
},
|
||||
};
|
||||
|
||||
const reminderConsumptionEventMeta: Record<
|
||||
AccountReminderConsumptionEvent,
|
||||
{ notificationKey: AccountNotificationKey; surface: Exclude<AccountNotificationConsumptionSurface, "system_failure">; productArea: string; reason: string }
|
||||
> = {
|
||||
change_review_high_risk: {
|
||||
notificationKey: "pendingConfirmation",
|
||||
surface: "change_review",
|
||||
productArea: "Change Review",
|
||||
reason: "高风险 ChangeSet 或 ChangeItem 进入待审时使用待确认偏好控制页面内提醒表达。",
|
||||
},
|
||||
iteration_draft_ready: {
|
||||
notificationKey: "pendingConfirmation",
|
||||
surface: "change_review",
|
||||
productArea: "Change Review",
|
||||
reason: "下一轮迭代建议草案准备好、需要人工拍板时使用待确认偏好控制页面内提醒表达。",
|
||||
},
|
||||
release_blocked: {
|
||||
notificationKey: "releaseGate",
|
||||
surface: "release_growth",
|
||||
productArea: "Release / Growth",
|
||||
reason: "发布、合规或增长验证受阻时使用发布门禁偏好控制页面内提醒表达。",
|
||||
},
|
||||
execution_stalled: {
|
||||
notificationKey: "executionWriteback",
|
||||
surface: "execution_bridge",
|
||||
productArea: "Execution Bridge",
|
||||
reason: "执行链路停滞、长时间无进展或等待运行端处理时使用执行回写偏好控制页面内提醒表达。",
|
||||
},
|
||||
};
|
||||
|
||||
const releaseNotificationEventMeta: Record<
|
||||
AccountReleaseNotificationEvent,
|
||||
{ notificationKey: AccountNotificationKey; surface: "release_growth" | "system_failure"; reason: string }
|
||||
|
|
@ -231,10 +300,34 @@ export function createDefaultAccountSettings(seed: AccountSettingsSeed = {}): Ac
|
|||
evidenceDisplay: "inline",
|
||||
},
|
||||
notifications: {
|
||||
pendingConfirmation: { enabled: true, desktop: true, emailDigest: false },
|
||||
executionWriteback: { enabled: true, desktop: true, emailDigest: false },
|
||||
releaseGate: { enabled: true, desktop: true, emailDigest: true },
|
||||
failureAlert: { enabled: true, desktop: true, emailDigest: true },
|
||||
pendingConfirmation: {
|
||||
enabled: true,
|
||||
desktop: true,
|
||||
emailDigest: false,
|
||||
reminderIntensity: "normal",
|
||||
evidenceDisplay: "compact",
|
||||
},
|
||||
executionWriteback: {
|
||||
enabled: true,
|
||||
desktop: true,
|
||||
emailDigest: false,
|
||||
reminderIntensity: "normal",
|
||||
evidenceDisplay: "compact",
|
||||
},
|
||||
releaseGate: {
|
||||
enabled: true,
|
||||
desktop: true,
|
||||
emailDigest: true,
|
||||
reminderIntensity: "normal",
|
||||
evidenceDisplay: "compact",
|
||||
},
|
||||
failureAlert: {
|
||||
enabled: true,
|
||||
desktop: true,
|
||||
emailDigest: true,
|
||||
reminderIntensity: "normal",
|
||||
evidenceDisplay: "compact",
|
||||
},
|
||||
},
|
||||
saveScope: "local",
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
|
@ -280,10 +373,7 @@ function cacheAccountSettingsSnapshot(settings: AccountSettings): void {
|
|||
function mergeAccountSettings(base: AccountSettings, stored: Partial<AccountSettings> | null): AccountSettings {
|
||||
if (!stored) return base;
|
||||
const mergedNotifications = accountNotificationKeys.reduce((result, key) => {
|
||||
result[key] = {
|
||||
...base.notifications[key],
|
||||
...(stored.notifications?.[key] || {}),
|
||||
};
|
||||
result[key] = normalizeNotificationPreference(base.notifications[key], stored.notifications?.[key]);
|
||||
return result;
|
||||
}, {} as AccountNotificationSettings);
|
||||
return {
|
||||
|
|
@ -301,6 +391,49 @@ function mergeAccountSettings(base: AccountSettings, stored: Partial<AccountSett
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeNotificationPreference(
|
||||
base: AccountNotificationPreference,
|
||||
stored?: Partial<AccountNotificationPreference>
|
||||
): AccountNotificationPreference {
|
||||
const raw = {
|
||||
...base,
|
||||
...(stored || {}),
|
||||
};
|
||||
const storedIntensity = stored?.reminderIntensity;
|
||||
const reminderIntensity: AccountReminderIntensity =
|
||||
storedIntensity === "silent" || storedIntensity === "normal" || storedIntensity === "prominent"
|
||||
? storedIntensity
|
||||
: raw.enabled
|
||||
? "normal"
|
||||
: "silent";
|
||||
const evidenceDisplay: AccountNotificationEvidenceDisplay =
|
||||
raw.evidenceDisplay === "expanded" ? "expanded" : "compact";
|
||||
return {
|
||||
...raw,
|
||||
enabled: reminderIntensity !== "silent",
|
||||
desktop: reminderIntensity !== "silent" && Boolean(raw.desktop),
|
||||
emailDigest: reminderIntensity !== "silent" && Boolean(raw.emailDigest),
|
||||
reminderIntensity,
|
||||
evidenceDisplay,
|
||||
};
|
||||
}
|
||||
|
||||
function getNotificationReminderIntensity(preference: AccountNotificationPreference): AccountReminderIntensity {
|
||||
return preference.reminderIntensity === "silent" ||
|
||||
preference.reminderIntensity === "normal" ||
|
||||
preference.reminderIntensity === "prominent"
|
||||
? preference.reminderIntensity
|
||||
: preference.enabled
|
||||
? "normal"
|
||||
: "silent";
|
||||
}
|
||||
|
||||
function getNotificationEvidenceDisplay(
|
||||
preference: AccountNotificationPreference
|
||||
): AccountNotificationEvidenceDisplay {
|
||||
return preference.evidenceDisplay === "expanded" ? "expanded" : "compact";
|
||||
}
|
||||
|
||||
export function shouldShowAccountNotification(
|
||||
settings: AccountSettings | Partial<AccountSettings> | null | undefined,
|
||||
key: AccountNotificationKey
|
||||
|
|
@ -317,7 +450,9 @@ export function getAccountNotificationConsumption(
|
|||
const preference = normalized.notifications[key];
|
||||
const label = accountNotificationLabels[key];
|
||||
const meta = accountNotificationConsumptionMeta[key];
|
||||
const enabled = Boolean(preference.enabled);
|
||||
const reminderIntensity = getNotificationReminderIntensity(preference);
|
||||
const evidenceDisplay = getNotificationEvidenceDisplay(preference);
|
||||
const enabled = reminderIntensity !== "silent" && Boolean(preference.enabled);
|
||||
return {
|
||||
key,
|
||||
surface: meta.surface,
|
||||
|
|
@ -325,17 +460,56 @@ export function getAccountNotificationConsumption(
|
|||
productArea: meta.productArea,
|
||||
trigger: meta.trigger,
|
||||
enabled,
|
||||
reminderIntensity,
|
||||
evidenceDisplay,
|
||||
showInAppReminder: enabled,
|
||||
showProminentReminder: enabled && reminderIntensity === "prominent",
|
||||
desktopPreference: enabled && Boolean(preference.desktop),
|
||||
emailDigestPreference: enabled && Boolean(preference.emailDigest),
|
||||
statusLabel: enabled ? "页面内提醒显示" : "页面内提醒静默",
|
||||
statusLabel: enabled
|
||||
? reminderIntensity === "prominent"
|
||||
? "页面内强提醒"
|
||||
: "页面内提醒显示"
|
||||
: "页面内提醒静默",
|
||||
detail: enabled
|
||||
? `${meta.productArea} 会显示该类页面内提醒;桌面/邮件只记录偏好,不代表系统级推送已接入。`
|
||||
? `${meta.productArea} 会按 ${reminderIntensity === "prominent" ? "强提醒" : "普通提醒"} 展示页面内提醒,证据以 ${evidenceDisplay === "expanded" ? "展开" : "紧凑"} 模式呈现;桌面/邮件只记录偏好,不代表系统级推送已接入。`
|
||||
: `${meta.productArea} 不显示该类页面内提醒;底层对象、状态和审计记录仍保留。`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function getAccountReminderConsumption(
|
||||
settings: AccountSettings | Partial<AccountSettings> | null | undefined,
|
||||
event: AccountReminderConsumptionEvent
|
||||
): AccountReminderConsumptionContract {
|
||||
const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null);
|
||||
const meta = reminderConsumptionEventMeta[event];
|
||||
const preference = normalized.notifications[meta.notificationKey];
|
||||
const reminderIntensity = getNotificationReminderIntensity(preference);
|
||||
const evidenceDisplay = getNotificationEvidenceDisplay(preference);
|
||||
const enabled = reminderIntensity !== "silent" && Boolean(preference.enabled);
|
||||
return {
|
||||
event,
|
||||
notificationKey: meta.notificationKey,
|
||||
surface: meta.surface,
|
||||
productArea: meta.productArea,
|
||||
title: accountNotificationLabels[meta.notificationKey].title,
|
||||
reason: meta.reason,
|
||||
reminderIntensity,
|
||||
evidenceDisplay,
|
||||
showInAppReminder: enabled,
|
||||
showProminentReminder: enabled && reminderIntensity === "prominent",
|
||||
desktopPreference: enabled && Boolean(preference.desktop),
|
||||
emailDigestPreference: enabled && Boolean(preference.emailDigest),
|
||||
notificationTransport: "in_app_only",
|
||||
evidencePolicy: "preserve_business_evidence",
|
||||
auditPolicy: "preserve_audit_state",
|
||||
mutedDetail: enabled
|
||||
? "仅影响页面内提醒强度和证据展开方式;不接真实邮件、桌面推送或系统通知中心。"
|
||||
: "仅静默页面内提醒;真实证据、ChangeSet、recommendation、执行状态和审计状态仍必须按业务页面展示。",
|
||||
};
|
||||
}
|
||||
|
||||
export function getReleaseGateNotificationPreference(
|
||||
settings: AccountSettings | Partial<AccountSettings> | null | undefined,
|
||||
event: AccountReleaseNotificationEvent = "release_gate_blocked"
|
||||
|
|
@ -343,14 +517,20 @@ export function getReleaseGateNotificationPreference(
|
|||
const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null);
|
||||
const meta = releaseNotificationEventMeta[event];
|
||||
const preference = normalized.notifications[meta.notificationKey];
|
||||
const enabled = Boolean(preference.enabled);
|
||||
const reminderIntensity = getNotificationReminderIntensity(preference);
|
||||
const evidenceDisplay = getNotificationEvidenceDisplay(preference);
|
||||
const enabled = reminderIntensity !== "silent" && Boolean(preference.enabled);
|
||||
return {
|
||||
event,
|
||||
notificationKey: meta.notificationKey,
|
||||
surface: meta.surface,
|
||||
inAppEnabled: enabled,
|
||||
reminderIntensity,
|
||||
evidenceDisplay,
|
||||
showProminentReminder: enabled && reminderIntensity === "prominent",
|
||||
desktopPreference: enabled && Boolean(preference.desktop),
|
||||
emailDigestPreference: enabled && Boolean(preference.emailDigest),
|
||||
notificationTransport: "in_app_only",
|
||||
title: accountNotificationLabels[meta.notificationKey].title,
|
||||
reason: meta.reason,
|
||||
};
|
||||
|
|
@ -363,7 +543,9 @@ export function getPostReleaseNotificationPreference(
|
|||
const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null);
|
||||
const meta = postReleaseNotificationEventMeta[event];
|
||||
const preference = normalized.notifications[meta.notificationKey];
|
||||
const enabled = Boolean(preference.enabled);
|
||||
const reminderIntensity = getNotificationReminderIntensity(preference);
|
||||
const evidenceDisplay = getNotificationEvidenceDisplay(preference);
|
||||
const enabled = reminderIntensity !== "silent" && Boolean(preference.enabled);
|
||||
return {
|
||||
event,
|
||||
notificationKey: meta.notificationKey,
|
||||
|
|
@ -371,8 +553,12 @@ export function getPostReleaseNotificationPreference(
|
|||
spaceReadable: true,
|
||||
inAppEnabled: enabled,
|
||||
showInAppReminder: enabled,
|
||||
reminderIntensity,
|
||||
evidenceDisplay,
|
||||
showProminentReminder: enabled && reminderIntensity === "prominent",
|
||||
desktopPreference: enabled && Boolean(preference.desktop),
|
||||
emailDigestPreference: enabled && Boolean(preference.emailDigest),
|
||||
notificationTransport: "in_app_only",
|
||||
title: accountNotificationLabels[meta.notificationKey].title,
|
||||
reason: meta.reason,
|
||||
evidencePolicy: "preserve_business_evidence",
|
||||
|
|
|
|||
|
|
@ -25,8 +25,10 @@ import {
|
|||
type AccountAnswerDepth,
|
||||
type AccountEvidenceDisplay,
|
||||
type AccountLanguage,
|
||||
type AccountNotificationEvidenceDisplay,
|
||||
type AccountNotificationKey,
|
||||
type AccountProfileSettings,
|
||||
type AccountReminderIntensity,
|
||||
type AccountSettings,
|
||||
type AccountSettingsSaveScope,
|
||||
} from "@/api/accountSettings";
|
||||
|
|
@ -58,6 +60,17 @@ const languageOptions: Array<{ value: AccountLanguage; title: string; detail: st
|
|||
{ value: "en-US", title: "English", detail: "English replies when product context allows it" },
|
||||
];
|
||||
|
||||
const reminderIntensityOptions: Array<{ value: AccountReminderIntensity; title: string; detail: string }> = [
|
||||
{ value: "silent", title: "静默", detail: "不显示页面内提醒,业务对象和证据仍保留" },
|
||||
{ value: "normal", title: "普通", detail: "在对应页面显示标准提醒" },
|
||||
{ value: "prominent", title: "强提醒", detail: "用于高风险、阻塞或长时间停滞场景" },
|
||||
];
|
||||
|
||||
const notificationEvidenceOptions: Array<{ value: AccountNotificationEvidenceDisplay; title: string; detail: string }> = [
|
||||
{ value: "compact", title: "紧凑", detail: "提醒旁只显示关键证据摘要" },
|
||||
{ value: "expanded", title: "展开", detail: "提醒旁展开来源、状态和审计线索" },
|
||||
];
|
||||
|
||||
function getDisplayName(user: ReturnType<typeof useAuthStore.getState>["user"]) {
|
||||
return user?.name || user?.username || "张子墨";
|
||||
}
|
||||
|
|
@ -312,7 +325,7 @@ function MobileAccountSettings({
|
|||
<MobileGroup title="设置">
|
||||
<MobileSettingRow icon={<UserRound className="h-5 w-5" />} title="个人资料" detail={`${draft.profile.displayName} · ${draft.profile.roleTitle}`} onClick={() => onOpenSheet("profile")} />
|
||||
<MobileSettingRow icon={<SlidersHorizontal className="h-5 w-5" />} title="AI 偏好" detail={`${getAnswerDepthTitle(draft.aiPreferences.answerDepth)} · ${getEvidenceTitle(draft.aiPreferences.evidenceDisplay)}`} onClick={() => onOpenSheet("ai")} />
|
||||
<MobileSettingRow icon={<Bell className="h-5 w-5" />} title="通知偏好" detail={`${getEnabledNotificationCount(draft)} 类提醒已开启`} onClick={() => onOpenSheet("notifications")} />
|
||||
<MobileSettingRow icon={<Bell className="h-5 w-5" />} title="通知偏好" detail={`${getEnabledNotificationCount(draft)} 类提醒已开启 · ${getProminentNotificationCount(draft)} 类强提醒`} onClick={() => onOpenSheet("notifications")} />
|
||||
</MobileGroup>
|
||||
|
||||
<MobileGroup title="账号操作">
|
||||
|
|
@ -570,6 +583,8 @@ function NotificationFields({
|
|||
{accountNotificationKeys.map((key) => {
|
||||
const item = notifications[key];
|
||||
const label = accountNotificationLabels[key];
|
||||
const reminderIntensity = item.reminderIntensity || (item.enabled ? "normal" : "silent");
|
||||
const evidenceDisplay = item.evidenceDisplay || "compact";
|
||||
return (
|
||||
<div key={key} className={cn("rounded-lg border border-[#e6edf7] bg-white p-4", mobile && "px-3")}>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
|
|
@ -578,22 +593,48 @@ function NotificationFields({
|
|||
<div className="mt-1 text-sm leading-6 text-slate-500">{label.detail}</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={item.enabled}
|
||||
onCheckedChange={(enabled) => onPatchNotification(key, { enabled })}
|
||||
checked={reminderIntensity !== "silent" && item.enabled}
|
||||
onCheckedChange={(enabled) =>
|
||||
onPatchNotification(key, {
|
||||
enabled,
|
||||
reminderIntensity: enabled ? (reminderIntensity === "silent" ? "normal" : reminderIntensity) : "silent",
|
||||
})
|
||||
}
|
||||
className="mt-1 data-[state=checked]:bg-[#2463EB]"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-4 xl:grid-cols-2">
|
||||
<LabeledControl label="提醒强度">
|
||||
<SegmentedOptions
|
||||
options={reminderIntensityOptions}
|
||||
value={reminderIntensity}
|
||||
onChange={(reminderIntensity) =>
|
||||
onPatchNotification(key, {
|
||||
reminderIntensity,
|
||||
enabled: reminderIntensity !== "silent",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</LabeledControl>
|
||||
<LabeledControl label="证据展示">
|
||||
<SegmentedOptions
|
||||
options={notificationEvidenceOptions}
|
||||
value={evidenceDisplay}
|
||||
onChange={(evidenceDisplay) => onPatchNotification(key, { evidenceDisplay })}
|
||||
/>
|
||||
</LabeledControl>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-2 sm:grid-cols-2">
|
||||
<ChannelToggle
|
||||
label="桌面提醒"
|
||||
checked={item.desktop}
|
||||
disabled={!item.enabled}
|
||||
checked={reminderIntensity !== "silent" && item.enabled && item.desktop}
|
||||
disabled={reminderIntensity === "silent" || !item.enabled}
|
||||
onChange={(desktop) => onPatchNotification(key, { desktop })}
|
||||
/>
|
||||
<ChannelToggle
|
||||
label="邮件摘要"
|
||||
checked={item.emailDigest}
|
||||
disabled={!item.enabled}
|
||||
checked={reminderIntensity !== "silent" && item.enabled && item.emailDigest}
|
||||
disabled={reminderIntensity === "silent" || !item.enabled}
|
||||
onChange={(emailDigest) => onPatchNotification(key, { emailDigest })}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -620,7 +661,7 @@ function NotificationConsumptionPreview({
|
|||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold text-slate-950">页面内提醒消费说明</div>
|
||||
<p className="mt-1 text-xs leading-5 text-slate-500">
|
||||
这些开关只控制 PMFlow 页面里的提醒展示:待确认、执行回写、发布门禁和失败提醒。当前不代表系统级桌面推送、邮件推送或真实通知中心已接入。
|
||||
这些偏好只控制 PMFlow 页面里的提醒强度和证据展开方式:待确认、执行回写、发布门禁和失败提醒。当前不代表系统级桌面推送、邮件推送或真实通知中心已接入。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -631,14 +672,21 @@ function NotificationConsumptionPreview({
|
|||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-semibold text-slate-900">{item.productArea}</div>
|
||||
<div className="mt-1 text-xs leading-5 text-slate-500">{item.trigger}</div>
|
||||
<div className="mt-2 text-xs font-semibold text-slate-500">
|
||||
{getReminderIntensityTitle(item.reminderIntensity || (item.enabled ? "normal" : "silent"))} · {getNotificationEvidenceTitle(item.evidenceDisplay || "compact")}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 rounded-full px-2.5 py-1 text-[11px] font-semibold",
|
||||
item.showInAppReminder ? "bg-[#eef4ff] text-[#2463EB]" : "bg-slate-100 text-slate-500"
|
||||
item.showProminentReminder
|
||||
? "bg-orange-50 text-orange-600"
|
||||
: item.showInAppReminder
|
||||
? "bg-[#eef4ff] text-[#2463EB]"
|
||||
: "bg-slate-100 text-slate-500"
|
||||
)}
|
||||
>
|
||||
{item.showInAppReminder ? "显示" : "静默"}
|
||||
{item.showProminentReminder ? "强提醒" : item.showInAppReminder ? "显示" : "静默"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-xs leading-5 text-slate-500">{item.detail}</div>
|
||||
|
|
@ -810,6 +858,10 @@ function getEnabledNotificationCount(settings: AccountSettings) {
|
|||
return accountNotificationKeys.filter((key) => settings.notifications[key].enabled).length;
|
||||
}
|
||||
|
||||
function getProminentNotificationCount(settings: AccountSettings) {
|
||||
return accountNotificationKeys.filter((key) => settings.notifications[key].reminderIntensity === "prominent").length;
|
||||
}
|
||||
|
||||
function getAnswerDepthTitle(value: AccountAnswerDepth) {
|
||||
return answerDepthOptions.find((option) => option.value === value)?.title || "标准";
|
||||
}
|
||||
|
|
@ -817,3 +869,11 @@ function getAnswerDepthTitle(value: AccountAnswerDepth) {
|
|||
function getEvidenceTitle(value: AccountEvidenceDisplay) {
|
||||
return evidenceOptions.find((option) => option.value === value)?.title || "内联";
|
||||
}
|
||||
|
||||
function getReminderIntensityTitle(value: AccountReminderIntensity) {
|
||||
return reminderIntensityOptions.find((option) => option.value === value)?.title || "普通";
|
||||
}
|
||||
|
||||
function getNotificationEvidenceTitle(value: AccountNotificationEvidenceDisplay) {
|
||||
return notificationEvidenceOptions.find((option) => option.value === value)?.title || "紧凑";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
|
|||
import {
|
||||
createDefaultAccountSettings,
|
||||
getAccountNotificationConsumption,
|
||||
getAccountReminderConsumption,
|
||||
getAccountSettings,
|
||||
getPostReleaseNotificationPreference,
|
||||
getReleaseGateNotificationPreference,
|
||||
|
|
@ -41,6 +42,8 @@ describe("account settings api", () => {
|
|||
expect(settings.notifications.executionWriteback.enabled).toBe(true);
|
||||
expect(settings.notifications.releaseGate.enabled).toBe(true);
|
||||
expect(settings.notifications.failureAlert.enabled).toBe(true);
|
||||
expect(settings.notifications.pendingConfirmation.reminderIntensity).toBe("normal");
|
||||
expect(settings.notifications.pendingConfirmation.evidenceDisplay).toBe("compact");
|
||||
});
|
||||
|
||||
it("persists updated preferences through the api layer", async () => {
|
||||
|
|
@ -51,7 +54,13 @@ describe("account settings api", () => {
|
|||
aiPreferences: { answerDepth: "deep", evidenceDisplay: "full" },
|
||||
notifications: {
|
||||
...defaults.notifications,
|
||||
failureAlert: { enabled: false, desktop: false, emailDigest: false },
|
||||
failureAlert: {
|
||||
...defaults.notifications.failureAlert,
|
||||
enabled: false,
|
||||
desktop: false,
|
||||
emailDigest: false,
|
||||
reminderIntensity: "silent" as const,
|
||||
},
|
||||
},
|
||||
});
|
||||
const restored = await getAccountSettings({ displayName: "陈雨" });
|
||||
|
|
@ -70,8 +79,21 @@ describe("account settings api", () => {
|
|||
...defaults,
|
||||
notifications: {
|
||||
...defaults.notifications,
|
||||
executionWriteback: { enabled: false, desktop: false, emailDigest: false },
|
||||
releaseGate: { enabled: true, desktop: false, emailDigest: true },
|
||||
executionWriteback: {
|
||||
...defaults.notifications.executionWriteback,
|
||||
enabled: false,
|
||||
desktop: false,
|
||||
emailDigest: false,
|
||||
reminderIntensity: "silent" as const,
|
||||
},
|
||||
releaseGate: {
|
||||
...defaults.notifications.releaseGate,
|
||||
enabled: true,
|
||||
desktop: false,
|
||||
emailDigest: true,
|
||||
reminderIntensity: "prominent" as const,
|
||||
evidenceDisplay: "expanded" as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -86,16 +108,73 @@ describe("account settings api", () => {
|
|||
expect(execution?.statusLabel).toBe("页面内提醒静默");
|
||||
expect(release?.productArea).toBe("Release / Growth");
|
||||
expect(release?.showInAppReminder).toBe(true);
|
||||
expect(release?.showProminentReminder).toBe(true);
|
||||
expect(release?.evidenceDisplay).toBe("expanded");
|
||||
expect(release?.emailDigestPreference).toBe(true);
|
||||
});
|
||||
|
||||
it("returns stable reminder intensity contracts for review, release, and execution events", () => {
|
||||
const defaults = createDefaultAccountSettings();
|
||||
const settings = {
|
||||
...defaults,
|
||||
notifications: {
|
||||
...defaults.notifications,
|
||||
pendingConfirmation: {
|
||||
...defaults.notifications.pendingConfirmation,
|
||||
reminderIntensity: "prominent" as const,
|
||||
evidenceDisplay: "expanded" as const,
|
||||
},
|
||||
releaseGate: {
|
||||
...defaults.notifications.releaseGate,
|
||||
reminderIntensity: "silent" as const,
|
||||
enabled: false,
|
||||
},
|
||||
executionWriteback: {
|
||||
...defaults.notifications.executionWriteback,
|
||||
reminderIntensity: "normal" as const,
|
||||
evidenceDisplay: "compact" as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const highRiskReview = getAccountReminderConsumption(settings, "change_review_high_risk");
|
||||
const iterationDraft = getAccountReminderConsumption(settings, "iteration_draft_ready");
|
||||
const releaseBlocked = getAccountReminderConsumption(settings, "release_blocked");
|
||||
const executionStalled = getAccountReminderConsumption(settings, "execution_stalled");
|
||||
|
||||
expect(highRiskReview.notificationKey).toBe("pendingConfirmation");
|
||||
expect(highRiskReview.surface).toBe("change_review");
|
||||
expect(highRiskReview.reminderIntensity).toBe("prominent");
|
||||
expect(highRiskReview.evidenceDisplay).toBe("expanded");
|
||||
expect(highRiskReview.showProminentReminder).toBe(true);
|
||||
expect(highRiskReview.notificationTransport).toBe("in_app_only");
|
||||
expect(highRiskReview.evidencePolicy).toBe("preserve_business_evidence");
|
||||
expect(highRiskReview.auditPolicy).toBe("preserve_audit_state");
|
||||
expect(iterationDraft.notificationKey).toBe("pendingConfirmation");
|
||||
expect(iterationDraft.showInAppReminder).toBe(true);
|
||||
expect(releaseBlocked.notificationKey).toBe("releaseGate");
|
||||
expect(releaseBlocked.surface).toBe("release_growth");
|
||||
expect(releaseBlocked.showInAppReminder).toBe(false);
|
||||
expect(releaseBlocked.mutedDetail).toContain("真实证据");
|
||||
expect(executionStalled.notificationKey).toBe("executionWriteback");
|
||||
expect(executionStalled.surface).toBe("execution_bridge");
|
||||
expect(executionStalled.showInAppReminder).toBe(true);
|
||||
expect(executionStalled.showProminentReminder).toBe(false);
|
||||
});
|
||||
|
||||
it("maps release gate and release validation events to the releaseGate preference", () => {
|
||||
const defaults = createDefaultAccountSettings();
|
||||
const settings = {
|
||||
...defaults,
|
||||
notifications: {
|
||||
...defaults.notifications,
|
||||
releaseGate: { enabled: true, desktop: false, emailDigest: true },
|
||||
releaseGate: {
|
||||
...defaults.notifications.releaseGate,
|
||||
enabled: true,
|
||||
desktop: false,
|
||||
emailDigest: true,
|
||||
reminderIntensity: "normal" as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -119,7 +198,13 @@ describe("account settings api", () => {
|
|||
...defaults,
|
||||
notifications: {
|
||||
...defaults.notifications,
|
||||
failureAlert: { enabled: false, desktop: true, emailDigest: true },
|
||||
failureAlert: {
|
||||
...defaults.notifications.failureAlert,
|
||||
enabled: false,
|
||||
desktop: true,
|
||||
emailDigest: true,
|
||||
reminderIntensity: "silent" as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -139,8 +224,20 @@ describe("account settings api", () => {
|
|||
...defaults,
|
||||
notifications: {
|
||||
...defaults.notifications,
|
||||
releaseGate: { enabled: true, desktop: false, emailDigest: true },
|
||||
pendingConfirmation: { enabled: false, desktop: true, emailDigest: true },
|
||||
releaseGate: {
|
||||
...defaults.notifications.releaseGate,
|
||||
enabled: true,
|
||||
desktop: false,
|
||||
emailDigest: true,
|
||||
reminderIntensity: "normal" as const,
|
||||
},
|
||||
pendingConfirmation: {
|
||||
...defaults.notifications.pendingConfirmation,
|
||||
enabled: false,
|
||||
desktop: true,
|
||||
emailDigest: true,
|
||||
reminderIntensity: "silent" as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -191,7 +288,9 @@ describe("account settings api", () => {
|
|||
|
||||
expect(restored.notifications.pendingConfirmation.desktop).toBe(true);
|
||||
expect(restored.notifications.failureAlert.enabled).toBe(false);
|
||||
expect(restored.notifications.failureAlert.desktop).toBe(true);
|
||||
expect(restored.notifications.failureAlert.desktop).toBe(false);
|
||||
expect(restored.notifications.failureAlert.reminderIntensity).toBe("silent");
|
||||
expect(restored.notifications.failureAlert.evidenceDisplay).toBe("compact");
|
||||
expect(shouldShowAccountNotification(restored, "failureAlert")).toBe(false);
|
||||
expect(consumption.find((item) => item.key === "failureAlert")?.detail).toContain("仍保留");
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue