v3.5.1 版本更新
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-08-19 15:54:51 +08:00
parent 7dd5fdfb12
commit 233ed0af3b
103 changed files with 5764 additions and 185 deletions
+23 -4
View File
@@ -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);
}
+10
View File
@@ -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()
+6
View File
@@ -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()
+10 -1
View File
@@ -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">
+6
View File
@@ -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);