feat(admin): 管理端 UI/UX 深度优化——错误/加载/空状态、表单快捷键、移动端适配与无障碍

This commit is contained in:
2026-08-08 15:05:33 +08:00
parent 57639a3869
commit a0829f17ce
54 changed files with 702 additions and 369 deletions

View File

@@ -0,0 +1,125 @@
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { MemoryRouter, Route, Routes, useNavigate } from 'react-router';
import { RefreshButton } from './RefreshButton';
import { BackTop } from './BackTop';
import { ScrollToTop } from './ScrollToTop';
import { useSubmitShortcut } from '../hooks/useSubmitShortcut';
let container: HTMLDivElement | null = null;
let root: ReturnType<typeof createRoot> | null = null;
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
const mount = (node: React.ReactNode) => {
const host = document.createElement('div');
container = host;
document.body.appendChild(host);
root = createRoot(host);
act(() => root?.render(node));
};
afterEach(async () => {
if (root) {
await act(async () => root?.unmount());
}
container?.remove();
root = null;
container = null;
vi.restoreAllMocks();
});
describe('UX 组件与交互', () => {
it('RefreshButton 点击触发 onRefreshloading 时展示加载态', async () => {
const onRefresh = vi.fn();
mount(<RefreshButton onRefresh={onRefresh} />);
const button = container?.querySelector('button');
if (!button) throw new Error('button not rendered');
act(() => button.dispatchEvent(new MouseEvent('click', { bubbles: true })));
await flush();
expect(onRefresh).toHaveBeenCalledTimes(1);
mount(<RefreshButton onRefresh={onRefresh} loading />);
expect(container?.querySelector('.ant-btn-loading')).toBeTruthy();
});
it('useSubmitShortcut 未激活时不响应 Cmd+Enter', async () => {
const onSubmit = vi.fn();
const Harness = () => {
useSubmitShortcut(false, onSubmit);
return <button type="button">ok</button>;
};
mount(<Harness />);
window.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Enter', metaKey: true, bubbles: true }),
);
await flush();
expect(onSubmit).not.toHaveBeenCalled();
});
it('useSubmitShortcut 激活时响应 Cmd/Ctrl+Enter 且阻止默认行为', async () => {
const onSubmit = vi.fn();
const Harness = () => {
useSubmitShortcut(true, onSubmit);
return <button type="button">ok</button>;
};
mount(<Harness />);
const metaEvent = new KeyboardEvent('keydown', {
key: 'Enter',
metaKey: true,
bubbles: true,
cancelable: true,
});
window.dispatchEvent(metaEvent);
expect(metaEvent.defaultPrevented).toBe(true);
window.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Enter', ctrlKey: true, bubbles: true }),
);
await flush();
expect(onSubmit).toHaveBeenCalledTimes(2);
});
it('BackTop 超过阈值后出现,点击回到顶部', async () => {
const scrollSpy = vi.spyOn(window, 'scrollTo').mockImplementation(() => {});
mount(<BackTop threshold={-1} />);
await flush();
const button = container?.querySelector('button');
expect(button).toBeTruthy();
if (button) {
act(() => button.dispatchEvent(new MouseEvent('click', { bubbles: true })));
}
await flush();
expect(scrollSpy).toHaveBeenCalledWith(expect.objectContaining({ top: 0 }));
});
it('ScrollToTop 在路由切换时把滚动位置复位到顶部', async () => {
const scrollSpy = vi.spyOn(window, 'scrollTo').mockImplementation(() => {});
const Nav = () => {
const navigate = useNavigate();
return (
<button type="button" onClick={() => navigate('/other')}>
go
</button>
);
};
mount(
<MemoryRouter initialEntries={['/']}>
<ScrollToTop />
<Routes>
<Route path="/" element={<Nav />} />
<Route path="/other" element={<div>other</div>} />
</Routes>
</MemoryRouter>,
);
scrollSpy.mockClear();
const button = container?.querySelector('button');
if (!button) throw new Error('button not rendered');
act(() => button.dispatchEvent(new MouseEvent('click', { bubbles: true })));
await flush();
expect(scrollSpy).toHaveBeenCalledWith(0, 0);
});
});