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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user