unify Semi UI filter workspaces and pagination

This commit is contained in:
lingniu
2026-07-18 01:29:30 +08:00
parent 5c0a61f7bd
commit 6d63783630
11 changed files with 318 additions and 57 deletions

View File

@@ -7,14 +7,13 @@ describe('TablePagination', () => {
test('renders one shared Semi pagination pattern and changes pages', () => {
const onPageChange = vi.fn();
render(<footer><TablePagination page={2} totalPages={4} info="共 80 条" onPageChange={onPageChange} /></footer>);
const { container } = render(<footer><TablePagination page={2} totalPages={4} info="共 80 条" onPageChange={onPageChange} /></footer>);
expect(screen.getByText('共 80 条')).toHaveClass('v2-table-pagination-info');
expect(screen.getByText('2').closest('.v2-table-pagination-current')).toHaveTextContent('2/4');
const previous = screen.getByRole('button', { name: '上一页' });
const next = screen.getByRole('button', { name: '下一页' });
expect(previous).toHaveClass('semi-button');
expect(next).toHaveClass('semi-button');
expect(container.querySelector('.v2-table-pagination')).toHaveClass('semi-page');
expect(container.querySelector('.semi-page-item-small')).toHaveTextContent('2/4');
const previous = screen.getByRole('button', { name: 'Previous' });
const next = screen.getByRole('button', { name: 'Next' });
fireEvent.click(previous);
fireEvent.click(next);
expect(onPageChange).toHaveBeenNthCalledWith(1, 1);
@@ -23,8 +22,8 @@ describe('TablePagination', () => {
test('clamps invalid pages and disables both boundaries for a single page', () => {
render(<footer><TablePagination page={0} totalPages={0} info="暂无数据" onPageChange={vi.fn()} /></footer>);
expect(screen.getByRole('button', { name: '上一页' })).toBeDisabled();
expect(screen.getByRole('button', { name: '下一页' })).toBeDisabled();
expect(document.querySelector('.v2-table-pagination-current')).toHaveTextContent('1/1');
expect(screen.getByRole('button', { name: 'Previous' })).toHaveAttribute('aria-disabled', 'true');
expect(screen.getByRole('button', { name: 'Next' })).toHaveAttribute('aria-disabled', 'true');
expect(document.querySelector('.semi-page-item-small')).toHaveTextContent('1/1');
});
});

View File

@@ -1,5 +1,4 @@
import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons';
import { Button, Select } from '@douyinfe/semi-ui';
import { Pagination, Select } from '@douyinfe/semi-ui';
import { useId, type ReactNode } from 'react';
type PageSizeOption = {
@@ -37,30 +36,17 @@ export function TablePagination({
return <>
<span className="v2-table-pagination-info">{info}</span>
<div className="v2-table-pagination-controls">
<Button
className="v2-table-pagination-button"
theme="light"
icon={<IconChevronLeft />}
disabled={disabled || safePage <= 1}
aria-label="上一页"
onClick={() => onPageChange(safePage - 1)}
>
</Button>
<span className="v2-table-pagination-current" aria-live="polite" aria-atomic="true">
<b>{safePage}</b><i>/</i><span>{safeTotalPages}</span>
</span>
<Button
className="v2-table-pagination-button"
theme="light"
icon={<IconChevronRight />}
iconPosition="right"
disabled={disabled || safePage >= safeTotalPages}
aria-label="下一页"
onClick={() => onPageChange(safePage + 1)}
>
</Button>
<Pagination
className="v2-table-pagination"
size="small"
currentPage={safePage}
total={safeTotalPages}
pageSize={1}
disabled={disabled}
hideOnSinglePage={false}
hoverShowPageSelect={!disabled}
onPageChange={onPageChange}
/>
{pageSize != null && pageSizeOptions?.length && onPageSizeChange ? <>
<span className="v2-sr-only" id={pageSizeID}>{pageSizeLabel}</span>
<Select

View File

@@ -0,0 +1,33 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, test, vi } from 'vitest';
import { WorkspaceFilterPanel } from './WorkspaceFilterPanel';
describe('WorkspaceFilterPanel', () => {
afterEach(cleanup);
test('keeps desktop context and mobile disclosure in one shared component', () => {
const onToggle = vi.fn();
const { container } = render(
<WorkspaceFilterPanel
title="事件筛选"
description="按关键词与状态收窄范围"
mobileSummary="已启用 2 项"
expanded={false}
status="2 项已启用"
statusColor="blue"
onToggle={onToggle}
>
<form aria-label="事件筛选表单"><input aria-label="关键词" /></form>
</WorkspaceFilterPanel>
);
expect(screen.getByText('按关键词与状态收窄范围')).toBeInTheDocument();
expect(screen.getByText('2 项已启用')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '修改事件筛选:已启用 2 项' })).toHaveAttribute('aria-expanded', 'false');
expect(container.querySelector('.v2-workspace-filter-panel')).toHaveClass('semi-card', 'is-mobile-collapsed');
expect(screen.getByRole('form', { name: '事件筛选表单' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '修改事件筛选:已启用 2 项' }));
expect(onToggle).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,59 @@
import { IconFilter } from '@douyinfe/semi-icons';
import { Card, Tag, Typography } from '@douyinfe/semi-ui';
import type { ComponentProps, ReactNode } from 'react';
import { MobileFilterToggle } from './MobileFilterToggle';
const { Text } = Typography;
type WorkspaceFilterPanelProps = {
title?: string;
description: string;
mobileSummary: string;
expanded: boolean;
onToggle: () => void;
children: ReactNode;
className?: string;
status?: ReactNode;
statusColor?: ComponentProps<typeof Tag>['color'];
collapsedLabel?: string;
};
export function WorkspaceFilterPanel({
title = '查询条件',
description,
mobileSummary,
expanded,
onToggle,
children,
className = '',
status = '默认范围',
statusColor = 'grey',
collapsedLabel = '修改'
}: WorkspaceFilterPanelProps) {
const panelClassName = [
'v2-workspace-filter-panel',
className,
expanded ? '' : 'is-mobile-collapsed'
].filter(Boolean).join(' ');
return <>
<MobileFilterToggle
title={title}
summary={mobileSummary}
expanded={expanded}
collapsedLabel={collapsedLabel}
onToggle={onToggle}
/>
<Card className={panelClassName} bodyStyle={{ padding: 0 }}>
<header className="v2-workspace-filter-heading">
<span className="v2-workspace-filter-icon" aria-hidden="true"><IconFilter /></span>
<span className="v2-workspace-filter-copy">
<Text strong>{title}</Text>
<Text type="tertiary" size="small">{description}</Text>
</span>
<Tag color={statusColor} type="light" size="small">{status}</Tag>
</header>
{children}
</Card>
</>;
}