From 1889f181839b9eac4fc1c23e024bf38e754bfb7b Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Wed, 7 Jan 2026 13:17:56 +0800 Subject: [PATCH] feat(notice): override context menu to copy error details --- Changelog.md | 1 + src/components/layout/notice-manager.tsx | 64 +++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 700f44c0e..ec5d96659 100644 --- a/Changelog.md +++ b/Changelog.md @@ -40,5 +40,6 @@ - 完善对 AnyTLS / Mieru / Sudoku 的 GUI 支持 - macOS 和 Linux 对服务 IPC 权限进一步限制 - 移除 Windows 自启动计划任务中冗余的 3 秒延时 +- 右键错误通知可复制错误详情 diff --git a/src/components/layout/notice-manager.tsx b/src/components/layout/notice-manager.tsx index 4c07671f4..57487a16d 100644 --- a/src/components/layout/notice-manager.tsx +++ b/src/components/layout/notice-manager.tsx @@ -6,13 +6,14 @@ import { Box, type SnackbarOrigin, } from "@mui/material"; -import React, { useMemo, useSyncExternalStore } from "react"; +import React, { useCallback, useMemo, useSyncExternalStore } from "react"; import { useTranslation } from "react-i18next"; import { subscribeNotices, hideNotice, getSnapshotNotices, + showNotice, } from "@/services/notice-service"; import type { TranslationKey } from "@/types/generated/i18n-keys"; @@ -85,6 +86,45 @@ const resolveNoticeMessage = ( }); }; +const extractNoticeCopyText = (input: unknown): string | undefined => { + if (input === null || input === undefined) return undefined; + if (typeof input === "string") return input; + if (typeof input === "number" || typeof input === "boolean") { + return String(input); + } + if (input instanceof Error) { + return input.message || input.name; + } + if (React.isValidElement(input)) return undefined; + if (typeof input === "object") { + const maybeMessage = (input as { message?: unknown }).message; + if (typeof maybeMessage === "string") return maybeMessage; + } + try { + return JSON.stringify(input); + } catch { + return String(input); + } +}; + +const resolveNoticeCopyText = ( + notice: NoticeItem, + t: TranslationFn, +): string | undefined => { + if ( + notice.i18n?.key === "shared.feedback.notices.prefixedRaw" || + notice.i18n?.key === "shared.feedback.notices.raw" + ) { + const rawText = extractNoticeCopyText(notice.i18n?.params?.message); + if (rawText) return rawText; + } + + return ( + extractNoticeCopyText(resolveNoticeMessage(notice, t)) ?? + extractNoticeCopyText(notice.message) + ); +}; + interface NoticeManagerProps { position?: NoticePosition | null; } @@ -105,6 +145,23 @@ export const NoticeManager: React.FC = ({ position }) => { hideNotice(id); }; + const handleNoticeCopy = useCallback( + async (notice: NoticeItem) => { + const text = resolveNoticeCopyText(notice, t); + if (!text) return; + try { + await navigator.clipboard.writeText(text); + showNotice.success( + "shared.feedback.notifications.common.copySuccess", + 1000, + ); + } catch (error) { + console.warn("[NoticeManager] copy to clipboard failed:", error); + } + }, + [t], + ); + return ( = ({ position }) => { severity={notice.type} variant="filled" sx={{ width: "100%" }} + onContextMenu={(event) => { + event.preventDefault(); + event.stopPropagation(); + void handleNoticeCopy(notice); + }} action={