子账号登录微信授权

This commit is contained in:
2026-07-10 11:03:11 +08:00
parent f4f4f1b4aa
commit 9c5cb69730
11 changed files with 535 additions and 251 deletions
+78 -59
View File
@@ -1,18 +1,23 @@
import { useEffect, useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { request, getLastPhone, getPartnerProfile, saveAuth } from '../lib/api';
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
import {
getLastPhone,
getPartnerProfile,
hasPartnerWxSession,
request,
saveAuth,
type PartnerSessionPayload,
} from '../lib/api';
import { usePartnerSession } from '../contexts/PartnerSessionContext';
import { partnerHomePath } from '../lib/partnerAccess';
import type { PartnerMe } from '@dukang/shared-types';
import {
bindPartnerWechatAfterSmsLogin,
fetchClientConfig,
handlePartnerWechatCallback,
handlePartnerWechatLoginResult,
loginPartnerWithWechat,
} from '../lib/wechat-auth';
import { isWechatEnv } from '../lib/weixin';
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
import { toastError } from '../lib/toast';
const REMEMBER_PHONE_KEY = 'partner_remember_phone';
@@ -44,9 +49,17 @@ 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 { refresh } = usePartnerSession();
const { applySession, refresh, account } = usePartnerSession();
const [params] = useSearchParams();
const quick = params.get('quick') === '1';
const savedProfile = getPartnerProfile();
@@ -71,26 +84,6 @@ export default function LoginPage() {
const quickCompany = savedProfile?.companyName ?? '';
const quickPhone = savedProfile?.phone || phone;
useEffect(() => {
if (!isWechatEnv() || !wxAuthorize || !params.get('code')) return;
void handlePartnerWechatCallback()
.then(async (result) => {
if (!result) return;
if (!handlePartnerWechatLoginResult(result)) return;
const account = await refresh();
navigate(partnerHomePath(account ?? undefined));
})
.catch((e) => setMsg(formatWechatError(e)));
}, [navigate, refresh, wxAuthorize, params]);
function formatWechatError(e: unknown): string {
const text = e instanceof Error ? e.message : '微信登录失败';
if (text.includes('首次登录') || text.includes('手机验证码')) {
return '该微信尚未绑定合伙人账号,请先使用手机验证码登录,登录后将自动关联微信';
}
return text;
}
function ensureAgreed() {
if (!agreed) {
const text = '请先勾选并同意用户协议';
@@ -142,28 +135,30 @@ export default function LoginPage() {
}
}
async function login(options?: { quick?: boolean }) {
if (!options?.quick && !ensureAgreed()) return;
async function finishLoginNavigate() {
const me = await refresh();
navigate(partnerHomePath(me ?? account ?? savedProfile));
}
async function login() {
if (!ensureAgreed()) return;
setLoading(true);
setMsg('');
try {
if (options?.quick) {
await request('PARTNER_H5', '/partner/auth/sms/send', {
method: 'POST',
body: JSON.stringify({ phone: quickPhone, scene: 'PARTNER_LOGIN' }),
silent: true,
});
}
const loginPhone = options?.quick ? quickPhone : phone;
const data = await request<{ accessToken: string; partner?: PartnerMe }>('PARTNER_H5', '/partner/auth/login/sms', {
const data = await request<PartnerSessionPayload>('PARTNER_H5', '/partner/auth/login/sms', {
method: 'POST',
body: JSON.stringify({ phone: loginPhone, code }),
body: JSON.stringify({ phone, code }),
silent: true,
});
saveAuth(data);
persistRememberAccount(loginPhone);
const account = await refresh();
navigate(partnerHomePath(account ?? data.partner));
applySession(data);
persistRememberAccount(phone);
if (isWechatEnv() && wxAuthorize) {
setMsg('登录成功,正在关联微信…');
await bindPartnerWechatAfterSmsLogin();
return;
}
await finishLoginNavigate();
} catch (e) {
const text = formatPartnerError(e);
setMsg(text);
@@ -183,10 +178,10 @@ export default function LoginPage() {
}
setWxLoading(true);
try {
const ok = await loginPartnerWithWechat();
if (ok) {
const account = await refresh();
navigate(partnerHomePath(account ?? undefined));
const session = await loginPartnerWithWechat();
if (session) {
applySession(session);
await finishLoginNavigate();
}
} catch (e) {
const text = formatWechatError(e);
@@ -198,6 +193,8 @@ export default function LoginPage() {
}
if (quick) {
const canWechatQuick = wxAuthorize && isWechatEnv() && hasPartnerWxSession() && !!savedProfile;
return (
<div className="partner-auth-page partner-auth-page--quick">
<header className="partner-auth-brand">
@@ -228,17 +225,37 @@ export default function LoginPage() {
<nav style={{ width: '100%', maxWidth: 384, marginTop: 16 }}>
{msg && <p className="partner-auth-msg" style={{ color: 'var(--color-error, #d33)', fontSize: 13, textAlign: 'center', marginBottom: 12 }}>{msg}</p>}
<button type="button" className="partner-btn-primary" onClick={() => void login({ quick: true })} disabled={loading || !quickPhone}>
<span>{loading ? '登录中...' : '一键登录'}</span>
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
</button>
{canWechatQuick ? (
<button
type="button"
className="partner-btn-primary"
disabled={wxLoading}
onClick={() => void wechatLogin()}
>
<span className="material-symbols-outlined">chat</span>
<span>{wxLoading ? '登录中...' : '微信一键登录'}</span>
</button>
) : (
<p className="partner-auth-msg" style={{ textAlign: 'center', marginBottom: 12 }}>
{wxAuthorize && !isWechatEnv()
? '请在微信内打开以使用一键登录'
: '请使用验证码登录并绑定微信后,即可 7 天内免登录'}
</p>
)}
{!canWechatQuick && (
<Link to="/login" className="partner-btn-primary" style={{ display: 'block', textAlign: 'center', textDecoration: 'none', marginTop: 12 }}>
</Link>
)}
<Link to="/login" className="partner-btn-ghost" style={{ display: 'block', marginTop: 12 }}></Link>
</nav>
<footer className="partner-auth-footer">
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span className="material-symbols-outlined">verified_user</span>
<span className="label-md" style={{ fontSize: 10, letterSpacing: '0.1em', textTransform: 'uppercase' }}>Secured by Dukang Heritage</span>
<span className="label-md" style={{ fontSize: 10, letterSpacing: '0.1em', textTransform: 'uppercase' }}>
{canWechatQuick ? '微信验证 · 7 天内免登录' : 'Secured by Dukang Heritage'}
</span>
</div>
</footer>
</div>
@@ -308,13 +325,13 @@ export default function LoginPage() {
{wxAuthorize && (
<>
<div className="partner-auth-divider"><span></span></div>
<button type="button" className="partner-btn-wechat" onClick={wechatLogin} disabled={wxLoading}>
<svg viewBox="0 0 24 24" fill="#07C160" aria-hidden>
<path d="M8.25 4.5C4.52 4.5 1.5 7.04 1.5 10.17c0 1.78.98 3.37 2.5 4.48l-.63 1.88 2.19-1.09c.84.24 1.74.38 2.69.38.25 0 .5 0 .75-.03-.16-.53-.25-1.09-.25-1.66 0-3.13 3.02-5.67 6.75-5.67.57 0 1.13.06 1.66.17C15.17 6.13 12 4.5 8.25 4.5zm10.5 6.33c-3.11 0-5.62 2.12-5.62 4.73 0 2.61 2.51 4.73 5.62 4.73.79 0 1.54-.14 2.24-.38l1.83.91-.53-1.57c1.27-.92 2.08-2.25 2.08-3.73 0-2.61-2.51-4.73-5.62-4.73z" />
</svg>
<span>{wxLoading ? '登录中...' : '微信一键授权'}</span>
</button>
<div className="partner-auth-divider"><span></span></div>
<button type="button" className="partner-btn-wechat" onClick={() => void wechatLogin()} disabled={wxLoading}>
<svg viewBox="0 0 24 24" fill="#07C160" aria-hidden>
<path d="M8.25 4.5C4.52 4.5 1.5 7.04 1.5 10.17c0 1.78.98 3.37 2.5 4.48l-.63 1.88 2.19-1.09c.84.24 1.74.38 2.69.38.25 0 .5 0 .75-.03-.16-.53-.25-1.09-.25-1.66 0-3.13 3.02-5.67 6.75-5.67.57 0 1.13.06 1.66.17C15.17 6.13 12 4.5 8.25 4.5zm10.5 6.33c-3.11 0-5.62 2.12-5.62 4.73 0 2.61 2.51 4.73 5.62 4.73.79 0 1.54-.14 2.24-.38l1.83.91-.53-1.57c1.27-.92 2.08-2.25 2.08-3.73 0-2.61-2.51-4.73-5.62-4.73z" />
</svg>
<span>{wxLoading ? '登录中...' : '微信一键登录'}</span>
</button>
</>
)}
@@ -327,7 +344,9 @@ export default function LoginPage() {
</div>
</main>
<Link to="/login?quick=1" className="partner-link"></Link>
{hasPartnerWxSession() && savedProfile && (
<Link to="/login?quick=1" className="partner-link"></Link>
)}
<footer className="partner-auth-footer">
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
+8 -62
View File
@@ -17,20 +17,8 @@ import { checkStorePhoneAvailable } from '../lib/storePhone';
import { fetchPartnerCities, type OpenCityOption } from '../lib/upload';
import {
fetchPartnerProfile,
handlePartnerWechatCallback,
savePartnerWechatAuth,
} from '../lib/wechat-auth';
import { isWechatEnv, weixinSdk } from '../lib/weixin';
import { stripOAuthParamsFromLocation } from '@dukang/weixin-sdk';
import { usePartnerSession } from '../contexts/PartnerSessionContext';
import {
@@ -79,10 +67,12 @@ export default function StoreCreatePage() {
const navigate = useNavigate();
const { account } = usePartnerSession();
const { account, refresh } = usePartnerSession();
const accountId = account?.id;
const wechatReady = !!account?.hasWechat;
const [params, setParams] = useSearchParams();
const saved = loadStoreDraft(accountId);
@@ -101,8 +91,6 @@ export default function StoreCreatePage() {
const [citiesError, setCitiesError] = useState('');
const [wechatReady, setWechatReady] = useState(false);
function reportFormError(message: string) {
setSubmitError(message);
toastError(message);
@@ -138,11 +126,7 @@ export default function StoreCreatePage() {
if (step !== 2 || !isWechatEnv()) return;
void fetchPartnerProfile()
.then((me) => setWechatReady(!!me.hasWechat))
.catch(() => setWechatReady(false));
void refresh();
void weixinSdk.init().catch(() => {
@@ -150,45 +134,7 @@ export default function StoreCreatePage() {
});
}, [step]);
useEffect(() => {
if (!isWechatEnv() || !params.get('code')) return;
void handlePartnerWechatCallback()
.then((result) => {
if (result && savePartnerWechatAuth(result)) {
setWechatReady(true);
}
stripOAuthParamsFromLocation();
const next = new URLSearchParams(params);
next.delete('code');
next.delete('state');
setParams(next, { replace: true });
void weixinSdk.init().catch(() => {
/* OssUploadField 点击时会再次初始化 */
});
})
.catch((e) => reportFormError(e instanceof Error ? e.message : '微信授权失败'));
}, [params, setParams]);
}, [step, refresh]);
@@ -693,7 +639,7 @@ export default function StoreCreatePage() {
wechatReady={wechatReady}
onWechatReadyChange={setWechatReady}
onWechatReadyChange={() => { void refresh(); }}
onChange={(coverUrl) => patchForm({ coverUrl })}
@@ -729,7 +675,7 @@ export default function StoreCreatePage() {
wechatReady={wechatReady}
onWechatReadyChange={setWechatReady}
onWechatReadyChange={() => { void refresh(); }}
onChange={(nextUrl) => {
@@ -769,7 +715,7 @@ export default function StoreCreatePage() {
wechatReady={wechatReady}
onWechatReadyChange={setWechatReady}
onWechatReadyChange={() => { void refresh(); }}
onChange={(contractUrl) => patchForm({ contractUrl })}