diff --git a/apps/admin-web/src/pages/PartnerAccountsPage.tsx b/apps/admin-web/src/pages/PartnerAccountsPage.tsx index 8fb3d31..3d21ab4 100644 --- a/apps/admin-web/src/pages/PartnerAccountsPage.tsx +++ b/apps/admin-web/src/pages/PartnerAccountsPage.tsx @@ -3,12 +3,23 @@ import { Button, Drawer, Form, Input, Modal, Select, Space, Table, Tabs, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; +import { PARTNER_STAFF_ROLE_LABELS } from '@dukang/shared-types'; import { request, type Paginated } from '../lib/api'; import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, ORDER_STATUS_LABELS, PARTNER_BILL_STATUS_LABELS, fmtTime } from '../lib/constants'; import { useAdminList } from '../lib/useAdminList'; +type ParentAccount = { id: string; name: string; phone: string }; + type Row = { - id: string; phone: string; name: string; status: string; isPrimary: number; createdAt: string; + id: string; + phone: string; + name: string; + status: string; + isPrimary: number; + staffRole?: string | null; + parentAccountId?: string | null; + parent?: ParentAccount | null; + createdAt: string; partner?: { id: string; companyName: string }; }; @@ -26,6 +37,21 @@ type PartnerOption = { id: string; companyName: string }; type Detail = Row & { bills?: BillRow[]; orders?: OrderRow[] }; +const ACCOUNT_TYPE_OPTIONS = [ + { value: '', label: '全部' }, + { value: '1', label: '主账号' }, + { value: '0', label: '子账号' }, +]; + +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(); @@ -37,6 +63,7 @@ export default function PartnerAccountsPage() { const qs = new URLSearchParams(); if (filters.phone) qs.set('phone', filters.phone); if (filters.status) qs.set('status', filters.status); + if (filters.isPrimary) qs.set('isPrimary', filters.isPrimary); return qs; }, [filters], @@ -71,7 +98,27 @@ export default function PartnerAccountsPage() { { title: '姓名', dataIndex: 'name', width: 100 }, { title: '手机', dataIndex: 'phone', width: 120 }, { title: '开城合伙人', dataIndex: ['partner', 'companyName'], width: 140 }, - { title: '主账号', dataIndex: 'isPrimary', width: 80, render: (v) => (v === 1 ? '是' : '否') }, + { + title: '账号类型', + dataIndex: 'isPrimary', + width: 90, + render: (v) => {accountTypeLabel(v)}, + }, + { + title: '角色', + dataIndex: 'staffRole', + width: 100, + render: (role) => staffRoleLabel(role), + }, + { + title: '所属主账号', + width: 140, + render: (_, row) => ( + row.isPrimary === 1 + ? '-' + : (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 }, { @@ -113,9 +160,12 @@ export default function PartnerAccountsPage() { item.value !== '')} /> + - { setPage(p); setPageSize(ps); } }} /> + + + + + + + {detail.isPrimary !== 1 && ( + + + + )} 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 : '上传失败'); @@ -111,7 +110,6 @@ export default function OssUploadField({ } catch (e) { const text = e instanceof Error ? e.message : '微信授权失败'; setError(text); - toastError(text); setAuthorizing(false); } } @@ -121,15 +119,23 @@ export default function OssUploadField({ } async function pickWechatImage() { - const files = await weixinSdk.chooseImages({ - count: 1, - sourceType: ['album', 'camera'], - }); - if (files?.[0]) { - await uploadSelectedFile(files[0]); - return; + setUploading(true); + setError(''); + try { + await enqueueUpload(async () => { + const files = await weixinSdk.chooseImages({ + count: 1, + sourceType: ['album', 'camera'], + }); + if (!files?.[0]) { + throw new Error('未能获取图片,请重试'); + } + const result = await uploadFileToOss(files[0], { bizType, mediaType }); + onChange?.(result.url); + }); + } finally { + setUploading(false); } - throw new Error('未能获取图片,请重试'); } async function pickFile() { @@ -139,7 +145,6 @@ export default function OssUploadField({ if (useWechatPicker && needsAuth) { const text = '请先完成微信授权后再上传照片'; setError(text); - toastError(text); return; } diff --git a/apps/h5-partner/src/lib/api.ts b/apps/h5-partner/src/lib/api.ts index 0060d14..594b717 100644 --- a/apps/h5-partner/src/lib/api.ts +++ b/apps/h5-partner/src/lib/api.ts @@ -1,6 +1,5 @@ import type { PartnerMe } from '@dukang/shared-types'; import { isOnAppPath, toAppPath } from '@dukang/weixin-sdk'; -import { showPartnerToast } from './toast'; export const apiBase = '/api/v1'; @@ -15,7 +14,7 @@ export type PartnerAuthPayload = { }; export type ApiRequestOptions = RequestInit & { - /** 为 true 时不弹出 toast(由调用方自行展示) */ + /** 保留字段,兼容旧调用;错误提示由页面自行处理 */ silent?: boolean; }; @@ -52,7 +51,6 @@ export async function request( if (res.status === 401 || json.code === 401) { if (token && localStorage.getItem('accessToken') === token) { clearAuth(); - if (!silent) showPartnerToast(message, 'error'); if (typeof window !== 'undefined' && !isOnAppPath('/login')) { window.location.href = toAppPath('/login'); } @@ -60,7 +58,6 @@ export async function request( throw new Error(message); } if (json.code !== 0) { - if (!silent) showPartnerToast(message, 'error'); throw new Error(message); } return json.data as T; diff --git a/apps/h5-partner/src/lib/staff.ts b/apps/h5-partner/src/lib/staff.ts index 3a09dd4..b891620 100644 --- a/apps/h5-partner/src/lib/staff.ts +++ b/apps/h5-partner/src/lib/staff.ts @@ -28,10 +28,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 783ec1e..dc10a23 100644 --- a/apps/h5-partner/src/lib/storeDraft.ts +++ b/apps/h5-partner/src/lib/storeDraft.ts @@ -41,7 +41,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(''); @@ -126,6 +126,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/pages/LoginPage.tsx b/apps/h5-partner/src/pages/LoginPage.tsx index cdb4a7c..fce59d0 100644 --- a/apps/h5-partner/src/pages/LoginPage.tsx +++ b/apps/h5-partner/src/pages/LoginPage.tsx @@ -13,7 +13,6 @@ import { import { isWechatEnv } from '../lib/weixin'; import { isWxAuthorizeEnabled } from '@dukang/shared-types'; import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk'; -import { toastError } from '../lib/toast'; const REMEMBER_PHONE_KEY = 'partner_remember_phone'; const REMEMBER_FLAG_KEY = 'partner_remember_account'; @@ -93,9 +92,7 @@ export default function LoginPage() { function ensureAgreed() { if (!agreed) { - const text = '请先勾选并同意用户协议'; - setMsg(text); - toastError(text); + setMsg('请先勾选并同意用户协议'); return false; } return true; @@ -136,9 +133,7 @@ export default function LoginPage() { }); }, 1000); } catch (e) { - const text = formatPartnerError(e); - setMsg(text); - toastError(text); + setMsg(formatPartnerError(e)); } } @@ -165,9 +160,7 @@ export default function LoginPage() { const account = await refresh(); navigate(partnerHomePath(account ?? data.partner)); } catch (e) { - const text = formatPartnerError(e); - setMsg(text); - toastError(text); + setMsg(formatPartnerError(e)); } finally { setLoading(false); } @@ -178,7 +171,6 @@ export default function LoginPage() { setMsg(''); if (!isWechatEnv()) { setMsg(WECHAT_INAPP_REQUIRED_MSG); - toastError(WECHAT_INAPP_REQUIRED_MSG); return; } setWxLoading(true); @@ -189,9 +181,7 @@ export default function LoginPage() { navigate(partnerHomePath(account ?? undefined)); } } catch (e) { - const text = formatWechatError(e); - setMsg(text); - toastError(text); + setMsg(formatWechatError(e)); } finally { setWxLoading(false); } diff --git a/apps/h5-partner/src/pages/StaffCreatePage.tsx b/apps/h5-partner/src/pages/StaffCreatePage.tsx index c3030bf..a772a94 100644 --- a/apps/h5-partner/src/pages/StaffCreatePage.tsx +++ b/apps/h5-partner/src/pages/StaffCreatePage.tsx @@ -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.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 48018cb..56e3271 100644 --- a/apps/h5-partner/src/pages/StoreCreatePage.tsx +++ b/apps/h5-partner/src/pages/StoreCreatePage.tsx @@ -49,6 +49,8 @@ import { validateStoreStep3, + patchEnvPhotoAt, + } from '../lib/storeDraft'; @@ -73,6 +75,14 @@ function isPhoneConflictMessage(message: string) { +function isPhoneValidationMessage(message: string) { + + return message.includes('联系电话') || message.includes('手机号'); + +} + + + export default function StoreCreatePage() { const navigate = useNavigate(); @@ -99,7 +109,6 @@ export default function StoreCreatePage() { function reportFormError(message: string) { setSubmitError(message); - toastError(message); } const stepFromUrl = Number(params.get('step') || 0); @@ -201,7 +210,6 @@ export default function StoreCreatePage() { .catch((e) => { const msg = e instanceof Error ? e.message : '加载开城城市失败'; setCitiesError(msg); - toastError(msg); }); }, []); @@ -244,6 +252,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); @@ -308,11 +332,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 { @@ -352,6 +376,10 @@ export default function StoreCreatePage() { const step1Msg = validateStoreStep1(form); if (step1Msg) { + if (isPhoneValidationMessage(step1Msg)) { + setFieldErrors({ phone: step1Msg }); + return; + } reportFormError(step1Msg); goStep(1); return; @@ -389,13 +417,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); @@ -444,11 +475,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); @@ -725,15 +755,7 @@ export default function StoreCreatePage() { onWechatReadyChange={setWechatReady} - onChange={(nextUrl) => { - - const envPhotoUrls = [...form.envPhotoUrls]; - - envPhotoUrls[index] = nextUrl; - - patchForm({ envPhotoUrls }); - - }} + onChange={(nextUrl) => patchEnvPhotoUrl(index, nextUrl)} /> @@ -805,22 +827,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/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..e460ae4 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 @@ -9,14 +9,6 @@ export class CreatePartnerStaffDto { @IsString() @IsNotEmpty() name: string; - - @IsString() - @IsIn(Object.values(PartnerStaffRole)) - staffRole: string; - - @IsString() - @IsNotEmpty() - code: 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..33d1ecb 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: 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 5c2a84b..7c51dc1 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 }), @@ -97,7 +101,10 @@ export class AdminPartnersService { async detailPartnerAccount(id: bigint) { const account = await this.prisma.partnerAccount.findUnique({ where: { id }, - include: { partner: true }, + include: { + partner: 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 {