优化系统日志页面样式布局

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-05-02 23:15:55 +08:00
parent 484b24f53c
commit 58a20f25cd
2 changed files with 110 additions and 73 deletions
+25
View File
@@ -190,3 +190,28 @@
- 风险与影响:
- 本地若仍保留旧文件名 `.env.prod`,将不再被 dev compose 自动读取。
## Work Log - 系统日志页面样式优化(2026-05-02)
- 背景:
- Issue [FL-170] 要求“参考菜单管理页面,调整系统日志页面样式和布局”。
- 本次改动(最小闭环):
- 文件:`web/src/app/admin/syslog/page.tsx`
- 对齐 `menus` 页面风格,保持日志查询与筛选逻辑不变,仅调整 UI 布局:
- 页面容器从 `Space` 改为 `div.space-y-6`,与后台管理页一致。
- 加载态改为居中 `Spin``min-h-[240px]`)。
- 未登录/无权限态改为与菜单页一致的 `main + Link` 布局与按钮样式。
- 筛选区改为 `Form inline`(动作、用户ID、查询/重置筛选)。
- 错误提示统一为 `Alert message + description`
- 列表分页补充 `showTotal`,信息文案统一。
- 验证:
- 差异检查:`git diff -- web/src/app/admin/syslog/page.tsx`
- 尝试运行类型检查(未安装依赖,不做编译):
- `npm --workspace web exec tsc --noEmit --pretty false`
- 当前环境报错 `EROFS`(npm 缓存目录只读),未执行到项目级类型检查。
- 风险与影响:
- 影响范围仅 `syslog` 页面前端展示层。
- 接口请求、权限判断、筛选参数、分页逻辑保持不变。
+85 -73
View File
@@ -1,17 +1,19 @@
"use client";
import Link from "next/link";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Alert, Button, Empty, Input, Space, Spin, Table, Tag, Typography } from "antd";
import { Alert, Button, Card, Empty, Form, Input, Space, Spin, Table, Tag, Typography, type CardProps } from "antd";
import type { ColumnsType } from "antd/es/table";
import type { ComponentType } from "react";
import { useCallback, useMemo, useState } from "react";
import { useAuth } from "@/components/auth-provider";
import { Card } from "@/components/ui-antd";
import { useTopicSubscription } from "@/hooks/use-topic-subscription";
import { readApiError } from "@/lib/api";
import type { AuditLogItem, AuditLogListResponse } from "@/types/auth";
const PAGE_SIZE = 50;
const AntCard = Card as unknown as ComponentType<CardProps>;
type Filters = {
action: string;
@@ -127,95 +129,104 @@ export default function AdminSyslogPage() {
if (initializing || logsQuery.isLoading) {
return (
<Card>
<Space>
<Spin size="small" />
<Typography.Text type="secondary">...</Typography.Text>
</Space>
</Card>
<div className="flex min-h-[240px] items-center justify-center">
<Spin tip="系统日志加载中..." />
</div>
);
}
if (!user) {
return (
<Card>
<Space direction="vertical" size={12}>
<Alert type="info" showIcon message="请先登录后再访问系统日志页面。" />
<Button href="/"></Button>
</Space>
</Card>
<main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col justify-center gap-4 px-6 py-20">
<p className="text-sm text-[var(--gray-11)]">访</p>
<Link
href="/"
className="inline-flex w-fit items-center justify-center rounded-md border border-[var(--gray-6)] bg-[var(--gray-a2)] px-4 py-2 text-sm font-medium text-[var(--gray-12)] transition hover:bg-[var(--gray-a3)]"
>
</Link>
</main>
);
}
if (!canRead) {
return (
<Card>
<Space direction="vertical" size={12}>
<Alert type="error" showIcon message="你没有访问该页面的权限(需要 `menu.read`)。" />
<Button href="/"></Button>
</Space>
</Card>
<main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col justify-center gap-4 px-6 py-20">
<p className="text-sm text-[var(--gray-11)]">访 `menu.read`</p>
<Link
href="/"
className="inline-flex w-fit items-center justify-center rounded-md border border-[var(--gray-6)] bg-[var(--gray-a2)] px-4 py-2 text-sm font-medium text-[var(--gray-12)] transition hover:bg-[var(--gray-a3)]"
>
</Link>
</main>
);
}
return (
<Space direction="vertical" size={16} className="w-full">
{error ? <Alert type="error" showIcon message={error} /> : null}
<div className="space-y-6">
{error ? <Alert type="error" showIcon message="日志加载失败" description={error} /> : null}
<Card title="系统日志" extra={<Typography.Text type="secondary">auth.login / auth.logout / auth.refresh</Typography.Text>}>
<AntCard title="系统日志" extra={<Typography.Text type="secondary">auth.login / auth.logout / auth.refresh</Typography.Text>}>
<Typography.Paragraph type="secondary" className="!mb-4">
</Typography.Paragraph>
<Space wrap size={12}>
<Input
allowClear
className="!w-72"
placeholder="按动作筛选(如 auth.login"
value={draftFilters.action}
onChange={(event) =>
setDraftFilters((prev) => ({
...prev,
action: event.target.value,
}))
}
/>
<Input
allowClear
className="!w-72"
placeholder="按用户ID筛选(如 openclaw"
value={draftFilters.user_id}
onChange={(event) =>
setDraftFilters((prev) => ({
...prev,
user_id: event.target.value,
}))
}
/>
<Button
type="primary"
onClick={() => {
setOffset(0);
setFilters({
action: draftFilters.action.trim(),
user_id: draftFilters.user_id.trim(),
});
}}
>
</Button>
<Button
onClick={() => {
setOffset(0);
setDraftFilters(EMPTY_FILTERS);
setFilters(EMPTY_FILTERS);
}}
>
</Button>
</Space>
<Form layout="inline" style={{ rowGap: 12 }}>
<Form.Item label="动作" className="min-w-[280px]">
<Input
allowClear
placeholder="按动作筛选(如 auth.login"
value={draftFilters.action}
onChange={(event) =>
setDraftFilters((prev) => ({
...prev,
action: event.target.value,
}))
}
/>
</Form.Item>
<Form.Item label="用户ID" className="min-w-[280px]">
<Input
allowClear
placeholder="按用户ID筛选(如 openclaw"
value={draftFilters.user_id}
onChange={(event) =>
setDraftFilters((prev) => ({
...prev,
user_id: event.target.value,
}))
}
/>
</Form.Item>
<Form.Item>
<Space size={8}>
<Button
type="primary"
onClick={() => {
setOffset(0);
setFilters({
action: draftFilters.action.trim(),
user_id: draftFilters.user_id.trim(),
});
}}
>
</Button>
<Button
onClick={() => {
setOffset(0);
setDraftFilters(EMPTY_FILTERS);
setFilters(EMPTY_FILTERS);
}}
>
</Button>
</Space>
</Form.Item>
</Form>
<div className="mb-4 mt-5 flex flex-wrap items-center justify-between gap-3">
<div className="mt-4 flex flex-wrap items-center justify-between gap-3">
<Typography.Text type="secondary">
{total} {currentPage}
</Typography.Text>
@@ -234,13 +245,14 @@ export default function AdminSyslogPage() {
onChange: (page) => setOffset((page - 1) * PAGE_SIZE),
showSizeChanger: false,
showQuickJumper: false,
showTotal: (value) => `${value}`,
}}
locale={{
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无日志数据" />,
}}
scroll={{ x: 980 }}
/>
</Card>
</Space>
</AntCard>
</div>
);
}