Merge commit 'fcf1d45522e3ba6e6faf2590b60f2db1ef90e73c' into dev_jacy

This commit is contained in:
2026-07-07 13:55:10 +08:00
85 changed files with 11210 additions and 164 deletions
+11
View File
@@ -1,4 +1,5 @@
import { Routes, Route, Navigate } from 'react-router-dom';
import { useEffect } from 'react';
import TabLayout from './layouts/TabLayout';
import LoginPage from './pages/LoginPage';
import HomePage from './pages/HomePage';
@@ -19,10 +20,20 @@ import RedeemSuccessPage from './pages/RedeemSuccessPage';
import PayPage from './pages/PayPage';
import CustomerServicePage from './pages/CustomerServicePage';
import { UserSessionProvider } from './contexts/UserSessionContext';
import { capturePromoFromUrl, touchPromoIfNeeded } from './lib/promo';
function PromoBootstrap() {
useEffect(() => {
capturePromoFromUrl();
void touchPromoIfNeeded();
}, []);
return null;
}
export default function App() {
return (
<UserSessionProvider>
<PromoBootstrap />
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route element={<TabLayout />}>
@@ -17,6 +17,7 @@ import {
type SessionPayload,
type UserProfile,
} from '../lib/api';
import { touchPromoIfNeeded } from '../lib/promo';
type UserSessionContextValue = {
ready: boolean;
@@ -62,6 +63,7 @@ export function UserSessionProvider({ children }: { children: ReactNode }) {
if (!session.user) {
await refreshProfile();
}
await touchPromoIfNeeded();
} catch {
if (!cancelled) {
try {
+53
View File
@@ -0,0 +1,53 @@
import { apiBase } from './api';
export const PROMO_STORAGE_KEY = 'dukang_promo_code';
function readPromoFromSearch(search: string): string | null {
const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search);
const code = params.get('promo')?.trim();
return code ? code.toUpperCase() : null;
}
/** 解析 URL 中的 ?promo= 并写入 sessionStorage */
export function capturePromoFromUrl(): string | null {
if (typeof window === 'undefined') return null;
let code = readPromoFromSearch(window.location.search);
if (!code && window.location.hash.includes('?')) {
const hashQuery = window.location.hash.slice(window.location.hash.indexOf('?'));
code = readPromoFromSearch(hashQuery);
}
if (code) {
sessionStorage.setItem(PROMO_STORAGE_KEY, code);
}
return code ?? sessionStorage.getItem(PROMO_STORAGE_KEY);
}
export function getStoredPromoCode(): string | null {
if (typeof window === 'undefined') return null;
return sessionStorage.getItem(PROMO_STORAGE_KEY);
}
/** 调用 /promo/touch 完成扫码归因(OptionalJwt:未登录也累加 scan_count */
export async function touchPromoIfNeeded(): Promise<void> {
const promoCode = getStoredPromoCode();
if (!promoCode) return;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Client-App': 'USER_H5',
};
const token = localStorage.getItem('accessToken');
if (token) headers.Authorization = `Bearer ${token}`;
try {
const res = await fetch(`${apiBase}/promo/touch`, {
method: 'POST',
headers,
body: JSON.stringify({ promoCode }),
});
const json = await res.json();
if (json.code !== 0) return;
} catch {
/* 静默失败,不阻断用户流程 */
}
}
+9 -2
View File
@@ -8,6 +8,12 @@ import { normalizePhoneInput, validateMobilePhone } from '../lib/phone';
import { useSmsCode } from '../lib/use-sms-code';
import { isWechatEnv, weixinSdk } from '../lib/weixin';
import { useUserSession } from '../contexts/UserSessionContext';
import { touchPromoIfNeeded } from '../lib/promo';
async function finishLogin(navigate: (path: string) => void, returnTo: string) {
await touchPromoIfNeeded();
navigate(returnTo.startsWith('/') ? returnTo : '/');
}
export default function LoginPage() {
const navigate = useNavigate();
@@ -50,7 +56,8 @@ export default function LoginPage() {
phoneVerified: !!result.phoneVerified,
user: result.user as SessionPayload['user'],
});
navigate(returnTo.startsWith('/') ? returnTo : '/');
void finishLogin(navigate, returnTo);
return;
}
}
@@ -97,7 +104,7 @@ export default function LoginPage() {
body: JSON.stringify({ phone, code }),
});
applySession(data);
navigate(returnTo.startsWith('/') ? returnTo : '/');
await finishLogin(navigate, returnTo);
} catch (e) {
setMsg(e instanceof Error ? e.message : '登录失败');
} finally {