From ba74c1af302b7e4a58fcbb0768c6173773437b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 1 Sep 2026 19:04:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E8=87=AA=E5=8A=A8=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E9=80=9A=E7=9F=A5=E4=B8=8E=E6=8A=A5=E5=91=8A=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增五秒聚合的非阻塞错误通知 支持事件选择并在日志读取完成前禁止提交 扩大移动端关闭按钮触控区域 --- .../error-report/ErrorReportDialog.tsx | 173 +++++++++++++++--- apps/ai-game-creator-shell/src/styles.css | 12 +- 2 files changed, 162 insertions(+), 23 deletions(-) diff --git a/apps/ai-game-creator-shell/src/components/error-report/ErrorReportDialog.tsx b/apps/ai-game-creator-shell/src/components/error-report/ErrorReportDialog.tsx index f96c667cc..a53bc22e4 100644 --- a/apps/ai-game-creator-shell/src/components/error-report/ErrorReportDialog.tsx +++ b/apps/ai-game-creator-shell/src/components/error-report/ErrorReportDialog.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { type ClientErrorEvent, @@ -13,43 +13,61 @@ import { ThemedModal } from '../modal/ThemedModal'; type ErrorReportDialogProps = { open: boolean; + events: ClientErrorEvent[]; onClose: () => void; }; -export function ErrorReportDialog({ open, onClose }: ErrorReportDialogProps) { - const [events, setEvents] = useState([]); +export function ErrorReportDialog({ + open, + events, + onClose, +}: ErrorReportDialogProps) { + const [selectedFingerprints, setSelectedFingerprints] = useState>( + new Set(), + ); const [logs, setLogs] = useState([]); const [includeLogs, setIncludeLogs] = useState(true); const [description, setDescription] = useState(''); const [busy, setBusy] = useState(false); const [status, setStatus] = useState(''); - - useEffect( - () => - subscribeClientErrorEvents(() => - setEvents(getPendingClientErrorEvents()), - ), - [], - ); + const [logsReady, setLogsReady] = useState(false); + const [logsError, setLogsError] = useState(false); useEffect(() => { if (!open) return; - setEvents(getPendingClientErrorEvents()); + setSelectedFingerprints( + new Set(events.slice(0, 1).map((event) => event.fingerprint)), + ); setStatus(''); - void readApplicationDiagnosticLogs().then(setLogs); - }, [open]); + setLogsReady(false); + setLogsError(false); + void readApplicationDiagnosticLogs() + .then((nextLogs) => { + setLogs(nextLogs); + setLogsReady(true); + }) + .catch(() => { + setLogs([]); + setLogsError(true); + setLogsReady(true); + }); + }, [events, open]); const submit = useCallback(async () => { - if (!events.length || busy) return; + const selectedEvents = events.filter((event) => + selectedFingerprints.has(event.fingerprint), + ); + if (!selectedEvents.length || busy || !logsReady) return; + if (includeLogs && logsError) return; setBusy(true); setStatus('正在提交…'); try { await submitErrorReportBatch({ - events, + events: selectedEvents, logs: includeLogs ? logs : [], userDescription: description, }); - markClientErrorEventsSubmitted(events); + markClientErrorEventsSubmitted(selectedEvents); setStatus('已提交,感谢你的反馈'); window.setTimeout(onClose, 700); } catch (error) { @@ -59,7 +77,17 @@ export function ErrorReportDialog({ open, onClose }: ErrorReportDialogProps) { } finally { setBusy(false); } - }, [busy, description, events, includeLogs, logs, onClose]); + }, [ + busy, + description, + events, + includeLogs, + logs, + logsError, + logsReady, + onClose, + selectedFingerprints, + ]); return ( {events.map((event) => (
  • - {event.message} + {event.source} · {event.count} 次 @@ -109,10 +152,16 @@ export function ErrorReportDialog({ open, onClose }: ErrorReportDialogProps) { checked={includeLogs} onChange={(event) => setIncludeLogs(event.target.checked)} /> - 附加应用诊断日志({logs.length} 个文件) + + {logsReady + ? logsError + ? '应用诊断日志暂不可用(可取消后提交)' + : `附加应用诊断日志(${logs.length} 个文件)` + : '正在读取应用诊断日志…'} +

    - 将上传错误信息、应用环境信息、脱敏系统日志和你填写的说明。 + 将上传选中的脱敏错误信息、可选的应用诊断日志和你填写的说明;不包含截图或项目文件。

    {status ?

    {status}

    : null} @@ -123,7 +172,12 @@ export function ErrorReportDialog({ open, onClose }: ErrorReportDialogProps) { @@ -131,3 +185,78 @@ export function ErrorReportDialog({ open, onClose }: ErrorReportDialogProps) { ); } + +export function ErrorReportCenter() { + const [pendingEvents, setPendingEvents] = useState([]); + const [notificationEvents, setNotificationEvents] = useState< + ClientErrorEvent[] + >([]); + const [reportEvents, setReportEvents] = useState([]); + const notifiedFingerprints = useRef(new Set()); + const aggregationTimer = useRef(null); + + const sync = useCallback(() => { + setPendingEvents(getPendingClientErrorEvents()); + }, []); + + useEffect(() => { + sync(); + return subscribeClientErrorEvents(sync); + }, [sync]); + + useEffect(() => { + const candidates = pendingEvents.filter( + (event) => !notifiedFingerprints.current.has(event.fingerprint), + ); + if ( + !candidates.length || + notificationEvents.length || + aggregationTimer.current !== null + ) + return; + aggregationTimer.current = window.setTimeout(() => { + aggregationTimer.current = null; + const next = getPendingClientErrorEvents(); + const newlyNotified = next.filter( + (event) => !notifiedFingerprints.current.has(event.fingerprint), + ); + newlyNotified.forEach((event) => + notifiedFingerprints.current.add(event.fingerprint), + ); + if (newlyNotified.length) setNotificationEvents(next); + }, 5000); + return () => { + if (aggregationTimer.current !== null) { + window.clearTimeout(aggregationTimer.current); + aggregationTimer.current = null; + } + }; + }, [notificationEvents.length, pendingEvents]); + + const ignore = () => setNotificationEvents([]); + const openReport = () => { + setReportEvents(notificationEvents); + setNotificationEvents([]); + }; + + return ( + <> + {notificationEvents.length ? ( + + ) : null} + 0} + events={reportEvents} + onClose={() => setReportEvents([])} + /> + + ); +} diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 4d8be93b5..30cd9e62c 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -690,7 +690,7 @@ textarea { .error-report-dialog__header h2 { margin: 0; } .error-report-dialog__consent { margin: 4px 0 0; color: var(--platform-text-soft); font-size: 13px; } -.error-report-dialog__header > button { border: 0; background: transparent; font-size: 24px; cursor: pointer; } +.error-report-dialog__header > button { width: 40px; min-height: 40px; border: 0; border-radius: 10px; background: transparent; font-size: 24px; cursor: pointer; } .error-report-dialog__body { display: grid; gap: 16px; margin: 20px 0; } .error-report-dialog__events { display: grid; gap: 8px; padding: 0; margin: 8px 0 0; list-style: none; } .error-report-dialog__events li { display: grid; gap: 3px; border: 1px solid var(--platform-border-subtle); border-radius: 10px; padding: 10px; } @@ -701,6 +701,16 @@ textarea { .error-report-dialog__actions { justify-content: flex-end; } .error-report-dialog__actions button { border: 0; border-radius: 9px; padding: 9px 14px; cursor: pointer; } .error-report-dialog__actions button:last-child { background: var(--platform-button-primary-fill); color: var(--platform-button-primary-text); } +.error-report-dialog__event-select { display: flex !important; grid-template-columns: none !important; align-items: flex-start; gap: 8px; } +.error-report-dialog__event-select input { margin-top: 3px; } +.error-report-notice { position: fixed; top: calc(var(--window-chrome-height) + 14px); right: 16px; z-index: 40; display: flex; align-items: center; gap: 10px; max-width: min(420px, calc(100vw - 2rem)); padding: 12px 14px; border: 1px solid var(--platform-surface-border); border-radius: 12px; background: var(--platform-subpanel-fill); color: var(--platform-text-strong); box-shadow: 0 12px 30px rgb(36 20 12 / 18%); } +.error-report-notice span { flex: 1 1 auto; } +.error-report-notice button { min-height: 36px; border: 0; border-radius: 8px; padding: 0 10px; background: var(--platform-button-primary-fill); color: var(--platform-button-primary-text); } +.error-report-notice button:last-child { width: 36px; padding: 0; background: transparent; color: var(--platform-text-soft); font-size: 20px; } + +@media (max-width: 560px) { + .error-report-notice { top: calc(var(--window-chrome-height) + 8px); right: 8px; left: 8px; max-width: none; } +} @media (max-width: 480px) { .client-auth-shell {