v4.0.11HQ后端优化(快链、账单日等)
CI / verify (pull_request) Waiting to run

This commit is contained in:
2026-09-02 15:45:12 +08:00
parent 1000b489a0
commit 6880b5daf4
31 changed files with 1014 additions and 402 deletions
@@ -0,0 +1,88 @@
import { useEffect, useState } from 'react';
import { Form, Input, Modal, message } from 'antd';
import type { UpdateMyHqCredentialsRequest } from '@dukang/shared-types';
import { request, type HqProfile } from '../lib/api';
type Props = {
open: boolean;
profile: HqProfile | null;
onClose: () => void;
onUpdated: (profile: HqProfile) => void;
};
export function HqAccountSettingsModal({ open, profile, onClose, onUpdated }: Props) {
const [form] = Form.useForm<UpdateMyHqCredentialsRequest & { confirmPassword?: string }>();
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!open) return;
form.setFieldsValue({
loginName: profile?.loginName ?? '',
oldPassword: '',
newPassword: '',
confirmPassword: '',
});
}, [open, profile, form]);
async function submit() {
const values = await form.validateFields();
if (values.newPassword && values.newPassword !== values.confirmPassword) {
message.error('两次输入的新密码不一致');
return;
}
const body: UpdateMyHqCredentialsRequest = {};
const nextLogin = values.loginName?.trim();
if (nextLogin && nextLogin !== (profile?.loginName ?? '')) {
body.loginName = nextLogin;
}
if (values.newPassword?.trim()) {
body.newPassword = values.newPassword.trim();
if (values.oldPassword?.trim()) body.oldPassword = values.oldPassword.trim();
}
if (!body.loginName && !body.newPassword) {
message.warning('请填写要修改的内容');
return;
}
setSaving(true);
try {
await request('/admin/me/credentials', {
method: 'PUT',
body: JSON.stringify(body),
});
const updated = await request<HqProfile>('/admin/auth/me');
message.success('账号信息已更新');
onUpdated(updated);
onClose();
} catch (e) {
message.error(e instanceof Error ? e.message : '保存失败');
} finally {
setSaving(false);
}
}
return (
<Modal
title="账号设置"
open={open}
onCancel={onClose}
onOk={() => void submit()}
confirmLoading={saving}
destroyOnClose
>
<Form form={form} layout="vertical">
<Form.Item name="loginName" label="登录用户名" rules={[{ required: true, message: '请输入用户名' }]}>
<Input autoComplete="username" placeholder="用于密码登录" />
</Form.Item>
<Form.Item name="oldPassword" label="当前密码" extra="已设置过密码时,修改密码必填">
<Input.Password autoComplete="current-password" placeholder="不修改密码请留空" />
</Form.Item>
<Form.Item name="newPassword" label="新密码" rules={[{ min: 6, message: '至少 6 位' }]}>
<Input.Password autoComplete="new-password" placeholder="不修改请留空" />
</Form.Item>
<Form.Item name="confirmPassword" label="确认新密码">
<Input.Password autoComplete="new-password" placeholder="不修改请留空" />
</Form.Item>
</Form>
</Modal>
);
}
@@ -30,6 +30,7 @@ import { clearAuth, request, type HqProfile } from '../lib/api';
import { bindAdminEllipsisTitle } from '../lib/ellipsis-title';
import { AUDIT_NOTICE_CHANGED_EVENT, PACKAGE_AUDIT_CHANGED_EVENT } from '../lib/admin-events';
import { ListColumnPrefsProvider } from '../lib/ListColumnPrefsContext';
import { HqAccountSettingsModal } from '../components/HqAccountSettingsModal';
const { Header, Sider, Content } = Layout;
@@ -292,6 +293,7 @@ export default function AdminLayout() {
const contentRef = useRef<HTMLDivElement>(null);
const [profile, setProfile] = useState<HqProfile | null>(null);
const [auditPendingCount, setAuditPendingCount] = useState(0);
const [accountSettingsOpen, setAccountSettingsOpen] = useState(false);
function refreshAuditPendingCount() {
Promise.all([
@@ -419,11 +421,20 @@ export default function AdminLayout() {
<span style={{ color: '#999' }}>
{HQ_ROLE_LABELS[profile?.adminRole ?? ''] || profile?.adminRole || ''}
</span>
<Button type="text" icon={<UserOutlined />} onClick={() => setAccountSettingsOpen(true)}>
</Button>
<Button type="text" icon={<LogoutOutlined />} onClick={logout}>
退
</Button>
</Space>
</Header>
<HqAccountSettingsModal
open={accountSettingsOpen}
profile={profile}
onClose={() => setAccountSettingsOpen(false)}
onUpdated={setProfile}
/>
<div
ref={contentRef}
className="admin-layout"
+1
View File
@@ -6,6 +6,7 @@ export const CLIENT_APP = 'HQ_WEB';
export type HqProfile = {
id: string;
phone: string;
loginName?: string | null;
name: string;
adminRole: string;
status: string;
+8
View File
@@ -0,0 +1,8 @@
/** 将列表可见列 key 追加到导出 query */
export function appendExportColumns(qs: URLSearchParams, keys: string[]) {
if (keys.length) qs.set('columns', keys.join(','));
}
export function exportColumnsBody(keys: string[]): { columns?: string[] } {
return keys.length ? { columns: keys } : {};
}
+12 -1
View File
@@ -11,6 +11,7 @@ import {
applyColumnPrefs,
columnKey,
settingItems,
visibleColumnKeys,
type ListColumnSettingItem,
} from './list-column-prefs';
import { beginColumnResize, withResizeTitle } from './column-resize';
@@ -48,6 +49,16 @@ export function useAdminListColumns<T>(
);
itemsRef.current = items;
const exportColumnKeys = useMemo(() => {
const keyed = (allColumns as ColumnType<T>[]).map((col, i) => ({
col,
key: columnKey(col, i),
actions: col.title === '操作' || col.key === ACTIONS_COLUMN_KEY || col.key === 'actions',
}));
const defaultKeys = keyed.filter((c) => !c.actions).map((c) => c.key);
return visibleColumnKeys(defaultKeys, pref);
}, [allColumns, pref]);
const serialCol: ColumnType<T> = useMemo(
() => ({
key: SERIAL_COLUMN_KEY,
@@ -171,5 +182,5 @@ export function useAdminListColumns<T>(
/>
);
return { columns, settingsButton, settingsModal };
return { columns, settingsButton, settingsModal, exportColumnKeys };
}
+73 -29
View File
@@ -29,6 +29,7 @@ import {
import { request } from '../lib/api';
import { fmtTime } from '../lib/constants';
import { downloadBase64File } from '../lib/exportExcel';
import { exportColumnsBody } from '../lib/export-columns';
import { useAdminList } from '../lib/useAdminList';
import OssUpload from '../components/OssUpload';
import { useAdminListColumns } from '../lib/useAdminListColumns';
@@ -62,6 +63,8 @@ export default function DevPlanTasksPage() {
const [dispatchOpen, setDispatchOpen] = useState(false);
const [batchEditOpen, setBatchEditOpen] = useState(false);
const [batchSaving, setBatchSaving] = useState(false);
const [creatingVersion, setCreatingVersion] = useState(false);
const [newVersionNo, setNewVersionNo] = useState('');
const [versions, setVersions] = useState<DevPlanVersionDto[]>([]);
const [supportTickets, setSupportTickets] = useState<SupportTicketDto[]>([]);
const [editing, setEditing] = useState<DevPlanTaskDto | null>(null);
@@ -172,9 +175,34 @@ export default function DevPlanTasksPage() {
function openBatchEdit() {
batchForm.setFieldsValue({ status: undefined, versionId: undefined });
setNewVersionNo('');
setBatchEditOpen(true);
}
async function createVersionInBatch() {
const versionNo = newVersionNo.trim();
if (!versionNo) {
message.warning('请输入版本号');
return;
}
setCreatingVersion(true);
try {
const created = await request<DevPlanVersionDto>('/admin/dev-plan/versions', {
method: 'POST',
body: JSON.stringify({ versionNo }),
});
const res = await request<{ items: DevPlanVersionDto[] }>('/admin/dev-plan/versions?pageSize=100');
setVersions(res.items ?? []);
batchForm.setFieldsValue({ versionId: created.id });
setNewVersionNo('');
message.success(`已创建版本 ${versionNo}`);
} catch (e) {
message.error(e instanceof Error ? e.message : '创建失败');
} finally {
setCreatingVersion(false);
}
}
async function submitBatchEdit() {
const values = await batchForm.validateFields();
if (!values.status && !values.versionId) {
@@ -224,34 +252,6 @@ 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 baseColumns: ColumnsType<DevPlanTaskDto> = [
{
title: '任务号',
@@ -285,6 +285,7 @@ export default function DevPlanTasksPage() {
},
{
title: '附件',
key: 'attachmentUrls',
width: 100,
render: (_, row) =>
row.attachmentUrls?.length ? (
@@ -314,6 +315,7 @@ export default function DevPlanTasksPage() {
},
{
title: '关联版本',
key: 'versions',
dataIndex: 'versions',
width: 140,
render: (_, row) =>
@@ -347,7 +349,36 @@ export default function DevPlanTasksPage() {
},
];
const { columns, settingsButton, settingsModal } = useAdminListColumns('dev-plan-tasks', baseColumns, { page, pageSize });
const { columns, settingsButton, settingsModal, exportColumnKeys } = useAdminListColumns('dev-plan-tasks', baseColumns, { page, pageSize });
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,
...exportColumnsBody(exportColumnKeys),
}),
});
downloadBase64File(result.contentBase64, result.filename, result.mimeType);
message.success(`已导出 ${result.count} 条任务`);
} catch (e) {
message.error(e instanceof Error ? e.message : '导出失败');
} finally {
setExporting(false);
}
}
return (
<div>
@@ -541,6 +572,19 @@ export default function DevPlanTasksPage() {
options={versions.map((v) => ({ value: v.id, label: v.versionNo }))}
/>
</Form.Item>
<Form.Item label="创建新版本">
<Space.Compact style={{ width: '100%' }}>
<Input
placeholder="版本号,如 v4.0.11"
value={newVersionNo}
onChange={(e) => setNewVersionNo(e.target.value)}
onPressEnter={() => void createVersionInBatch()}
/>
<Button loading={creatingVersion} onClick={() => void createVersionInBatch()}>
</Button>
</Space.Compact>
</Form.Item>
</Form>
</Modal>
</div>
@@ -57,6 +57,7 @@ export default function DevPlanVersionsPage() {
useAdminList<DevPlanVersionDto>('/admin/dev-plan/versions', () => {
const qs = new URLSearchParams();
if (filters.status) qs.set('status', filters.status);
if (filters.keyword) qs.set('keyword', filters.keyword);
return qs;
}, [filters]);
@@ -190,6 +191,9 @@ export default function DevPlanVersionsPage() {
<Form.Item name="status" label="状态">
<Select allowClear style={{ width: 120 }} options={STATUS_OPTIONS} />
</Form.Item>
<Form.Item name="keyword" label="查找">
<Input allowClear placeholder="版本号或内容" style={{ width: 220 }} />
</Form.Item>
<Button type="primary" htmlType="submit">
</Button>
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Button,
Card,
@@ -27,6 +28,7 @@ import {
} from '@dukang/shared-types';
import { fmtTime } from '../lib/constants';
import { downloadExcelCsv } from '../lib/exportExcel';
import { appendExportColumns } from '../lib/export-columns';
import { request } from '../lib/api';
import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns';
@@ -98,6 +100,7 @@ const STATUS_COLORS: Record<string, string> = {
};
export default function LogisticsBillsPage() {
const navigate = useNavigate();
const [form] = Form.useForm();
const [summaryForm] = Form.useForm();
const [filters, setFilters] = useState<Record<string, string>>({});
@@ -202,6 +205,7 @@ export default function LogisticsBillsPage() {
if (filters.providerId) qs.set('providerId', filters.providerId);
if (filters.year) qs.set('year', filters.year);
if (filters.month) qs.set('month', filters.month);
appendExportColumns(qs, exportColumnKeys);
const result = await request<{ csv: string; count: number }>(
`/admin/logistics-bills/export?${qs}`,
);
@@ -250,6 +254,7 @@ export default function LogisticsBillsPage() {
const billColumns: ColumnsType<BillRow> = [
{
title: '账单号',
key: '账单号',
dataIndex: 'billNo',
width: 170,
render: (v, row) => (
@@ -258,42 +263,49 @@ export default function LogisticsBillsPage() {
},
{
title: '承运商',
key: '承运商',
width: 140,
render: (_, r) => `${r.providerName || ''} (${r.providerCode || ''})`,
},
{
title: '账期',
key: '账期',
width: 200,
render: (_, r) =>
`${String(r.periodStart || '').slice(0, 10)} ~ ${String(r.periodEnd || '').slice(0, 10)}`,
},
{
title: '结算方式',
key: '结算方式',
dataIndex: 'settlementMethod',
width: 100,
render: (v) =>
LOGISTICS_SETTLEMENT_METHOD_LABELS[v as LogisticsSettlementMethod] || v,
},
{ title: '订单数', dataIndex: 'orderCount', width: 80 },
{ title: '瓶数', dataIndex: 'bottleCount', width: 80 },
{ title: '订单数', key: '订单数', dataIndex: 'orderCount', width: 80 },
{ title: '瓶数', key: '瓶数', dataIndex: 'bottleCount', width: 80 },
{
title: '物流费',
key: '物流费',
dataIndex: 'logisticsAmount',
width: 110,
render: (v) => `¥${Number(v).toFixed(2)}`,
},
{
title: '收款户名',
key: '收款户名',
width: 100,
render: (_, r) => providerBank(r)?.bankAccountName || '—',
},
{
title: '收款账号',
key: '收款账号',
width: 140,
render: (_, r) => providerBank(r)?.bankAccountNo || '—',
},
{
title: '状态',
key: '状态',
dataIndex: 'status',
width: 90,
render: (s) => <Tag color={STATUS_COLORS[s] || 'default'}>{STATUS_LABELS[s] || s}</Tag>,
@@ -398,7 +410,7 @@ export default function LogisticsBillsPage() {
},
];
const { columns, settingsButton, settingsModal } = useAdminListColumns('finance-logistics-bills', billColumns, {
const { columns, settingsButton, settingsModal, exportColumnKeys } = useAdminListColumns('finance-logistics-bills', billColumns, {
page,
pageSize,
});
@@ -627,7 +639,20 @@ export default function LogisticsBillsPage() {
pagination={false}
dataSource={detail.items ?? []}
columns={[
{ title: '订单号', dataIndex: 'orderNo' },
{
title: '订单号',
dataIndex: 'orderNo',
render: (v: string) =>
v ? (
<AdminPrimaryLink
onClick={() => navigate(`/orders?orderNo=${encodeURIComponent(v)}`)}
>
{v}
</AdminPrimaryLink>
) : (
'—'
),
},
{ title: '瓶数', dataIndex: 'quantity', width: 70 },
{
title: '物流费',
+19 -6
View File
@@ -29,6 +29,7 @@ import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
import { downloadBase64File } from '../lib/exportExcel';
import { exportColumnsBody } from '../lib/export-columns';
import {
ADMIN_OPTIONS_PAGE_SIZE,
DELIVERY_PROVIDER_LABELS,
@@ -265,8 +266,9 @@ function buildExportPayload(
format: OrderExportFormat,
filters: OrderExportFilters,
selectedIds: string[],
columns?: string[],
) {
const payload: Record<string, unknown> = { scope, format };
const payload: Record<string, unknown> = { scope, format, ...exportColumnsBody(columns ?? []) };
if (scope === 'selected') {
payload.ids = selectedIds;
return payload;
@@ -671,6 +673,7 @@ export default function OrdersPage() {
status: selectedStatuses(values.status).length ? values.status : initialStatuses,
},
selectedRowKeys,
exportColumnKeys,
);
const result = await request<OrderExportResult>('/admin/orders/export', {
method: 'POST',
@@ -688,6 +691,7 @@ export default function OrdersPage() {
const baseColumns: ColumnsType<AdminOrderRow> = [
{
title: '订单号',
key: '订单号',
dataIndex: 'orderNo',
width: 180,
render: (v, row) => (
@@ -699,7 +703,7 @@ export default function OrdersPage() {
},
{
title: '用户',
key: 'user',
key: '用户',
width: 200,
render: (_, row) =>
row.user?.id ? (
@@ -712,7 +716,7 @@ export default function OrdersPage() {
},
{
title: '商品',
key: 'productName',
key: '商品',
width: 180,
render: (_, row) => (
<Space size={4} wrap>
@@ -726,12 +730,14 @@ export default function OrdersPage() {
},
{
title: '规格',
key: '规格',
dataIndex: 'productSpec',
width: 140,
render: (v: string | undefined) => v || '—',
},
{
title: '数量',
key: '数量',
dataIndex: 'quantity',
width: 80,
render: (v: number | undefined, row) =>
@@ -739,12 +745,14 @@ export default function OrdersPage() {
},
{
title: '配送方式',
key: '配送方式',
dataIndex: 'deliveryType',
width: 100,
render: (v: string | undefined) => DELIVERY_TYPE_LABELS[v ?? ''] || v || '—',
},
{
title: '状态',
key: '状态',
dataIndex: 'status',
width: 100,
render: (s: string) => (
@@ -753,42 +761,47 @@ export default function OrdersPage() {
},
{
title: '实付',
key: '实付',
dataIndex: 'payAmount',
width: 90,
render: (v: number) => `¥${v}`,
},
{
title: '运费',
key: 'logisticsFee',
key: '运费',
width: 90,
render: (_, row) =>
row.delivery?.logisticsFee == null ? '—' : `¥${Number(row.delivery.logisticsFee).toFixed(2)}`,
},
{
title: '好客权益',
key: '好客权益',
width: 200,
render: (_, row) => formatBenefitBrief(row),
},
{
title: '收货人',
key: '收货人',
dataIndex: 'receiverName',
width: 90,
render: (v: string | undefined) => v || '—',
},
{
title: '电话',
key: '电话',
dataIndex: 'receiverPhone',
width: 120,
render: (v: string | undefined) => v || '—',
},
{
title: '地址',
key: 'receiverAddress',
key: '地址',
width: 260,
render: (_, row) => formatReceiverAddress(row) || '—',
},
{
title: '下单时间',
key: '下单时间',
dataIndex: 'createdAt',
width: 170,
render: fmtTime,
@@ -823,7 +836,7 @@ export default function OrdersPage() {
},
];
const { columns, settingsButton, settingsModal } = useAdminListColumns('orders', baseColumns, {
const { columns, settingsButton, settingsModal, exportColumnKeys } = useAdminListColumns('orders', baseColumns, {
page,
pageSize,
});
+43 -2
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Button,
Card,
@@ -14,6 +15,7 @@ import {
Table,
Tabs,
Tag,
Tooltip,
Typography,
message,
} from 'antd';
@@ -22,6 +24,7 @@ import dayjs, { type Dayjs } from 'dayjs';
import { request, type Paginated } from '../lib/api';
import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
import { downloadExcelCsv } from '../lib/exportExcel';
import { appendExportColumns } from '../lib/export-columns';
import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
@@ -48,6 +51,7 @@ type Row = {
redeemCommission: number;
totalAmount: number;
status: string;
billDate: string;
periodStart: string;
periodEnd: string;
rejectReason?: string | null;
@@ -109,6 +113,7 @@ function weekMonday(d: Dayjs) {
}
export default function PartnerBillsPage() {
const navigate = useNavigate();
const [form] = Form.useForm();
const [filters, setFilters] = useState<Record<string, string>>({});
const [partners, setPartners] = useState<PartnerOption[]>([]);
@@ -263,6 +268,7 @@ export default function PartnerBillsPage() {
if (filters.status) qs.set('status', filters.status);
if (filters.partnerId) qs.set('partnerId', filters.partnerId);
if (filters.weekStartYmd) qs.set('weekStartYmd', filters.weekStartYmd);
appendExportColumns(qs, exportColumnKeys);
const result = await request<{ csv: string; count: number }>(`/admin/partner-bills/export?${qs}`);
const suffix = filters.weekStartYmd || 'all';
downloadExcelCsv(result.csv, `合伙人账单_${suffix}.csv`);
@@ -285,6 +291,7 @@ export default function PartnerBillsPage() {
const baseColumns: ColumnsType<Row> = [
{
title: '账单号',
key: '账单号',
dataIndex: 'billNo',
width: 180,
render: (v, row) => (
@@ -293,29 +300,45 @@ export default function PartnerBillsPage() {
},
{
title: '合伙人',
key: '合伙人',
width: 200,
render: (_, r) => partnerName(r),
},
{
title: (
<Tooltip title="周账出账日为账期结束后的下周一;历史月账为次月 1 日">
</Tooltip>
),
key: '出账日',
dataIndex: 'billDate',
width: 110,
render: (v) => String(v || '').slice(0, 10),
},
{
title: '账期',
key: '账期',
width: 200,
render: (_, r) =>
`${String(r.periodStart || '').slice(0, 10)} ~ ${String(r.periodEnd || '').slice(0, 10)}`,
},
{
title: '酒单佣金',
key: '酒单佣金',
dataIndex: 'orderCommission',
width: 110,
render: (v) => `¥${Number(v).toFixed(2)}`,
},
{
title: '核销佣金',
key: '核销佣金',
dataIndex: 'redeemCommission',
width: 110,
render: (v) => `¥${Number(v).toFixed(2)}`,
},
{
title: '合计应付',
key: '合计应付',
dataIndex: 'totalAmount',
width: 130,
render: (v) =>
@@ -323,16 +346,19 @@ export default function PartnerBillsPage() {
},
{
title: '收款户名',
key: '收款户名',
width: 100,
render: (_, r) => partnerBank(r)?.bankAccountName || '—',
},
{
title: '收款账号',
key: '收款账号',
width: 140,
render: (_, r) => partnerBank(r)?.bankAccountNo || '—',
},
{
title: '状态',
key: '状态',
dataIndex: 'status',
width: 130,
render: (s, row) => (
@@ -380,7 +406,7 @@ export default function PartnerBillsPage() {
},
];
const { columns, settingsButton, settingsModal } = useAdminListColumns('finance-partner-bills', baseColumns, { page, pageSize });
const { columns, settingsButton, settingsModal, exportColumnKeys } = useAdminListColumns('finance-partner-bills', baseColumns, { page, pageSize });
return (
<div>
@@ -572,6 +598,7 @@ export default function PartnerBillsPage() {
<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.billDate || '').slice(0, 10)}</Descriptions.Item>
<Descriptions.Item label="账期">
{String(detail.periodStart).slice(0, 10)} ~ {String(detail.periodEnd).slice(0, 10)}
</Descriptions.Item>
@@ -610,7 +637,21 @@ export default function PartnerBillsPage() {
pagination={false}
dataSource={detail.orderItems ?? []}
columns={[
{ title: '订单号', dataIndex: 'refNo', width: 160 },
{
title: '订单号',
dataIndex: 'refNo',
width: 160,
render: (v: string) =>
v ? (
<AdminPrimaryLink
onClick={() => navigate(`/orders?orderNo=${encodeURIComponent(v)}`)}
>
{v}
</AdminPrimaryLink>
) : (
'—'
),
},
{ title: '商品', dataIndex: 'title' },
{ title: '数量', dataIndex: 'extra', width: 70 },
{
+34 -4
View File
@@ -31,6 +31,7 @@ import {
import { request, type Paginated } from '../lib/api';
import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
import { downloadExcelCsv } from '../lib/exportExcel';
import { appendExportColumns } from '../lib/export-columns';
import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
@@ -44,6 +45,8 @@ type Row = {
amount: number;
status: StoreSettlementStatus;
date: string;
periodStart?: string | null;
periodEnd?: string | null;
overdue?: boolean;
redeemCount?: number | null;
redeemAmount?: number | null;
@@ -198,6 +201,7 @@ export default function StoreBillsPage() {
if (filters.storeId) qs.set('storeId', filters.storeId);
if (filters.dateFrom) qs.set('dateFrom', filters.dateFrom);
if (filters.dateTo) qs.set('dateTo', filters.dateTo);
appendExportColumns(qs, exportColumnKeys);
const result = await request<{ csv: string; count: number }>(`/admin/store-bills/export?${qs}`);
downloadExcelCsv(result.csv, `门店对账单_${filters.dateFrom || 'all'}_${filters.dateTo || 'all'}.csv`);
message.success(`已导出 ${result.count} 条 T+1 账单`);
@@ -218,6 +222,7 @@ export default function StoreBillsPage() {
const baseColumns: ColumnsType<Row> = [
{
title: '类型',
key: '类型',
dataIndex: 'kind',
width: 100,
render: (k: StoreSettlementKind) => (
@@ -228,6 +233,7 @@ export default function StoreBillsPage() {
},
{
title: '单号',
key: '账单号',
dataIndex: 'billNo',
width: 180,
render: (v, row) => (
@@ -243,39 +249,58 @@ export default function StoreBillsPage() {
</Tooltip>
),
key: '出账日',
dataIndex: 'date',
width: 160,
render: (v, row) => (row.kind === 'T1_BILL' ? String(v || '').slice(0, 10) : fmtTime(v)),
},
{ title: '门店', dataIndex: ['store', 'name'], width: 140 },
{ title: '登录手机', dataIndex: ['store', 'phone'], width: 120, render: (v) => v || '—' },
{ title: '城市', dataIndex: ['store', 'cityName'], width: 90 },
{
title: (
<Tooltip title="T+1 账期为出账日前一日的核销窗口;手动提现无账期">
</Tooltip>
),
key: '账期',
width: 200,
render: (_, row) =>
row.kind === 'T1_BILL' && row.periodStart
? `${row.periodStart} ~ ${row.periodEnd}`
: '—',
},
{ title: '门店', key: '门店', dataIndex: ['store', 'name'], width: 140 },
{ title: '登录手机', key: '登录手机', dataIndex: ['store', 'phone'], width: 120, render: (v) => v || '—' },
{ title: '城市', key: '城市', dataIndex: ['store', 'cityName'], width: 90 },
{
title: '收款户名',
key: '收款户名',
width: 100,
render: (_, row) =>
row.kind === 'T1_BILL' ? row.bankAccount?.bankAccountName || '—' : '—',
},
{
title: '收款账号',
key: '收款账号',
width: 140,
render: (_, row) =>
row.kind === 'T1_BILL' ? row.bankAccount?.bankAccountNo || '—' : '—',
},
{
title: '笔数',
key: '笔数',
width: 80,
render: (_, row) =>
row.kind === 'T1_BILL' ? (row.redeemCount ?? '—') : (row.payoutCount ?? '—'),
},
{
title: '应付金额',
key: '应付金额',
dataIndex: 'amount',
width: 110,
render: (v) => `¥${Number(v).toFixed(2)}`,
},
{
title: '状态',
key: '状态',
dataIndex: 'status',
width: 100,
render: (s: string, row) => (
@@ -325,7 +350,7 @@ export default function StoreBillsPage() {
}
| undefined;
const { columns, settingsButton, settingsModal } = useAdminListColumns('finance-store-bills', baseColumns, { page, pageSize });
const { columns, settingsButton, settingsModal, exportColumnKeys } = useAdminListColumns('finance-store-bills', baseColumns, { page, pageSize });
return (
<div>
@@ -495,6 +520,11 @@ export default function StoreBillsPage() {
<Descriptions.Item label="出账日">
{String(detail.billDate || '').slice(0, 10)}
</Descriptions.Item>
<Descriptions.Item label="账期">
{detail.periodStart
? `${String(detail.periodStart).slice(0, 10)} ~ ${String(detail.periodEnd || '').slice(0, 10)}`
: '—'}
</Descriptions.Item>
<Descriptions.Item label="应付">
¥{Number(detail.payoutAmount ?? 0).toFixed(2)}
</Descriptions.Item>
+45 -7
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Button,
Card,
@@ -26,6 +27,7 @@ import {
} from '@dukang/shared-types';
import { fmtTime } from '../lib/constants';
import { downloadExcelCsv } from '../lib/exportExcel';
import { appendExportColumns } from '../lib/export-columns';
import { request, type HqProfile } from '../lib/api';
import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns';
@@ -37,6 +39,8 @@ type Row = {
id: string;
billNo: string;
billDate: string;
periodStart: string;
periodEnd: string;
orderCount: number;
orderAmount: number;
wineryRate: number;
@@ -107,6 +111,7 @@ const WINERY_BANK_KEYS = [
] as const;
export default function WineryBillsPage() {
const navigate = useNavigate();
const [form] = Form.useForm();
const [bankForm] = Form.useForm<Record<string, string>>();
const [reconcileForm] = Form.useForm<{ range?: [Dayjs, Dayjs] }>();
@@ -196,6 +201,7 @@ export default function WineryBillsPage() {
if (filters.month) qs.set('month', filters.month);
if (filters.dateFrom) qs.set('dateFrom', filters.dateFrom);
if (filters.dateTo) qs.set('dateTo', filters.dateTo);
appendExportColumns(qs, exportColumnKeys);
const result = await request<{ csv: string; count: number }>(`/admin/winery-bills/export?${qs}`);
const suffix = filters.year && filters.month ? `${filters.year}-${filters.month}` : 'all';
downloadExcelCsv(result.csv, `酒厂对账单_${suffix}.csv`);
@@ -275,6 +281,7 @@ export default function WineryBillsPage() {
const baseColumns: ColumnsType<Row> = [
{
title: '账单号',
key: '账单号',
dataIndex: 'billNo',
width: 170,
render: (v, row) => (
@@ -284,34 +291,49 @@ export default function WineryBillsPage() {
{
title: (
<Tooltip title="出账当天的北京日历日;T+3 只决定纳入哪天完成的订单">
</Tooltip>
),
key: '出账日',
dataIndex: 'billDate',
width: 110,
render: (v) => String(v || '').slice(0, 10),
},
{ title: '订单数', dataIndex: 'orderCount', width: 80 },
{
title: '酒单实付合计',
title: (
<Tooltip title="纳入本账单的订单完成日区间(含起止日)">
</Tooltip>
),
key: '账期',
width: 200,
render: (_, row) => `${row.periodStart} ~ ${row.periodEnd}`,
},
{ title: '订单数', key: '订单数', dataIndex: 'orderCount', width: 80 },
{
title: '订单总额',
key: '订单总额',
dataIndex: 'orderAmount',
width: 120,
render: (v) => `¥${Number(v).toFixed(2)}`,
},
{
title: '酒厂比例',
key: '酒厂比例',
dataIndex: 'wineryRate',
width: 90,
render: (v) => `${Math.round(Number(v) * 100)}%`,
},
{
title: '应付',
key: '应付',
dataIndex: 'wineryAmount',
width: 110,
render: (v) => `¥${Number(v).toFixed(2)}`,
},
{
title: '状态',
key: '状态',
dataIndex: 'status',
width: 100,
render: (s, row) => {
@@ -338,7 +360,7 @@ export default function WineryBillsPage() {
},
];
const { columns, settingsButton, settingsModal } = useAdminListColumns('finance-winery-bills', baseColumns, { page, pageSize });
const { columns, settingsButton, settingsModal, exportColumnKeys } = useAdminListColumns('finance-winery-bills', baseColumns, { page, pageSize });
return (
<div>
@@ -365,7 +387,7 @@ export default function WineryBillsPage() {
<Card size="small" style={{ marginBottom: 16 }}>
<Space size="large" wrap>
<Statistic title="账单数" value={summary.count} />
<Statistic title="酒单实付合计" value={summary.orderAmount ?? 0} prefix="¥" precision={2} />
<Statistic title="订单总额" value={summary.orderAmount ?? 0} prefix="¥" precision={2} />
<Statistic title="应付合计" value={summary.wineryAmount ?? 0} prefix="¥" precision={2} />
</Space>
</Card>
@@ -460,7 +482,10 @@ export default function WineryBillsPage() {
<>
<Descriptions column={1} size="small" bordered>
<Descriptions.Item label="账单号">{detail.billNo}</Descriptions.Item>
<Descriptions.Item label="账日">{String(detail.billDate).slice(0, 10)}</Descriptions.Item>
<Descriptions.Item label="账日">{String(detail.billDate).slice(0, 10)}</Descriptions.Item>
<Descriptions.Item label="账期">
{detail.periodStart} ~ {detail.periodEnd}
</Descriptions.Item>
<Descriptions.Item label="应付">¥{Number(detail.wineryAmount).toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="状态">
{displayWineryStatus(detail.status, detail.wineryAmount).label}
@@ -500,7 +525,20 @@ export default function WineryBillsPage() {
pagination={false}
dataSource={detail.items ?? []}
columns={[
{ title: '订单号', dataIndex: 'orderNo' },
{
title: '订单号',
dataIndex: 'orderNo',
render: (v: string) =>
v ? (
<AdminPrimaryLink
onClick={() => navigate(`/orders?orderNo=${encodeURIComponent(v)}`)}
>
{v}
</AdminPrimaryLink>
) : (
'—'
),
},
{
title: '配送',
dataIndex: 'deliveryType',