verified_user
diff --git a/apps/h5-shop/src/components/AuthGate.tsx b/apps/h5-shop/src/components/AuthGate.tsx
index ab26614..46fee16 100644
--- a/apps/h5-shop/src/components/AuthGate.tsx
+++ b/apps/h5-shop/src/components/AuthGate.tsx
@@ -1,4 +1,5 @@
import { Navigate, useLocation } from 'react-router-dom';
+import { getStoreProfile, hasShopWxSession } from '../lib/api';
import { useStoreSession } from '../contexts/StoreSessionContext';
const PUBLIC_PATHS = new Set(['/login', '/legal/user-agreement', '/legal/privacy-policy']);
@@ -25,6 +26,10 @@ export default function AuthGate({ children }: { children: React.ReactNode }) {
}
if (!authenticated && !PUBLIC_PATHS.has(location.pathname)) {
+ const profile = getStoreProfile();
+ if (profile && hasShopWxSession() && location.pathname !== '/login') {
+ return
;
+ }
return
;
}
diff --git a/apps/h5-shop/src/contexts/StoreSessionContext.tsx b/apps/h5-shop/src/contexts/StoreSessionContext.tsx
index edd3d32..9d1214c 100644
--- a/apps/h5-shop/src/contexts/StoreSessionContext.tsx
+++ b/apps/h5-shop/src/contexts/StoreSessionContext.tsx
@@ -7,6 +7,8 @@ import {
useState,
type ReactNode,
} from 'react';
+import { stripOAuthParamsFromLocation } from '@dukang/weixin-sdk';
+import { isWxAuthorizeEnabled } from '@dukang/shared-types';
import {
clearAuth,
ensureSession,
@@ -14,6 +16,12 @@ import {
type ShopSessionPayload,
type StoreSessionStore,
} from '../lib/api';
+import {
+ fetchClientConfig,
+ handleShopWechatCallback,
+ handleShopWechatLoginResult,
+} from '../lib/wechat-auth';
+import { isWechatEnv } from '../lib/weixin';
type StoreSessionContextValue = {
ready: boolean;
@@ -26,6 +34,12 @@ type StoreSessionContextValue = {
const StoreSessionContext = createContext
(null);
+function deriveSelectStore(session: ShopSessionPayload, nextStore: StoreSessionStore | null) {
+ const storeId = nextStore?.storeId || session.selectedStoreId || '';
+ const stores = session.stores ?? nextStore?.stores ?? [];
+ return stores.length > 1 && !storeId;
+}
+
export function StoreSessionProvider({ children }: { children: ReactNode }) {
const [ready, setReady] = useState(false);
const [authenticated, setAuthenticated] = useState(false);
@@ -43,9 +57,7 @@ export function StoreSessionProvider({ children }: { children: ReactNode }) {
}
: null;
setStore(nextStore);
- const storeId = nextStore?.storeId || session.selectedStoreId || '';
- const stores = session.stores ?? nextStore?.stores ?? [];
- setNeedsSelectStore(stores.length > 1 && !storeId);
+ setNeedsSelectStore(deriveSelectStore(session, nextStore));
}, []);
const resetSession = useCallback(() => {
@@ -59,6 +71,23 @@ export function StoreSessionProvider({ children }: { children: ReactNode }) {
let cancelled = false;
(async () => {
try {
+ const params = new URLSearchParams(window.location.search);
+ if (isWechatEnv() && params.get('code')) {
+ try {
+ const config = await fetchClientConfig();
+ if (isWxAuthorizeEnabled(config)) {
+ const result = await handleShopWechatCallback();
+ if (result && !cancelled) {
+ const session = handleShopWechatLoginResult(result);
+ if (session) applySession(session);
+ }
+ stripOAuthParamsFromLocation();
+ }
+ } catch {
+ stripOAuthParamsFromLocation();
+ }
+ }
+
const result = await ensureSession();
if (cancelled) return;
setAuthenticated(result.authenticated);
@@ -73,7 +102,7 @@ export function StoreSessionProvider({ children }: { children: ReactNode }) {
return () => {
cancelled = true;
};
- }, [resetSession]);
+ }, [applySession, resetSession]);
const value = useMemo(
() => ({ ready, authenticated, needsSelectStore, store, applySession, resetSession }),
diff --git a/apps/h5-shop/src/pages/LoginPage.tsx b/apps/h5-shop/src/pages/LoginPage.tsx
index 5c52e89..4316037 100644
--- a/apps/h5-shop/src/pages/LoginPage.tsx
+++ b/apps/h5-shop/src/pages/LoginPage.tsx
@@ -1,14 +1,39 @@
-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 AppImage from '@dukang/shared-ui/AppImage';
+import { isWxAuthorizeEnabled } from '@dukang/shared-types';
+import { stripOAuthParamsFromLocation } from '@dukang/weixin-sdk';
import { useStoreSession } from '../contexts/StoreSessionContext';
import {
getLastPhone,
+ getStoreProfile,
+ hasShopWxSession,
request,
saveRememberedSession,
type ShopSessionPayload,
} from '../lib/api';
import { routeAfterShopLogin } from './SelectStorePage';
+import {
+ bindShopWechatAfterSmsLogin,
+ fetchClientConfig,
+ handleShopWechatCallback,
+ handleShopWechatLoginResult,
+ loginShopWithWechat,
+} from '../lib/wechat-auth';
+import { isWechatEnv } from '../lib/weixin';
+
+function maskPhone(phone: string) {
+ if (phone.length < 7) return phone;
+ return `${phone.slice(0, 3)} **** ${phone.slice(-4)}`;
+}
+
+function formatWechatError(e: unknown): string {
+ const text = e instanceof Error ? e.message : '微信登录失败';
+ if (text.includes('首次登录') || text.includes('手机验证码')) {
+ return '该微信尚未绑定门店账号,请先使用手机验证码登录,登录后将自动关联微信';
+ }
+ return text;
+}
function ShopAgreementCheckbox({
agreed,
@@ -43,14 +68,44 @@ function ShopAgreementCheckbox({
export default function LoginPage() {
const navigate = useNavigate();
const { applySession } = useStoreSession();
+ const [params, setSearchParams] = useSearchParams();
+ const quick = params.get('quick') === '1';
+ const savedProfile = getStoreProfile();
const [phone, setPhone] = useState(getLastPhone());
const [code, setCode] = useState('');
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));
+ }, []);
+
+ useEffect(() => {
+ if (!isWechatEnv() || !wxAuthorize || !params.get('code')) return;
+ void handleShopWechatCallback()
+ .then((result) => {
+ if (!result) return;
+ const session = handleShopWechatLoginResult(result);
+ if (session) {
+ applySession(session);
+ stripOAuthParamsFromLocation();
+ setSearchParams({}, { replace: true });
+ routeAfterShopLogin(session, navigate);
+ }
+ })
+ .catch((e) => setMsg(formatWechatError(e)));
+ }, [applySession, navigate, params, setSearchParams, wxAuthorize]);
+
+ const quickStoreName = savedProfile?.storeName ?? '门店管理中心';
+ const quickPhone = savedProfile?.phone || phone;
+
function ensureAgreed() {
if (!agreed) {
setMsg('请先阅读并同意用户协议');
@@ -95,6 +150,11 @@ export default function LoginPage() {
});
saveRememberedSession(data);
applySession(data);
+ if (isWechatEnv() && wxAuthorize) {
+ setMsg('登录成功,正在关联微信…');
+ await bindShopWechatAfterSmsLogin();
+ return;
+ }
routeAfterShopLogin(data, navigate);
} catch (e) {
setMsg(e instanceof Error ? e.message : '登录失败');
@@ -103,6 +163,103 @@ export default function LoginPage() {
}
}
+ async function wechatLogin() {
+ if (!ensureAgreed()) return;
+ setMsg('');
+ if (!isWechatEnv()) {
+ setMsg('请在微信内打开以使用微信一键登录');
+ return;
+ }
+ setWxLoading(true);
+ try {
+ const session = await loginShopWithWechat();
+ if (session) {
+ applySession(session);
+ routeAfterShopLogin(session, navigate);
+ }
+ } catch (e) {
+ setMsg(formatWechatError(e));
+ } finally {
+ setWxLoading(false);
+ }
+ }
+
+ if (quick) {
+ const canWechatQuick = wxAuthorize && isWechatEnv() && hasShopWxSession() && !!savedProfile;
+
+ return (
+
+
+
+
+
+
+ storefront
+
+
+
{quickStoreName}
+
{maskPhone(quickPhone)}
+
+
+ verified_user
+ 认证门店
+
+
+
+ sync
+ 切换账号
+
+
+
+
+
+
+ {msg &&
{msg}
}
+
+ {canWechatQuick ? (
+
+ ) : (
+
+ {wxAuthorize && !isWechatEnv()
+ ? '请在微信内打开以使用一键登录'
+ : '请使用验证码登录并绑定微信后,即可 7 天内免登录'}
+
+ )}
+ {!canWechatQuick && (
+
+ 验证码登录
+
+ )}
+
+ lock
+ {canWechatQuick ? '微信验证 · 7 天内免登录' : '加密环境安全登录中'}
+
+
+
+
+
+ );
+ }
+
return (
@@ -183,6 +361,11 @@ export default function LoginPage() {
security
+ {hasShopWxSession() && savedProfile && (
+
+ 微信快捷登录
+
+ )}
);