@@ -156,10 +156,19 @@ async function rawRequest<T>(
|
||||
if (authToken) headers.Authorization = `Bearer ${authToken}`;
|
||||
|
||||
const res = await fetch(`${apiBase}${path}`, { ...options, headers });
|
||||
const json = await res.json().catch(() => ({ code: res.status, message: '网络异常' }));
|
||||
const json = await res.json().catch(() => ({ code: res.status, message: '网络异常' })) as {
|
||||
code: number;
|
||||
message?: string;
|
||||
data?: T;
|
||||
reason?: string;
|
||||
};
|
||||
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 = json.code === 401 ? 401 : json.code;
|
||||
err.reason = json.reason;
|
||||
if (json.code === 400) {
|
||||
reportApiError(
|
||||
{ apiBase, clientApp: CLIENT_APP, getToken: () => localStorage.getItem(ACCESS_TOKEN) },
|
||||
@@ -224,8 +233,19 @@ export async function request<T>(
|
||||
try {
|
||||
return await requestWithAuthRetry<T>(path, fetchOptions);
|
||||
} catch (e) {
|
||||
const err = e as Error & { status?: number };
|
||||
const err = e as Error & { status?: number; reason?: string };
|
||||
const message = err.message || '请求失败';
|
||||
// 账号停用 / 合伙人绑定失效:强制退出登录
|
||||
if (err.reason === 'ACCOUNT_DISABLED') {
|
||||
if (localStorage.getItem(ACCESS_TOKEN)) {
|
||||
clearAuth({ keepProfile: true });
|
||||
if (!silent) showPartnerToast(message, 'error');
|
||||
if (typeof window !== 'undefined' && !isOnAppPath('/login')) {
|
||||
window.location.href = toAppPath('/login?disabled=1');
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (err.status === 401) {
|
||||
if (localStorage.getItem(ACCESS_TOKEN)) {
|
||||
clearAuth({ keepProfile: true });
|
||||
@@ -260,7 +280,14 @@ export async function ensureSession(): Promise<{ authenticated: boolean; partner
|
||||
touchPartnerSession();
|
||||
return { authenticated: true, partner };
|
||||
} catch (e) {
|
||||
const err = e as Error & { status?: number };
|
||||
const err = e as Error & { status?: number; reason?: string };
|
||||
if (err.reason === 'ACCOUNT_DISABLED') {
|
||||
clearAuth({ keepProfile: true });
|
||||
if (typeof window !== 'undefined' && !isOnAppPath('/login')) {
|
||||
window.location.href = toAppPath('/login?disabled=1');
|
||||
}
|
||||
return { authenticated: false, partner: getPartnerProfile() };
|
||||
}
|
||||
if (err.status === 401) {
|
||||
const refreshed = await refreshSession();
|
||||
if (refreshed?.partner) {
|
||||
@@ -277,3 +304,17 @@ export async function ensureSession(): Promise<{ authenticated: boolean; partner
|
||||
|
||||
/** @deprecated 使用 PartnerSessionPayload */
|
||||
export type PartnerAuthPayload = PartnerSessionPayload;
|
||||
|
||||
export function submitStoreInfoChangeRequest(storeId: string, fields: Record<string, unknown>) {
|
||||
return request('PARTNER_H5', `/partner/stores/${storeId}/info-change-request`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(fields),
|
||||
});
|
||||
}
|
||||
|
||||
export function listStoreInfoChangeRequests(storeId: string) {
|
||||
return request<Array<{ status?: string }>>(
|
||||
'PARTNER_H5',
|
||||
`/partner/stores/${storeId}/info-change-requests`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ function formatWechatError(e: unknown): string {
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const { applySession, refresh, account } = usePartnerSession();
|
||||
const [params] = useSearchParams();
|
||||
const [params, setSearchParams] = useSearchParams();
|
||||
const quick = params.get('quick') === '1';
|
||||
const savedProfile = getPartnerProfile();
|
||||
const remembered = loadRememberedPhone();
|
||||
@@ -120,6 +120,18 @@ export default function LoginPage() {
|
||||
.catch(() => setWxAuthorize(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (params.get('disabled') === '1') {
|
||||
const tip = '账号已停用或解绑,请重新登录';
|
||||
setMsg(tip);
|
||||
toastError(tip);
|
||||
const next = new URLSearchParams(params);
|
||||
next.delete('disabled');
|
||||
setSearchParams(next, { replace: true });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const quickName = savedProfile?.name ?? '城市合伙人';
|
||||
const quickCompany = savedProfile?.companyName ?? '';
|
||||
const quickPhone = savedProfile?.phone || phone;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import AppImage from '@dukang/shared-ui/AppImage';
|
||||
import { request } from '../lib/api';
|
||||
import { request, submitStoreInfoChangeRequest, listStoreInfoChangeRequests } from '../lib/api';
|
||||
import { toastError, toastSuccess } from '../lib/toast';
|
||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||
import { canManagePartnerStore } from '../lib/partnerAccess';
|
||||
@@ -60,6 +60,7 @@ export default function StoreDetailPage() {
|
||||
const [mapPickerOpen, setMapPickerOpen] = useState(false);
|
||||
const [actionError, setActionError] = useState('');
|
||||
const [closeConfirmOpen, setCloseConfirmOpen] = useState(false);
|
||||
const [pendingInfoChange, setPendingInfoChange] = useState(false);
|
||||
|
||||
function applyStore(data: Record<string, unknown>) {
|
||||
setStore(data);
|
||||
@@ -97,6 +98,12 @@ export default function StoreDetailPage() {
|
||||
setStore(null);
|
||||
setLoadError(e instanceof Error ? e.message : '加载失败');
|
||||
});
|
||||
// 拉取该门店的信息变更审核记录,若有 PENDING 则展示「审核中」横幅
|
||||
listStoreInfoChangeRequests(id)
|
||||
.then((reqs) =>
|
||||
setPendingInfoChange(Array.isArray(reqs) && reqs.some((r) => r.status === 'PENDING')),
|
||||
)
|
||||
.catch(() => {});
|
||||
}, [id]);
|
||||
|
||||
async function changeStatus(next: StoreStatusValue) {
|
||||
@@ -162,26 +169,24 @@ export default function StoreDetailPage() {
|
||||
setSaving(true);
|
||||
setActionError('');
|
||||
try {
|
||||
const data = await request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}/basic`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
name: form.name.trim(),
|
||||
contactPhone: form.contactPhone.trim(),
|
||||
address: form.address.trim(),
|
||||
intro: form.intro.trim(),
|
||||
benefitUsageRule: form.benefitUsageRule.trim() || null,
|
||||
...(form.latitude.trim() && form.longitude.trim()
|
||||
? {
|
||||
latitude: Number(form.latitude),
|
||||
longitude: Number(form.longitude),
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
// v3.5.1 #5:基本信息变更走「提交变更」审核流,由总部审核通过后覆盖门店
|
||||
await submitStoreInfoChangeRequest(id, {
|
||||
name: form.name.trim(),
|
||||
contactPhone: form.contactPhone.trim(),
|
||||
address: form.address.trim(),
|
||||
intro: form.intro.trim(),
|
||||
benefitUsageRule: form.benefitUsageRule.trim() || null,
|
||||
...(form.latitude.trim() && form.longitude.trim()
|
||||
? {
|
||||
latitude: Number(form.latitude),
|
||||
longitude: Number(form.longitude),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
applyStore(data);
|
||||
toastSuccess(auditStatus === 'REJECTED' ? '已保存并重新提交审核' : '已保存');
|
||||
setPendingInfoChange(true);
|
||||
toastSuccess('变更已提交,等待总部审核');
|
||||
} catch (e) {
|
||||
setActionError(e instanceof Error ? e.message : '保存失败');
|
||||
setActionError(e instanceof Error ? e.message : '提交失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -279,6 +284,16 @@ export default function StoreDetailPage() {
|
||||
{auditStatus === 'APPROVED' && (
|
||||
<p className="label-md text-muted">总部审核已通过,可将门店设为营业中。</p>
|
||||
)}
|
||||
{pendingInfoChange && (
|
||||
<div style={{ marginTop: 12, background: 'rgba(245,166,35,0.08)', border: '1px solid rgba(245,166,35,0.3)', borderRadius: 8, padding: 12 }}>
|
||||
<p className="body-md" style={{ fontWeight: 600, marginBottom: 4 }}>
|
||||
基础信息变更审核中
|
||||
</p>
|
||||
<p className="label-md text-muted" style={{ margin: 0 }}>
|
||||
已提交的资料变更正在等待总部审核,审核通过后才会覆盖门店线上信息。审核期间可继续修改并重新提交。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{canMutate && !auditPending && status !== 'CLOSED' && (
|
||||
@@ -481,8 +496,8 @@ export default function StoreDetailPage() {
|
||||
<button type="button" className="partner-save-cancel" onClick={() => navigate('/stores')}>返回</button>
|
||||
{canMutate && (
|
||||
<button type="button" className="partner-save-submit" onClick={() => void saveBasic()} disabled={readOnly || saving}>
|
||||
<span className="material-symbols-outlined">save</span>
|
||||
{saving ? '保存中…' : auditRejected ? '保存并重新提交' : '保存修改'}
|
||||
<span className="material-symbols-outlined">send</span>
|
||||
{saving ? '提交中…' : '提交变更'}
|
||||
</button>
|
||||
)}
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user