Merge branch 'dev_new_ljy' into dev
This commit is contained in:
@@ -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 />;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user