diff --git a/apps/admin-web/src/pages/PartnerAccountsPage.tsx b/apps/admin-web/src/pages/PartnerAccountsPage.tsx index 3177781..85a8a6f 100644 --- a/apps/admin-web/src/pages/PartnerAccountsPage.tsx +++ b/apps/admin-web/src/pages/PartnerAccountsPage.tsx @@ -9,6 +9,7 @@ import { AdminCellLine } from '../components/AdminCellLine'; import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, ORDER_STATUS_LABELS, PARTNER_BILL_STATUS_LABELS, fmtTime } from '../lib/constants'; type PartnerOption = { id: string; companyName: string }; +type ParentAccount = { id: string; name: string; phone: string }; type AccountTreeRow = { id: string; @@ -16,10 +17,11 @@ type AccountTreeRow = { name: string; status: string; isPrimary: number; - staffRole: string | null; - parentAccountId: string | null; + staffRole?: string | null; + parentAccountId?: string | null; + parent?: ParentAccount | null; createdAt: string; - lastLoginAt: string | null; + lastLoginAt?: string | null; partner?: { id: string; companyName: string }; children?: AccountTreeRow[]; }; @@ -49,20 +51,29 @@ function filterTree(rows: AccountTreeRow[], phone?: string, status?: string): Ac return phoneOk && statusOk; }; - const walk = (list: AccountTreeRow[]): AccountTreeRow[] => - list - .map((row) => { - const children = row.children?.length ? walk(row.children) : undefined; - if (match(row) || (children && children.length > 0)) { - return { ...row, children: children?.length ? children : undefined }; - } - return null; - }) - .filter((row): row is AccountTreeRow => row !== null); + const walk = (list: AccountTreeRow[]): AccountTreeRow[] => { + const result: AccountTreeRow[] = []; + for (const row of list) { + const children = row.children?.length ? walk(row.children) : undefined; + if (match(row) || (children && children.length > 0)) { + result.push({ ...row, children: children?.length ? children : undefined }); + } + } + return result; + }; return walk(rows); } +function accountTypeLabel(isPrimary: number) { + return isPrimary === 1 ? '主账号' : '子账号'; +} + +function staffRoleLabel(role?: string | null) { + if (!role) return '-'; + return PARTNER_STAFF_ROLE_LABELS[role as keyof typeof PARTNER_STAFF_ROLE_LABELS] ?? role; +} + export default function PartnerAccountsPage() { const [form] = Form.useForm(); const [editForm] = Form.useForm(); @@ -167,7 +178,7 @@ export default function PartnerAccountsPage() { primary={row.name} secondary={ row.parentAccountId - ? `子账号 · ${PARTNER_STAFF_ROLE_LABELS[row.staffRole as keyof typeof PARTNER_STAFF_ROLE_LABELS] || row.staffRole || '—'}` + ? `子账号 · ${staffRoleLabel(row.staffRole)}` : row.isPrimary === 1 ? '主账号' : '合伙人账号' @@ -183,16 +194,29 @@ export default function PartnerAccountsPage() { render: (_, row) => row.partner?.companyName || '—', }, { - title: '标识', + title: '账号类型', + dataIndex: 'isPrimary', + width: 90, + render: (v, row) => ( + + {row.parentAccountId ? '子账号' : accountTypeLabel(v)} + + ), + }, + { + title: '角色', + dataIndex: 'staffRole', width: 100, - render: (_, row) => - row.parentAccountId ? ( - 子账号 - ) : row.isPrimary === 1 ? ( - 主账号 - ) : ( - 账号 - ), + render: (role) => staffRoleLabel(role), + }, + { + title: '所属主账号', + width: 140, + render: (_, row) => ( + row.isPrimary === 1 || !row.parentAccountId + ? '-' + : (row.parent ? `${row.parent.name} / ${row.parent.phone}` : '-') + ), }, { title: '状态', dataIndex: 'status', width: 80, render: (s) => {ACCOUNT_STATUS_LABELS[s] || s} }, { title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime }, @@ -282,7 +306,7 @@ export default function PartnerAccountsPage() { loading={loading} columns={columns} dataSource={treeData} - scroll={{ x: 1100 }} + scroll={{ x: 1200 }} pagination={false} expandable={{ expandedRowKeys, @@ -320,12 +344,24 @@ export default function PartnerAccountsPage() { - {detail.parentAccountId ? ( - - + + + + {detail.staffRole ? ( + + + + ) : null} + {detail.parentAccountId && detail.parent ? ( + + ) : null} diff --git a/apps/h5-partner/src/components/OssUploadField.tsx b/apps/h5-partner/src/components/OssUploadField.tsx index d3a3ac7..a961841 100644 --- a/apps/h5-partner/src/components/OssUploadField.tsx +++ b/apps/h5-partner/src/components/OssUploadField.tsx @@ -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; } diff --git a/apps/h5-partner/src/contexts/PartnerSessionContext.tsx b/apps/h5-partner/src/contexts/PartnerSessionContext.tsx index 2675131..657db83 100644 --- a/apps/h5-partner/src/contexts/PartnerSessionContext.tsx +++ b/apps/h5-partner/src/contexts/PartnerSessionContext.tsx @@ -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(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)); } }, []); diff --git a/apps/h5-partner/src/lib/api.ts b/apps/h5-partner/src/lib/api.ts index 5f705a9..8d4612c 100644 --- a/apps/h5-partner/src/lib/api.ts +++ b/apps/h5-partner/src/lib/api.ts @@ -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; +export type PartnerSessionProfile = Pick & { + 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, }; } diff --git a/apps/h5-partner/src/lib/staff.ts b/apps/h5-partner/src/lib/staff.ts index 3a09dd4..bb2a5d7 100644 --- a/apps/h5-partner/src/lib/staff.ts +++ b/apps/h5-partner/src/lib/staff.ts @@ -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('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' }), - }); -} diff --git a/apps/h5-partner/src/lib/storeDraft.ts b/apps/h5-partner/src/lib/storeDraft.ts index 47bf3ab..6451c6c 100644 --- a/apps/h5-partner/src/lib/storeDraft.ts +++ b/apps/h5-partner/src/lib/storeDraft.ts @@ -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, ): string | null { diff --git a/apps/h5-partner/src/lib/upload-lock.ts b/apps/h5-partner/src/lib/upload-lock.ts new file mode 100644 index 0000000..8047467 --- /dev/null +++ b/apps/h5-partner/src/lib/upload-lock.ts @@ -0,0 +1,11 @@ +/** 串行化上传,避免微信 JSSDK 并发选图/读图导致卡死 */ +let uploadChain: Promise = Promise.resolve(); + +export function enqueueUpload(task: () => Promise): Promise { + const next = uploadChain.then(task, task); + uploadChain = next.then( + () => undefined, + () => undefined, + ); + return next; +} diff --git a/apps/h5-partner/src/lib/upload.ts b/apps/h5-partner/src/lib/upload.ts index 356341d..59ea2c2 100644 --- a/apps/h5-partner/src/lib/upload.ts +++ b/apps/h5-partner/src/lib/upload.ts @@ -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 { @@ -25,29 +25,48 @@ export async function uploadFileToOss( const headers: Record = { '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 { + return uploadFileToOssInner(file, options); } export type OpenCityOption = { diff --git a/apps/h5-partner/src/lib/wechat-auth.ts b/apps/h5-partner/src/lib/wechat-auth.ts index d7986cb..745173e 100644 --- a/apps/h5-partner/src/lib/wechat-auth.ts +++ b/apps/h5-partner/src/lib/wechat-auth.ts @@ -107,6 +107,6 @@ export async function processPartnerWechatOAuthCallback(): Promise(PartnerStaffRole.INTERNAL); - const [sending, setSending] = useState(false); const [submitting, setSubmitting] = useState(false); - const [msg, setMsg] = useState(''); + const [submitError, setSubmitError] = useState(''); + const [fieldErrors, setFieldErrors] = useState({}); - 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 ( -
+
navigate('/center/staff')} /> -
- {msg &&

{msg}

} + {submitError && ( +

{submitError}

+ )} -
- -
+
+
+
+

子账号信息

+
+ +
+ +
+ person setName(e.target.value)} + onChange={(e) => patchName(e.target.value)} /> - person
+ {fieldErrors.name && ( +

{fieldErrors.name}

+ )}
-
- -
+
+ +
+ call setPhone(e.target.value)} - inputMode="numeric" + onChange={(e) => patchPhone(e.target.value)} /> -
- setCode(e.target.value)} - inputMode="numeric" - /> + {fieldErrors.phone && ( +

{fieldErrors.phone}

+ )}
+
-
- -
- - expand_more -
+
+
+ info
- -

- 创建即代表同意合伙人管理协议。子账号默认禁用,需手动启用。 +

+ 子账号创建后默认禁用,需在子账号列表中手动启用后方可登录。

-
- -
-
+ +
+ +
); } diff --git a/apps/h5-partner/src/pages/StaffListPage.tsx b/apps/h5-partner/src/pages/StaffListPage.tsx index 0283b3a..fe54507 100644 --- a/apps/h5-partner/src/pages/StaffListPage.tsx +++ b/apps/h5-partner/src/pages/StaffListPage.tsx @@ -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(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); } diff --git a/apps/h5-partner/src/pages/StoreCreatePage.tsx b/apps/h5-partner/src/pages/StoreCreatePage.tsx index fc12911..a0de627 100644 --- a/apps/h5-partner/src/pages/StoreCreatePage.tsx +++ b/apps/h5-partner/src/pages/StoreCreatePage.tsx @@ -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() {
-
+
-
+
- 门店登录手机号 + - {form.phone.trim() || '未填写'} +
+ + call + + patchForm({ phone: e.target.value })} + + /> + +
+ + {fieldErrors.phone && ( + +

{fieldErrors.phone}

+ + )} + +

+ + 该手机号将作为门店端登录账号,提交前会再次校验是否已被占用。 + +

- -
diff --git a/apps/h5-partner/src/pages/StoreDetailPage.tsx b/apps/h5-partner/src/pages/StoreDetailPage.tsx index 630b78c..33878fa 100644 --- a/apps/h5-partner/src/pages/StoreDetailPage.tsx +++ b/apps/h5-partner/src/pages/StoreDetailPage.tsx @@ -21,6 +21,7 @@ export default function StoreDetailPage() { const [status, setStatus] = useState('OPEN'); const [statusSaving, setStatusSaving] = useState(false); const [saving, setSaving] = useState(false); + const [actionError, setActionError] = useState(''); function applyStore(data: Record) { setStore(data); @@ -52,6 +53,7 @@ export default function StoreDetailPage() { if (!ok) return; } setStatusSaving(true); + setActionError(''); try { const updated = await request>('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>('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() { navigate('/stores')} />
+ {actionError &&

{actionError}

} {!subReadonly && (
diff --git a/packages/shared-types/src/partner.ts b/packages/shared-types/src/partner.ts index 9f3246b..7751bc7 100644 --- a/packages/shared-types/src/partner.ts +++ b/packages/shared-types/src/partner.ts @@ -22,8 +22,6 @@ export interface PartnerStaffItem { export interface CreatePartnerStaffRequest { phone: string; name: string; - staffRole: PartnerStaffRole; - code: string; } export interface UpdatePartnerStaffRequest { diff --git a/scripts/test-partner-auth.mjs b/scripts/test-partner-auth.mjs index 25ba9ed..94c394e 100644 --- a/scripts/test-partner-auth.mjs +++ b/scripts/test-partner-auth.mjs @@ -55,7 +55,11 @@ async function main() { ); if (!login.accessToken) throw new Error('login missing token'); - console.log('4. Admin partner logs list'); + console.log('4. PARTNER /partner/me after login'); + const me = await req('PARTNER_H5', '/partner/me', { token: login.accessToken }); + if (!me?.id || !me?.phone) throw new Error('partner/me missing profile'); + + console.log('5. Admin partner logs list'); const admin = await req('HQ_WEB', '/admin/auth/login/password', { method: 'POST', body: JSON.stringify({ diff --git a/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts b/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts index 7ac31f6..a6c78fc 100644 --- a/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts +++ b/server/dukang-api/src/modules/iam/dto/partner-staff.dto.ts @@ -10,13 +10,11 @@ export class CreatePartnerStaffDto { @IsNotEmpty() name: string; + /** 未传时服务端默认 INTERNAL */ + @IsOptional() @IsString() @IsIn(Object.values(PartnerStaffRole)) - staffRole: string; - - @IsString() - @IsNotEmpty() - code: string; + staffRole?: string; } export class UpdatePartnerStaffDto { diff --git a/server/dukang-api/src/modules/iam/partner-staff.service.ts b/server/dukang-api/src/modules/iam/partner-staff.service.ts index 3b74c84..942e494 100644 --- a/server/dukang-api/src/modules/iam/partner-staff.service.ts +++ b/server/dukang-api/src/modules/iam/partner-staff.service.ts @@ -3,18 +3,14 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; -import { PartnerStaffRole, SmsScene } from '@dukang/shared-types'; +import { PartnerStaffRole } from '@dukang/shared-types'; import { PrismaService } from '../../common/prisma/prisma.module'; import { serializeBigInt } from '../../common/decorators/current-user.decorator'; -import { AuthService } from './auth.service'; import { CreatePartnerStaffDto, UpdatePartnerStaffDto } from './dto/partner-staff.dto'; @Injectable() export class PartnerStaffService { - constructor( - private readonly prisma: PrismaService, - private readonly authService: AuthService, - ) {} + constructor(private readonly prisma: PrismaService) {} async listStaff(parentAccountId: bigint) { const rows = await this.prisma.partnerAccount.findMany({ @@ -40,8 +36,6 @@ export class PartnerStaffService { const existing = await this.prisma.partnerAccount.findUnique({ where: { phone } }); if (existing) throw new BadRequestException('该手机号已被使用'); - await this.authService.verifySmsCode(phone, dto.code, SmsScene.PARTNER_STAFF_ADD); - const name = dto.name.trim(); if (!name) throw new BadRequestException('请填写真实姓名'); @@ -50,7 +44,7 @@ export class PartnerStaffService { partnerId: parent.partnerId, phone, name, - staffRole: dto.staffRole as PartnerStaffRole, + staffRole: (dto.staffRole as PartnerStaffRole | undefined) ?? PartnerStaffRole.INTERNAL, isPrimary: 0, parentAccountId: parent.id, status: 'DISABLED', diff --git a/server/dukang-api/src/modules/ops/admin-partners.service.ts b/server/dukang-api/src/modules/ops/admin-partners.service.ts index eb8c57a..4415794 100644 --- a/server/dukang-api/src/modules/ops/admin-partners.service.ts +++ b/server/dukang-api/src/modules/ops/admin-partners.service.ts @@ -78,6 +78,9 @@ export class AdminPartnersService { if (query.phone) where.phone = { contains: query.phone }; if (query.partnerId) where.partnerId = BigInt(query.partnerId); if (query.status) where.status = query.status as Prisma.EnumAccountStatusFilter['equals']; + if (query.isPrimary === '0' || query.isPrimary === '1') { + where.isPrimary = Number(query.isPrimary); + } const [items, total] = await Promise.all([ this.prisma.partnerAccount.findMany({ @@ -87,6 +90,7 @@ export class AdminPartnersService { take: pageSize, include: { partner: { select: { id: true, companyName: true } }, + parent: { select: { id: true, name: true, phone: true } }, }, }), this.prisma.partnerAccount.count({ where }), @@ -152,6 +156,7 @@ export class AdminPartnersService { address: true, }, }, + parent: { select: { id: true, name: true, phone: true } }, }, }); if (!account) throw new NotFoundException('开城合伙人账号不存在'); diff --git a/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts b/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts index b55667b..6bc9193 100644 --- a/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts +++ b/server/dukang-api/src/modules/ops/dto/admin-query.dto.ts @@ -123,6 +123,10 @@ export class AdminPartnerAccountsQueryDto extends PaginationQueryDto { @IsOptional() @IsString() status?: string; + + @IsOptional() + @IsString() + isPrimary?: string; } export class AdminBenefitCouponsQueryDto extends PaginationQueryDto {