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() {
+
+
- { 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 {