fix(h5-partner): resolve merge conflicts for auth and sub-accounts

Merge partner session model (AuthGate, ensureSession, WeChat OAuth in Context) with sub-account routing, admin partner-account tree CRUD, upload lock, and partner staff flows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-10 11:24:48 +08:00
20 changed files with 378 additions and 264 deletions
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { uploadFileToOss, type OssMediaType } from '../lib/upload';
import { toastError } from '../lib/toast';
import { enqueueUpload } from '../lib/upload-lock';
import {
authorizePartnerWechat,
fetchClientConfig,
@@ -92,13 +92,12 @@ export default function OssUploadField({
if (file.size > DEFAULT_MAX_MB * 1024 * 1024) {
const text = `文件不能超过 ${DEFAULT_MAX_MB}MB`;
setError(text);
toastError(text);
return;
}
setUploading(true);
setError('');
try {
const result = await uploadFileToOss(file, { bizType, mediaType });
const result = await enqueueUpload(() => uploadFileToOss(file, { bizType, mediaType }));
onChange?.(result.url);
} catch (e) {
setError(e instanceof Error ? e.message : '上传失败');
@@ -116,7 +115,6 @@ export default function OssUploadField({
} catch (e) {
const text = e instanceof Error ? e.message : '微信授权失败';
setError(text);
toastError(text);
setAuthorizing(false);
}
}
@@ -126,16 +124,23 @@ export default function OssUploadField({
}
async function pickWechatImage() {
await weixinSdk.init();
const files = await weixinSdk.chooseImages({
count: 1,
sourceType: ['album', 'camera'],
});
if (files?.[0]) {
await uploadSelectedFile(files[0]);
return;
setUploading(true);
setError('');
try {
await weixinSdk.init();
await enqueueUpload(async () => {
const files = await weixinSdk.chooseImages({
count: 1,
sourceType: ['album', 'camera'],
});
if (!files?.[0]) {
throw new Error('未能获取图片,请重试');
}
await uploadSelectedFile(files[0]);
});
} finally {
setUploading(false);
}
throw new Error('未能获取图片,请重试');
}
async function pickFile() {
@@ -145,7 +150,6 @@ export default function OssUploadField({
if (useWechatPicker && needsAuth) {
const text = '请先完成微信授权后再上传照片';
setError(text);
toastError(text);
return;
}
@@ -9,6 +9,7 @@ import {
} from 'react';
import { stripOAuthParamsFromLocation, toAppPath } from '@dukang/weixin-sdk';
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
import type { PartnerMe, PartnerStaffRole } from '@dukang/shared-types';
import {
clearAuth,
ensureSession,
@@ -17,7 +18,6 @@ import {
type PartnerSessionPayload,
type PartnerSessionProfile,
} from '../lib/api';
import type { PartnerMe, PartnerStaffRole } from '@dukang/shared-types';
import { fetchClientConfig, processPartnerWechatOAuthCallback } from '../lib/wechat-auth';
import { isWechatEnv } from '../lib/weixin';
@@ -40,6 +40,17 @@ type PartnerSessionValue = {
const PartnerSessionContext = createContext<PartnerSessionValue | null>(null);
function accountFromProfile(profile: PartnerSessionProfile): PartnerAccount {
return {
id: profile.id,
name: profile.name,
phone: profile.phone,
companyName: profile.companyName ?? '',
isPrimary: profile.isPrimary ?? true,
staffRole: profile.staffRole,
};
}
export function PartnerSessionProvider({ children }: { children: ReactNode }) {
const [ready, setReady] = useState(false);
const [authenticated, setAuthenticated] = useState(false);
@@ -49,13 +60,7 @@ export function PartnerSessionProvider({ children }: { children: ReactNode }) {
saveAuth(session);
setAuthenticated(true);
if (session.partner) {
setAccount({
id: session.partner.id,
name: session.partner.name,
phone: session.partner.phone,
companyName: session.partner.companyName,
isPrimary: session.partner.isPrimary,
});
setAccount(accountFromProfile(session.partner));
}
}, []);
+6 -3
View File
@@ -1,4 +1,4 @@
import type { PartnerMe } from '@dukang/shared-types';
import type { PartnerMe, PartnerStaffRole } from '@dukang/shared-types';
import { isOnAppPath, toAppPath } from '@dukang/weixin-sdk';
import { showPartnerToast } from './toast';
@@ -22,7 +22,9 @@ const AUTH_RECOVERY_EXEMPT_PATHS = [
'/partner/auth/login/wechat',
];
export type PartnerSessionProfile = Pick<PartnerMe, 'id' | 'name' | 'phone' | 'companyName' | 'isPrimary'>;
export type PartnerSessionProfile = Pick<PartnerMe, 'id' | 'name' | 'phone' | 'companyName' | 'isPrimary'> & {
staffRole?: PartnerStaffRole;
};
export type PartnerSessionPayload = {
accessToken: string;
@@ -31,7 +33,7 @@ export type PartnerSessionPayload = {
};
export type ApiRequestOptions = RequestInit & {
/** 为 true 时不弹出 toast(由调用方自行展示) */
/** 保留字段,兼容旧调用;错误提示由页面自行处理 */
silent?: boolean;
};
@@ -101,6 +103,7 @@ function profileFromMe(me: PartnerMe): PartnerSessionProfile {
phone: me.phone,
companyName: me.companyName,
isPrimary: me.isPrimary,
staffRole: me.staffRole ?? undefined,
};
}
+10 -12
View File
@@ -1,7 +1,8 @@
import type {
CreatePartnerStaffRequest,
PartnerStaffItem,
UpdatePartnerStaffRequest,
import {
PartnerStaffRole,
type CreatePartnerStaffRequest,
type PartnerStaffItem,
type UpdatePartnerStaffRequest,
} from '@dukang/shared-types';
import { request } from './api';
@@ -12,7 +13,11 @@ export function listPartnerStaff() {
export function createPartnerStaff(body: CreatePartnerStaffRequest) {
return request<PartnerStaffItem>('PARTNER_H5', '/partner/staff', {
method: 'POST',
body: JSON.stringify(body),
body: JSON.stringify({
name: body.name,
phone: body.phone,
staffRole: PartnerStaffRole.INTERNAL,
}),
});
}
@@ -28,10 +33,3 @@ export function deletePartnerStaff(id: string) {
method: 'DELETE',
});
}
export function sendStaffAddSms(phone: string) {
return request('PARTNER_H5', '/partner/auth/sms/send', {
method: 'POST',
body: JSON.stringify({ phone, scene: 'PARTNER_STAFF_ADD' }),
});
}
+7 -1
View File
@@ -45,7 +45,7 @@ export const defaultStoreForm = (): StoreDraftForm => ({
bankBranch: '',
});
function normalizeStringArray(raw: unknown, length: number): string[] {
export function normalizeStringArray(raw: unknown, length: number): string[] {
if (!Array.isArray(raw)) return Array.from({ length }, () => '');
const items = raw.map((item) => String(item ?? ''));
while (items.length < length) items.push('');
@@ -138,6 +138,12 @@ export function validateStoreStep2(
return null;
}
export function patchEnvPhotoAt(urls: string[], index: number, url: string): string[] {
const envPhotoUrls = normalizeStringArray(urls, 3);
envPhotoUrls[index] = url;
return envPhotoUrls;
}
export function validateStoreStep3(
form: Pick<StoreDraftForm, 'bankAccountName' | 'bankAccountNo' | 'bankBranch'>,
): string | null {
+11
View File
@@ -0,0 +1,11 @@
/** 串行化上传,避免微信 JSSDK 并发选图/读图导致卡死 */
let uploadChain: Promise<unknown> = Promise.resolve();
export function enqueueUpload<T>(task: () => Promise<T>): Promise<T> {
const next = uploadChain.then(task, task);
uploadChain = next.then(
() => undefined,
() => undefined,
);
return next;
}
+44 -25
View File
@@ -1,5 +1,6 @@
import { apiBase, request } from './api';
import { showPartnerToast } from './toast';
const UPLOAD_TIMEOUT_MS = 120_000;
export type OssMediaType = 'IMAGE' | 'VIDEO' | 'FILE';
@@ -10,8 +11,7 @@ export type UploadFileResult = {
mock: boolean;
};
/** 经 API 服务端转存 OSS,避免浏览器直传跨域 */
export async function uploadFileToOss(
async function uploadFileToOssInner(
file: File,
options: { bizType: string; mediaType?: OssMediaType },
): Promise<UploadFileResult> {
@@ -25,29 +25,48 @@ export async function uploadFileToOss(
const headers: Record<string, string> = { 'X-Client-App': 'PARTNER_H5' };
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`${apiBase}/common/resources/upload`, {
method: 'POST',
headers,
body: formData,
});
const json = await res.json();
if (json.code === 401) {
localStorage.removeItem('accessToken');
showPartnerToast('未登录', 'error');
throw new Error('未登录');
}
if (json.code !== 0) {
showPartnerToast(json.message || '上传失败', 'error');
throw new Error(json.message || '上传失败');
}
const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS);
const data = json.data as UploadFileResult;
return {
url: data.url,
ossKey: data.ossKey,
bucket: data.bucket,
mock: data.mock,
};
try {
const res = await fetch(`${apiBase}/common/resources/upload`, {
method: 'POST',
headers,
body: formData,
signal: controller.signal,
});
const json = await res.json();
if (json.code === 401) {
localStorage.removeItem('accessToken');
throw new Error('未登录');
}
if (json.code !== 0) {
throw new Error(json.message || '上传失败');
}
const data = json.data as UploadFileResult;
return {
url: data.url,
ossKey: data.ossKey,
bucket: data.bucket,
mock: data.mock,
};
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') {
throw new Error('上传超时,请检查网络后重试');
}
throw e;
} finally {
window.clearTimeout(timeoutId);
}
}
/** 经 API 服务端转存 OSS,避免浏览器直传跨域 */
export function uploadFileToOss(
file: File,
options: { bizType: string; mediaType?: OssMediaType },
): Promise<UploadFileResult> {
return uploadFileToOssInner(file, options);
}
export type OpenCityOption = {
+2 -2
View File
@@ -107,6 +107,6 @@ export async function processPartnerWechatOAuthCallback(): Promise<PartnerSessio
}
/** @deprecated 使用 handlePartnerWechatLoginResult */
export function savePartnerWechatAuth(result: WechatLoginResult): boolean {
return !!handlePartnerWechatLoginResult(result);
export function savePartnerWechatAuth(result: WechatLoginResult): PartnerSessionPayload | null {
return handlePartnerWechatLoginResult(result);
}
+4 -14
View File
@@ -18,7 +18,6 @@ import {
loginPartnerWithWechat,
} from '../lib/wechat-auth';
import { isWechatEnv } from '../lib/weixin';
import { toastError } from '../lib/toast';
const REMEMBER_PHONE_KEY = 'partner_remember_phone';
const REMEMBER_FLAG_KEY = 'partner_remember_account';
@@ -86,9 +85,7 @@ export default function LoginPage() {
function ensureAgreed() {
if (!agreed) {
const text = '请先勾选并同意用户协议';
setMsg(text);
toastError(text);
setMsg('请先勾选并同意用户协议');
return false;
}
return true;
@@ -129,9 +126,7 @@ export default function LoginPage() {
});
}, 1000);
} catch (e) {
const text = formatPartnerError(e);
setMsg(text);
toastError(text);
setMsg(formatPartnerError(e));
}
}
@@ -160,9 +155,7 @@ export default function LoginPage() {
}
await finishLoginNavigate();
} catch (e) {
const text = formatPartnerError(e);
setMsg(text);
toastError(text);
setMsg(formatPartnerError(e));
} finally {
setLoading(false);
}
@@ -173,7 +166,6 @@ export default function LoginPage() {
setMsg('');
if (!isWechatEnv()) {
setMsg(WECHAT_INAPP_REQUIRED_MSG);
toastError(WECHAT_INAPP_REQUIRED_MSG);
return;
}
setWxLoading(true);
@@ -184,9 +176,7 @@ export default function LoginPage() {
await finishLoginNavigate();
}
} catch (e) {
const text = formatWechatError(e);
setMsg(text);
toastError(text);
setMsg(formatWechatError(e));
} finally {
setWxLoading(false);
}
+89 -97
View File
@@ -1,149 +1,141 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import { PARTNER_STAFF_ROLE_LABELS, PartnerStaffRole } from '@dukang/shared-types';
import { createPartnerStaff, sendStaffAddSms } from '../lib/staff';
import { createPartnerStaff } from '../lib/staff';
import { toastError, toastSuccess } from '../lib/toast';
const ROLE_OPTIONS = Object.values(PartnerStaffRole);
type FieldErrors = {
name?: string;
phone?: string;
};
export default function StaffCreatePage() {
const navigate = useNavigate();
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [staffRole, setStaffRole] = useState<PartnerStaffRole>(PartnerStaffRole.INTERNAL);
const [sending, setSending] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [msg, setMsg] = useState('');
const [submitError, setSubmitError] = useState('');
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({});
async function sendCode() {
if (!/^1[3-9]\d{9}$/.test(phone.trim())) {
const text = '请输入正确的11位手机号';
setMsg(text);
toastError(text);
return;
}
setSending(true);
setMsg('');
try {
await sendStaffAddSms(phone.trim());
setMsg('验证码已发送');
} catch {
/* request 已 toast */
} finally {
setSending(false);
}
function patchName(value: string) {
setName(value);
setSubmitError('');
setFieldErrors((prev) => ({ ...prev, name: undefined }));
}
function patchPhone(value: string) {
setPhone(value);
setSubmitError('');
setFieldErrors((prev) => ({ ...prev, phone: undefined }));
}
async function submit() {
if (!name.trim()) {
const text = '请填写真实姓名';
setMsg(text);
toastError(text);
return;
}
if (!/^1[3-9]\d{9}$/.test(phone.trim())) {
const text = '请输入正确的11位手机号';
setMsg(text);
toastError(text);
return;
}
if (!code.trim()) {
const text = '请输入验证码';
setMsg(text);
toastError(text);
const trimmedName = name.trim();
const trimmedPhone = phone.trim();
const nextFieldErrors: FieldErrors = {};
if (!trimmedName) nextFieldErrors.name = '请填写真实姓名';
if (!trimmedPhone) {
nextFieldErrors.phone = '请填写手机号码';
} else if (!/^1[3-9]\d{9}$/.test(trimmedPhone)) {
nextFieldErrors.phone = '请输入正确的11位手机号';
}
if (Object.keys(nextFieldErrors).length) {
setFieldErrors(nextFieldErrors);
return;
}
setSubmitting(true);
setMsg('');
setSubmitError('');
setFieldErrors({});
try {
await createPartnerStaff({
name: name.trim(),
phone: phone.trim(),
staffRole,
code: code.trim(),
name: trimmedName,
phone: trimmedPhone,
});
toastSuccess('子账号创建成功');
navigate('/center/staff');
} catch {
/* request 已 toast */
} catch (e) {
const message = e instanceof Error ? e.message : '创建失败';
if (message.includes('手机号')) {
setFieldErrors({ phone: message });
return;
}
setSubmitError(message);
toastError(message);
} finally {
setSubmitting(false);
}
}
return (
<div className="page-no-tab partner-staff-create-page">
<div className="partner-page-sticky">
<PageHeader title="添加子账号" onBack={() => navigate('/center/staff')} />
<main style={{ padding: '16px 20px 120px' }}>
{msg && <p className="partner-form-error" role="alert" style={{ marginBottom: 12 }}>{msg}</p>}
{submitError && (
<p className="partner-form-error" role="alert">{submitError}</p>
)}
<div className="partner-form-card">
<label className="partner-form-label"> *</label>
<div className="partner-form-input-wrap">
<section className="partner-form-card">
<div className="partner-section-title">
<div className="partner-section-bar" />
<h2 className="headline-md"></h2>
</div>
<div className="partner-field">
<label> <span className="text-primary">*</span></label>
<div className="partner-field-input">
<span className="material-symbols-outlined">person</span>
<input
className="partner-form-input"
placeholder="请输入员工姓名"
value={name}
onChange={(e) => setName(e.target.value)}
onChange={(e) => patchName(e.target.value)}
/>
<span className="material-symbols-outlined">person</span>
</div>
{fieldErrors.name && (
<p className="partner-field-error" role="alert">{fieldErrors.name}</p>
)}
</div>
<div className="partner-form-card">
<label className="partner-form-label"> *</label>
<div style={{ display: 'flex', gap: 8 }}>
<div className="partner-field">
<label> <span className="text-primary">*</span></label>
<div className="partner-field-input">
<span className="material-symbols-outlined">call</span>
<input
className="partner-form-input"
type="tel"
placeholder="请输入11位手机号"
value={phone}
onChange={(e) => setPhone(e.target.value)}
inputMode="numeric"
onChange={(e) => patchPhone(e.target.value)}
/>
<button type="button" className="btn btn-outline" disabled={sending} onClick={() => void sendCode()}>
{sending ? '发送中' : '获取验证码'}
</button>
</div>
<input
className="partner-form-input"
style={{ marginTop: 8 }}
placeholder="验证码"
value={code}
onChange={(e) => setCode(e.target.value)}
inputMode="numeric"
/>
{fieldErrors.phone && (
<p className="partner-field-error" role="alert">{fieldErrors.phone}</p>
)}
</div>
</section>
<div className="partner-form-card">
<label className="partner-form-label"> *</label>
<div className="partner-form-input-wrap">
<select
className="partner-form-input"
value={staffRole}
onChange={(e) => setStaffRole(e.target.value as PartnerStaffRole)}
>
{ROLE_OPTIONS.map((role) => (
<option key={role} value={role}>{PARTNER_STAFF_ROLE_LABELS[role]}</option>
))}
</select>
<span className="material-symbols-outlined">expand_more</span>
</div>
<div className="partner-info-banner">
<div className="partner-bills-icon">
<span className="material-symbols-outlined">info</span>
</div>
<p className="label-md text-muted" style={{ marginTop: 16, textAlign: 'center' }}>
<p className="label-md text-variant" style={{ marginTop: 0, lineHeight: 1.5 }}>
</p>
</main>
<div className="partner-sticky-footer">
<button type="button" className="partner-btn-primary" disabled={submitting} onClick={() => void submit()}>
<span className="material-symbols-outlined">person_add</span>
{submitting ? '创建中...' : '确认创建'}
</button>
</div>
<footer className="partner-sticky-footer">
<button
type="button"
className="partner-btn-primary"
disabled={submitting}
onClick={() => void submit()}
>
<span className="material-symbols-outlined" style={{ fontSize: 20 }}>person_add</span>
{submitting ? '创建中…' : '确认创建'}
</button>
</footer>
</div>
);
}
+9 -10
View File
@@ -8,7 +8,6 @@ import {
type PartnerStaffItem,
} from '@dukang/shared-types';
import { deletePartnerStaff, listPartnerStaff, updatePartnerStaff } from '../lib/staff';
import { toastError } from '../lib/toast';
export default function StaffListPage() {
const navigate = useNavigate();
@@ -18,8 +17,8 @@ export default function StaffListPage() {
const [busyId, setBusyId] = useState<string | null>(null);
const loadStaff = useCallback(() => {
return listPartnerStaff().then(setStaff).catch(() => {
/* request 已 toast */
return listPartnerStaff().then(setStaff).catch((e) => {
setError(e instanceof Error ? e.message : '加载失败');
});
}, []);
@@ -40,8 +39,8 @@ export default function StaffListPage() {
try {
await updatePartnerStaff(item.id, { status: next });
await loadStaff();
} catch {
/* request 已 toast */
} catch (e) {
setError(e instanceof Error ? e.message : '操作失败');
} finally {
setBusyId(null);
}
@@ -55,8 +54,8 @@ export default function StaffListPage() {
try {
await deletePartnerStaff(item.id);
await loadStaff();
} catch {
/* request 已 toast */
} catch (e) {
setError(e instanceof Error ? e.message : '删除失败');
} finally {
setBusyId(null);
}
@@ -71,7 +70,7 @@ export default function StaffListPage() {
if (roleInput == null) return;
const staffRole = roleOptions.find((r) => r === roleInput || PARTNER_STAFF_ROLE_LABELS[r] === roleInput);
if (!staffRole) {
toastError('无效的角色');
setError('无效的角色');
return;
}
setBusyId(item.id);
@@ -79,8 +78,8 @@ export default function StaffListPage() {
try {
await updatePartnerStaff(item.id, { name: name.trim(), staffRole });
await loadStaff();
} catch {
/* request 已 toast */
} catch (e) {
setError(e instanceof Error ? e.message : '保存失败');
} finally {
setBusyId(null);
}
+71 -27
View File
@@ -39,6 +39,8 @@ import {
validateStoreStep3,
patchEnvPhotoAt,
} from '../lib/storeDraft';
@@ -63,6 +65,14 @@ function isPhoneConflictMessage(message: string) {
function isPhoneValidationMessage(message: string) {
return message.includes('联系电话') || message.includes('手机号');
}
export default function StoreCreatePage() {
const navigate = useNavigate();
@@ -93,7 +103,6 @@ export default function StoreCreatePage() {
function reportFormError(message: string) {
setSubmitError(message);
toastError(message);
}
const stepFromUrl = Number(params.get('step') || 0);
@@ -153,7 +162,6 @@ export default function StoreCreatePage() {
.catch((e) => {
const msg = e instanceof Error ? e.message : '加载开城城市失败';
setCitiesError(msg);
toastError(msg);
});
}, []);
@@ -196,6 +204,22 @@ export default function StoreCreatePage() {
function patchEnvPhotoUrl(index: number, url: string) {
setForm((prev) => ({
...prev,
envPhotoUrls: patchEnvPhotoAt(prev.envPhotoUrls, index, url),
}));
setSubmitError('');
}
function bindRegionSelection(codes: string[]) {
const binding = resolveRegionBinding(codes, cities);
@@ -260,11 +284,11 @@ export default function StoreCreatePage() {
if (!phoneCheck.available) {
const phoneMsg = phoneCheck.message ?? '该手机号已绑定门店,请更换';
setFieldErrors({ phone: phoneMsg });
toastError(phoneMsg);
return;
}
} catch {
} catch (e) {
reportFormError(e instanceof Error ? e.message : '手机号校验失败');
return;
} finally {
@@ -304,6 +328,10 @@ export default function StoreCreatePage() {
const step1Msg = validateStoreStep1(form);
if (step1Msg) {
if (isPhoneValidationMessage(step1Msg)) {
setFieldErrors({ phone: step1Msg });
return;
}
reportFormError(step1Msg);
goStep(1);
return;
@@ -341,13 +369,16 @@ export default function StoreCreatePage() {
});
goStep(1);
return;
}
} catch (e) {
reportFormError(e instanceof Error ? e.message : '手机号校验失败');
return;
}
try {
const envPhotoUrls = form.envPhotoUrls.map((u) => u.trim()).filter(Boolean);
@@ -396,11 +427,10 @@ export default function StoreCreatePage() {
const message = e instanceof Error ? e.message : '提交失败';
if (isPhoneConflictMessage(message)) {
setFieldErrors({ phone: message });
toastError(message);
goStep(1);
return;
}
setSubmitError(message);
toastError(message);
} finally {
setSubmitting(false);
@@ -677,15 +707,7 @@ export default function StoreCreatePage() {
onWechatReadyChange={() => { void refresh(); }}
onChange={(nextUrl) => {
const envPhotoUrls = [...form.envPhotoUrls];
envPhotoUrls[index] = nextUrl;
patchForm({ envPhotoUrls });
}}
onChange={(nextUrl) => patchEnvPhotoUrl(index, nextUrl)}
/>
@@ -757,22 +779,44 @@ export default function StoreCreatePage() {
</div>
<section className="partner-form-card partner-store-phone-summary">
<section className="partner-form-card">
<div className="partner-store-phone-summary-main">
<div className="partner-field">
<span className="label-md text-muted"></span>
<label> <span className="text-primary">*</span></label>
<span className="body-md">{form.phone.trim() || '未填写'}</span>
<div className="partner-field-input">
<span className="material-symbols-outlined">call</span>
<input
type="tel"
placeholder="请输入11位手机号"
value={form.phone}
onChange={(e) => patchForm({ phone: e.target.value })}
/>
</div>
{fieldErrors.phone && (
<p className="partner-field-error" role="alert">{fieldErrors.phone}</p>
)}
<p className="label-md text-muted" style={{ marginTop: 8 }}>
</p>
</div>
<button type="button" className="partner-weekly-link" onClick={() => goStep(1)}>
</button>
</section>
<section className="partner-form-card">
@@ -21,6 +21,7 @@ export default function StoreDetailPage() {
const [status, setStatus] = useState<StoreStatusValue>('OPEN');
const [statusSaving, setStatusSaving] = useState(false);
const [saving, setSaving] = useState(false);
const [actionError, setActionError] = useState('');
function applyStore(data: Record<string, unknown>) {
setStore(data);
@@ -52,6 +53,7 @@ export default function StoreDetailPage() {
if (!ok) return;
}
setStatusSaving(true);
setActionError('');
try {
const updated = await request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}/status`, {
method: 'PUT',
@@ -60,8 +62,8 @@ export default function StoreDetailPage() {
setStatus(next);
setStore((prev) => (prev ? { ...prev, ...updated, status: next } : prev));
toastSuccess('状态已更新');
} catch {
/* request 已 toast */
} catch (e) {
setActionError(e instanceof Error ? e.message : '状态更新失败');
} finally {
setStatusSaving(false);
}
@@ -70,6 +72,7 @@ export default function StoreDetailPage() {
async function saveBasic() {
if (!id || saving || status === 'CLOSED') return;
setSaving(true);
setActionError('');
try {
const data = await request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}/basic`, {
method: 'PUT',
@@ -82,8 +85,8 @@ export default function StoreDetailPage() {
});
applyStore(data);
toastSuccess('已保存');
} catch {
/* request 已 toast */
} catch (e) {
setActionError(e instanceof Error ? e.message : '保存失败');
} finally {
setSaving(false);
}
@@ -110,6 +113,7 @@ export default function StoreDetailPage() {
<PageHeader title="门店详情" onBack={() => navigate('/stores')} />
<main style={{ padding: '16px 20px' }}>
{actionError && <p className="partner-form-error" role="alert" style={{ marginBottom: 12 }}>{actionError}</p>}
{!subReadonly && (
<section className="partner-form-card" style={{ margin: '0 0 16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>