@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 } : {};
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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: '物流费',
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user