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 {