Merge branch 'dev_new_ljy' into dev

This commit is contained in:
ljy
2026-07-10 01:54:39 +08:00
7 changed files with 88 additions and 28 deletions
+23 -4
View File
@@ -1,13 +1,32 @@
import { Routes, Route, Navigate } from 'react-router-dom'; import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
import LoginPage from './pages/LoginPage'; import LoginPage from './pages/LoginPage';
import PartnerAppRoutes from './PartnerAppRoutes'; import PartnerAppRoutes from './PartnerAppRoutes';
import { isLoggedIn } from './lib/api'; import { isLoggedIn } from './lib/api';
import { usePartnerSession } from './contexts/PartnerSessionContext';
import { partnerHomePath } from './lib/partnerAccess';
function LoginRoute() {
const { account } = usePartnerSession();
if (isLoggedIn()) {
return <Navigate to={partnerHomePath(account)} replace />;
}
return <LoginPage />;
}
function AppRoutes() {
const location = useLocation();
export default function App() {
return ( return (
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginRoute />} />
<Route path="/*" element={isLoggedIn() ? <PartnerAppRoutes /> : <Navigate to="/login" replace />} /> <Route
path="/*"
element={isLoggedIn() ? <PartnerAppRoutes /> : <Navigate to="/login" replace state={{ from: location }} />}
/>
</Routes> </Routes>
); );
} }
export default function App() {
return <AppRoutes />;
}
+3 -3
View File
@@ -61,12 +61,12 @@ function SubAccountRoutes() {
} }
export default function PartnerAppRoutes() { export default function PartnerAppRoutes() {
const { account, loading, loggedIn } = usePartnerSession(); const { account, loading } = usePartnerSession();
if (!loggedIn && !isLoggedIn()) { if (!isLoggedIn()) {
return <Navigate to="/login" replace />; return <Navigate to="/login" replace />;
} }
if (loading) { if (loading && !account) {
return <SessionLoading />; return <SessionLoading />;
} }
if (isSubAccount(account)) { if (isSubAccount(account)) {
@@ -1,6 +1,6 @@
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react'; import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react';
import { toAppPath } from '@dukang/weixin-sdk'; import { toAppPath } from '@dukang/weixin-sdk';
import { clearAuth, isLoggedIn, request } from '../lib/api'; import { clearAuth, isLoggedIn, request, saveAuth, type PartnerAuthPayload } from '../lib/api';
import type { PartnerMe, PartnerStaffRole } from '@dukang/shared-types'; import type { PartnerMe, PartnerStaffRole } from '@dukang/shared-types';
@@ -12,6 +12,7 @@ type PartnerSessionValue = {
account: PartnerAccount | null; account: PartnerAccount | null;
loading: boolean; loading: boolean;
loggedIn: boolean; loggedIn: boolean;
applySession: (session: PartnerAuthPayload) => void;
refresh: () => Promise<PartnerAccount | null>; refresh: () => Promise<PartnerAccount | null>;
logout: () => void; logout: () => void;
}; };
@@ -23,6 +24,15 @@ export function PartnerSessionProvider({ children }: { children: ReactNode }) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [loggedIn, setLoggedIn] = useState(() => isLoggedIn()); const [loggedIn, setLoggedIn] = useState(() => isLoggedIn());
const applySession = useCallback((session: PartnerAuthPayload) => {
saveAuth(session);
setLoggedIn(true);
setLoading(false);
if (session.partner) {
setAccount(session.partner as PartnerAccount);
}
}, []);
const refresh = useCallback(async (): Promise<PartnerAccount | null> => { const refresh = useCallback(async (): Promise<PartnerAccount | null> => {
if (!isLoggedIn()) { if (!isLoggedIn()) {
setAccount(null); setAccount(null);
@@ -38,6 +48,9 @@ export function PartnerSessionProvider({ children }: { children: ReactNode }) {
return data; return data;
} catch { } catch {
setAccount(null); setAccount(null);
if (!isLoggedIn()) {
setLoggedIn(false);
}
return null; return null;
} finally { } finally {
setLoading(false); setLoading(false);
@@ -57,7 +70,7 @@ export function PartnerSessionProvider({ children }: { children: ReactNode }) {
return ( return (
<PartnerSessionContext.Provider <PartnerSessionContext.Provider
value={{ account, loading, loggedIn, refresh, logout }} value={{ account, loading, loggedIn, applySession, refresh, logout }}
> >
{children} {children}
</PartnerSessionContext.Provider> </PartnerSessionContext.Provider>
+5 -2
View File
@@ -1,4 +1,4 @@
import type { PartnerMe } from '@dukang/shared-types'; import type { PartnerMe, PartnerStaffRole } from '@dukang/shared-types';
import { isOnAppPath, toAppPath } from '@dukang/weixin-sdk'; import { isOnAppPath, toAppPath } from '@dukang/weixin-sdk';
export const apiBase = '/api/v1'; export const apiBase = '/api/v1';
@@ -6,7 +6,10 @@ export const apiBase = '/api/v1';
const LAST_PHONE = 'partnerLastPhone'; const LAST_PHONE = 'partnerLastPhone';
const PARTNER_PROFILE = 'partnerProfile'; const PARTNER_PROFILE = 'partnerProfile';
export type PartnerSessionProfile = Pick<PartnerMe, 'id' | 'name' | 'phone' | 'companyName'>; export type PartnerSessionProfile = Pick<PartnerMe, 'id' | 'name' | 'phone' | 'companyName'> & {
isPrimary?: PartnerMe['isPrimary'];
staffRole?: PartnerStaffRole;
};
export type PartnerAuthPayload = { export type PartnerAuthPayload = {
accessToken: string; accessToken: string;
+5 -2
View File
@@ -2,7 +2,7 @@ import type { ClientRuntimeConfig, WechatLoginResult } from '@dukang/shared-type
import { isWxAuthorizeEnabled } from '@dukang/shared-types'; import { isWxAuthorizeEnabled } from '@dukang/shared-types';
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk'; import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
import { isWechatEnv, weixinSdk } from './weixin'; import { isWechatEnv, weixinSdk } from './weixin';
import { request, saveAuth } from './api'; import { request, saveAuth, type PartnerSessionProfile } from './api';
export type PartnerProfile = { export type PartnerProfile = {
id: string; id: string;
@@ -37,7 +37,10 @@ export async function checkNeedsWechatAuth(profile: PartnerProfile | null): Prom
/** 处理微信登录/绑定结果,返回是否已拿到 token 可进入首页 */ /** 处理微信登录/绑定结果,返回是否已拿到 token 可进入首页 */
export function handlePartnerWechatLoginResult(result: WechatLoginResult): boolean { export function handlePartnerWechatLoginResult(result: WechatLoginResult): boolean {
if (!result.accessToken) return false; if (!result.accessToken) return false;
saveAuth({ accessToken: result.accessToken }); saveAuth({
accessToken: result.accessToken,
partner: result.partner as PartnerSessionProfile | undefined,
});
return true; return true;
} }
+32 -14
View File
@@ -1,13 +1,13 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { request, getLastPhone, getPartnerProfile, saveAuth } from '../lib/api'; import { request, getLastPhone, getPartnerProfile, type PartnerAuthPayload } from '../lib/api';
import { usePartnerSession } from '../contexts/PartnerSessionContext'; import { usePartnerSession } from '../contexts/PartnerSessionContext';
import type { PartnerAccount } from '../contexts/PartnerSessionContext';
import { partnerHomePath } from '../lib/partnerAccess'; import { partnerHomePath } from '../lib/partnerAccess';
import type { PartnerMe } from '@dukang/shared-types'; import type { PartnerMe } from '@dukang/shared-types';
import { import {
fetchClientConfig, fetchClientConfig,
handlePartnerWechatCallback, handlePartnerWechatCallback,
handlePartnerWechatLoginResult,
loginPartnerWithWechat, loginPartnerWithWechat,
} from '../lib/wechat-auth'; } from '../lib/wechat-auth';
import { isWechatEnv } from '../lib/weixin'; import { isWechatEnv } from '../lib/weixin';
@@ -43,9 +43,21 @@ function formatPartnerError(e: unknown): string {
return text; return text;
} }
function toLoginSession(data: { accessToken: string; partner?: PartnerMe }): PartnerAuthPayload {
return {
accessToken: data.accessToken,
partner: data.partner,
};
}
function toPartnerAccount(partner?: Record<string, unknown>): PartnerAccount | undefined {
if (!partner || typeof partner.id !== 'string') return undefined;
return partner as unknown as PartnerAccount;
}
export default function LoginPage() { export default function LoginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { refresh } = usePartnerSession(); const { applySession } = usePartnerSession();
const [params] = useSearchParams(); const [params] = useSearchParams();
const quick = params.get('quick') === '1'; const quick = params.get('quick') === '1';
const savedProfile = getPartnerProfile(); const savedProfile = getPartnerProfile();
@@ -73,14 +85,17 @@ export default function LoginPage() {
useEffect(() => { useEffect(() => {
if (!isWechatEnv() || !wxAuthorize || !params.get('code')) return; if (!isWechatEnv() || !wxAuthorize || !params.get('code')) return;
void handlePartnerWechatCallback() void handlePartnerWechatCallback()
.then(async (result) => { .then((result) => {
if (!result) return; if (!result?.accessToken) return;
if (!handlePartnerWechatLoginResult(result)) return; const session: PartnerAuthPayload = {
const account = await refresh(); accessToken: result.accessToken,
navigate(partnerHomePath(account ?? undefined)); partner: toPartnerAccount(result.partner),
};
applySession(session);
navigate(partnerHomePath(session.partner), { replace: true });
}) })
.catch((e) => setMsg(formatWechatError(e))); .catch((e) => setMsg(formatWechatError(e)));
}, [navigate, refresh, wxAuthorize, params]); }, [applySession, navigate, wxAuthorize, params]);
function formatWechatError(e: unknown): string { function formatWechatError(e: unknown): string {
const text = e instanceof Error ? e.message : '微信登录失败'; const text = e instanceof Error ? e.message : '微信登录失败';
@@ -155,10 +170,10 @@ export default function LoginPage() {
body: JSON.stringify({ phone: loginPhone, code }), body: JSON.stringify({ phone: loginPhone, code }),
silent: true, silent: true,
}); });
saveAuth(data); const session = toLoginSession(data);
applySession(session);
persistRememberAccount(loginPhone); persistRememberAccount(loginPhone);
const account = await refresh(); navigate(partnerHomePath(session.partner), { replace: true });
navigate(partnerHomePath(account ?? data.partner));
} catch (e) { } catch (e) {
setMsg(formatPartnerError(e)); setMsg(formatPartnerError(e));
} finally { } finally {
@@ -177,8 +192,11 @@ export default function LoginPage() {
try { try {
const ok = await loginPartnerWithWechat(); const ok = await loginPartnerWithWechat();
if (ok) { if (ok) {
const account = await refresh(); applySession({
navigate(partnerHomePath(account ?? undefined)); accessToken: localStorage.getItem('accessToken') ?? '',
partner: getPartnerProfile() ?? undefined,
});
navigate(partnerHomePath(getPartnerProfile()), { replace: true });
} }
} catch (e) { } catch (e) {
setMsg(formatWechatError(e)); setMsg(formatWechatError(e));
+5 -1
View File
@@ -55,7 +55,11 @@ async function main() {
); );
if (!login.accessToken) throw new Error('login missing token'); if (!login.accessToken) throw new Error('login missing token');
console.log('4. Admin partner logs list'); console.log('4. PARTNER /partner/me after login');
const me = await req('PARTNER_H5', '/partner/me', { token: login.accessToken });
if (!me?.id || !me?.phone) throw new Error('partner/me missing profile');
console.log('5. Admin partner logs list');
const admin = await req('HQ_WEB', '/admin/auth/login/password', { const admin = await req('HQ_WEB', '/admin/auth/login/password', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({