@@ -202,7 +202,7 @@ async function rawRequest<T>(
|
||||
if (authToken) headers.Authorization = `Bearer ${authToken}`;
|
||||
|
||||
const res = await fetch(`${apiBase}${path}`, { ...options, headers });
|
||||
let json: { code: number; message?: string; data?: T };
|
||||
let json: { code: number; message?: string; data?: T; reason?: string };
|
||||
try {
|
||||
json = await res.json();
|
||||
} catch {
|
||||
@@ -213,8 +213,12 @@ async function rawRequest<T>(
|
||||
throw err;
|
||||
}
|
||||
if (json.code !== 0) {
|
||||
const err = new Error(json.message || '请求失败') as Error & { status?: number };
|
||||
const err = new Error(json.message || '请求失败') as Error & {
|
||||
status?: number;
|
||||
reason?: string;
|
||||
};
|
||||
err.status = res.status >= 500 ? res.status : json.code;
|
||||
err.reason = json.reason;
|
||||
if (json.code === 400) {
|
||||
reportApiError(
|
||||
{ apiBase, clientApp: CLIENT_APP, getToken: () => localStorage.getItem(ACCESS_TOKEN) },
|
||||
@@ -254,7 +258,15 @@ async function requestWithAuthRetry<T>(
|
||||
try {
|
||||
return await rawRequest<T>(path, options);
|
||||
} catch (e) {
|
||||
const err = e as Error & { status?: number };
|
||||
const err = e as Error & { status?: number; reason?: string };
|
||||
// 账号停用 / 门店关闭 / 合伙人绑定失效:强制退出登录
|
||||
if (err.reason === 'ACCOUNT_DISABLED') {
|
||||
clearAuth();
|
||||
if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) {
|
||||
window.location.replace('/login?disabled=1');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
const canRecover =
|
||||
err.status === 401 &&
|
||||
!retried &&
|
||||
@@ -303,7 +315,14 @@ export async function ensureSession(): Promise<{
|
||||
needsSelectStore: needsStoreSelection({ store, stores: me.stores }),
|
||||
};
|
||||
} catch (e) {
|
||||
const err = e as Error & { status?: number };
|
||||
const err = e as Error & { status?: number; reason?: string };
|
||||
if (err.reason === 'ACCOUNT_DISABLED') {
|
||||
clearAuth();
|
||||
if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) {
|
||||
window.location.replace('/login?disabled=1');
|
||||
}
|
||||
return { authenticated: false, store: null, needsSelectStore: false };
|
||||
}
|
||||
if (err.status === 401) {
|
||||
const refreshed = await refreshSession();
|
||||
if (refreshed) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* v3.5.1 #2:门店端核销即时刷新。
|
||||
* 核销成功后,通过浏览器自定义事件通知「门店信息页 / 提现页」即时刷新余额与提现按钮状态,
|
||||
* 避免用户手动下拉刷新。
|
||||
*/
|
||||
|
||||
const REDEEM_SUCCESS_EVENT = 'shop:redeem-success';
|
||||
|
||||
export type RedeemSuccessPayload = {
|
||||
redeemNo?: string;
|
||||
amount?: number;
|
||||
storeId?: string;
|
||||
};
|
||||
|
||||
/** 核销成功页 mount 时调用,广播核销成功事件 */
|
||||
export function notifyRedeemSuccess(payload: RedeemSuccessPayload = {}) {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.dispatchEvent(new CustomEvent(REDEEM_SUCCESS_EVENT, { detail: payload }));
|
||||
}
|
||||
|
||||
/** 订阅核销成功事件,回调在事件触发时执行(通常用于刷新余额/提现状态) */
|
||||
export function useRedeemSuccessListener(
|
||||
callback: (payload: RedeemSuccessPayload) => void,
|
||||
deps: React.DependencyList = [],
|
||||
) {
|
||||
const savedCallback = useRef(callback);
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
function handler(e: Event) {
|
||||
const payload = (e as CustomEvent<RedeemSuccessPayload>).detail ?? {};
|
||||
savedCallback.current(payload);
|
||||
}
|
||||
window.addEventListener(REDEEM_SUCCESS_EVENT, handler);
|
||||
return () => window.removeEventListener(REDEEM_SUCCESS_EVENT, handler);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
}
|
||||
@@ -86,6 +86,16 @@ export default function LoginPage() {
|
||||
if (hint) setMsg(hint);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (params.get('disabled') === '1') {
|
||||
setMsg('账号已被停用或门店已关闭,请重新登录');
|
||||
const next = new URLSearchParams(params);
|
||||
next.delete('disabled');
|
||||
setSearchParams(next, { replace: true });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWechatEnv() || !wxAuthorize || !params.get('code')) return;
|
||||
void handleShopWechatCallbackOnce()
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '../lib/wechat-auth';
|
||||
import { isWechatEnv } from '../lib/weixin';
|
||||
import { useStorePageView } from '../lib/usePageView';
|
||||
import { useRedeemSuccessListener } from '../lib/useRedeemSuccessBus';
|
||||
|
||||
export default function MinePage() {
|
||||
useStorePageView('store_mine_view');
|
||||
@@ -43,6 +44,11 @@ export default function MinePage() {
|
||||
void loadMine();
|
||||
}, [loadMine]);
|
||||
|
||||
// v3.5.1 #2:核销成功后即时刷新门店信息(余额等)
|
||||
useRedeemSuccessListener(() => {
|
||||
void loadMine();
|
||||
}, [loadMine]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWechatEnv() || !wxAuthorize || !searchParams.get('code')) return;
|
||||
void handleShopWechatCallbackOnce()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { notifyRedeemSuccess } from '../lib/useRedeemSuccessBus';
|
||||
|
||||
function formatAmount(n: number) {
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
@@ -29,6 +30,14 @@ export default function RedeemSuccessPage() {
|
||||
? new Date(String(result.createdAt)).toLocaleString('zh-CN')
|
||||
: new Date().toLocaleString('zh-CN');
|
||||
|
||||
// v3.5.1 #2:核销成功后广播事件,通知门店信息页 / 提现页即时刷新
|
||||
useEffect(() => {
|
||||
notifyRedeemSuccess({
|
||||
redeemNo: redeemNo !== '—' ? redeemNo : undefined,
|
||||
amount,
|
||||
});
|
||||
}, [redeemNo, amount]);
|
||||
|
||||
return (
|
||||
<div className="shop-success-page">
|
||||
<header className="shop-success-header">
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||
import { request } from '../lib/api';
|
||||
import { useStorePageView } from '../lib/usePageView';
|
||||
import { useRedeemSuccessListener } from '../lib/useRedeemSuccessBus';
|
||||
|
||||
type StatusFilter = 'all' | StoreWithdrawStatus;
|
||||
|
||||
@@ -42,6 +43,11 @@ export default function WithdrawPage() {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
// v3.5.1 #2:核销成功后即时刷新可提余额与提现按钮状态
|
||||
useRedeemSuccessListener(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (statusFilter === 'all') return items;
|
||||
return items.filter((r) => r.status === statusFilter);
|
||||
|
||||
Reference in New Issue
Block a user