diff --git a/server/mihome/client.js b/server/mihome/client.js index 201d9c0..f9edf13 100644 --- a/server/mihome/client.js +++ b/server/mihome/client.js @@ -91,8 +91,13 @@ export function getQrImageUrl() { } export async function startQrLogin() { - // 已有一轮在跑就直接复用: 同一张二维码继续有效, 不重复开 - if (qrState.running) return getQrState() + // 已有一轮在跑就直接复用: 同一张二维码继续有效, 不重复开. + // 注意: 这里绝不能提前 return 而不清 done —— 上一轮成功时 done=true 会残留, + // 前端第一次轮询就拿到 {done:true, running:false}, 立刻误判成"扫码成功"关掉弹窗. + if (qrState.running) { + const snap = getQrState() + return { ...snap, done: false, running: true } + } const api = getApi() qrState.running = true qrState.done = false diff --git a/src/pages/monitor/EnvView.jsx b/src/pages/monitor/EnvView.jsx index eb42649..92a596f 100644 --- a/src/pages/monitor/EnvView.jsx +++ b/src/pages/monitor/EnvView.jsx @@ -275,36 +275,60 @@ function AuthPopup({ open, onClose, onOk }) { const [imgUrl, setImgUrl] = useState('') const imgSigRef = useRef('') const imgUrlRef = useRef('') + // 用 ref 持有最新回调: 父组件每次渲染都会新建 onClose/onOk, + // 若把它们放进 effect 依赖会导致 start() 反复重启、弹窗被反复关掉 + const cbRef = useRef({ onClose, onOk }) + cbRef.current = { onClose, onOk } + // 本轮是否已经结束(成功/失败), 防止多次 poll 重复触发 onOk + const settledRef = useRef(false) const stopPoll = () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null } } const start = async () => { stopPoll() + settledRef.current = false setState(null) try { await startMihomeAuth() } catch (e) { + settledRef.current = true setState({ state: 'failed', lastError: e.message }) return } const poll = () => { getMihomeAuthStatus() .then((s) => { + if (settledRef.current) return setState(s) - if (s.state === 'failed') { stopPoll(); return } - // done 只由真正登录成功置位; 轮询结束但没 done(用户取消/超时) 不能当成功 - if (!s.running && s.done) { stopPoll(); onOk() } + if (s.state === 'failed') { + settledRef.current = true + stopPoll() + return + } + // done 只由服务端真正登录成功置位; 且必须 running=false 才算流程走完 + if (!s.running && s.done) { + settledRef.current = true + stopPoll() + cbRef.current.onOk() + } + }) + .catch((e) => { + if (settledRef.current) return + settledRef.current = true + stopPoll() + setState({ state: 'failed', lastError: e.message }) }) - .catch((e) => { stopPoll(); setState({ state: 'failed', lastError: e.message }) }) } poll() timerRef.current = setInterval(poll, 3000) } + // 只依赖 open: start 内部通过 ref 读回调, 不会因父组件重渲染而重启 useEffect(() => { if (open) start() - else { stopPoll(); setState(null) } + else { stopPoll(); settledRef.current = false; setState(null) } return stopPoll + // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]) // 二维码变化时才重新拉图 (轮询 3s 一次, 用字符串签名去重, 避免每轮都请求图片) @@ -341,7 +365,8 @@ function AuthPopup({ open, onClose, onOk }) { const cancel = async () => { try { await cancelMihomeAuth() } catch { /* ignore */ } stopPoll() - onClose() + settledRef.current = true + cbRef.current.onClose() } const waiting = state?.state === 'waiting' && !!imgUrl @@ -349,7 +374,8 @@ function AuthPopup({ open, onClose, onOk }) { const failed = state?.state === 'failed' return ( - + // 关闭遮罩点击: 手机端很容易误触导致弹窗一闪而过; 只允许显式点「取消」关闭 +
米家扫码授权
{waiting ? (