@@ -23,6 +23,11 @@ export default function RedeemRecordDetailDescriptions({ detail, maskUserPhone =
|
||||
cityName?: string;
|
||||
address?: string;
|
||||
partnerAccount?: { companyName?: string };
|
||||
primaryAccount?: {
|
||||
bankAccountName?: string | null;
|
||||
bankAccountNo?: string | null;
|
||||
bankBranch?: string | null;
|
||||
} | null;
|
||||
}
|
||||
| undefined;
|
||||
const coupon = detail.coupon as { couponNo?: string; order?: { orderNo?: string } } | undefined;
|
||||
@@ -46,6 +51,15 @@ export default function RedeemRecordDetailDescriptions({ detail, maskUserPhone =
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="门店地址">{store?.address ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="合伙人">{store?.partnerAccount?.companyName ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="收款户名">
|
||||
{store?.primaryAccount?.bankAccountName || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="收款账号">
|
||||
{store?.primaryAccount?.bankAccountNo || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="开户行">
|
||||
{store?.primaryAccount?.bankBranch || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="权益券号">{coupon?.couponNo ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="关联订单">{coupon?.order?.orderNo ?? '—'}</Descriptions.Item>
|
||||
{Array.isArray(detail.allocations) && (detail.allocations as unknown[]).length > 0 ? (
|
||||
|
||||
@@ -174,11 +174,13 @@ function PackageDetailCard({
|
||||
pkg,
|
||||
change,
|
||||
changes,
|
||||
imageSize = 72,
|
||||
}: {
|
||||
title?: string;
|
||||
pkg: StorePackageItemDto | StorePackageViewDto;
|
||||
change?: keyof typeof CHANGE_LABELS;
|
||||
changes?: FieldChange[];
|
||||
imageSize?: number;
|
||||
}) {
|
||||
const images = normalizeStorePackageImageUrls(pkg);
|
||||
const meta = change ? CHANGE_LABELS[change] : null;
|
||||
@@ -217,7 +219,7 @@ function PackageDetailCard({
|
||||
<Image.PreviewGroup>
|
||||
<Space wrap size={8}>
|
||||
{images.map((url) => (
|
||||
<Image key={url} src={url} width={72} height={72} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||
<Image key={url} src={url} width={imageSize} height={imageSize} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||
))}
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
@@ -265,6 +267,7 @@ export type StorePackageAuditPanelProps = {
|
||||
requestId: string;
|
||||
/** 是否在面板顶部显示通过/驳回(抽屉 extra 另有按钮时可关) */
|
||||
showActions?: boolean;
|
||||
fullscreen?: boolean;
|
||||
onAudited?: () => void;
|
||||
onDetailLoaded?: (detail: StorePackageAuditDetailDto | null) => void;
|
||||
};
|
||||
@@ -273,6 +276,7 @@ export type StorePackageAuditPanelProps = {
|
||||
export default function StorePackageAuditPanel({
|
||||
requestId,
|
||||
showActions = true,
|
||||
fullscreen = false,
|
||||
onAudited,
|
||||
onDetailLoaded,
|
||||
}: StorePackageAuditPanelProps) {
|
||||
@@ -389,6 +393,7 @@ export default function StorePackageAuditPanel({
|
||||
title={`套餐 ${index + 1}`}
|
||||
pkg={pkg}
|
||||
change={changeByKey.get(packageKey(pkg, index))}
|
||||
imageSize={fullscreen ? 160 : 72}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
@@ -407,6 +412,7 @@ export default function StorePackageAuditPanel({
|
||||
pkg={pkg}
|
||||
change={changeByKey.get(packageKey(pkg, index))}
|
||||
changes={changesByKey.get(packageKey(pkg, index))}
|
||||
imageSize={fullscreen ? 160 : 72}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -196,10 +196,10 @@ function menuAllowed(key: string, permissionKeys: string[]): boolean {
|
||||
'/benefit/ledgers': 'benefit',
|
||||
'/redeem-records': 'benefit',
|
||||
'/redeem-pending': 'benefit',
|
||||
'/redeem/debug': 'benefit',
|
||||
'/redeem/debug': 'benefit_debug',
|
||||
'deliveries-group': 'deliveries',
|
||||
'/deliveries': 'deliveries',
|
||||
'/deliveries/xiaofeixia': 'deliveries',
|
||||
'/deliveries/xiaofeixia': 'deliveries_debug',
|
||||
'/tickets': 'tickets',
|
||||
'/tickets/support': 'tech_support',
|
||||
'dev-plan-group': 'dev_plan',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
DEV_PLAN_TASK_TYPE_LABELS,
|
||||
SUPPORT_TICKET_STATUS_LABELS,
|
||||
type DevPlanTaskDto,
|
||||
type DevPlanTaskExportFormat,
|
||||
type DevPlanTaskStatusDto,
|
||||
type DevPlanTaskTypeDto,
|
||||
type DevPlanVersionDto,
|
||||
@@ -25,7 +27,9 @@ import {
|
||||
} from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
import { downloadBase64File } from '../lib/exportExcel';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
import OssUpload from '../components/OssUpload';
|
||||
|
||||
const TYPE_OPTIONS = (Object.keys(DEV_PLAN_TASK_TYPE_LABELS) as DevPlanTaskTypeDto[]).map((v) => ({
|
||||
value: v,
|
||||
@@ -56,6 +60,8 @@ export default function DevPlanTasksPage() {
|
||||
const [editing, setEditing] = useState<DevPlanTaskDto | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [dispatching, setDispatching] = useState(false);
|
||||
const [exportFormat, setExportFormat] = useState<DevPlanTaskExportFormat>('xlsx');
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [dispatchForm] = Form.useForm<{ supplement?: string }>();
|
||||
const [batchForm] = Form.useForm<{ status?: DevPlanTaskStatusDto; versionId?: string }>();
|
||||
@@ -88,24 +94,30 @@ export default function DevPlanTasksPage() {
|
||||
|
||||
function openCreate() {
|
||||
setEditing(null);
|
||||
form.setFieldsValue({ content: '', type: 'BUG', status: 'TODO', supportTicketId: undefined });
|
||||
form.setFieldsValue({ content: '', type: 'BUG', status: 'TODO', supportTicketId: undefined, attachmentUrls: [''] });
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
function openEdit(row: DevPlanTaskDto) {
|
||||
setEditing(row);
|
||||
form.setFieldsValue({ content: row.content, type: row.type, status: row.status });
|
||||
form.setFieldsValue({
|
||||
content: row.content,
|
||||
type: row.type,
|
||||
status: row.status,
|
||||
attachmentUrls: row.attachmentUrls?.length ? row.attachmentUrls : [''],
|
||||
});
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const values = await form.validateFields();
|
||||
const attachmentUrls = (values.attachmentUrls ?? []).map((u: string) => u?.trim()).filter(Boolean);
|
||||
setSaving(true);
|
||||
try {
|
||||
if (editing) {
|
||||
await request(`/admin/dev-plan/tasks/${editing.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(values),
|
||||
body: JSON.stringify({ ...values, attachmentUrls }),
|
||||
});
|
||||
message.success('已更新');
|
||||
} else {
|
||||
@@ -115,6 +127,7 @@ export default function DevPlanTasksPage() {
|
||||
content: values.content,
|
||||
type: values.type,
|
||||
supportTicketId: values.supportTicketId || undefined,
|
||||
attachmentUrls,
|
||||
}),
|
||||
});
|
||||
message.success('已创建');
|
||||
@@ -199,6 +212,34 @@ export default function DevPlanTasksPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function exportTasks(scope: 'filter' | 'selected') {
|
||||
setExporting(true);
|
||||
try {
|
||||
const result = await request<{
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
contentBase64: string;
|
||||
count: number;
|
||||
}>('/admin/dev-plan/tasks/export', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
scope,
|
||||
format: exportFormat,
|
||||
ids: scope === 'selected' ? selectedRowKeys : undefined,
|
||||
status: filters.status || undefined,
|
||||
type: filters.type || undefined,
|
||||
keyword: filters.keyword || undefined,
|
||||
}),
|
||||
});
|
||||
downloadBase64File(result.contentBase64, result.filename, result.mimeType);
|
||||
message.success(`已导出 ${result.count} 条任务`);
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '导出失败');
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<DevPlanTaskDto> = [
|
||||
{ title: '任务号', dataIndex: 'taskNo', width: 160 },
|
||||
{
|
||||
@@ -216,6 +257,22 @@ export default function DevPlanTasksPage() {
|
||||
),
|
||||
},
|
||||
{ title: '内容', dataIndex: 'content', ellipsis: true },
|
||||
{
|
||||
title: '附件',
|
||||
width: 100,
|
||||
render: (_, row) =>
|
||||
row.attachmentUrls?.length ? (
|
||||
<Image.PreviewGroup>
|
||||
<Space size={4}>
|
||||
{row.attachmentUrls.slice(0, 3).map((url) => (
|
||||
<Image key={url} src={url} width={32} height={32} style={{ objectFit: 'cover' }} />
|
||||
))}
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
) : (
|
||||
'—'
|
||||
),
|
||||
},
|
||||
{ title: '来源工单', dataIndex: 'supportTicketNo', width: 140, render: (v) => v || '—' },
|
||||
{ title: '创建人', dataIndex: 'creatorName', width: 90 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||
@@ -243,7 +300,24 @@ export default function DevPlanTasksPage() {
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
开发计划 · 任务列表
|
||||
</Typography.Title>
|
||||
<Space>
|
||||
<Space wrap>
|
||||
<Select
|
||||
value={exportFormat}
|
||||
style={{ width: 120 }}
|
||||
onChange={setExportFormat}
|
||||
options={[
|
||||
{ value: 'markdown', label: 'Markdown' },
|
||||
{ value: 'docx', label: 'Word' },
|
||||
{ value: 'xlsx', label: 'Excel' },
|
||||
{ value: 'pdf', label: 'PDF' },
|
||||
]}
|
||||
/>
|
||||
<Button loading={exporting} disabled={!selectedRowKeys.length} onClick={() => void exportTasks('selected')}>
|
||||
导出已勾选
|
||||
</Button>
|
||||
<Button loading={exporting} onClick={() => void exportTasks('filter')}>
|
||||
导出全部筛选
|
||||
</Button>
|
||||
<Button disabled={!selectedRowKeys.length} onClick={openBatchEdit}>
|
||||
批量编辑
|
||||
</Button>
|
||||
@@ -333,6 +407,29 @@ export default function DevPlanTasksPage() {
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item label="附件图片">
|
||||
<Form.List name="attachmentUrls">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Space key={field.key} align="start" style={{ display: 'flex', marginBottom: 8 }}>
|
||||
<Form.Item {...field} style={{ flex: 1, marginBottom: 0 }}>
|
||||
<OssUpload bizType="SUPPORT_TICKET_ATTACHMENT" mediaType="IMAGE" />
|
||||
</Form.Item>
|
||||
{fields.length > 1 ? (
|
||||
<Button type="link" danger onClick={() => remove(field.name)}>
|
||||
删除
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add('')} block>
|
||||
添加图片
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -44,6 +44,13 @@ type BillRow = {
|
||||
settlementMethod: string;
|
||||
status: string;
|
||||
paidAt?: string | null;
|
||||
paymentRef?: string | null;
|
||||
fulfillmentProvider?: {
|
||||
bankAccountName?: string | null;
|
||||
bankName?: string | null;
|
||||
bankBranch?: string | null;
|
||||
bankAccountNo?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
type BillItem = {
|
||||
@@ -140,14 +147,30 @@ export default function LogisticsBillsPage() {
|
||||
}, [summaryPeriod]);
|
||||
|
||||
function confirmPay(ids: string[], amountHint?: number) {
|
||||
let paymentRef = '';
|
||||
Modal.confirm({
|
||||
title: '确认结算?',
|
||||
content: `将确认 ${ids.length} 笔物流对账单${amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。充值模式将扣减余额,挂账模式标记已打款。`,
|
||||
content: (
|
||||
<div>
|
||||
<div>
|
||||
将确认 {ids.length} 笔物流对账单
|
||||
{amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。充值模式将扣减余额,挂账模式标记已打款。
|
||||
</div>
|
||||
<Input
|
||||
placeholder="打款凭证号(可选)"
|
||||
style={{ marginTop: 8 }}
|
||||
onChange={(e) => {
|
||||
paymentRef = e.target.value;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
okText: '确认结算',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const body = JSON.stringify({ paymentRef: paymentRef.trim() || undefined });
|
||||
if (ids.length === 1) {
|
||||
await request(`/admin/logistics-bills/${ids[0]}/confirm`, { method: 'POST' });
|
||||
await request(`/admin/logistics-bills/${ids[0]}/confirm`, { method: 'POST', body });
|
||||
} else {
|
||||
await request('/admin/logistics-bills/batch-confirm', {
|
||||
method: 'POST',
|
||||
@@ -217,6 +240,7 @@ export default function LogisticsBillsPage() {
|
||||
}
|
||||
|
||||
const summary = data?.summary;
|
||||
const providerBank = (r: BillRow) => r.fulfillmentProvider;
|
||||
const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id));
|
||||
const selectedAmount = selectedRows.reduce((s, r) => s + Number(r.logisticsAmount), 0);
|
||||
|
||||
@@ -248,6 +272,18 @@ export default function LogisticsBillsPage() {
|
||||
width: 110,
|
||||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '收款户名',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
render: (_, r) => providerBank(r)?.bankAccountName || '—',
|
||||
},
|
||||
{
|
||||
title: '收款账号',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
render: (_, r) => providerBank(r)?.bankAccountNo || '—',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
@@ -550,7 +586,31 @@ export default function LogisticsBillsPage() {
|
||||
<Descriptions.Item label="结算时间">
|
||||
{detail.paidAt ? fmtTime(detail.paidAt) : '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="打款凭证">
|
||||
{detail.paymentRef ? String(detail.paymentRef) : '—'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{providerBank(detail) ? (
|
||||
<>
|
||||
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||
收款账户
|
||||
</Typography.Title>
|
||||
<Descriptions column={1} size="small" bordered style={{ marginBottom: 16 }}>
|
||||
<Descriptions.Item label="户名">
|
||||
{providerBank(detail)?.bankAccountName || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="开户银行">
|
||||
{providerBank(detail)?.bankName || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="开户支行">
|
||||
{providerBank(detail)?.bankBranch || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="账号">
|
||||
{providerBank(detail)?.bankAccountNo || '—'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</>
|
||||
) : null}
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
DatePicker,
|
||||
Descriptions,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
@@ -31,8 +33,22 @@ type Row = {
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
rejectReason?: string | null;
|
||||
partner?: { companyName?: string; phone?: string };
|
||||
partnerAccount?: { companyName?: string; phone?: string };
|
||||
paymentRef?: string | null;
|
||||
paidAt?: string | null;
|
||||
partner?: {
|
||||
companyName?: string;
|
||||
phone?: string;
|
||||
bankAccountName?: string | null;
|
||||
bankAccountNo?: string | null;
|
||||
bankBranch?: string | null;
|
||||
};
|
||||
partnerAccount?: {
|
||||
companyName?: string;
|
||||
phone?: string;
|
||||
bankAccountName?: string | null;
|
||||
bankAccountNo?: string | null;
|
||||
bankBranch?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
type PartnerOption = { id: string; companyName: string; phone?: string };
|
||||
@@ -77,6 +93,8 @@ export default function PartnerBillsPage() {
|
||||
const [rejectForm] = Form.useForm();
|
||||
const [rejecting, setRejecting] = useState(false);
|
||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||
const [detail, setDetail] = useState<Row | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
void request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
||||
@@ -127,17 +145,30 @@ export default function PartnerBillsPage() {
|
||||
}
|
||||
|
||||
function markPaid(ids: string[], amountHint?: number) {
|
||||
let paymentRef = '';
|
||||
Modal.confirm({
|
||||
title: '确认打款?',
|
||||
content: `将标记 ${ids.length} 笔账单为已打款${amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。`,
|
||||
content: (
|
||||
<div>
|
||||
<div>
|
||||
将标记 {ids.length} 笔账单为已打款
|
||||
{amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。
|
||||
</div>
|
||||
<Input
|
||||
placeholder="打款凭证号(可选)"
|
||||
style={{ marginTop: 8 }}
|
||||
onChange={(e) => {
|
||||
paymentRef = e.target.value;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
okText: '确认打款',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const body = JSON.stringify({ paymentRef: paymentRef.trim() || undefined });
|
||||
if (ids.length === 1) {
|
||||
await request(`/admin/partner-bills/${ids[0]}/mark-paid`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ paymentRef: `PAY-${Date.now()}` }),
|
||||
});
|
||||
await request(`/admin/partner-bills/${ids[0]}/mark-paid`, { method: 'POST', body });
|
||||
} else {
|
||||
await request('/admin/partner-bills/batch-mark-paid', {
|
||||
method: 'POST',
|
||||
@@ -182,6 +213,12 @@ export default function PartnerBillsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openDetail(id: string) {
|
||||
const d = await request<Row>(`/admin/partner-bills/${id}`);
|
||||
setDetail(d);
|
||||
setDrawerOpen(true);
|
||||
}
|
||||
|
||||
async function exportExcel() {
|
||||
setExporting(true);
|
||||
try {
|
||||
@@ -201,6 +238,7 @@ export default function PartnerBillsPage() {
|
||||
|
||||
const summary = data?.summary;
|
||||
const partnerName = (r: Row) => r.partner?.companyName || r.partnerAccount?.companyName || '—';
|
||||
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');
|
||||
const canPay = selectedRows.filter((r) => r.status === 'UNPAID');
|
||||
@@ -238,6 +276,18 @@ export default function PartnerBillsPage() {
|
||||
width: 110,
|
||||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '收款户名',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
render: (_, r) => partnerBank(r)?.bankAccountName || '—',
|
||||
},
|
||||
{
|
||||
title: '收款账号',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
render: (_, r) => partnerBank(r)?.bankAccountNo || '—',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
@@ -259,6 +309,9 @@ export default function PartnerBillsPage() {
|
||||
fixed: 'right',
|
||||
render: (_, row) => (
|
||||
<Space size={0} wrap>
|
||||
<Button type="link" size="small" onClick={() => void openDetail(row.id)}>
|
||||
明细
|
||||
</Button>
|
||||
{(row.status === 'PENDING_REVIEW' || row.status === 'REJECTED') && (
|
||||
<Button type="link" size="small" onClick={() => sendBills([row.id])}>
|
||||
发送
|
||||
@@ -465,6 +518,59 @@ export default function PartnerBillsPage() {
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Drawer
|
||||
title="合伙人账单明细"
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
width={560}
|
||||
>
|
||||
{detail && (
|
||||
<>
|
||||
<Descriptions column={1} size="small" bordered>
|
||||
<Descriptions.Item label="账单号">{detail.billNo}</Descriptions.Item>
|
||||
<Descriptions.Item label="合伙人">{partnerName(detail)}</Descriptions.Item>
|
||||
<Descriptions.Item label="账期">
|
||||
{String(detail.periodStart).slice(0, 10)} ~ {String(detail.periodEnd).slice(0, 10)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="订单佣金">
|
||||
¥{Number(detail.orderCommission).toFixed(2)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="核销佣金">
|
||||
¥{Number(detail.redeemCommission).toFixed(2)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="应付合计">
|
||||
¥{Number(detail.totalAmount).toFixed(2)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag color={STATUS_COLORS[detail.status] || 'default'}>
|
||||
{STATUS_LABELS[detail.status] || detail.status}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="打款时间">
|
||||
{detail.paidAt ? fmtTime(detail.paidAt) : '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="打款凭证">
|
||||
{detail.paymentRef ? String(detail.paymentRef) : '—'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||
收款账户
|
||||
</Typography.Title>
|
||||
<Descriptions column={1} size="small" bordered>
|
||||
<Descriptions.Item label="户名">
|
||||
{partnerBank(detail)?.bankAccountName || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="账号">
|
||||
{partnerBank(detail)?.bankAccountNo || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="开户行">
|
||||
{partnerBank(detail)?.bankBranch || '—'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,11 @@ type Row = {
|
||||
settlementRate?: number | null;
|
||||
payoutCount?: number | null;
|
||||
store?: { id: string; name: string; cityName: string; phone?: string | null };
|
||||
bankAccount?: {
|
||||
bankAccountName?: string | null;
|
||||
bankAccountNo?: string | null;
|
||||
bankBranch?: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type StoreOption = { id: string; name: string; phone: string };
|
||||
@@ -55,6 +60,11 @@ const STATUS_COLORS: Record<string, string> = {
|
||||
REJECTED: 'red',
|
||||
};
|
||||
|
||||
function settlementStatusColor(kind: StoreSettlementKind, status: string) {
|
||||
if (status === 'PAID') return kind === 'WITHDRAW' ? 'cyan' : 'green';
|
||||
return STATUS_COLORS[status] || 'default';
|
||||
}
|
||||
|
||||
const KIND_COLORS: Record<StoreSettlementKind, string> = {
|
||||
T1_BILL: 'blue',
|
||||
WITHDRAW: 'purple',
|
||||
@@ -118,14 +128,30 @@ export default function StoreBillsPage() {
|
||||
}, []);
|
||||
|
||||
function confirmPay(ids: string[], amountHint?: number) {
|
||||
let paymentRef = '';
|
||||
Modal.confirm({
|
||||
title: '确认打款?',
|
||||
content: `将确认 ${ids.length} 笔 T+1 门店对账单${amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。此操作不可撤销。`,
|
||||
content: (
|
||||
<div>
|
||||
<div>
|
||||
将确认 {ids.length} 笔 T+1 门店对账单
|
||||
{amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。此操作不可撤销。
|
||||
</div>
|
||||
<Input
|
||||
placeholder="打款凭证号(可选)"
|
||||
style={{ marginTop: 8 }}
|
||||
onChange={(e) => {
|
||||
paymentRef = e.target.value;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
okText: '确认打款',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const body = JSON.stringify({ paymentRef: paymentRef.trim() || undefined });
|
||||
if (ids.length === 1) {
|
||||
await request(`/admin/store-bills/${ids[0]}/confirm`, { method: 'POST' });
|
||||
await request(`/admin/store-bills/${ids[0]}/confirm`, { method: 'POST', body });
|
||||
} else {
|
||||
setBatchLoading(true);
|
||||
try {
|
||||
@@ -247,7 +273,9 @@ export default function StoreBillsPage() {
|
||||
dataIndex: 'kind',
|
||||
width: 100,
|
||||
render: (k: StoreSettlementKind) => (
|
||||
<Tag color={KIND_COLORS[k]}>{STORE_SETTLEMENT_KIND_LABELS[k] ?? k}</Tag>
|
||||
<Tag color={KIND_COLORS[k]} style={{ minWidth: 72, textAlign: 'center' }}>
|
||||
{STORE_SETTLEMENT_KIND_LABELS[k] ?? k}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -271,6 +299,20 @@ export default function StoreBillsPage() {
|
||||
{ title: '门店', dataIndex: ['store', 'name'], width: 140, ellipsis: true },
|
||||
{ title: '登录手机', dataIndex: ['store', 'phone'], width: 120, render: (v) => v || '—' },
|
||||
{ title: '城市', dataIndex: ['store', 'cityName'], width: 90 },
|
||||
{
|
||||
title: '收款户名',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
render: (_, row) =>
|
||||
row.kind === 'T1_BILL' ? row.bankAccount?.bankAccountName || '—' : '—',
|
||||
},
|
||||
{
|
||||
title: '收款账号',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
render: (_, row) =>
|
||||
row.kind === 'T1_BILL' ? row.bankAccount?.bankAccountNo || '—' : '—',
|
||||
},
|
||||
{
|
||||
title: '笔数',
|
||||
width: 80,
|
||||
@@ -288,7 +330,9 @@ export default function StoreBillsPage() {
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: (s: string, row) => (
|
||||
<Tag color={STATUS_COLORS[s] || 'default'}>{storeSettlementStatusLabel(row.kind, s)}</Tag>
|
||||
<Tag color={settlementStatusColor(row.kind, s)}>
|
||||
{storeSettlementStatusLabel(row.kind, s)}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -339,7 +383,8 @@ export default function StoreBillsPage() {
|
||||
门店账单
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
含 T+1 自动出账与门店手动提现;按类型筛选,详情与审核动作沿用原接口
|
||||
含 T+1 自动出账与门店手动提现;按类型筛选,详情与审核动作沿用原接口。
|
||||
「已打款」= T+1 出账终态,「已结算」= 手动提现终态。
|
||||
</Typography.Text>
|
||||
{overdueSummary && overdueSummary.overdueCount > 0 ? (
|
||||
<Typography.Text type="danger">
|
||||
@@ -503,7 +548,32 @@ export default function StoreBillsPage() {
|
||||
<Descriptions.Item label="打款时间">
|
||||
{detail.paidAt ? fmtTime(String(detail.paidAt)) : '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="打款凭证">
|
||||
{detail.paymentRef ? String(detail.paymentRef) : '—'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{(detail.storeAccount as {
|
||||
bankAccountName?: string;
|
||||
bankAccountNo?: string;
|
||||
bankBranch?: string;
|
||||
} | null) ? (
|
||||
<>
|
||||
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||
收款账户
|
||||
</Typography.Title>
|
||||
<Descriptions column={1} size="small" bordered>
|
||||
<Descriptions.Item label="户名">
|
||||
{(detail.storeAccount as { bankAccountName?: string }).bankAccountName || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="账号">
|
||||
{(detail.storeAccount as { bankAccountNo?: string }).bankAccountNo || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="开户行">
|
||||
{(detail.storeAccount as { bankBranch?: string }).bankBranch || '—'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</>
|
||||
) : null}
|
||||
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||
核销明细
|
||||
</Typography.Title>
|
||||
|
||||
@@ -55,10 +55,12 @@ function InfoChangeImageDiff({
|
||||
field,
|
||||
live,
|
||||
proposed,
|
||||
imageSize = 72,
|
||||
}: {
|
||||
field: string;
|
||||
live: unknown;
|
||||
proposed: unknown;
|
||||
imageSize?: number;
|
||||
}) {
|
||||
const liveUrls =
|
||||
field === 'coverUrl'
|
||||
@@ -83,7 +85,7 @@ function InfoChangeImageDiff({
|
||||
<Image.PreviewGroup>
|
||||
<Space wrap size={8}>
|
||||
{liveUrls.map((url) => (
|
||||
<Image key={`live-${url}`} src={url} width={72} height={72} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||
<Image key={`live-${url}`} src={url} width={imageSize} height={imageSize} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||
))}
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
@@ -99,7 +101,7 @@ function InfoChangeImageDiff({
|
||||
<Image.PreviewGroup>
|
||||
<Space wrap size={8}>
|
||||
{proposedUrls.map((url) => (
|
||||
<Image key={`new-${url}`} src={url} width={72} height={72} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||
<Image key={`new-${url}`} src={url} width={imageSize} height={imageSize} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||
))}
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
@@ -128,6 +130,7 @@ function InfoChangeAuditPanel({
|
||||
const [rejectOpen, setRejectOpen] = useState(false);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [infoFullscreen, setInfoFullscreen] = useState(false);
|
||||
|
||||
async function reload(nextPage = page, nextStatus = status) {
|
||||
setLoading(true);
|
||||
@@ -286,25 +289,33 @@ function InfoChangeAuditPanel({
|
||||
|
||||
<Drawer
|
||||
title={detail ? `${detail.storeName || detail.storeId} · 信息变更` : '信息变更详情'}
|
||||
width={680}
|
||||
width={infoFullscreen ? '100%' : 680}
|
||||
open={detailOpen}
|
||||
onClose={() => setDetailOpen(false)}
|
||||
onClose={() => {
|
||||
setDetailOpen(false);
|
||||
setInfoFullscreen(false);
|
||||
}}
|
||||
extra={
|
||||
detail?.status === 'PENDING' ? (
|
||||
<Space>
|
||||
<Button onClick={() => void audit(detail.id, 'APPROVE')}>通过</Button>
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
setActiveId(detail.id);
|
||||
setRejectReason('');
|
||||
setRejectOpen(true);
|
||||
}}
|
||||
>
|
||||
驳回
|
||||
</Button>
|
||||
</Space>
|
||||
) : null
|
||||
<Space>
|
||||
<Button onClick={() => setInfoFullscreen((v) => !v)}>
|
||||
{infoFullscreen ? '退出全屏' : '全屏查看'}
|
||||
</Button>
|
||||
{detail?.status === 'PENDING' ? (
|
||||
<>
|
||||
<Button onClick={() => void audit(detail.id, 'APPROVE')}>通过</Button>
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
setActiveId(detail.id);
|
||||
setRejectReason('');
|
||||
setRejectOpen(true);
|
||||
}}
|
||||
>
|
||||
驳回
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{detailLoading ? (
|
||||
@@ -328,7 +339,12 @@ function InfoChangeAuditPanel({
|
||||
label={STORE_INFO_CHANGEABLE_FIELD_LABELS[d.field] ?? d.field}
|
||||
>
|
||||
{d.field === 'coverUrl' || d.field === 'envPhotoUrls' ? (
|
||||
<InfoChangeImageDiff field={d.field} live={d.live} proposed={d.proposed} />
|
||||
<InfoChangeImageDiff
|
||||
field={d.field}
|
||||
live={d.live}
|
||||
proposed={d.proposed}
|
||||
imageSize={infoFullscreen ? 160 : 72}
|
||||
/>
|
||||
) : (
|
||||
<span>
|
||||
<Typography.Text delete type="secondary">
|
||||
@@ -388,6 +404,7 @@ export default function StorePackageAuditsPage() {
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [activeRequestId, setActiveRequestId] = useState<string | null>(null);
|
||||
const [drawerTitle, setDrawerTitle] = useState('套餐变更详情');
|
||||
const [packageFullscreen, setPackageFullscreen] = useState(false);
|
||||
|
||||
async function reload(nextPage = page, nextStatus = status) {
|
||||
setLoading(true);
|
||||
@@ -554,14 +571,23 @@ export default function StorePackageAuditsPage() {
|
||||
|
||||
<Drawer
|
||||
title={drawerTitle}
|
||||
width={880}
|
||||
width={packageFullscreen ? '100%' : 880}
|
||||
open={detailOpen}
|
||||
onClose={() => setDetailOpen(false)}
|
||||
onClose={() => {
|
||||
setDetailOpen(false);
|
||||
setPackageFullscreen(false);
|
||||
}}
|
||||
destroyOnClose
|
||||
extra={
|
||||
<Button onClick={() => setPackageFullscreen((v) => !v)}>
|
||||
{packageFullscreen ? '退出全屏' : '全屏查看'}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{activeRequestId ? (
|
||||
<StorePackageAuditPanel
|
||||
requestId={activeRequestId}
|
||||
fullscreen={packageFullscreen}
|
||||
onAudited={() => {
|
||||
setDetailOpen(false);
|
||||
void reload(page, status);
|
||||
|
||||
@@ -218,8 +218,13 @@ export default function SupportTicketsPage() {
|
||||
|
||||
setReviewOpen(false);
|
||||
const summary = [detail!.title, detail!.content].filter(Boolean).join('\n').slice(0, 500);
|
||||
const ticketUrls = (detail!.attachmentUrls ?? []).filter(Boolean);
|
||||
tasksForm.setFieldsValue({
|
||||
tasks: [{ content: summary || detail!.title, type: mapSupportTicketTypeToDevPlanTask(detail!.ticketType) }],
|
||||
tasks: [{
|
||||
content: summary || detail!.title,
|
||||
type: mapSupportTicketTypeToDevPlanTask(detail!.ticketType),
|
||||
attachmentUrls: ticketUrls,
|
||||
}],
|
||||
dispatchToWecom: localStorage.getItem(DISPATCH_WECOM_STORAGE_KEY) !== 'false',
|
||||
dispatchSupplement: DEFAULT_DISPATCH_SUPPLEMENT,
|
||||
});
|
||||
@@ -364,7 +369,11 @@ export default function SupportTicketsPage() {
|
||||
body: JSON.stringify({
|
||||
decision: 'APPROVE',
|
||||
note: note?.trim() || undefined,
|
||||
tasks: values.tasks.map((t) => ({ content: t.content.trim(), type: t.type })),
|
||||
tasks: values.tasks.map((t) => ({
|
||||
content: t.content.trim(),
|
||||
type: t.type,
|
||||
attachmentUrls: t.attachmentUrls?.length ? t.attachmentUrls : detail!.attachmentUrls,
|
||||
})),
|
||||
dispatchToWecom: !!values.dispatchToWecom,
|
||||
dispatchSupplement: values.dispatchSupplement?.trim() || undefined,
|
||||
}),
|
||||
|
||||
@@ -37,6 +37,13 @@ type Row = {
|
||||
wineryAmount: number;
|
||||
status: string;
|
||||
paidAt?: string | null;
|
||||
paymentRef?: string | null;
|
||||
wineryBank?: {
|
||||
bankAccountName?: string | null;
|
||||
bankName?: string | null;
|
||||
bankBranch?: string | null;
|
||||
bankAccountNo?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
type BillItem = {
|
||||
@@ -127,14 +134,30 @@ export default function WineryBillsPage() {
|
||||
(profile?.permissionKeys ?? []).includes('system_settings_winery_bank');
|
||||
|
||||
function confirmPay(ids: string[], amountHint?: number) {
|
||||
let paymentRef = '';
|
||||
Modal.confirm({
|
||||
title: '确认打款?',
|
||||
content: `将确认 ${ids.length} 笔酒厂对账单${amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。此操作不可撤销。`,
|
||||
content: (
|
||||
<div>
|
||||
<div>
|
||||
将确认 {ids.length} 笔酒厂对账单
|
||||
{amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。此操作不可撤销。
|
||||
</div>
|
||||
<Input
|
||||
placeholder="打款凭证号(可选)"
|
||||
style={{ marginTop: 8 }}
|
||||
onChange={(e) => {
|
||||
paymentRef = e.target.value;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
okText: '确认打款',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const body = JSON.stringify({ paymentRef: paymentRef.trim() || undefined });
|
||||
if (ids.length === 1) {
|
||||
await request(`/admin/winery-bills/${ids[0]}/confirm`, { method: 'POST' });
|
||||
await request(`/admin/winery-bills/${ids[0]}/confirm`, { method: 'POST', body });
|
||||
} else {
|
||||
await request('/admin/winery-bills/batch-confirm', {
|
||||
method: 'POST',
|
||||
@@ -399,7 +422,31 @@ export default function WineryBillsPage() {
|
||||
{displayWineryStatus(detail.status, detail.wineryAmount).label}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="打款时间">{detail.paidAt ? fmtTime(detail.paidAt) : '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="打款凭证">
|
||||
{detail.paymentRef ? String(detail.paymentRef) : '—'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{detail.wineryBank ? (
|
||||
<>
|
||||
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||
收款账户(酒厂)
|
||||
</Typography.Title>
|
||||
<Descriptions column={1} size="small" bordered>
|
||||
<Descriptions.Item label="户名">
|
||||
{detail.wineryBank.bankAccountName || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="开户银行">
|
||||
{detail.wineryBank.bankName || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="开户支行">
|
||||
{detail.wineryBank.bankBranch || '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="账号">
|
||||
{detail.wineryBank.bankAccountNo || '—'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</>
|
||||
) : null}
|
||||
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||
订单明细
|
||||
</Typography.Title>
|
||||
|
||||
Reference in New Issue
Block a user