feat(ops): v4.0.4 开发任务多行与版本状态同步、合伙人/门店删除、账单合伙人列

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-31 16:08:55 +08:00
parent e26206b93c
commit ac94f5f5de
15 changed files with 568 additions and 59 deletions
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Button,
Checkbox,
@@ -7,6 +8,7 @@ import {
Input,
InputNumber,
Modal,
Popconfirm,
Select,
Space,
Table,
@@ -42,6 +44,7 @@ type PartnerRow = {
districtCodes?: string[] | null;
orderCommissionRate?: number;
redeemCommissionRate?: number;
storeCount?: number;
accountCount: number;
createdAt: string;
};
@@ -121,6 +124,7 @@ export default function CityPartnersPanel({
maxPartnerCommissionRate = 0.05,
onChanged,
}: Props) {
const navigate = useNavigate();
const [editForm] = Form.useForm();
const [createForm] = Form.useForm();
const [subForm] = Form.useForm();
@@ -204,6 +208,31 @@ export default function CityPartnersPanel({
}
}
async function deletePartner(row: { id: string; storeCount?: number }) {
if ((row.storeCount ?? 0) > 0) {
Modal.warning({
title: '请先删除门店',
content: `该城市合伙人下还有 ${row.storeCount} 家门店,请先删除门店后再删除合伙人。`,
okText: '查看门店',
onOk: () => navigate(`/stores?partnerId=${row.id}`),
});
return;
}
try {
await request(`/admin/partners/${row.id}`, { method: 'DELETE' });
message.success('城市合伙人已删除');
if (detail?.id === row.id) {
setDrawerOpen(false);
setDetail(null);
}
void loadPartners();
onChanged?.();
} catch (e) {
const msg = formatApiError(e);
if (msg) message.error(msg);
}
}
async function deleteSubAccount(subId: string) {
await request(`/admin/partner-accounts/${subId}`, { method: 'DELETE' });
message.success('子账号已删除');
@@ -237,11 +266,24 @@ export default function CityPartnersPanel({
{ title: '创建', dataIndex: 'createdAt', width: 150, render: fmtTime },
{
title: '操作',
width: 80,
width: 140,
render: (_, row) => (
<Button type="link" size="small" onClick={() => void openPartner(row.id)}>
</Button>
<Space size={0}>
<Button type="link" size="small" onClick={() => void openPartner(row.id)}>
</Button>
<Popconfirm
title="确认删除该城市合伙人?"
description="若有门店需先删除门店;账号下有订单或核销单将禁止删除。"
okText="删除"
okButtonProps={{ danger: true }}
onConfirm={() => void deletePartner(row)}
>
<Button type="link" size="small" danger>
</Button>
</Popconfirm>
</Space>
),
},
];
@@ -272,7 +314,22 @@ export default function CityPartnersPanel({
width={640}
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
extra={<Button type="primary" onClick={() => void savePartner()}></Button>}
extra={
<Space>
{detail ? (
<Popconfirm
title="确认删除该城市合伙人?"
description="若有门店需先删除门店;账号下有订单或核销单将禁止删除。"
okText="删除"
okButtonProps={{ danger: true }}
onConfirm={() => void deletePartner(detail)}
>
<Button danger></Button>
</Popconfirm>
) : null}
<Button type="primary" onClick={() => void savePartner()}></Button>
</Space>
}
>
{detail && (
<Tabs
+59 -8
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import {
Alert,
Button,
@@ -127,6 +127,7 @@ function formatApiError(err: unknown): string | null {
}
export default function CityPartnersPage() {
const navigate = useNavigate();
const [filterForm] = Form.useForm();
const [editForm] = Form.useForm();
const [createForm] = Form.useForm();
@@ -234,6 +235,30 @@ export default function CityPartnersPage() {
void reload();
}
async function deletePartner(row: { id: string; storeCount?: number }) {
if ((row.storeCount ?? 0) > 0) {
Modal.warning({
title: '请先删除门店',
content: `该城市合伙人下还有 ${row.storeCount} 家门店,请先删除门店后再删除合伙人。`,
okText: '查看门店',
onOk: () => navigate(`/stores?partnerId=${row.id}`),
});
return;
}
try {
await request(`/admin/partners/${row.id}`, { method: 'DELETE' });
message.success('城市合伙人已删除');
if (detail?.id === row.id) {
setDrawerOpen(false);
setDetail(null);
}
void reload();
} catch (e) {
const msg = formatApiError(e);
if (msg) message.error(msg);
}
}
async function deleteSubAccount(subId: string, parentId: string) {
await request(`/admin/partner-accounts/${subId}`, { method: 'DELETE' });
message.success('子账号已删除');
@@ -352,12 +377,25 @@ export default function CityPartnersPage() {
{ title: '创建', dataIndex: 'createdAt', width: 150, render: fmtTime },
{
title: '操作',
width: 80,
width: 140,
fixed: 'right',
render: (_, row) => (
<Button type="link" size="small" onClick={() => void openPartner(row.id)}>
</Button>
<Space size={0}>
<Button type="link" size="small" onClick={() => void openPartner(row.id)}>
</Button>
<Popconfirm
title="确认删除该城市合伙人?"
description="若有门店需先删除门店;账号下有订单或核销单将禁止删除。"
okText="删除"
okButtonProps={{ danger: true }}
onConfirm={() => void deletePartner(row)}
>
<Button type="link" size="small" danger>
</Button>
</Popconfirm>
</Space>
),
},
];
@@ -491,9 +529,22 @@ export default function CityPartnersPage() {
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
extra={
<Button type="primary" onClick={() => void savePartner()}>
</Button>
<Space>
{detail ? (
<Popconfirm
title="确认删除该城市合伙人?"
description="若有门店需先删除门店;账号下有订单或核销单将禁止删除。"
okText="删除"
okButtonProps={{ danger: true }}
onConfirm={() => void deletePartner(detail)}
>
<Button danger></Button>
</Popconfirm>
) : null}
<Button type="primary" onClick={() => void savePartner()}>
</Button>
</Space>
}
>
{detail && (
@@ -274,7 +274,14 @@ export default function DevPlanTasksPage() {
<Tag color={STATUS_COLOR[s]}>{DEV_PLAN_TASK_STATUS_LABELS[s]}</Tag>
),
},
{ title: '内容', dataIndex: 'content' },
{
title: '内容',
dataIndex: 'content',
width: 360,
render: (v: string) => (
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxWidth: 480 }}>{v}</div>
),
},
{
title: '附件',
width: 100,
+19 -5
View File
@@ -57,6 +57,7 @@ type Row = {
redeemItems?: BillItemRow[];
partner?: {
companyName?: string;
name?: string;
phone?: string;
bankAccountName?: string | null;
bankAccountNo?: string | null;
@@ -64,6 +65,7 @@ type Row = {
};
partnerAccount?: {
companyName?: string;
name?: string;
phone?: string;
bankAccountName?: string | null;
bankAccountNo?: string | null;
@@ -71,7 +73,7 @@ type Row = {
};
};
type PartnerOption = { id: string; companyName: string; phone?: string };
type PartnerOption = { id: string; companyName?: string | null; name?: string | null; phone?: string };
const STATUS_LABELS: Record<string, string> = {
PENDING_REVIEW: '待审核',
@@ -89,6 +91,18 @@ const STATUS_COLORS: Record<string, string> = {
REJECTED: 'red',
};
function partnerDashLabel(p?: {
companyName?: string | null;
name?: string | null;
phone?: string | null;
} | null): string {
if (!p) return '—';
const company = (p.companyName || '').trim();
const person = (p.name || '').trim();
if (company && person) return `${company}-${person}`;
return company || person || (p.phone || '').trim() || '—';
}
export default function PartnerBillsPage() {
const [form] = Form.useForm();
const [filters, setFilters] = useState<Record<string, string>>({});
@@ -257,7 +271,7 @@ export default function PartnerBillsPage() {
}
const summary = data?.summary;
const partnerName = (r: Row) => r.partner?.companyName || r.partnerAccount?.companyName || '—';
const partnerName = (r: Row) => partnerDashLabel(r.partnerAccount ?? r.partner);
const partnerBank = (r: Row) => r.partnerAccount ?? r.partner;
const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id));
const canSend = selectedRows.filter((r) => r.status === 'PENDING_REVIEW' || r.status === 'REJECTED');
@@ -275,7 +289,7 @@ export default function PartnerBillsPage() {
},
{
title: '合伙人',
width: 160,
width: 200,
render: (_, r) => partnerName(r),
},
{
@@ -406,7 +420,7 @@ export default function PartnerBillsPage() {
optionFilterProp="label"
options={partners.map((p) => ({
value: p.id,
label: p.companyName || p.phone || p.id,
label: partnerDashLabel(p),
}))}
/>
</Form.Item>
@@ -503,7 +517,7 @@ export default function PartnerBillsPage() {
placeholder="全部主合伙人"
options={partners.map((p) => ({
value: p.id,
label: p.companyName || p.phone || p.id,
label: partnerDashLabel(p),
}))}
/>
</Form.Item>
+42 -1
View File
@@ -12,6 +12,7 @@ import {
Input,
InputNumber,
Modal,
Popconfirm,
Select,
Space,
Steps,
@@ -302,6 +303,7 @@ export default function StoresPage() {
const [categoryTree, setCategoryTree] = useState<CategoryNode[]>([]);
const [optionsLoading, setOptionsLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState(false);
const [phoneMismatch, setPhoneMismatch] = useState<string | null>(null);
const deepLinkStoreOpenedRef = useRef(false);
@@ -372,6 +374,23 @@ export default function StoresPage() {
.map((n) => ({ value: n.id, label: n.name }));
}, [categoryTree, editCategoryParentId]);
async function deleteStore(id: string) {
setDeleting(true);
try {
await request(`/admin/stores/${id}`, { method: 'DELETE' });
message.success('门店及门店账号已删除');
if (detail && String(detail.id) === id) {
setDrawerOpen(false);
setDetail(null);
}
void reload();
} catch (e) {
message.error(e instanceof Error ? e.message : '删除失败');
} finally {
setDeleting(false);
}
}
async function openStoreDetail(row: StoreRow, opts?: { tab?: string }) {
const [d, cats] = await Promise.all([
request<Record<string, unknown>>(`/admin/stores/${row.id}`),
@@ -821,7 +840,7 @@ export default function StoresPage() {
{
key: 'actions',
title: '操作',
width: 280,
width: 320,
fixed: 'right',
render: (_, row) => (
<Space size={0} wrap>
@@ -861,6 +880,17 @@ export default function StoresPage() {
>
</Button>
<Popconfirm
title="确认删除该门店?"
description="将同时删除门店账号;若有关联订单或核销单将禁止删除。"
okText="删除"
okButtonProps={{ danger: true }}
onConfirm={() => void deleteStore(row.id)}
>
<Button type="link" size="small" danger>
</Button>
</Popconfirm>
</Space>
),
},
@@ -1034,6 +1064,17 @@ export default function StoresPage() {
void reload();
}} />
<Button type="primary" loading={saving} onClick={() => void saveStoreDetail()}></Button>
{detail?.id ? (
<Popconfirm
title="确认删除该门店?"
description="将同时删除门店账号;若有关联订单或核销单将禁止删除。"
okText="删除"
okButtonProps={{ danger: true }}
onConfirm={() => void deleteStore(String(detail.id))}
>
<Button danger loading={deleting}></Button>
</Popconfirm>
) : null}
</Space>
)}>
{detail && (