fix(web): 操作日志列头吸顶并移除重复列表工具条

Semi Table 增加 scroll.y(按容器高度测量),表头固定、仅表体滚动;移除「日志列表 / 已加载 N 条」重复工具条,刷新按钮移到页头,条数保留在底部分页;表格宽度统一跟随容器,修复宽屏右侧空白与 1024 留白。
This commit is contained in:
Stev_Wang committed 2026-11-04 16:55:54 +08:00
1 parent ab951894b9
commit dca498ec15
3 files changed
+35 -57

No files matched your search

@@ -5,7 +5,7 @@ import type { AuditLogItem } from '@lingxi/shared';
import { notifyError, notifySuccess, PageEmpty } from '@lingxi/ui'; import { notifyError, notifySuccess, PageEmpty } from '@lingxi/ui';
import { IconCheck, IconCopy, IconRefresh } from '@lingxi/ui/icons'; import { IconCheck, IconCopy, IconRefresh } from '@lingxi/ui/icons';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { copyToClipboard } from './audit-clipboard'; import { copyToClipboard } from './audit-clipboard';
import { import {
@@ -19,7 +19,6 @@ import {
export interface AuditLogTableProps { export interface AuditLogTableProps {
items: readonly AuditLogItem[]; items: readonly AuditLogItem[];
loading: boolean; loading: boolean;
refreshing?: boolean;
isFetchingNextPage: boolean; isFetchingNextPage: boolean;
hasNextPage: boolean; hasNextPage: boolean;
/** 追加分页失败时的提示(保留已有数据) */ /** 追加分页失败时的提示(保留已有数据) */
@@ -27,7 +26,6 @@ export interface AuditLogTableProps {
onOpen: (logId: string) => void; onOpen: (logId: string) => void;
onLoadMore: () => void; onLoadMore: () => void;
onRetryMore: () => void; onRetryMore: () => void;
onRefresh: () => void;
onClearFilters: () => void; onClearFilters: () => void;
} }
@@ -92,17 +90,29 @@ function useNarrowTable(): boolean {
export function AuditLogTable({ export function AuditLogTable({
items, items,
loading, loading,
refreshing = false,
isFetchingNextPage, isFetchingNextPage,
hasNextPage, hasNextPage,
errorMessage = null, errorMessage = null,
onOpen, onOpen,
onLoadMore, onLoadMore,
onRetryMore, onRetryMore,
onRefresh,
onClearFilters, onClearFilters,
}: AuditLogTableProps): ReactNode { }: AuditLogTableProps): ReactNode {
const narrow = useNarrowTable(); const narrow = useNarrowTable();
const wrapRef = useRef<HTMLDivElement>(null);
const [tableHeight, setTableHeight] = useState(360);
useEffect(() => {
const element = wrapRef.current;
if (element === null || typeof ResizeObserver === 'undefined') return undefined;
const observer = new ResizeObserver((entries) => {
const height = Math.floor(entries[0]?.contentRect.height ?? 0);
if (height > 0) setTableHeight(Math.max(160, height - 40));
});
observer.observe(element);
return () => {
observer.disconnect();
};
}, []);
const columns: ColumnProps<AuditLogItem>[] = [ const columns: ColumnProps<AuditLogItem>[] = [
{ {
title: '时间', title: '时间',
@@ -250,21 +260,7 @@ export function AuditLogTable({
return ( return (
<section className="lx-audit-table-card" aria-label="操作日志列表"> <section className="lx-audit-table-card" aria-label="操作日志列表">
<div className="lx-audit-table-card__head"> <div className="lx-audit-table-wrap" ref={wrapRef}>
<b>日志列表</b>
<span className="lx-audit-table-card__count">{pagerText}</span>
<div className="lx-audit-table-card__tools">
<Button
theme="borderless"
icon={<IconRefresh size={16} />}
loading={refreshing}
aria-label="刷新日志"
onClick={onRefresh}
/>
</div>
</div>
<div className="lx-audit-table-wrap">
<Table<AuditLogItem> <Table<AuditLogItem>
className="lx-audit-table" className="lx-audit-table"
columns={visibleColumns} columns={visibleColumns}
@@ -273,7 +269,7 @@ export function AuditLogTable({
pagination={false} pagination={false}
loading={loading} loading={loading}
size="middle" size="middle"
scroll={{ x: narrow ? 760 : '100%' }} scroll={{ x: '100%', y: tableHeight }}
empty={emptyState(onClearFilters)} empty={emptyState(onClearFilters)}
onRow={(record) => onRow={(record) =>
record === undefined record === undefined
@@ -297,7 +293,7 @@ export function AuditLogTable({
<div className="lx-audit-pager"> <div className="lx-audit-pager">
<span> <span>
游标分页 · 按操作时间倒序 {pagerText} · 游标分页 · 按操作时间倒序
{errorMessage === null ? null : ( {errorMessage === null ? null : (
<em className="lx-audit-pager__error"> · {errorMessage}</em> <em className="lx-audit-pager__error"> · {errorMessage}</em>
)} )}
@@ -2,7 +2,7 @@
import { Button, Modal } from '@douyinfe/semi-ui-19'; import { Button, Modal } from '@douyinfe/semi-ui-19';
import { Permission } from '@lingxi/shared'; import { Permission } from '@lingxi/shared';
import { ErrorState } from '@lingxi/ui'; import { ErrorState } from '@lingxi/ui';
import { IconAlert, IconDownload, IconLock } from '@lingxi/ui/icons'; import { IconAlert, IconDownload, IconLock, IconRefresh } from '@lingxi/ui/icons';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router'; import { useNavigate, useSearchParams } from 'react-router';
@@ -113,6 +113,17 @@ export function AuditLogsPage(): ReactNode {
<p>关键操作留痕 · 只读 · append-only</p> <p>关键操作留痕 · 只读 · append-only</p>
</div> </div>
<div className="lx-audit-page__actions"> <div className="lx-audit-page__actions">
<Button
theme="borderless"
icon={<IconRefresh size={16} />}
loading={logs.isFetching}
aria-label="刷新日志"
onClick={() => {
void logs.refetch();
}}
>
刷新
</Button>
{canExport ? ( {canExport ? (
<> <>
<span className="lx-audit-readonly"> <span className="lx-audit-readonly">
@@ -181,7 +192,6 @@ export function AuditLogsPage(): ReactNode {
<AuditLogTable <AuditLogTable
items={items} items={items}
loading={logs.isPending} loading={logs.isPending}
refreshing={logs.isFetching && !logs.isFetchingNextPage}
isFetchingNextPage={logs.isFetchingNextPage} isFetchingNextPage={logs.isFetchingNextPage}
hasNextPage={logs.hasNextPage} hasNextPage={logs.hasNextPage}
errorMessage={moreError} errorMessage={moreError}
@@ -192,9 +202,6 @@ export function AuditLogsPage(): ReactNode {
onRetryMore={() => { onRetryMore={() => {
void logs.fetchNextPage(); void logs.fetchNextPage();
}} }}
onRefresh={() => {
void logs.refetch();
}}
onClearFilters={resetFilters} onClearFilters={resetFilters}
/> />
)} )}
+5 -30
View File
@@ -202,47 +202,22 @@
box-shadow: var(--lx-shadow-panel); box-shadow: var(--lx-shadow-panel);
} }
.lx-audit-table-card__head {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 10px;
min-height: 48px;
padding: 0 16px;
border-bottom: 1px solid var(--lx-color-border);
}
.lx-audit-table-card__head b {
font-size: var(--lx-font-size-sm);
font-weight: 600;
}
.lx-audit-table-card__count {
color: var(--lx-color-text-tertiary);
font-size: var(--lx-font-size-xs);
font-variant-numeric: tabular-nums;
}
.lx-audit-table-card__tools {
margin-left: auto;
}
.lx-audit-table-wrap { .lx-audit-table-wrap {
min-height: 0; min-height: 0;
flex: 1; flex: 1;
overflow: auto; overflow: hidden;
}
.lx-audit-table .semi-table {
min-width: 1080px;
} }
.lx-audit-table .semi-table-thead > .semi-table-row > .semi-table-row-head { .lx-audit-table .semi-table-thead > .semi-table-row > .semi-table-row-head {
position: sticky;
top: 0;
z-index: 3;
height: 40px; height: 40px;
background: var(--lx-color-bg-subtle); background: var(--lx-color-bg-subtle);
color: var(--lx-color-text-tertiary); color: var(--lx-color-text-tertiary);
font-size: var(--lx-font-size-xs); font-size: var(--lx-font-size-xs);
font-weight: 500; font-weight: 500;
box-shadow: inset 0 -1px 0 var(--lx-color-border);
} }
.lx-audit-table .semi-table-tbody > .semi-table-row > .semi-table-row-cell { .lx-audit-table .semi-table-tbody > .semi-table-row > .semi-table-row-cell {