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
+45 -4
View File
@@ -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`,
);
}
+13 -1
View File
@@ -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;
+36 -21
View File
@@ -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>