rwadurian/backend/mpc-system/services/service-party-app/src/pages/Session.tsx

281 lines
9.3 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import styles from './Session.module.css';
interface Participant {
partyId: string;
name: string;
status: 'waiting' | 'ready' | 'processing' | 'completed' | 'failed';
joinedAt: string;
}
interface SessionState {
sessionId: string;
walletName: string;
threshold: { t: number; n: number };
status: 'waiting' | 'ready' | 'processing' | 'completed' | 'failed';
participants: Participant[];
currentRound: number;
totalRounds: number;
publicKey?: string;
error?: string;
}
export default function Session() {
const { sessionId } = useParams<{ sessionId: string }>();
const navigate = useNavigate();
const [session, setSession] = useState<SessionState | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchSessionStatus = useCallback(async () => {
if (!sessionId) return;
try {
const result = await window.electronAPI.grpc.getSessionStatus(sessionId);
if (result.success && result.session) {
setSession(result.session);
} else {
setError(result.error || '获取会话状态失败');
}
} catch (err) {
setError('获取会话状态失败');
} finally {
setIsLoading(false);
}
}, [sessionId]);
useEffect(() => {
fetchSessionStatus();
// 订阅会话事件
const unsubscribe = window.electronAPI.grpc.subscribeSessionEvents(
sessionId!,
(event: any) => {
if (event.type === 'participant_joined') {
setSession(prev => prev ? {
...prev,
participants: [...prev.participants, event.participant]
} : null);
} else if (event.type === 'status_changed') {
setSession(prev => prev ? {
...prev,
status: event.status,
currentRound: event.currentRound ?? prev.currentRound,
} : null);
} else if (event.type === 'completed') {
setSession(prev => prev ? {
...prev,
status: 'completed',
publicKey: event.publicKey,
} : null);
// Auto-navigate to home after a short delay to show completion status
setTimeout(() => {
navigate('/');
}, 2000);
} else if (event.type === 'failed') {
setSession(prev => prev ? {
...prev,
status: 'failed',
error: event.error,
} : null);
}
}
);
return () => {
unsubscribe();
};
}, [sessionId, fetchSessionStatus]);
const getStatusText = (status: string) => {
switch (status) {
case 'waiting': return '等待参与方';
case 'ready': return '准备就绪';
case 'processing': return '密钥生成中';
case 'completed': return '已完成';
case 'failed': return '失败';
default: return status;
}
};
const getStatusClass = (status: string) => {
switch (status) {
case 'waiting': return styles.statusWaiting;
case 'ready': return styles.statusReady;
case 'processing': return styles.statusProcessing;
case 'completed': return styles.statusCompleted;
case 'failed': return styles.statusFailed;
default: return '';
}
};
const getParticipantStatusIcon = (status: string) => {
switch (status) {
case 'waiting': return '⏳';
case 'ready': return '✓';
case 'processing': return '⚡';
case 'completed': return '✓';
case 'failed': return '✗';
default: return '•';
}
};
const handleCopyPublicKey = async () => {
if (session?.publicKey) {
try {
await navigator.clipboard.writeText(session.publicKey);
} catch (err) {
console.error('Failed to copy:', err);
}
}
};
if (isLoading) {
return (
<div className={styles.container}>
<div className={styles.loading}>
<div className={styles.spinner}></div>
<p>...</p>
</div>
</div>
);
}
if (error || !session) {
return (
<div className={styles.container}>
<div className={styles.error}>
<div className={styles.errorIcon}>!</div>
<h3></h3>
<p>{error || '无法获取会话信息'}</p>
<button className={styles.primaryButton} onClick={() => navigate('/')}>
</button>
</div>
</div>
);
}
return (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.header}>
<div>
<h1 className={styles.title}>{session.walletName}</h1>
<p className={styles.sessionId}> ID: {session.sessionId}</p>
</div>
<span className={`${styles.status} ${getStatusClass(session.status)}`}>
{getStatusText(session.status)}
</span>
</div>
<div className={styles.content}>
{/* 进度部分 */}
{session.status === 'processing' && (
<div className={styles.progress}>
<div className={styles.progressHeader}>
<span></span>
<span>{session.currentRound} / {session.totalRounds}</span>
</div>
<div className={styles.progressBar}>
<div
className={styles.progressFill}
style={{ width: `${(session.currentRound / session.totalRounds) * 100}%` }}
></div>
</div>
</div>
)}
{/* 参与方列表 */}
<div className={styles.section}>
<h3 className={styles.sectionTitle}>
({(session.participants || []).length} / {session.threshold?.n || 0})
</h3>
<div className={styles.participantList}>
{(session.participants || []).map((participant, index) => (
<div key={participant.partyId} className={styles.participant}>
<div className={styles.participantInfo}>
<span className={styles.participantIndex}>#{index + 1}</span>
<span className={styles.participantName}>{participant.name}</span>
</div>
<span className={`${styles.participantStatus} ${getStatusClass(participant.status)}`}>
{getParticipantStatusIcon(participant.status)}
</span>
</div>
))}
{Array.from({ length: Math.max(0, (session.threshold?.n || 0) - (session.participants || []).length) }).map((_, index) => (
<div key={`empty-${index}`} className={`${styles.participant} ${styles.participantEmpty}`}>
<div className={styles.participantInfo}>
<span className={styles.participantIndex}>#{(session.participants || []).length + index + 1}</span>
<span className={styles.participantName}>...</span>
</div>
<span className={styles.participantStatus}></span>
</div>
))}
</div>
</div>
{/* 阈值信息 */}
{session.threshold && (
<div className={styles.section}>
<h3 className={styles.sectionTitle}></h3>
<div className={styles.thresholdInfo}>
<span className={styles.thresholdBadge}>
{session.threshold.t}-of-{session.threshold.n}
</span>
<span className={styles.thresholdText}>
{session.threshold.t}
</span>
</div>
</div>
)}
{/* 完成状态 */}
{session.status === 'completed' && session.publicKey && (
<div className={styles.section}>
<h3 className={styles.sectionTitle}></h3>
<div className={styles.publicKeyWrapper}>
<code className={styles.publicKey}>{session.publicKey}</code>
<button className={styles.copyButton} onClick={handleCopyPublicKey}>
</button>
</div>
<p className={styles.successMessage}>
</p>
</div>
)}
{/* 失败状态 */}
{session.status === 'failed' && session.error && (
<div className={styles.section}>
<div className={styles.failureMessage}>
<span className={styles.failureIcon}>!</span>
<span>{session.error}</span>
</div>
</div>
)}
</div>
<div className={styles.footer}>
{session.status === 'completed' ? (
<button className={styles.primaryButton} onClick={() => navigate('/')}>
</button>
) : session.status === 'failed' ? (
<button className={styles.primaryButton} onClick={() => navigate('/')}>
</button>
) : (
<button className={styles.secondaryButton} onClick={() => navigate('/')}>
</button>
)}
</div>
</div>
</div>
);
}