diff --git a/apps/h5-partner/src/components/AuthGate.tsx b/apps/h5-partner/src/components/AuthGate.tsx index eab698c..56229d5 100644 --- a/apps/h5-partner/src/components/AuthGate.tsx +++ b/apps/h5-partner/src/components/AuthGate.tsx @@ -1,5 +1,5 @@ import { Navigate, useLocation } from 'react-router-dom'; -import { getPartnerProfile } from '../lib/api'; +import { getPartnerProfile, hasPartnerWxSession } from '../lib/api'; import { usePartnerSession } from '../contexts/PartnerSessionContext'; import { partnerHomePath } from '../lib/partnerAccess'; @@ -18,6 +18,10 @@ export default function AuthGate({ children }: { children: React.ReactNode }) { } if (!authenticated && !PUBLIC_PATHS.has(location.pathname)) { + const profile = getPartnerProfile(); + if (profile && hasPartnerWxSession() && profile.hasWechat) { + return ; + } return ; } diff --git a/apps/h5-partner/src/pages/LoginPage.tsx b/apps/h5-partner/src/pages/LoginPage.tsx index b7c2a1a..96c310c 100644 --- a/apps/h5-partner/src/pages/LoginPage.tsx +++ b/apps/h5-partner/src/pages/LoginPage.tsx @@ -1,14 +1,25 @@ -import { useRef, useState, type RefObject } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { useEffect, useRef, useState, type RefObject } from 'react'; +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import { isWxAuthorizeEnabled } from '@dukang/shared-types'; +import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk'; import { getLastPhone, getPartnerProfile, + hasPartnerWxSession, request, saveRememberedSession, type PartnerSessionPayload, } from '../lib/api'; import { usePartnerSession } from '../contexts/PartnerSessionContext'; import { partnerHomePath } from '../lib/partnerAccess'; +import { + bindPartnerWechatAfterSmsLogin, + canPartnerUseWechatLogin, + fetchClientConfig, + loginPartnerWithWechat, + PARTNER_WECHAT_LOGIN_HINT, +} from '../lib/wechat-auth'; +import { isWechatEnv } from '../lib/weixin'; const REMEMBER_PHONE_KEY = 'partner_remember_phone'; const REMEMBER_FLAG_KEY = 'partner_remember_account'; @@ -39,6 +50,11 @@ function AgreementCheckbox({ ); } +function maskPhone(phone: string) { + if (phone.length < 7) return phone; + return `${phone.slice(0, 3)} **** ${phone.slice(-4)}`; +} + function loadRememberedPhone(): { phone: string; remember: boolean } { try { const remember = localStorage.getItem(REMEMBER_FLAG_KEY) === '1'; @@ -60,9 +76,19 @@ function formatPartnerError(e: unknown): string { return text; } +function formatWechatError(e: unknown): string { + const text = e instanceof Error ? e.message : '微信登录失败'; + if (text.includes('首次登录') || text.includes('手机验证码')) { + return '该微信尚未绑定合伙人账号,请先使用手机验证码登录,登录后将自动关联微信'; + } + return text; +} + export default function LoginPage() { const navigate = useNavigate(); const { applySession, refresh, account } = usePartnerSession(); + const [params] = useSearchParams(); + const quick = params.get('quick') === '1'; const savedProfile = getPartnerProfile(); const remembered = loadRememberedPhone(); const [phone, setPhone] = useState(remembered.phone || getLastPhone()); @@ -70,10 +96,22 @@ export default function LoginPage() { const [rememberAccount, setRememberAccount] = useState(remembered.remember); const [agreed, setAgreed] = useState(false); const [loading, setLoading] = useState(false); + const [wxLoading, setWxLoading] = useState(false); const [msg, setMsg] = useState(''); const [codeCooldown, setCodeCooldown] = useState(0); + const [wxAuthorize, setWxAuthorize] = useState(false); const agreementRef = useRef(null); + useEffect(() => { + fetchClientConfig() + .then((config) => setWxAuthorize(isWxAuthorizeEnabled(config))) + .catch(() => setWxAuthorize(false)); + }, []); + + const quickName = savedProfile?.name ?? '城市合伙人'; + const quickCompany = savedProfile?.companyName ?? ''; + const quickPhone = savedProfile?.phone || phone; + function ensureAgreed() { if (!agreed) { setMsg('请先勾选并同意用户协议'); @@ -140,6 +178,11 @@ export default function LoginPage() { saveRememberedSession(data); applySession(data); persistRememberAccount(phone); + if (isWechatEnv() && wxAuthorize) { + setMsg('登录成功,正在关联微信…'); + await bindPartnerWechatAfterSmsLogin(); + return; + } await finishLoginNavigate(); } catch (e) { setMsg(formatPartnerError(e)); @@ -148,6 +191,116 @@ export default function LoginPage() { } } + async function wechatLogin() { + if (!ensureAgreed()) return; + setMsg(''); + if (!isWechatEnv()) { + setMsg(WECHAT_INAPP_REQUIRED_MSG); + return; + } + const canUseWechat = await canPartnerUseWechatLogin({ + profile: savedProfile, + phone, + }); + if (!canUseWechat) { + setMsg(PARTNER_WECHAT_LOGIN_HINT); + return; + } + setWxLoading(true); + try { + const session = await loginPartnerWithWechat(); + if (session) { + applySession(session); + await finishLoginNavigate(); + } + } catch (e) { + setMsg(formatWechatError(e)); + } finally { + setWxLoading(false); + } + } + + if (quick) { + const canWechatQuick = + wxAuthorize && + isWechatEnv() && + hasPartnerWxSession() && + !!savedProfile && + savedProfile.hasWechat === true; + + return ( +
+
+
+ wine_bar +
+

杜康好客

+

城市合伙人端

+
+ +
+
已识别账号
+
+ person +
+
+

+ {quickName} + {quickCompany ? ( + ({quickCompany}) + ) : null} +

+

+ {quickPhone ? maskPhone(quickPhone) : '暂无已保存账号'} +

+
+
+ + + +
+
+ verified_user + + {canWechatQuick ? '微信验证 · 7 天内免登录' : 'Secured by Dukang Heritage'} + +
+
+
+ ); + } + return (
@@ -214,12 +367,29 @@ export default function LoginPage() { {loading ? '登录中...' : '登录'} {!loading && arrow_forward} + + {wxAuthorize && ( + <> +
其他登录方式
+ + + )} +

- 手机号验证成功后,7 天内无需再次输入验证码 + 手机号验证成功后,7 天内无需再次输入验证码;微信内登录将自动关联微信

+ {hasPartnerWxSession() && savedProfile?.hasWechat && ( + 微信快捷登录 + )} +
);