import { useEffect, useState } from 'react'; import { View, Text, Input, Image } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; import { SmsScene, isWxAuthorizeEnabled, type ClientRuntimeConfig, type WechatLoginResult, } from '@dukang/shared-types'; import PageShell from '../../components/PageShell'; import WechatLoginButton from '../../components/WechatLoginButton'; import { BRAND_LOGO_WIDE_URL } from '@dukang/shared-types'; import { finishLoginNavigate } from '../../lib/auth-nav'; import { bindWechatForUser, loginWithWechat, } from '../../lib/wechat-auth'; import { fetchUserProfile } from '../../lib/pay-wechat'; import { saveUserPhone, resolveDefaultUserPhone } from '../../lib/user-phone'; import { fetchMiniWechatUserInfo, getCachedWxProfile, syncMiniWechatProfile, type MiniWechatProfile, } from '../../lib/mini-wechat-profile'; import { isLoggedIn, request, saveAuth, toast, type SessionPayload } from '../../lib/api'; function normalizePhone(value: string) { return value.replace(/\D/g, '').slice(0, 11); } function isValidPhone(phone: string) { return /^1[3-9]\d{9}$/.test(phone); } export default function LoginPage() { const router = useRouter(); const returnTo = router.params.return || ''; const needPhone = router.params.needPhone === '1'; const needWechat = router.params.needWechat === '1'; const initialBindMode = router.params.bindMode === '1'; const initialWxSessionKey = router.params.wxSessionKey || null; const [phone, setPhone] = useState(''); const [code, setCode] = useState(''); const [loading, setLoading] = useState(false); const [wxLoading, setWxLoading] = useState(false); const [sending, setSending] = useState(false); const [cooldown, setCooldown] = useState(0); const [agreed, setAgreed] = useState(false); const [msg, setMsg] = useState(''); const [sentHint, setSentHint] = useState(''); const [bindMode, setBindMode] = useState(initialBindMode); const [wxSessionKey, setWxSessionKey] = useState(initialWxSessionKey); const [wxAuthorize, setWxAuthorize] = useState(true); const [completeMode, setCompleteMode] = useState<'phone' | 'wechat' | null>(null); useEffect(() => { request('/common/client-config') .then((config) => setWxAuthorize(isWxAuthorizeEnabled(config))) .catch(() => setWxAuthorize(true)); }, []); useEffect(() => { if (!isLoggedIn()) { setCompleteMode(null); return; } // 完善资料场景才拉 profile;普通登录勿抢跑 /auth/me,避免旧 token 401 与短信登录竞态 if (!needPhone && !needWechat) { setCompleteMode(null); return; } let cancelled = false; fetchUserProfile() .then((me) => { if (cancelled) return; if (needPhone && !me.phoneVerified) { setCompleteMode('phone'); return; } if (needWechat && !me.hasWechat) { setCompleteMode('wechat'); return; } finishLoginNavigate(returnTo); }) .catch(() => { if (!cancelled) setCompleteMode(null); }); return () => { cancelled = true; }; }, [needPhone, needWechat, returnTo]); useEffect(() => { if (cooldown <= 0) return; const timer = setTimeout(() => setCooldown((c) => Math.max(0, c - 1)), 1000); return () => clearTimeout(timer); }, [cooldown]); function ensureAgreed() { if (!agreed) { setMsg('请先勾选并同意用户协议'); return false; } return true; } function applySessionAndLeave( data: SessionPayload | WechatLoginResult, phone?: string, wxInfo?: MiniWechatProfile | null, successToast = '登录成功', ) { if (!data.accessToken) return; if (phone) saveUserPhone(phone); saveAuth({ accessToken: data.accessToken, refreshToken: data.refreshToken, }); void syncMiniWechatProfile(wxInfo ?? getCachedWxProfile()); if (!phone) { void fetchUserProfile() .then((me) => resolveDefaultUserPhone(me)) .catch(() => {}); } toast(successToast, 'success'); finishLoginNavigate(returnTo); } function handleWechatLoginResult(result: WechatLoginResult, wxInfo?: MiniWechatProfile | null) { // 微信授权成功即登录;手机号改为下单页可选绑定 if (result.accessToken) { applySessionAndLeave(result, undefined, wxInfo); return; } if (result.needBindPhone && result.wxSessionKey) { setBindMode(true); setWxSessionKey(result.wxSessionKey); setMsg('微信授权成功,可绑定手机号(也可跳过,稍后在下单时再绑定)'); setSentHint(''); return; } setMsg('微信登录未完成,请重试或使用手机号登录'); } async function onSendCode() { if (!ensureAgreed()) return; if (cooldown > 0 || sending) return; const normalized = phone.trim(); if (!isValidPhone(normalized)) { setMsg('请输入正确的手机号'); return; } setMsg(''); setSentHint(''); setSending(true); try { const scene = bindMode || completeMode === 'phone' ? SmsScene.BIND_PHONE : SmsScene.USER_LOGIN; await request('/auth/sms/send', { method: 'POST', data: { phone: normalized, scene }, }); setCooldown(60); setSentHint('验证码已发送'); } catch (e) { setMsg(e instanceof Error ? e.message : '发送失败'); } finally { setSending(false); } } async function login() { if (!ensureAgreed()) return; const normalized = phone.trim(); if (!isValidPhone(normalized)) { setMsg('请输入正确的手机号'); return; } if (!code.trim()) { setMsg('请输入验证码'); return; } setLoading(true); setMsg(''); setSentHint(''); try { if (bindMode && wxSessionKey) { const data = await request('/auth/wechat/bind-phone', { method: 'POST', data: { wxSessionKey, phone: normalized, code: code.trim() }, }); handleWechatLoginResult(data); saveUserPhone(normalized); return; } if (completeMode === 'phone' && isLoggedIn()) { // bind 返回新 session(合并账号后旧 guest JWT 立刻失效),必须落盘后再离开 const data = await request('/auth/phone/bind', { method: 'POST', data: { phone: normalized, code: code.trim() }, }); if (!data?.accessToken) { setMsg('手机号验证成功但会话未返回,请重新登录'); return; } applySessionAndLeave(data, normalized, null, '手机号验证成功'); return; } const data = await request('/auth/login/sms', { method: 'POST', data: { phone: normalized, code: code.trim() }, }); if (!data?.accessToken) { setMsg('登录成功但未返回令牌,请重试'); return; } applySessionAndLeave(data, normalized); } catch (e) { setMsg(e instanceof Error ? e.message : '登录失败'); } finally { setLoading(false); } } async function wechatLogin() { if (!ensureAgreed()) return; setMsg(''); setSentHint(''); setWxLoading(true); try { let wxInfo: MiniWechatProfile | null = null; if (process.env.TARO_ENV === 'weapp') { try { wxInfo = await fetchMiniWechatUserInfo(); } catch (e) { toast(e instanceof Error ? e.message : '需要授权微信头像和昵称'); return; } } if (completeMode === 'wechat' && isLoggedIn()) { const result = await bindWechatForUser(wxInfo); if (!result.ok && 'redirecting' in result && result.redirecting) { return; } if (!result.ok && 'needBindPhone' in result && result.needBindPhone) { setBindMode(true); setWxSessionKey(result.wxSessionKey); setCompleteMode('phone'); setMsg('请绑定手机号完成认证'); return; } if (result.ok) { await syncMiniWechatProfile(wxInfo); toast('微信授权成功', 'success'); finishLoginNavigate(returnTo); return; } return; } const result = await loginWithWechat(); if (result) handleWechatLoginResult(result, wxInfo); } catch (e) { const raw = e instanceof Error ? e.message : '微信登录失败'; const hint = /invalid code/i.test(raw) ? process.env.TARO_ENV === 'weapp' ? '微信 code 无效:请确认后端 WX_MINI_APP_ID 与小程序 appid 一致,或本地联调使用 MOCK_WECHAT' : '微信授权失败:请确认公众号 WX_APP_ID / 网页授权域名配置正确' : raw; setMsg(hint); } finally { setWxLoading(false); } } const displayMsg = msg || sentHint; const codeDisabled = cooldown > 0 || sending; const showWechatLogin = (completeMode === 'wechat' || (!bindMode && !completeMode)) && (process.env.TARO_ENV === 'weapp' || wxAuthorize); const showSmsForm = completeMode !== 'wechat'; const cardTitle = completeMode === 'phone' ? '验证手机号' : bindMode ? '绑定手机号' : completeMode === 'wechat' ? '微信授权' : '手机验证码登录'; return ( 官方 {completeMode === 'phone' ? '建议绑定手机号' : completeMode === 'wechat' ? '完成微信授权' : '欢迎来到杜康好客'} {completeMode === 'phone' ? '便于订单通知与售后,也可稍后绑定' : completeMode === 'wechat' ? '完成后将返回继续支付' : '买美酒,享好礼'} {completeMode === 'wechat' ? ( 微信一键授权 使用微信支付前需授权微信账号 {showWechatLogin ? ( void wechatLogin()} /> ) : null} ) : ( {cardTitle} +86 { setPhone(normalizePhone(e.detail.value)); setMsg(''); setSentHint(''); }} /> setCode(e.detail.value.replace(/\D/g, '').slice(0, 6))} /> void onSendCode()} > {sending ? '发送中...' : cooldown > 0 ? `${cooldown}s 后重新获取` : '获取验证码'} {displayMsg ? ( {displayMsg} ) : null} void login()} > {loading ? '处理中...' : completeMode === 'phone' ? '完成验证' : bindMode ? '绑定并登录' : '登录'} {completeMode === 'phone' ? ( finishLoginNavigate(returnTo)} style={{ marginTop: 12, textAlign: 'center' }} > 暂不绑定,继续下单 ) : null} )} {showWechatLogin && completeMode !== 'wechat' ? ( <> 或者 void wechatLogin()} /> ) : null} setAgreed((v) => !v)}> {agreed ? : null} 我已阅读并同意 { e.stopPropagation(); Taro.navigateTo({ url: '/pages/user-agreement/index' }); }} > 《用户协议》 { e.stopPropagation(); Taro.navigateTo({ url: '/pages/privacy-policy/index' }); }} > 《隐私政策》 ); }