增加核销接口

增加hq管理端日志
This commit is contained in:
2026-07-06 19:17:27 +08:00
parent f0b99ea9da
commit 4c38a9dd2b
29 changed files with 904 additions and 37 deletions
+2
View File
@@ -25,6 +25,7 @@ import StorePayoutsPage from './pages/StorePayoutsPage';
import PartnerBillsPage from './pages/PartnerBillsPage';
import TicketsPage from './pages/TicketsPage';
import UserLogsPage from './pages/UserLogsPage';
import HqLogsPage from './pages/HqLogsPage';
import ThirdPartyLogsPage from './pages/ThirdPartyLogsPage';
function RequireAuth({ children }: { children: React.ReactNode }) {
@@ -63,6 +64,7 @@ export default function App() {
<Route path="/partner-bills" element={<PartnerBillsPage />} />
<Route path="/tickets" element={<TicketsPage />} />
<Route path="/logs/users" element={<UserLogsPage />} />
<Route path="/logs/hq" element={<HqLogsPage />} />
<Route path="/logs/third-party" element={<ThirdPartyLogsPage />} />
<Route path="/deliveries" element={<DeliveriesPage />} />
<Route path="/deliveries/xiaofeixia" element={<XiaofeixiaTestPage />} />
@@ -73,6 +73,7 @@ const MENU_ITEMS: MenuProps['items'] = [
label: '日志',
children: [
{ key: '/logs/users', label: '用户日志' },
{ key: '/logs/hq', label: 'HQ 操作日志' },
{ key: '/logs/third-party', label: '第三方日志' },
],
},
+42
View File
@@ -0,0 +1,42 @@
export const HQ_OPERATION_ACTION_OPTIONS = [
{ value: 'CITY_CREATE', label: '新增开城城市' },
{ value: 'CITY_UPDATE', label: '编辑开城城市' },
{ value: 'PARTNER_CREATE', label: '新增城市合伙人' },
{ value: 'PARTNER_UPDATE', label: '编辑城市合伙人' },
{ value: 'PARTNER_ACCOUNT_CREATE', label: '新增合伙人账户' },
{ value: 'PARTNER_ACCOUNT_UPDATE', label: '编辑合伙人账户' },
{ value: 'HQ_ACCOUNT_CREATE', label: '新增 HQ 管理员' },
{ value: 'HQ_ACCOUNT_UPDATE', label: '编辑 HQ 管理员/权限' },
{ value: 'USER_DELETE', label: '删除用户' },
{ value: 'USER_BATCH_DELETE', label: '批量删除用户' },
{ value: 'ORDER_SHIP', label: '订单发货' },
{ value: 'ORDER_STATUS_DEBUG', label: '订单状态调试' },
{ value: 'ORDER_BATCH_DELETE', label: '批量删除订单' },
{ value: 'STORE_CREATE', label: '新增门店' },
{ value: 'STORE_UPDATE', label: '编辑门店' },
{ value: 'STORE_STATUS', label: '变更门店状态' },
{ value: 'STORE_AUDIT', label: '门店审核' },
{ value: 'STORE_ACCOUNT_CREATE', label: '新增门店账户' },
{ value: 'STORE_ACCOUNT_UPDATE', label: '编辑门店账户' },
{ value: 'PRODUCT_CREATE', label: '新增商品' },
{ value: 'PRODUCT_UPDATE', label: '编辑商品' },
{ value: 'PRODUCT_DELETE', label: '删除商品' },
{ value: 'BENEFIT_COUPON_VOID', label: '作废权益券' },
{ value: 'DELIVERY_UPDATE', label: '编辑配送单' },
{ value: 'TICKET_APPROVE', label: '工单通过' },
{ value: 'TICKET_REJECT', label: '工单驳回' },
{ value: 'STORE_PAYOUT_CONFIRM', label: '门店打款确认' },
{ value: 'STORE_PAYOUT_BATCH_CONFIRM', label: '批量门店打款' },
{ value: 'PARTNER_BILL_GENERATE', label: '生成合伙人账单' },
{ value: 'PARTNER_BILL_CONFIRM', label: '确认合伙人账单' },
{ value: 'PARTNER_BILL_MARK_PAID', label: '合伙人账单结算' },
] as const;
export const HQ_OPERATION_ACTION_LABELS: Record<string, string> = Object.fromEntries(
HQ_OPERATION_ACTION_OPTIONS.map((o) => [o.value, o.label]),
);
export function resolveHqOperationLabel(action: string | null | undefined): string {
if (!action) return '—';
return HQ_OPERATION_ACTION_LABELS[action] || action;
}
+196
View File
@@ -0,0 +1,196 @@
import { useEffect, useState } from 'react';
import {
Button, Descriptions, Drawer, Form, Input, Select, Space, Table, Tag, Typography,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { useSearchParams } from 'react-router-dom';
import { request } from '../lib/api';
import { fmtTime } from '../lib/constants';
import { HQ_OPERATION_ACTION_OPTIONS, resolveHqOperationLabel } from '../lib/hq-log';
import { useAdminList } from '../lib/useAdminList';
type Row = {
id: string;
hqAccountId: string | null;
hqName: string | null;
hqPhone: string | null;
hqRole: string | null;
action: string | null;
actionLabel: string;
refType: string | null;
refId: string | null;
status: string | null;
remark: string | null;
detail: Record<string, unknown> | null;
createdAt: string;
};
export default function HqLogsPage() {
const [searchParams, setSearchParams] = useSearchParams();
const [form] = Form.useForm();
const [filters, setFilters] = useState<Record<string, string>>(() => ({
hqAccountId: searchParams.get('hqAccountId') ?? '',
action: searchParams.get('action') ?? '',
refType: searchParams.get('refType') ?? '',
}));
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
'/admin/logs/hq',
() => {
const qs = new URLSearchParams();
if (filters.hqAccountId) qs.set('hqAccountId', filters.hqAccountId);
if (filters.action) qs.set('action', filters.action);
if (filters.refType) qs.set('refType', filters.refType);
return qs;
},
[filters],
);
const [detail, setDetail] = useState<Row | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
useEffect(() => {
form.setFieldsValue(filters);
}, [form, filters]);
const columns: ColumnsType<Row> = [
{ title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
{
title: '操作人',
width: 160,
render: (_, r) => (
<div>
<div>{r.hqName || '—'}</div>
<div style={{ color: '#999', fontSize: 12 }}>
{r.hqPhone || ''} {r.hqAccountId ? `(#${r.hqAccountId})` : ''}
</div>
</div>
),
},
{
title: '行为',
dataIndex: 'actionLabel',
width: 160,
render: (v, r) => <Tag color="blue">{v || resolveHqOperationLabel(r.action)}</Tag>,
},
{ title: '对象类型', dataIndex: 'refType', width: 120, render: (v) => v || '—' },
{ title: '对象 ID', dataIndex: 'refId', width: 100, render: (v) => v || '—' },
{
title: '操作',
width: 80,
render: (_, row) => (
<Button
type="link"
size="small"
onClick={async () => {
const res = await request<Row>(`/admin/logs/hq/${row.id}`);
setDetail(res);
setDrawerOpen(true);
}}
>
</Button>
),
},
];
return (
<div>
<Typography.Title level={4}>HQ </Typography.Title>
<Typography.Paragraph type="secondary">
</Typography.Paragraph>
<Form
form={form}
layout="inline"
style={{ marginBottom: 16 }}
onFinish={(values) => {
setFilters(values);
setPage(1);
const qs = new URLSearchParams();
if (values.hqAccountId) qs.set('hqAccountId', values.hqAccountId);
if (values.action) qs.set('action', values.action);
if (values.refType) qs.set('refType', values.refType);
setSearchParams(qs);
}}
>
<Form.Item name="hqAccountId" label="HQ 账户 ID">
<Input allowClear style={{ width: 140 }} />
</Form.Item>
<Form.Item name="action" label="行为">
<Select
allowClear
showSearch
optionFilterProp="label"
style={{ width: 180 }}
options={HQ_OPERATION_ACTION_OPTIONS.map((o) => ({ value: o.value, label: o.label }))}
/>
</Form.Item>
<Form.Item name="refType" label="对象类型">
<Input allowClear placeholder="ORDER / USER / CITY..." style={{ width: 140 }} />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit"></Button>
<Button onClick={() => {
form.resetFields();
setFilters({ hqAccountId: '', action: '', refType: '' });
setSearchParams({});
setPage(1);
void reload();
}}
>
</Button>
</Space>
</Form.Item>
</Form>
<Table
rowKey="id"
loading={loading}
columns={columns}
dataSource={data?.items ?? []}
scroll={{ x: 900 }}
pagination={{
current: page,
pageSize,
total: data?.total ?? 0,
showSizeChanger: true,
onChange: (p, ps) => {
setPage(p);
setPageSize(ps);
},
}}
/>
<Drawer title="操作详情" width={640} open={drawerOpen} onClose={() => setDrawerOpen(false)}>
{detail && (
<>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="日志 ID">{detail.id}</Descriptions.Item>
<Descriptions.Item label="时间">{fmtTime(detail.createdAt)}</Descriptions.Item>
<Descriptions.Item label="操作人">
{detail.hqName} / {detail.hqPhone} (ID: {detail.hqAccountId})
</Descriptions.Item>
<Descriptions.Item label="角色">{detail.hqRole || '—'}</Descriptions.Item>
<Descriptions.Item label="行为">
{detail.actionLabel || resolveHqOperationLabel(detail.action)}
</Descriptions.Item>
<Descriptions.Item label="对象">{detail.refType} / {detail.refId}</Descriptions.Item>
<Descriptions.Item label="状态">{detail.status || '—'}</Descriptions.Item>
<Descriptions.Item label="备注">{detail.remark || '—'}</Descriptions.Item>
</Descriptions>
<Typography.Title level={5} style={{ marginTop: 16 }}>/</Typography.Title>
<pre style={{
margin: 0, padding: 12, background: '#f5f5f5', borderRadius: 4,
maxHeight: 400, overflow: 'auto', fontSize: 12,
}}
>
{JSON.stringify(detail.detail, null, 2)}
</pre>
</>
)}
</Drawer>
</div>
);
}