4c38a9dd2b
增加hq管理端日志
197 lines
6.6 KiB
TypeScript
197 lines
6.6 KiB
TypeScript
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>
|
|
);
|
|
}
|