Files
ark-stronghold-protocol/public/js/skland/resume.js
T
herryyun 7c25c89992
CI / test (ubuntu-latest, node 22) (push) Failing after 33m10s
CI / test (ubuntu-latest, node 24) (push) Failing after 34m41s
CI / tooling (lint, imports, typecheck) (push) Failing after 36m11s
CI / test (windows-latest, node 22) (push) Canceled after 0s
CI / test (windows-latest, node 24) (push) Canceled after 0s
实现账号绑定、持久登录与跨设备对局恢复
2026-10-14 23:22:50 +08:00

68 lines
3.6 KiB
JavaScript

// Cross-device recovery uses authenticated server state, not the browser's recent-token list.
import { useEffect, useRef, useState } from '../../vendor/hooks.module.js';
import { html, Button, Modal } from '../ui/components.js';
import { useLang } from '../ui/lang.js';
import { t } from '../../../shared/i18n.js';
import { accountRecovery } from './gate.js';
function recoveryError(error) {
if (error.code === 'resume_expired') return t('此对局已结束或过期,请刷新后重试。');
if (error.code === 'session') return t('游戏会话已失效,请关闭后重试。');
return t('连接失败,请重试。');
}
export function AccountResumeMatchButton({ bound, onResume }) {
useLang();
const [open, setOpen] = useState(false);
const [item, setItem] = useState(null);
const [loading, setLoading] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const [revision, setRevision] = useState(0);
const lock = useRef(false);
const alive = useRef(true);
useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []);
useEffect(() => {
if (!bound) { setOpen(false); setItem(null); return undefined; }
if (!open) return undefined;
const abort = new AbortController();
setLoading(true); setItem(null); setError('');
accountRecovery({ signal: abort.signal }).then((value) => {
if (!abort.signal.aborted) setItem(value);
}).catch((failure) => {
if (!abort.signal.aborted) setError(recoveryError(failure));
}).finally(() => { if (!abort.signal.aborted) setLoading(false); });
return () => abort.abort();
}, [open, bound, revision]);
if (!bound) return null;
const resume = async () => {
if (!item || lock.current) return;
lock.current = true; setBusy(true); setError('');
try {
await onResume(item.id);
if (alive.current) setOpen(false);
} catch (failure) {
if (alive.current) {
setError(recoveryError(failure));
if (failure.code === 'resume_expired') setItem(null);
}
} finally {
lock.current = false;
if (alive.current) setBusy(false);
}
};
return html`<${Button} variant="secondary" size="sm" data-testid="resume-account-match" onClick=${() => setOpen(true)}>${t('恢复账号对局')}<//>
<${Modal} open=${open} onClose=${() => !busy && setOpen(false)} title=${t('恢复账号对局')} class="sp-account-resume"
actions=${html`<${Button} variant="secondary" disabled=${busy || loading} onClick=${() => setRevision((value) => value + 1)}>${t('刷新')}<//>
<${Button} variant="ghost" disabled=${busy} onClick=${() => setOpen(false)}>${t('关闭')}<//>`}>
<p>${t('登录同一账号并选择同一区服角色,可在其他设备继续尚未过期的对局。服务器重启后无法恢复。')}</p>
${loading ? html`<p role="status">${t('正在查询账号对局…')}</p>` : null}
${error ? html`<p role="alert">${error}</p>` : null}
${!loading && !item && !error ? html`<p role="status">${t('当前角色没有可恢复的对局。')}</p>` : null}
${item ? html`<p>${item.inMatch ? t('对局进行中') : t('同盟等候中')}${item.inMatch && item.round > 0 ? ' · ' + t('第 {r} 回合', { r: item.round }) : ''}</p>
<p class="t-lo">${t('如果此角色仍在其他设备在线,恢复后将断开旧连接。')}</p>
<${Button} variant="primary" block=${true} disabled=${busy || loading} data-testid="confirm-account-resume" onClick=${resume}>
${t('{name} · 同盟 {code}', { name: item.name || t('博士'), code: item.code })}<//>` : null}
<//>`;
}