fix(web): 操作日志列头吸顶并移除重复列表工具条
Semi Table 增加 scroll.y(按容器高度测量),表头固定、仅表体滚动;移除「日志列表 / 已加载 N 条」重复工具条,刷新按钮移到页头,条数保留在底部分页;表格宽度统一跟随容器,修复宽屏右侧空白与 1024 留白。
This commit is contained in:
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user