feat: surface platform errors in copyable dialogs
This commit is contained in:
120
src/components/platform-entry/PlatformErrorDialog.tsx
Normal file
120
src/components/platform-entry/PlatformErrorDialog.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { copyTextToClipboard } from '../../services/clipboard';
|
||||
import { UnifiedModal } from '../common/UnifiedModal';
|
||||
|
||||
export type PlatformErrorDialogPayload = {
|
||||
source: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type PlatformErrorDialogProps = {
|
||||
error: PlatformErrorDialogPayload | null;
|
||||
onClose: () => void;
|
||||
overlayClassName?: string;
|
||||
panelClassName?: string;
|
||||
};
|
||||
|
||||
function buildPlatformErrorReport(error: PlatformErrorDialogPayload) {
|
||||
return [`来源:${error.source}`, `错误:${error.message}`].join('\n');
|
||||
}
|
||||
|
||||
export function PlatformErrorDialog({
|
||||
error,
|
||||
onClose,
|
||||
overlayClassName = 'platform-theme platform-theme--light !items-center',
|
||||
panelClassName = 'platform-remap-surface rounded-[1.5rem]',
|
||||
}: PlatformErrorDialogProps) {
|
||||
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>(
|
||||
'idle',
|
||||
);
|
||||
const resetTimerRef = useRef<number | null>(null);
|
||||
const reportText = useMemo(
|
||||
() => (error ? buildPlatformErrorReport(error) : ''),
|
||||
[error],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (resetTimerRef.current !== null) {
|
||||
window.clearTimeout(resetTimerRef.current);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCopyState('idle');
|
||||
}, [error?.source, error?.message]);
|
||||
|
||||
const copyError = () => {
|
||||
if (!reportText) {
|
||||
return;
|
||||
}
|
||||
|
||||
void copyTextToClipboard(reportText).then((copied) => {
|
||||
setCopyState(copied ? 'copied' : 'failed');
|
||||
if (resetTimerRef.current !== null) {
|
||||
window.clearTimeout(resetTimerRef.current);
|
||||
}
|
||||
resetTimerRef.current = window.setTimeout(() => {
|
||||
resetTimerRef.current = null;
|
||||
setCopyState('idle');
|
||||
}, 1400);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<UnifiedModal
|
||||
open={Boolean(error)}
|
||||
title="发生错误"
|
||||
onClose={onClose}
|
||||
size="sm"
|
||||
overlayClassName={overlayClassName}
|
||||
panelClassName={panelClassName}
|
||||
bodyClassName="space-y-3 px-4 py-4 sm:px-5 sm:py-5"
|
||||
footerClassName="justify-end px-4 py-4 sm:px-5"
|
||||
footer={
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyError}
|
||||
disabled={!reportText}
|
||||
className="platform-button platform-button--primary w-full justify-center gap-2 sm:w-auto"
|
||||
>
|
||||
{copyState === 'copied' ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
{copyState === 'copied'
|
||||
? '已复制'
|
||||
: copyState === 'failed'
|
||||
? '复制失败'
|
||||
: '复制报错'}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
{error ? (
|
||||
<>
|
||||
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-2">
|
||||
<div className="text-xs font-bold text-[var(--platform-text-soft)]">
|
||||
来源
|
||||
</div>
|
||||
<div className="mt-1 break-words text-sm font-semibold leading-5 text-[var(--platform-text-strong)]">
|
||||
{error.source}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-2">
|
||||
<div className="text-xs font-bold text-[var(--platform-text-soft)]">
|
||||
错误
|
||||
</div>
|
||||
<div className="mt-1 whitespace-pre-wrap break-words text-sm leading-6 text-[var(--platform-text-strong)]">
|
||||
{error.message}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</UnifiedModal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user