合伙人端登录接口优化
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Button, Descriptions, 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 { request, type Paginated } from '../lib/api';
|
import { request, type Paginated } from '../lib/api';
|
||||||
@@ -28,6 +28,7 @@ type Detail = Row & { bills?: BillRow[]; orders?: OrderRow[] };
|
|||||||
|
|
||||||
export default function PartnerAccountsPage() {
|
export default function PartnerAccountsPage() {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const [editForm] = Form.useForm();
|
||||||
const [createForm] = Form.useForm();
|
const [createForm] = Form.useForm();
|
||||||
const [filters, setFilters] = useState<Record<string, string>>({});
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
||||||
@@ -50,6 +51,22 @@ export default function PartnerAccountsPage() {
|
|||||||
setPartners(res.items);
|
setPartners(res.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openAccount(id: string) {
|
||||||
|
const d = await request<Detail>(`/admin/partner-accounts/${id}`);
|
||||||
|
setDetail(d);
|
||||||
|
editForm.setFieldsValue({ name: d.name, phone: d.phone, status: d.status });
|
||||||
|
setDrawerOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveAccount() {
|
||||||
|
if (!detail) return;
|
||||||
|
const v = await editForm.validateFields();
|
||||||
|
await request(`/admin/partner-accounts/${detail.id}`, { method: 'PUT', body: JSON.stringify(v) });
|
||||||
|
message.success('已保存');
|
||||||
|
setDrawerOpen(false);
|
||||||
|
void reload();
|
||||||
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<Row> = [
|
const columns: ColumnsType<Row> = [
|
||||||
{ title: '姓名', dataIndex: 'name', width: 100 },
|
{ title: '姓名', dataIndex: 'name', width: 100 },
|
||||||
{ title: '手机', dataIndex: 'phone', width: 120 },
|
{ title: '手机', dataIndex: 'phone', width: 120 },
|
||||||
@@ -58,12 +75,12 @@ export default function PartnerAccountsPage() {
|
|||||||
{ 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 },
|
||||||
{
|
{
|
||||||
title: '操作', width: 80,
|
title: '操作', width: 120,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Button type="link" size="small" onClick={async () => {
|
<Space>
|
||||||
setDetail(await request<Detail>(`/admin/partner-accounts/${row.id}`));
|
<Button type="link" size="small" onClick={() => void openAccount(row.id)}>详情</Button>
|
||||||
setDrawerOpen(true);
|
<Button type="link" size="small" onClick={() => void openAccount(row.id)}>编辑</Button>
|
||||||
}}>详情</Button>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -100,27 +117,41 @@ export default function PartnerAccountsPage() {
|
|||||||
</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: 900 }}
|
||||||
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 title="开城合伙人账户" width={720} open={drawerOpen} onClose={() => setDrawerOpen(false)}
|
<Drawer
|
||||||
extra={detail && (
|
title="编辑开城合伙人账户"
|
||||||
<Select defaultValue={detail.status} style={{ width: 100 }}
|
width={720}
|
||||||
options={Object.entries(ACCOUNT_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
|
open={drawerOpen}
|
||||||
onChange={async (status) => {
|
onClose={() => setDrawerOpen(false)}
|
||||||
await request(`/admin/partner-accounts/${detail.id}`, { method: 'PUT', body: JSON.stringify({ status }) });
|
extra={<Button type="primary" onClick={() => void saveAccount()}>保存</Button>}
|
||||||
message.success('已更新');
|
>
|
||||||
void reload();
|
|
||||||
}} />
|
|
||||||
)}>
|
|
||||||
{detail && (
|
{detail && (
|
||||||
<Tabs items={[
|
<Tabs items={[
|
||||||
{
|
{
|
||||||
key: 'info',
|
key: 'info',
|
||||||
label: '基本信息',
|
label: '基本信息',
|
||||||
children: (
|
children: (
|
||||||
<Descriptions column={1} bordered size="small">
|
<Form form={editForm} layout="vertical">
|
||||||
<Descriptions.Item label="姓名">{detail.name}</Descriptions.Item>
|
<Form.Item label="开城合伙人">
|
||||||
<Descriptions.Item label="手机">{detail.phone}</Descriptions.Item>
|
<Input value={detail.partner?.companyName} disabled />
|
||||||
<Descriptions.Item label="开城合伙人">{detail.partner?.companyName}</Descriptions.Item>
|
</Form.Item>
|
||||||
</Descriptions>
|
<Form.Item name="name" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="phone"
|
||||||
|
label="登录手机"
|
||||||
|
rules={[
|
||||||
|
{ required: true },
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input maxLength={11} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="status" label="状态" rules={[{ required: true }]}>
|
||||||
|
<Select options={Object.entries(ACCOUNT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||||||
|
</Form.Item>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
合伙人 H5 登录使用「登录手机」,与开城合伙人主体的「联系电话」可不同。
|
||||||
|
</Typography.Text>
|
||||||
|
</Form>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -155,7 +186,16 @@ export default function PartnerAccountsPage() {
|
|||||||
<Select showSearch optionFilterProp="label" options={partners.map((p) => ({ value: p.id, label: p.companyName }))} />
|
<Select showSearch optionFilterProp="label" options={partners.map((p) => ({ value: p.id, label: p.companyName }))} />
|
||||||
</Form.Item>
|
</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 name="phone" label="手机" rules={[{ required: true }]}><Input /></Form.Item>
|
<Form.Item
|
||||||
|
name="phone"
|
||||||
|
label="登录手机"
|
||||||
|
rules={[
|
||||||
|
{ required: true },
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input maxLength={11} />
|
||||||
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,24 @@ type Row = {
|
|||||||
storeCount: number; accountCount: number; cityCount: number; createdAt: string;
|
storeCount: number; accountCount: number; cityCount: number; createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PartnerDetail = Row & {
|
||||||
|
bankAccountName?: string | null;
|
||||||
|
bankAccountNo?: string | null;
|
||||||
|
bankBranch?: string | null;
|
||||||
|
cities?: Array<{ name: string; code: string; status: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function pickPartnerFormValues(d: PartnerDetail) {
|
||||||
|
return {
|
||||||
|
companyName: d.companyName,
|
||||||
|
contactPhone: d.contactPhone,
|
||||||
|
address: d.address,
|
||||||
|
bankAccountName: d.bankAccountName ?? '',
|
||||||
|
bankAccountNo: d.bankAccountNo ?? '',
|
||||||
|
bankBranch: d.bankBranch ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function PartnersPage() {
|
export default function PartnersPage() {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [editForm] = Form.useForm();
|
const [editForm] = Form.useForm();
|
||||||
@@ -27,10 +45,26 @@ export default function PartnersPage() {
|
|||||||
},
|
},
|
||||||
[filters],
|
[filters],
|
||||||
);
|
);
|
||||||
const [detail, setDetail] = useState<Record<string, unknown> | null>(null);
|
const [detail, setDetail] = useState<PartnerDetail | null>(null);
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
|
||||||
|
async function openPartner(id: string) {
|
||||||
|
const d = await request<PartnerDetail>(`/admin/partners/${id}`);
|
||||||
|
setDetail(d);
|
||||||
|
editForm.setFieldsValue(pickPartnerFormValues(d));
|
||||||
|
setDrawerOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePartner() {
|
||||||
|
if (!detail) return;
|
||||||
|
const v = await editForm.validateFields();
|
||||||
|
await request(`/admin/partners/${detail.id}`, { method: 'PUT', body: JSON.stringify(v) });
|
||||||
|
message.success('已保存');
|
||||||
|
setDrawerOpen(false);
|
||||||
|
void reload();
|
||||||
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<Row> = [
|
const columns: ColumnsType<Row> = [
|
||||||
{ title: '公司名', dataIndex: 'companyName' },
|
{ title: '公司名', dataIndex: 'companyName' },
|
||||||
{ title: '联系电话', dataIndex: 'contactPhone', width: 130 },
|
{ title: '联系电话', dataIndex: 'contactPhone', width: 130 },
|
||||||
@@ -42,15 +76,8 @@ export default function PartnersPage() {
|
|||||||
title: '操作', width: 120,
|
title: '操作', width: 120,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="link" size="small" onClick={async () => {
|
<Button type="link" size="small" onClick={() => void openPartner(row.id)}>详情</Button>
|
||||||
setDetail(await request(`/admin/partners/${row.id}`));
|
<Button type="link" size="small" onClick={() => void openPartner(row.id)}>编辑</Button>
|
||||||
setDrawerOpen(true);
|
|
||||||
}}>详情</Button>
|
|
||||||
<Button type="link" size="small" onClick={() => {
|
|
||||||
editForm.setFieldsValue(row);
|
|
||||||
setDetail(row as unknown as Record<string, unknown>);
|
|
||||||
setDrawerOpen(true);
|
|
||||||
}}>编辑</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -69,30 +96,29 @@ export default function PartnersPage() {
|
|||||||
</Form>
|
</Form>
|
||||||
<Table rowKey="id" className="admin-table-nowrap" loading={loading} columns={columns} dataSource={data?.items ?? []}
|
<Table rowKey="id" className="admin-table-nowrap" loading={loading} columns={columns} dataSource={data?.items ?? []}
|
||||||
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 title="开城合伙人详情" width={560} open={drawerOpen} onClose={() => setDrawerOpen(false)}
|
<Drawer
|
||||||
extra={detail && (
|
title="编辑开城合伙人"
|
||||||
<Button type="primary" onClick={async () => {
|
width={560}
|
||||||
const v = await editForm.validateFields();
|
open={drawerOpen}
|
||||||
await request(`/admin/partners/${detail.id}`, { method: 'PUT', body: JSON.stringify(v) });
|
onClose={() => setDrawerOpen(false)}
|
||||||
message.success('已保存');
|
extra={<Button type="primary" onClick={() => void savePartner()}>保存</Button>}
|
||||||
void reload();
|
>
|
||||||
}}>保存</Button>
|
|
||||||
)}>
|
|
||||||
{detail && (
|
{detail && (
|
||||||
<>
|
<>
|
||||||
{Array.isArray(detail.cities) && (
|
{Array.isArray(detail.cities) && detail.cities.length > 0 && (
|
||||||
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }} title="关联开城城市">
|
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }} title="关联开城城市">
|
||||||
{(detail.cities as Array<{ name: string; code: string; status: string }>).map((c) => (
|
{detail.cities.map((c) => (
|
||||||
<Descriptions.Item key={c.code} label={c.code}>{c.name} ({c.status})</Descriptions.Item>
|
<Descriptions.Item key={c.code} label={c.code}>{c.name} ({c.status})</Descriptions.Item>
|
||||||
))}
|
))}
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
)}
|
)}
|
||||||
<Form form={editForm} layout="vertical" initialValues={detail as Record<string, unknown>}>
|
<Form form={editForm} layout="vertical">
|
||||||
<Form.Item name="companyName" label="公司名" rules={[{ required: true }]}><Input /></Form.Item>
|
<Form.Item name="companyName" label="公司名" rules={[{ required: true }]}><Input /></Form.Item>
|
||||||
<Form.Item name="contactPhone" label="联系电话" rules={[{ required: true }]}><Input /></Form.Item>
|
<Form.Item name="contactPhone" label="联系电话" rules={[{ required: true }]}><Input /></Form.Item>
|
||||||
<Form.Item name="address" label="地址" rules={[{ required: true }]}><Input /></Form.Item>
|
<Form.Item name="address" label="地址" rules={[{ required: true }]}><Input /></Form.Item>
|
||||||
<Form.Item name="bankAccountName" label="户名"><Input /></Form.Item>
|
<Form.Item name="bankAccountName" label="户名"><Input /></Form.Item>
|
||||||
<Form.Item name="bankAccountNo" label="账号"><Input /></Form.Item>
|
<Form.Item name="bankAccountNo" label="账号"><Input /></Form.Item>
|
||||||
|
<Form.Item name="bankBranch" label="开户行"><Input /></Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -141,13 +141,25 @@ export class AdminPartnersService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updatePartnerAccount(id: bigint, dto: UpdatePartnerAccountDto) {
|
async updatePartnerAccount(id: bigint, dto: UpdatePartnerAccountDto) {
|
||||||
const account = await this.prisma.partnerAccount.update({
|
const existing = await this.prisma.partnerAccount.findUnique({ where: { id } });
|
||||||
where: { id },
|
if (!existing) throw new NotFoundException('开城合伙人账号不存在');
|
||||||
data: {
|
|
||||||
...(dto.name !== undefined ? { name: dto.name } : {}),
|
const data: Prisma.PartnerAccountUpdateInput = {};
|
||||||
...(dto.status !== undefined ? { status: dto.status as 'ACTIVE' | 'DISABLED' } : {}),
|
if (dto.name !== undefined) data.name = dto.name;
|
||||||
},
|
if (dto.status !== undefined) data.status = dto.status as 'ACTIVE' | 'DISABLED';
|
||||||
});
|
if (dto.phone !== undefined) {
|
||||||
|
const phone = dto.phone.trim();
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||||
|
throw new BadRequestException('请输入正确的手机号码');
|
||||||
|
}
|
||||||
|
const phoneTaken = await this.prisma.partnerAccount.findUnique({ where: { phone } });
|
||||||
|
if (phoneTaken && phoneTaken.id !== id) {
|
||||||
|
throw new BadRequestException('该手机号已被使用');
|
||||||
|
}
|
||||||
|
data.phone = phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = await this.prisma.partnerAccount.update({ where: { id }, data });
|
||||||
return serializeBigInt(account);
|
return serializeBigInt(account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,10 @@ export class UpdatePartnerAccountDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsIn(['ACTIVE', 'DISABLED'])
|
@IsIn(['ACTIVE', 'DISABLED'])
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user