fix;提交代码子账号
This commit is contained in:
@@ -3,12 +3,23 @@ import {
|
|||||||
Button, Drawer, Form, Input, Modal, Select, Space, Table, Tabs, Tag, Typography, message,
|
Button, Drawer, Form, Input, Modal, Select, Space, Table, Tabs, Tag, Typography, message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
import { PARTNER_STAFF_ROLE_LABELS } from '@dukang/shared-types';
|
||||||
import { request, type Paginated } from '../lib/api';
|
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 { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, ORDER_STATUS_LABELS, PARTNER_BILL_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||||||
import { useAdminList } from '../lib/useAdminList';
|
import { useAdminList } from '../lib/useAdminList';
|
||||||
|
|
||||||
|
type ParentAccount = { id: string; name: string; phone: string };
|
||||||
|
|
||||||
type Row = {
|
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 };
|
partner?: { id: string; companyName: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,6 +37,21 @@ type PartnerOption = { id: string; companyName: string };
|
|||||||
|
|
||||||
type Detail = Row & { bills?: BillRow[]; orders?: OrderRow[] };
|
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() {
|
export default function PartnerAccountsPage() {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [editForm] = Form.useForm();
|
const [editForm] = Form.useForm();
|
||||||
@@ -37,6 +63,7 @@ export default function PartnerAccountsPage() {
|
|||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
if (filters.phone) qs.set('phone', filters.phone);
|
if (filters.phone) qs.set('phone', filters.phone);
|
||||||
if (filters.status) qs.set('status', filters.status);
|
if (filters.status) qs.set('status', filters.status);
|
||||||
|
if (filters.isPrimary) qs.set('isPrimary', filters.isPrimary);
|
||||||
return qs;
|
return qs;
|
||||||
},
|
},
|
||||||
[filters],
|
[filters],
|
||||||
@@ -71,7 +98,27 @@ export default function PartnerAccountsPage() {
|
|||||||
{ title: '姓名', dataIndex: 'name', width: 100 },
|
{ title: '姓名', dataIndex: 'name', width: 100 },
|
||||||
{ title: '手机', dataIndex: 'phone', width: 120 },
|
{ title: '手机', dataIndex: 'phone', width: 120 },
|
||||||
{ title: '开城合伙人', dataIndex: ['partner', 'companyName'], width: 140 },
|
{ title: '开城合伙人', dataIndex: ['partner', 'companyName'], width: 140 },
|
||||||
{ title: '主账号', dataIndex: 'isPrimary', width: 80, render: (v) => (v === 1 ? '是' : '否') },
|
{
|
||||||
|
title: '账号类型',
|
||||||
|
dataIndex: 'isPrimary',
|
||||||
|
width: 90,
|
||||||
|
render: (v) => <Tag color={v === 1 ? 'blue' : 'default'}>{accountTypeLabel(v)}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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) => <Tag>{ACCOUNT_STATUS_LABELS[s] || s}</Tag> },
|
{ title: '状态', dataIndex: 'status', width: 80, render: (s) => <Tag>{ACCOUNT_STATUS_LABELS[s] || s}</Tag> },
|
||||||
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||||
{
|
{
|
||||||
@@ -113,9 +160,12 @@ export default function PartnerAccountsPage() {
|
|||||||
<Form.Item name="status" label="状态">
|
<Form.Item name="status" label="状态">
|
||||||
<Select allowClear style={{ width: 100 }} options={Object.entries(ACCOUNT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
<Select allowClear style={{ width: 100 }} options={Object.entries(ACCOUNT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item name="isPrimary" label="账号类型">
|
||||||
|
<Select allowClear style={{ width: 110 }} options={ACCOUNT_TYPE_OPTIONS.filter((item) => item.value !== '')} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item><Button type="primary" htmlType="submit">查询</Button></Form.Item>
|
<Form.Item><Button type="primary" htmlType="submit">查询</Button></Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
<Table rowKey="id" className="admin-table-nowrap" loading={loading} columns={columns} dataSource={data?.items ?? []} scroll={{ x: 900 }}
|
<Table rowKey="id" className="admin-table-nowrap" loading={loading} columns={columns} dataSource={data?.items ?? []} scroll={{ x: 1100 }}
|
||||||
pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); } }} />
|
pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); } }} />
|
||||||
<Drawer
|
<Drawer
|
||||||
title="编辑开城合伙人账户"
|
title="编辑开城合伙人账户"
|
||||||
@@ -134,6 +184,20 @@ export default function PartnerAccountsPage() {
|
|||||||
<Form.Item label="开城合伙人">
|
<Form.Item label="开城合伙人">
|
||||||
<Input value={detail.partner?.companyName} disabled />
|
<Input value={detail.partner?.companyName} disabled />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label="账号类型">
|
||||||
|
<Input value={accountTypeLabel(detail.isPrimary)} disabled />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="角色">
|
||||||
|
<Input value={staffRoleLabel(detail.staffRole)} disabled />
|
||||||
|
</Form.Item>
|
||||||
|
{detail.isPrimary !== 1 && (
|
||||||
|
<Form.Item label="所属主账号">
|
||||||
|
<Input
|
||||||
|
value={detail.parent ? `${detail.parent.name} / ${detail.parent.phone}` : '-'}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
<Form.Item name="name" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
|
<Form.Item name="name" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="phone"
|
name="phone"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useId, useRef, useState } from 'react';
|
import { useEffect, useId, useRef, useState } from 'react';
|
||||||
import { uploadFileToOss, type OssMediaType } from '../lib/upload';
|
import { uploadFileToOss, type OssMediaType } from '../lib/upload';
|
||||||
import { toastError } from '../lib/toast';
|
import { enqueueUpload } from '../lib/upload-lock';
|
||||||
import {
|
import {
|
||||||
authorizePartnerWechat,
|
authorizePartnerWechat,
|
||||||
fetchClientConfig,
|
fetchClientConfig,
|
||||||
@@ -87,13 +87,12 @@ export default function OssUploadField({
|
|||||||
if (file.size > DEFAULT_MAX_MB * 1024 * 1024) {
|
if (file.size > DEFAULT_MAX_MB * 1024 * 1024) {
|
||||||
const text = `文件不能超过 ${DEFAULT_MAX_MB}MB`;
|
const text = `文件不能超过 ${DEFAULT_MAX_MB}MB`;
|
||||||
setError(text);
|
setError(text);
|
||||||
toastError(text);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const result = await uploadFileToOss(file, { bizType, mediaType });
|
const result = await enqueueUpload(() => uploadFileToOss(file, { bizType, mediaType }));
|
||||||
onChange?.(result.url);
|
onChange?.(result.url);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : '上传失败');
|
setError(e instanceof Error ? e.message : '上传失败');
|
||||||
@@ -111,7 +110,6 @@ export default function OssUploadField({
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
const text = e instanceof Error ? e.message : '微信授权失败';
|
const text = e instanceof Error ? e.message : '微信授权失败';
|
||||||
setError(text);
|
setError(text);
|
||||||
toastError(text);
|
|
||||||
setAuthorizing(false);
|
setAuthorizing(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,15 +119,23 @@ export default function OssUploadField({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function pickWechatImage() {
|
async function pickWechatImage() {
|
||||||
const files = await weixinSdk.chooseImages({
|
setUploading(true);
|
||||||
count: 1,
|
setError('');
|
||||||
sourceType: ['album', 'camera'],
|
try {
|
||||||
});
|
await enqueueUpload(async () => {
|
||||||
if (files?.[0]) {
|
const files = await weixinSdk.chooseImages({
|
||||||
await uploadSelectedFile(files[0]);
|
count: 1,
|
||||||
return;
|
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() {
|
async function pickFile() {
|
||||||
@@ -139,7 +145,6 @@ export default function OssUploadField({
|
|||||||
if (useWechatPicker && needsAuth) {
|
if (useWechatPicker && needsAuth) {
|
||||||
const text = '请先完成微信授权后再上传照片';
|
const text = '请先完成微信授权后再上传照片';
|
||||||
setError(text);
|
setError(text);
|
||||||
toastError(text);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { PartnerMe } from '@dukang/shared-types';
|
import type { PartnerMe } from '@dukang/shared-types';
|
||||||
import { isOnAppPath, toAppPath } from '@dukang/weixin-sdk';
|
import { isOnAppPath, toAppPath } from '@dukang/weixin-sdk';
|
||||||
import { showPartnerToast } from './toast';
|
|
||||||
|
|
||||||
export const apiBase = '/api/v1';
|
export const apiBase = '/api/v1';
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ export type PartnerAuthPayload = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ApiRequestOptions = RequestInit & {
|
export type ApiRequestOptions = RequestInit & {
|
||||||
/** 为 true 时不弹出 toast(由调用方自行展示) */
|
/** 保留字段,兼容旧调用;错误提示由页面自行处理 */
|
||||||
silent?: boolean;
|
silent?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,7 +51,6 @@ export async function request<T>(
|
|||||||
if (res.status === 401 || json.code === 401) {
|
if (res.status === 401 || json.code === 401) {
|
||||||
if (token && localStorage.getItem('accessToken') === token) {
|
if (token && localStorage.getItem('accessToken') === token) {
|
||||||
clearAuth();
|
clearAuth();
|
||||||
if (!silent) showPartnerToast(message, 'error');
|
|
||||||
if (typeof window !== 'undefined' && !isOnAppPath('/login')) {
|
if (typeof window !== 'undefined' && !isOnAppPath('/login')) {
|
||||||
window.location.href = toAppPath('/login');
|
window.location.href = toAppPath('/login');
|
||||||
}
|
}
|
||||||
@@ -60,7 +58,6 @@ export async function request<T>(
|
|||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
if (json.code !== 0) {
|
if (json.code !== 0) {
|
||||||
if (!silent) showPartnerToast(message, 'error');
|
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
return json.data as T;
|
return json.data as T;
|
||||||
|
|||||||
@@ -28,10 +28,3 @@ export function deletePartnerStaff(id: string) {
|
|||||||
method: 'DELETE',
|
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' }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const defaultStoreForm = (): StoreDraftForm => ({
|
|||||||
bankBranch: '',
|
bankBranch: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
function normalizeStringArray(raw: unknown, length: number): string[] {
|
export function normalizeStringArray(raw: unknown, length: number): string[] {
|
||||||
if (!Array.isArray(raw)) return Array.from({ length }, () => '');
|
if (!Array.isArray(raw)) return Array.from({ length }, () => '');
|
||||||
const items = raw.map((item) => String(item ?? ''));
|
const items = raw.map((item) => String(item ?? ''));
|
||||||
while (items.length < length) items.push('');
|
while (items.length < length) items.push('');
|
||||||
@@ -126,6 +126,12 @@ export function validateStoreStep2(
|
|||||||
return null;
|
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(
|
export function validateStoreStep3(
|
||||||
form: Pick<StoreDraftForm, 'bankAccountName' | 'bankAccountNo' | 'bankBranch'>,
|
form: Pick<StoreDraftForm, 'bankAccountName' | 'bankAccountNo' | 'bankBranch'>,
|
||||||
): string | null {
|
): string | null {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/** 串行化上传,避免微信 JSSDK 并发选图/读图导致卡死 */
|
||||||
|
let uploadChain: Promise<unknown> = Promise.resolve();
|
||||||
|
|
||||||
|
export function enqueueUpload<T>(task: () => Promise<T>): Promise<T> {
|
||||||
|
const next = uploadChain.then(task, task);
|
||||||
|
uploadChain = next.then(
|
||||||
|
() => undefined,
|
||||||
|
() => undefined,
|
||||||
|
);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { apiBase, request } from './api';
|
import { apiBase, request } from './api';
|
||||||
import { showPartnerToast } from './toast';
|
|
||||||
|
const UPLOAD_TIMEOUT_MS = 120_000;
|
||||||
|
|
||||||
export type OssMediaType = 'IMAGE' | 'VIDEO' | 'FILE';
|
export type OssMediaType = 'IMAGE' | 'VIDEO' | 'FILE';
|
||||||
|
|
||||||
@@ -10,8 +11,7 @@ export type UploadFileResult = {
|
|||||||
mock: boolean;
|
mock: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 经 API 服务端转存 OSS,避免浏览器直传跨域 */
|
async function uploadFileToOssInner(
|
||||||
export async function uploadFileToOss(
|
|
||||||
file: File,
|
file: File,
|
||||||
options: { bizType: string; mediaType?: OssMediaType },
|
options: { bizType: string; mediaType?: OssMediaType },
|
||||||
): Promise<UploadFileResult> {
|
): Promise<UploadFileResult> {
|
||||||
@@ -25,29 +25,48 @@ export async function uploadFileToOss(
|
|||||||
const headers: Record<string, string> = { 'X-Client-App': 'PARTNER_H5' };
|
const headers: Record<string, string> = { 'X-Client-App': 'PARTNER_H5' };
|
||||||
if (token) headers.Authorization = `Bearer ${token}`;
|
if (token) headers.Authorization = `Bearer ${token}`;
|
||||||
|
|
||||||
const res = await fetch(`${apiBase}/common/resources/upload`, {
|
const controller = new AbortController();
|
||||||
method: 'POST',
|
const timeoutId = window.setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS);
|
||||||
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 data = json.data as UploadFileResult;
|
try {
|
||||||
return {
|
const res = await fetch(`${apiBase}/common/resources/upload`, {
|
||||||
url: data.url,
|
method: 'POST',
|
||||||
ossKey: data.ossKey,
|
headers,
|
||||||
bucket: data.bucket,
|
body: formData,
|
||||||
mock: data.mock,
|
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<UploadFileResult> {
|
||||||
|
return uploadFileToOssInner(file, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OpenCityOption = {
|
export type OpenCityOption = {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
import { isWechatEnv } from '../lib/weixin';
|
import { isWechatEnv } from '../lib/weixin';
|
||||||
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||||
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
|
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
|
||||||
import { toastError } from '../lib/toast';
|
|
||||||
|
|
||||||
const REMEMBER_PHONE_KEY = 'partner_remember_phone';
|
const REMEMBER_PHONE_KEY = 'partner_remember_phone';
|
||||||
const REMEMBER_FLAG_KEY = 'partner_remember_account';
|
const REMEMBER_FLAG_KEY = 'partner_remember_account';
|
||||||
@@ -93,9 +92,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
function ensureAgreed() {
|
function ensureAgreed() {
|
||||||
if (!agreed) {
|
if (!agreed) {
|
||||||
const text = '请先勾选并同意用户协议';
|
setMsg('请先勾选并同意用户协议');
|
||||||
setMsg(text);
|
|
||||||
toastError(text);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -136,9 +133,7 @@ export default function LoginPage() {
|
|||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const text = formatPartnerError(e);
|
setMsg(formatPartnerError(e));
|
||||||
setMsg(text);
|
|
||||||
toastError(text);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,9 +160,7 @@ export default function LoginPage() {
|
|||||||
const account = await refresh();
|
const account = await refresh();
|
||||||
navigate(partnerHomePath(account ?? data.partner));
|
navigate(partnerHomePath(account ?? data.partner));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const text = formatPartnerError(e);
|
setMsg(formatPartnerError(e));
|
||||||
setMsg(text);
|
|
||||||
toastError(text);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -178,7 +171,6 @@ export default function LoginPage() {
|
|||||||
setMsg('');
|
setMsg('');
|
||||||
if (!isWechatEnv()) {
|
if (!isWechatEnv()) {
|
||||||
setMsg(WECHAT_INAPP_REQUIRED_MSG);
|
setMsg(WECHAT_INAPP_REQUIRED_MSG);
|
||||||
toastError(WECHAT_INAPP_REQUIRED_MSG);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setWxLoading(true);
|
setWxLoading(true);
|
||||||
@@ -189,9 +181,7 @@ export default function LoginPage() {
|
|||||||
navigate(partnerHomePath(account ?? undefined));
|
navigate(partnerHomePath(account ?? undefined));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const text = formatWechatError(e);
|
setMsg(formatWechatError(e));
|
||||||
setMsg(text);
|
|
||||||
toastError(text);
|
|
||||||
} finally {
|
} finally {
|
||||||
setWxLoading(false);
|
setWxLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,149 +1,141 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||||
import { PARTNER_STAFF_ROLE_LABELS, PartnerStaffRole } from '@dukang/shared-types';
|
import { createPartnerStaff } from '../lib/staff';
|
||||||
import { createPartnerStaff, sendStaffAddSms } from '../lib/staff';
|
|
||||||
import { toastError, toastSuccess } from '../lib/toast';
|
import { toastError, toastSuccess } from '../lib/toast';
|
||||||
|
|
||||||
const ROLE_OPTIONS = Object.values(PartnerStaffRole);
|
type FieldErrors = {
|
||||||
|
name?: string;
|
||||||
|
phone?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function StaffCreatePage() {
|
export default function StaffCreatePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
const [code, setCode] = useState('');
|
|
||||||
const [staffRole, setStaffRole] = useState<PartnerStaffRole>(PartnerStaffRole.INTERNAL);
|
|
||||||
const [sending, setSending] = useState(false);
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [msg, setMsg] = useState('');
|
const [submitError, setSubmitError] = useState('');
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({});
|
||||||
|
|
||||||
async function sendCode() {
|
function patchName(value: string) {
|
||||||
if (!/^1[3-9]\d{9}$/.test(phone.trim())) {
|
setName(value);
|
||||||
const text = '请输入正确的11位手机号';
|
setSubmitError('');
|
||||||
setMsg(text);
|
setFieldErrors((prev) => ({ ...prev, name: undefined }));
|
||||||
toastError(text);
|
}
|
||||||
return;
|
|
||||||
}
|
function patchPhone(value: string) {
|
||||||
setSending(true);
|
setPhone(value);
|
||||||
setMsg('');
|
setSubmitError('');
|
||||||
try {
|
setFieldErrors((prev) => ({ ...prev, phone: undefined }));
|
||||||
await sendStaffAddSms(phone.trim());
|
|
||||||
setMsg('验证码已发送');
|
|
||||||
} catch {
|
|
||||||
/* request 已 toast */
|
|
||||||
} finally {
|
|
||||||
setSending(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (!name.trim()) {
|
const trimmedName = name.trim();
|
||||||
const text = '请填写真实姓名';
|
const trimmedPhone = phone.trim();
|
||||||
setMsg(text);
|
const nextFieldErrors: FieldErrors = {};
|
||||||
toastError(text);
|
|
||||||
return;
|
if (!trimmedName) nextFieldErrors.name = '请填写真实姓名';
|
||||||
}
|
if (!trimmedPhone) {
|
||||||
if (!/^1[3-9]\d{9}$/.test(phone.trim())) {
|
nextFieldErrors.phone = '请填写手机号码';
|
||||||
const text = '请输入正确的11位手机号';
|
} else if (!/^1[3-9]\d{9}$/.test(trimmedPhone)) {
|
||||||
setMsg(text);
|
nextFieldErrors.phone = '请输入正确的11位手机号';
|
||||||
toastError(text);
|
}
|
||||||
return;
|
|
||||||
}
|
if (Object.keys(nextFieldErrors).length) {
|
||||||
if (!code.trim()) {
|
setFieldErrors(nextFieldErrors);
|
||||||
const text = '请输入验证码';
|
|
||||||
setMsg(text);
|
|
||||||
toastError(text);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setMsg('');
|
setSubmitError('');
|
||||||
|
setFieldErrors({});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createPartnerStaff({
|
await createPartnerStaff({
|
||||||
name: name.trim(),
|
name: trimmedName,
|
||||||
phone: phone.trim(),
|
phone: trimmedPhone,
|
||||||
staffRole,
|
|
||||||
code: code.trim(),
|
|
||||||
});
|
});
|
||||||
toastSuccess('子账号创建成功');
|
toastSuccess('子账号创建成功');
|
||||||
navigate('/center/staff');
|
navigate('/center/staff');
|
||||||
} catch {
|
} catch (e) {
|
||||||
/* request 已 toast */
|
const message = e instanceof Error ? e.message : '创建失败';
|
||||||
|
if (message.includes('手机号')) {
|
||||||
|
setFieldErrors({ phone: message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSubmitError(message);
|
||||||
|
toastError(message);
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-no-tab partner-staff-create-page">
|
<div className="partner-page-sticky">
|
||||||
<PageHeader title="添加子账号" onBack={() => navigate('/center/staff')} />
|
<PageHeader title="添加子账号" onBack={() => navigate('/center/staff')} />
|
||||||
|
|
||||||
<main style={{ padding: '16px 20px 120px' }}>
|
{submitError && (
|
||||||
{msg && <p className="partner-form-error" role="alert" style={{ marginBottom: 12 }}>{msg}</p>}
|
<p className="partner-form-error" role="alert">{submitError}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="partner-form-card">
|
<section className="partner-form-card">
|
||||||
<label className="partner-form-label">真实姓名 *</label>
|
<div className="partner-section-title">
|
||||||
<div className="partner-form-input-wrap">
|
<div className="partner-section-bar" />
|
||||||
|
<h2 className="headline-md">子账号信息</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="partner-field">
|
||||||
|
<label>真实姓名 <span className="text-primary">*</span></label>
|
||||||
|
<div className="partner-field-input">
|
||||||
|
<span className="material-symbols-outlined">person</span>
|
||||||
<input
|
<input
|
||||||
className="partner-form-input"
|
|
||||||
placeholder="请输入员工姓名"
|
placeholder="请输入员工姓名"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => patchName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<span className="material-symbols-outlined">person</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
{fieldErrors.name && (
|
||||||
|
<p className="partner-field-error" role="alert">{fieldErrors.name}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="partner-form-card">
|
<div className="partner-field">
|
||||||
<label className="partner-form-label">手机号码 *</label>
|
<label>手机号码 <span className="text-primary">*</span></label>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div className="partner-field-input">
|
||||||
|
<span className="material-symbols-outlined">call</span>
|
||||||
<input
|
<input
|
||||||
className="partner-form-input"
|
type="tel"
|
||||||
placeholder="请输入11位手机号"
|
placeholder="请输入11位手机号"
|
||||||
value={phone}
|
value={phone}
|
||||||
onChange={(e) => setPhone(e.target.value)}
|
onChange={(e) => patchPhone(e.target.value)}
|
||||||
inputMode="numeric"
|
|
||||||
/>
|
/>
|
||||||
<button type="button" className="btn btn-outline" disabled={sending} onClick={() => void sendCode()}>
|
|
||||||
{sending ? '发送中' : '获取验证码'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
{fieldErrors.phone && (
|
||||||
className="partner-form-input"
|
<p className="partner-field-error" role="alert">{fieldErrors.phone}</p>
|
||||||
style={{ marginTop: 8 }}
|
)}
|
||||||
placeholder="验证码"
|
|
||||||
value={code}
|
|
||||||
onChange={(e) => setCode(e.target.value)}
|
|
||||||
inputMode="numeric"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="partner-form-card">
|
<div className="partner-info-banner">
|
||||||
<label className="partner-form-label">分配角色 *</label>
|
<div className="partner-bills-icon">
|
||||||
<div className="partner-form-input-wrap">
|
<span className="material-symbols-outlined">info</span>
|
||||||
<select
|
|
||||||
className="partner-form-input"
|
|
||||||
value={staffRole}
|
|
||||||
onChange={(e) => setStaffRole(e.target.value as PartnerStaffRole)}
|
|
||||||
>
|
|
||||||
{ROLE_OPTIONS.map((role) => (
|
|
||||||
<option key={role} value={role}>{PARTNER_STAFF_ROLE_LABELS[role]}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<span className="material-symbols-outlined">expand_more</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="label-md text-variant" style={{ marginTop: 0, lineHeight: 1.5 }}>
|
||||||
<p className="label-md text-muted" style={{ marginTop: 16, textAlign: 'center' }}>
|
子账号创建后默认禁用,需在子账号列表中手动启用后方可登录。
|
||||||
创建即代表同意合伙人管理协议。子账号默认禁用,需手动启用。
|
|
||||||
</p>
|
</p>
|
||||||
</main>
|
|
||||||
|
|
||||||
<div className="partner-sticky-footer">
|
|
||||||
<button type="button" className="partner-btn-primary" disabled={submitting} onClick={() => void submit()}>
|
|
||||||
<span className="material-symbols-outlined">person_add</span>
|
|
||||||
{submitting ? '创建中...' : '确认创建'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<footer className="partner-sticky-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="partner-btn-primary"
|
||||||
|
disabled={submitting}
|
||||||
|
onClick={() => void submit()}
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined" style={{ fontSize: 20 }}>person_add</span>
|
||||||
|
{submitting ? '创建中…' : '确认创建'}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
type PartnerStaffItem,
|
type PartnerStaffItem,
|
||||||
} from '@dukang/shared-types';
|
} from '@dukang/shared-types';
|
||||||
import { deletePartnerStaff, listPartnerStaff, updatePartnerStaff } from '../lib/staff';
|
import { deletePartnerStaff, listPartnerStaff, updatePartnerStaff } from '../lib/staff';
|
||||||
import { toastError } from '../lib/toast';
|
|
||||||
|
|
||||||
export default function StaffListPage() {
|
export default function StaffListPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -18,8 +17,8 @@ export default function StaffListPage() {
|
|||||||
const [busyId, setBusyId] = useState<string | null>(null);
|
const [busyId, setBusyId] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadStaff = useCallback(() => {
|
const loadStaff = useCallback(() => {
|
||||||
return listPartnerStaff().then(setStaff).catch(() => {
|
return listPartnerStaff().then(setStaff).catch((e) => {
|
||||||
/* request 已 toast */
|
setError(e instanceof Error ? e.message : '加载失败');
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -40,8 +39,8 @@ export default function StaffListPage() {
|
|||||||
try {
|
try {
|
||||||
await updatePartnerStaff(item.id, { status: next });
|
await updatePartnerStaff(item.id, { status: next });
|
||||||
await loadStaff();
|
await loadStaff();
|
||||||
} catch {
|
} catch (e) {
|
||||||
/* request 已 toast */
|
setError(e instanceof Error ? e.message : '操作失败');
|
||||||
} finally {
|
} finally {
|
||||||
setBusyId(null);
|
setBusyId(null);
|
||||||
}
|
}
|
||||||
@@ -55,8 +54,8 @@ export default function StaffListPage() {
|
|||||||
try {
|
try {
|
||||||
await deletePartnerStaff(item.id);
|
await deletePartnerStaff(item.id);
|
||||||
await loadStaff();
|
await loadStaff();
|
||||||
} catch {
|
} catch (e) {
|
||||||
/* request 已 toast */
|
setError(e instanceof Error ? e.message : '删除失败');
|
||||||
} finally {
|
} finally {
|
||||||
setBusyId(null);
|
setBusyId(null);
|
||||||
}
|
}
|
||||||
@@ -71,7 +70,7 @@ export default function StaffListPage() {
|
|||||||
if (roleInput == null) return;
|
if (roleInput == null) return;
|
||||||
const staffRole = roleOptions.find((r) => r === roleInput || PARTNER_STAFF_ROLE_LABELS[r] === roleInput);
|
const staffRole = roleOptions.find((r) => r === roleInput || PARTNER_STAFF_ROLE_LABELS[r] === roleInput);
|
||||||
if (!staffRole) {
|
if (!staffRole) {
|
||||||
toastError('无效的角色');
|
setError('无效的角色');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setBusyId(item.id);
|
setBusyId(item.id);
|
||||||
@@ -79,8 +78,8 @@ export default function StaffListPage() {
|
|||||||
try {
|
try {
|
||||||
await updatePartnerStaff(item.id, { name: name.trim(), staffRole });
|
await updatePartnerStaff(item.id, { name: name.trim(), staffRole });
|
||||||
await loadStaff();
|
await loadStaff();
|
||||||
} catch {
|
} catch (e) {
|
||||||
/* request 已 toast */
|
setError(e instanceof Error ? e.message : '保存失败');
|
||||||
} finally {
|
} finally {
|
||||||
setBusyId(null);
|
setBusyId(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ import {
|
|||||||
|
|
||||||
validateStoreStep3,
|
validateStoreStep3,
|
||||||
|
|
||||||
|
patchEnvPhotoAt,
|
||||||
|
|
||||||
} from '../lib/storeDraft';
|
} from '../lib/storeDraft';
|
||||||
|
|
||||||
|
|
||||||
@@ -73,6 +75,14 @@ function isPhoneConflictMessage(message: string) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function isPhoneValidationMessage(message: string) {
|
||||||
|
|
||||||
|
return message.includes('联系电话') || message.includes('手机号');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function StoreCreatePage() {
|
export default function StoreCreatePage() {
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -99,7 +109,6 @@ export default function StoreCreatePage() {
|
|||||||
|
|
||||||
function reportFormError(message: string) {
|
function reportFormError(message: string) {
|
||||||
setSubmitError(message);
|
setSubmitError(message);
|
||||||
toastError(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stepFromUrl = Number(params.get('step') || 0);
|
const stepFromUrl = Number(params.get('step') || 0);
|
||||||
@@ -201,7 +210,6 @@ export default function StoreCreatePage() {
|
|||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
const msg = e instanceof Error ? e.message : '加载开城城市失败';
|
const msg = e instanceof Error ? e.message : '加载开城城市失败';
|
||||||
setCitiesError(msg);
|
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[]) {
|
function bindRegionSelection(codes: string[]) {
|
||||||
|
|
||||||
const binding = resolveRegionBinding(codes, cities);
|
const binding = resolveRegionBinding(codes, cities);
|
||||||
@@ -308,11 +332,11 @@ export default function StoreCreatePage() {
|
|||||||
if (!phoneCheck.available) {
|
if (!phoneCheck.available) {
|
||||||
const phoneMsg = phoneCheck.message ?? '该手机号已绑定门店,请更换';
|
const phoneMsg = phoneCheck.message ?? '该手机号已绑定门店,请更换';
|
||||||
setFieldErrors({ phone: phoneMsg });
|
setFieldErrors({ phone: phoneMsg });
|
||||||
toastError(phoneMsg);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
reportFormError(e instanceof Error ? e.message : '手机号校验失败');
|
||||||
return;
|
return;
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
@@ -352,6 +376,10 @@ export default function StoreCreatePage() {
|
|||||||
const step1Msg = validateStoreStep1(form);
|
const step1Msg = validateStoreStep1(form);
|
||||||
|
|
||||||
if (step1Msg) {
|
if (step1Msg) {
|
||||||
|
if (isPhoneValidationMessage(step1Msg)) {
|
||||||
|
setFieldErrors({ phone: step1Msg });
|
||||||
|
return;
|
||||||
|
}
|
||||||
reportFormError(step1Msg);
|
reportFormError(step1Msg);
|
||||||
goStep(1);
|
goStep(1);
|
||||||
return;
|
return;
|
||||||
@@ -389,13 +417,16 @@ export default function StoreCreatePage() {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
goStep(1);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
reportFormError(e instanceof Error ? e.message : '手机号校验失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
const envPhotoUrls = form.envPhotoUrls.map((u) => u.trim()).filter(Boolean);
|
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 : '提交失败';
|
const message = e instanceof Error ? e.message : '提交失败';
|
||||||
if (isPhoneConflictMessage(message)) {
|
if (isPhoneConflictMessage(message)) {
|
||||||
setFieldErrors({ phone: message });
|
setFieldErrors({ phone: message });
|
||||||
toastError(message);
|
|
||||||
goStep(1);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmitError(message);
|
setSubmitError(message);
|
||||||
|
toastError(message);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
@@ -725,15 +755,7 @@ export default function StoreCreatePage() {
|
|||||||
|
|
||||||
onWechatReadyChange={setWechatReady}
|
onWechatReadyChange={setWechatReady}
|
||||||
|
|
||||||
onChange={(nextUrl) => {
|
onChange={(nextUrl) => patchEnvPhotoUrl(index, nextUrl)}
|
||||||
|
|
||||||
const envPhotoUrls = [...form.envPhotoUrls];
|
|
||||||
|
|
||||||
envPhotoUrls[index] = nextUrl;
|
|
||||||
|
|
||||||
patchForm({ envPhotoUrls });
|
|
||||||
|
|
||||||
}}
|
|
||||||
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -805,22 +827,44 @@ export default function StoreCreatePage() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="partner-form-card partner-store-phone-summary">
|
<section className="partner-form-card">
|
||||||
|
|
||||||
<div className="partner-store-phone-summary-main">
|
<div className="partner-field">
|
||||||
|
|
||||||
<span className="label-md text-muted">门店登录手机号</span>
|
<label>门店登录手机号 <span className="text-primary">*</span></label>
|
||||||
|
|
||||||
<span className="body-md">{form.phone.trim() || '未填写'}</span>
|
<div className="partner-field-input">
|
||||||
|
|
||||||
|
<span className="material-symbols-outlined">call</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
|
||||||
|
type="tel"
|
||||||
|
|
||||||
|
placeholder="请输入11位手机号"
|
||||||
|
|
||||||
|
value={form.phone}
|
||||||
|
|
||||||
|
onChange={(e) => patchForm({ phone: e.target.value })}
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{fieldErrors.phone && (
|
||||||
|
|
||||||
|
<p className="partner-field-error" role="alert">{fieldErrors.phone}</p>
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||||||
|
|
||||||
|
该手机号将作为门店端登录账号,提交前会再次校验是否已被占用。
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" className="partner-weekly-link" onClick={() => goStep(1)}>
|
|
||||||
|
|
||||||
修改
|
|
||||||
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="partner-form-card">
|
<section className="partner-form-card">
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default function StoreDetailPage() {
|
|||||||
const [status, setStatus] = useState<StoreStatusValue>('OPEN');
|
const [status, setStatus] = useState<StoreStatusValue>('OPEN');
|
||||||
const [statusSaving, setStatusSaving] = useState(false);
|
const [statusSaving, setStatusSaving] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [actionError, setActionError] = useState('');
|
||||||
|
|
||||||
function applyStore(data: Record<string, unknown>) {
|
function applyStore(data: Record<string, unknown>) {
|
||||||
setStore(data);
|
setStore(data);
|
||||||
@@ -52,6 +53,7 @@ export default function StoreDetailPage() {
|
|||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
}
|
}
|
||||||
setStatusSaving(true);
|
setStatusSaving(true);
|
||||||
|
setActionError('');
|
||||||
try {
|
try {
|
||||||
const updated = await request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}/status`, {
|
const updated = await request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}/status`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -60,8 +62,8 @@ export default function StoreDetailPage() {
|
|||||||
setStatus(next);
|
setStatus(next);
|
||||||
setStore((prev) => (prev ? { ...prev, ...updated, status: next } : prev));
|
setStore((prev) => (prev ? { ...prev, ...updated, status: next } : prev));
|
||||||
toastSuccess('状态已更新');
|
toastSuccess('状态已更新');
|
||||||
} catch {
|
} catch (e) {
|
||||||
/* request 已 toast */
|
setActionError(e instanceof Error ? e.message : '状态更新失败');
|
||||||
} finally {
|
} finally {
|
||||||
setStatusSaving(false);
|
setStatusSaving(false);
|
||||||
}
|
}
|
||||||
@@ -70,6 +72,7 @@ export default function StoreDetailPage() {
|
|||||||
async function saveBasic() {
|
async function saveBasic() {
|
||||||
if (!id || saving || status === 'CLOSED') return;
|
if (!id || saving || status === 'CLOSED') return;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
setActionError('');
|
||||||
try {
|
try {
|
||||||
const data = await request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}/basic`, {
|
const data = await request<Record<string, unknown>>('PARTNER_H5', `/partner/stores/${id}/basic`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -82,8 +85,8 @@ export default function StoreDetailPage() {
|
|||||||
});
|
});
|
||||||
applyStore(data);
|
applyStore(data);
|
||||||
toastSuccess('已保存');
|
toastSuccess('已保存');
|
||||||
} catch {
|
} catch (e) {
|
||||||
/* request 已 toast */
|
setActionError(e instanceof Error ? e.message : '保存失败');
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -110,6 +113,7 @@ export default function StoreDetailPage() {
|
|||||||
<PageHeader title="门店详情" onBack={() => navigate('/stores')} />
|
<PageHeader title="门店详情" onBack={() => navigate('/stores')} />
|
||||||
|
|
||||||
<main style={{ padding: '16px 20px' }}>
|
<main style={{ padding: '16px 20px' }}>
|
||||||
|
{actionError && <p className="partner-form-error" role="alert" style={{ marginBottom: 12 }}>{actionError}</p>}
|
||||||
{!subReadonly && (
|
{!subReadonly && (
|
||||||
<section className="partner-form-card" style={{ margin: '0 0 16px' }}>
|
<section className="partner-form-card" style={{ margin: '0 0 16px' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ export interface PartnerStaffItem {
|
|||||||
export interface CreatePartnerStaffRequest {
|
export interface CreatePartnerStaffRequest {
|
||||||
phone: string;
|
phone: string;
|
||||||
name: string;
|
name: string;
|
||||||
staffRole: PartnerStaffRole;
|
|
||||||
code: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdatePartnerStaffRequest {
|
export interface UpdatePartnerStaffRequest {
|
||||||
|
|||||||
@@ -9,14 +9,6 @@ export class CreatePartnerStaffDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsIn(Object.values(PartnerStaffRole))
|
|
||||||
staffRole: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
code: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdatePartnerStaffDto {
|
export class UpdatePartnerStaffDto {
|
||||||
|
|||||||
@@ -3,18 +3,14 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { PartnerStaffRole, SmsScene } from '@dukang/shared-types';
|
import { PartnerStaffRole } from '@dukang/shared-types';
|
||||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||||
import { AuthService } from './auth.service';
|
|
||||||
import { CreatePartnerStaffDto, UpdatePartnerStaffDto } from './dto/partner-staff.dto';
|
import { CreatePartnerStaffDto, UpdatePartnerStaffDto } from './dto/partner-staff.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PartnerStaffService {
|
export class PartnerStaffService {
|
||||||
constructor(
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly authService: AuthService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async listStaff(parentAccountId: bigint) {
|
async listStaff(parentAccountId: bigint) {
|
||||||
const rows = await this.prisma.partnerAccount.findMany({
|
const rows = await this.prisma.partnerAccount.findMany({
|
||||||
@@ -40,8 +36,6 @@ export class PartnerStaffService {
|
|||||||
const existing = await this.prisma.partnerAccount.findUnique({ where: { phone } });
|
const existing = await this.prisma.partnerAccount.findUnique({ where: { phone } });
|
||||||
if (existing) throw new BadRequestException('该手机号已被使用');
|
if (existing) throw new BadRequestException('该手机号已被使用');
|
||||||
|
|
||||||
await this.authService.verifySmsCode(phone, dto.code, SmsScene.PARTNER_STAFF_ADD);
|
|
||||||
|
|
||||||
const name = dto.name.trim();
|
const name = dto.name.trim();
|
||||||
if (!name) throw new BadRequestException('请填写真实姓名');
|
if (!name) throw new BadRequestException('请填写真实姓名');
|
||||||
|
|
||||||
@@ -50,7 +44,7 @@ export class PartnerStaffService {
|
|||||||
partnerId: parent.partnerId,
|
partnerId: parent.partnerId,
|
||||||
phone,
|
phone,
|
||||||
name,
|
name,
|
||||||
staffRole: dto.staffRole as PartnerStaffRole,
|
staffRole: PartnerStaffRole.INTERNAL,
|
||||||
isPrimary: 0,
|
isPrimary: 0,
|
||||||
parentAccountId: parent.id,
|
parentAccountId: parent.id,
|
||||||
status: 'DISABLED',
|
status: 'DISABLED',
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ export class AdminPartnersService {
|
|||||||
if (query.phone) where.phone = { contains: query.phone };
|
if (query.phone) where.phone = { contains: query.phone };
|
||||||
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
|
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
|
||||||
if (query.status) where.status = query.status as Prisma.EnumAccountStatusFilter['equals'];
|
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([
|
const [items, total] = await Promise.all([
|
||||||
this.prisma.partnerAccount.findMany({
|
this.prisma.partnerAccount.findMany({
|
||||||
@@ -87,6 +90,7 @@ export class AdminPartnersService {
|
|||||||
take: pageSize,
|
take: pageSize,
|
||||||
include: {
|
include: {
|
||||||
partner: { select: { id: true, companyName: true } },
|
partner: { select: { id: true, companyName: true } },
|
||||||
|
parent: { select: { id: true, name: true, phone: true } },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.prisma.partnerAccount.count({ where }),
|
this.prisma.partnerAccount.count({ where }),
|
||||||
@@ -97,7 +101,10 @@ export class AdminPartnersService {
|
|||||||
async detailPartnerAccount(id: bigint) {
|
async detailPartnerAccount(id: bigint) {
|
||||||
const account = await this.prisma.partnerAccount.findUnique({
|
const account = await this.prisma.partnerAccount.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: { partner: true },
|
include: {
|
||||||
|
partner: true,
|
||||||
|
parent: { select: { id: true, name: true, phone: true } },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!account) throw new NotFoundException('开城合伙人账号不存在');
|
if (!account) throw new NotFoundException('开城合伙人账号不存在');
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ export class AdminPartnerAccountsQueryDto extends PaginationQueryDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
isPrimary?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdminBenefitCouponsQueryDto extends PaginationQueryDto {
|
export class AdminBenefitCouponsQueryDto extends PaginationQueryDto {
|
||||||
|
|||||||
Reference in New Issue
Block a user