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

279 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import styles from './Join.module.css';
interface SessionInfo {
sessionId: string;
walletName: string;
threshold: { t: number; n: number };
initiator: string;
createdAt: string;
currentParticipants: number;
totalParticipants?: number;
}
// 生成默认参与者名称
function generateParticipantName(): string {
const timestamp = Date.now().toString(36).slice(-4);
return `参与者-${timestamp}`;
}
interface ValidateResult {
success: boolean;
error?: string;
sessionInfo?: SessionInfo;
joinToken?: string;
}
export default function Join() {
const { inviteCode } = useParams<{ inviteCode?: string }>();
const navigate = useNavigate();
const [code, setCode] = useState(inviteCode || '');
const [participantName] = useState(generateParticipantName());
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [sessionInfo, setSessionInfo] = useState<SessionInfo | null>(null);
const [joinToken, setJoinToken] = useState<string | null>(null);
const [partyId, setPartyId] = useState<string | null>(null);
const [step, setStep] = useState<'input' | 'confirm' | 'joining'>('input');
const [autoJoinAttempted, setAutoJoinAttempted] = useState(false);
useEffect(() => {
if (inviteCode) {
handleValidateCode(inviteCode);
}
}, [inviteCode]);
// 自动加入:验证成功后自动加入会话
useEffect(() => {
if (
step === 'confirm' &&
sessionInfo &&
joinToken &&
partyId &&
participantName &&
!autoJoinAttempted &&
!isLoading
) {
setAutoJoinAttempted(true);
handleJoinSession();
}
}, [step, sessionInfo, joinToken, partyId, participantName, autoJoinAttempted, isLoading]);
const handleValidateCode = async (codeToValidate: string) => {
if (!codeToValidate.trim()) {
setError('请输入邀请码');
return;
}
setIsLoading(true);
setError(null);
try {
// 获取当前 partyId
const partyResult = await window.electronAPI.grpc.getPartyId();
if (!partyResult.success || !partyResult.partyId) {
setError('请先连接到消息路由器');
setIsLoading(false);
return;
}
setPartyId(partyResult.partyId);
// 解析邀请码获取会话信息
const result: ValidateResult = await window.electronAPI.grpc.validateInviteCode(codeToValidate);
if (result.success && result.sessionInfo) {
setSessionInfo(result.sessionInfo);
if (result.joinToken) {
setJoinToken(result.joinToken);
}
setStep('confirm');
} else {
setError(result.error || '无效的邀请码');
}
} catch (err) {
setError('验证邀请码失败,请检查网络连接');
} finally {
setIsLoading(false);
}
};
const handleJoinSession = async () => {
if (!sessionInfo) {
setError('会话信息不完整');
return;
}
if (!partyId) {
setError('未获取到 Party ID请重试');
return;
}
if (!joinToken) {
setError('未获取到加入令牌,请重新验证邀请码');
return;
}
setStep('joining');
setIsLoading(true);
setError(null);
try {
const result = await window.electronAPI.grpc.joinSession({
sessionId: sessionInfo.sessionId,
partyId: partyId,
joinToken: joinToken,
walletName: sessionInfo.walletName,
});
if (result.success) {
navigate(`/session/${sessionInfo.sessionId}`);
} else {
setError(result.error || '加入会话失败');
setStep('confirm');
}
} catch (err) {
setError('加入会话失败,请重试');
setStep('confirm');
} finally {
setIsLoading(false);
}
};
const handlePaste = async () => {
try {
const text = await navigator.clipboard.readText();
setCode(text.trim());
} catch (err) {
console.error('Failed to read clipboard:', err);
}
};
return (
<div className={styles.container}>
<div className={styles.card}>
<h1 className={styles.title}></h1>
<p className={styles.subtitle}></p>
{step === 'input' && (
<div className={styles.form}>
<div className={styles.inputGroup}>
<label className={styles.label}></label>
<div className={styles.inputWrapper}>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="粘贴邀请码或邀请链接"
className={styles.input}
disabled={isLoading}
/>
<button
className={styles.pasteButton}
onClick={handlePaste}
disabled={isLoading}
>
</button>
</div>
</div>
{error && <div className={styles.error}>{error}</div>}
<div className={styles.actions}>
<button
className={styles.secondaryButton}
onClick={() => navigate('/')}
disabled={isLoading}
>
</button>
<button
className={styles.primaryButton}
onClick={() => handleValidateCode(code)}
disabled={isLoading || !code.trim()}
>
{isLoading ? '验证中...' : '下一步'}
</button>
</div>
</div>
)}
{step === 'confirm' && sessionInfo && (
<div className={styles.form}>
<div className={styles.sessionInfo}>
<h3 className={styles.sessionTitle}></h3>
<div className={styles.infoGrid}>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>{sessionInfo.walletName}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>
{sessionInfo.threshold.t}-of-{sessionInfo.threshold.n}
</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>{sessionInfo.initiator}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>
{sessionInfo.currentParticipants} / {sessionInfo.threshold.n}
</span>
</div>
</div>
</div>
{error ? (
<>
<div className={styles.error}>{error}</div>
<div className={styles.actions}>
<button
className={styles.secondaryButton}
onClick={() => {
setStep('input');
setSessionInfo(null);
setJoinToken(null);
setPartyId(null);
setError(null);
setAutoJoinAttempted(false);
}}
disabled={isLoading}
>
</button>
<button
className={styles.primaryButton}
onClick={() => {
setAutoJoinAttempted(false);
setError(null);
}}
disabled={isLoading}
>
</button>
</div>
</>
) : (
<div className={styles.joining}>
<div className={styles.spinner}></div>
<p>...</p>
</div>
)}
</div>
)}
{step === 'joining' && (
<div className={styles.joining}>
<div className={styles.spinner}></div>
<p>...</p>
</div>
)}
</div>
</div>
);
}