fix: 完善监控页签与通知弹窗异步交互
CSS quality / css-quality (push) Has been cancelled

This commit is contained in:
hectorzhao
2026-09-06 19:46:08 +08:00
parent 457319e627
commit 247fee6d6b
6 changed files with 68 additions and 33 deletions
+14 -22
View File
@@ -12,6 +12,7 @@ import {
Pagination,
Select,
Table,
Tabs,
Tag,
type DateRangeValue,
type TableColumn,
@@ -150,29 +151,20 @@ function RecordDetailModal({ record, onClose }: { record: ReportRecord; onClose:
export function AdminReportRecordsPage() {
const [params, setParams] = useSearchParams();
const readiness = params.get('tab') === 'readiness';
return (
<section className="page-stack">
<div className="page-heading" role="tablist" aria-label="状态记录类型">
<Button
role="tab"
aria-selected={!readiness}
variant={readiness ? 'secondary' : 'primary'}
onClick={() => setParams({})}
>
</Button>
<Button
role="tab"
aria-selected={readiness}
variant={readiness ? 'primary' : 'secondary'}
onClick={() => setParams({ tab: 'readiness' })}
>
</Button>
</div>
{readiness ? <ReportNotificationsPage portal="admin" /> : <ReportStatusRecords />}
</section>
<Tabs
value={params.get('tab') === 'readiness' ? 'readiness' : 'records'}
onChange={(value) => {
const next = new URLSearchParams(params);
if (value === 'readiness') next.set('tab', value);
else next.delete('tab');
setParams(next);
}}
items={[
{ label: '状态记录', value: 'records', content: <ReportStatusRecords /> },
{ label: '报备状态变化消息', value: 'readiness', content: <ReportNotificationsPage portal="admin" /> },
]}
/>
);
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Activity } from 'lucide-react';
import { Breadcrumb, Button, CarrierTag, Table, Tag, type TableColumn } from '@/components/ui';
import { Button, CarrierTag, Table, Tag, type TableColumn } from '@/components/ui';
import { adminApi, type AdminChannel } from '@/api/adminApi';
const columns: Array<TableColumn<AdminChannel>> = [
@@ -77,7 +77,7 @@ export function MonitorRuntimeOverview() {
<section className="page-stack">
<div className="page-heading">
<div>
<Breadcrumb items={['发送监控']} />
<h2></h2>
</div>
<Button
icon={<Activity size={16} />}
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, expect, it, vi } from 'vitest';
import { ReportNotificationsPage } from './ReportNotificationsPage';
@@ -57,3 +57,21 @@ it('reports a failed read without pretending success', async () => {
expect(await screen.findByRole('alert')).toHaveTextContent('标记失败');
expect(screen.getByRole('button', { name: '标记已读' })).toBeEnabled();
});
it('does not reopen a closed dialog when the read response arrives later', async () => {
const user = userEvent.setup();
render(<ReportNotificationsPage />);
await user.click(await screen.findByRole('button', { name: '查看消息' }));
let finish!: (result: unknown) => void;
api.request.mockImplementationOnce(
() =>
new Promise((resolve) => {
finish = resolve;
}),
);
await user.click(await screen.findByRole('button', { name: '标记已读' }));
const closeButtons = screen.getAllByRole('button', { name: /^关闭$/ });
await user.click(closeButtons[closeButtons.length - 1]);
await act(async () => finish({ success: true }));
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
});
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Breadcrumb, Button, Modal, Table, Tag, type TableColumn } from '@/components/ui';
import { request } from '@/api/core/httpClient';
import './report-notifications.css';
@@ -32,6 +32,12 @@ export function ReportNotificationsPage({ portal = 'client' }: { portal?: 'admin
const [detail, setDetail] = useState<{ hour: Hour; items: Event[]; total: number; page: number } | null>(null);
const [detailError, setDetailError] = useState('');
const [busy, setBusy] = useState(false);
const detailRequest = useRef(0);
const closeDetail = () => {
detailRequest.current++;
setDetail(null);
setBusy(false);
};
const base = `/${portal}/report-notifications`;
const load = useCallback(
async (signal?: AbortSignal) => {
@@ -54,32 +60,40 @@ export function ReportNotificationsPage({ portal = 'client' }: { portal?: 'admin
return () => controller.abort();
}, [load]);
async function open(item: Hour, detailPage = 1) {
const sequence = ++detailRequest.current;
setBusy(true);
setDetailError('');
try {
const result = await request<{ hour: Hour } & Page<Event>>(`${base}/${item.id}?page=${detailPage}`);
if (sequence !== detailRequest.current) return;
setDetail({ ...result, hour: { ...result.hour, unread: item.unread } });
} catch (e) {
setError(e instanceof Error ? e.message : '详情加载失败');
if (sequence !== detailRequest.current) return;
const message = e instanceof Error ? e.message : '详情加载失败';
if (detail) setDetailError(message);
else setError(message);
} finally {
setBusy(false);
if (sequence === detailRequest.current) setBusy(false);
}
}
async function markRead() {
if (!detail) return;
const sequence = ++detailRequest.current;
setDetailError('');
setBusy(true);
try {
await request(`${base}/${detail.hour.id}/read`, {
method: 'POST',
body: JSON.stringify({ revision: detail.hour.revision }),
});
setDetail({ ...detail, hour: { ...detail.hour, unread: false } });
if (sequence === detailRequest.current)
setDetail((current) => (current ? { ...current, hour: { ...current.hour, unread: false } } : null));
window.dispatchEvent(new Event('cmpp-report-notification-refresh'));
await load();
} catch (e) {
setDetailError(e instanceof Error ? e.message : '标记已读失败');
if (sequence === detailRequest.current) setDetailError(e instanceof Error ? e.message : '标记已读失败');
} finally {
setBusy(false);
if (sequence === detailRequest.current) setBusy(false);
}
}
const columns: TableColumn<Hour>[] = [
@@ -163,10 +177,10 @@ export function ReportNotificationsPage({ portal = 'client' }: { portal?: 'admin
open
title="报备状态变化消息"
size="xl"
onClose={() => setDetail(null)}
onClose={closeDetail}
footer={
<>
<Button variant="secondary" onClick={() => setDetail(null)}>
<Button variant="secondary" onClick={closeDetail}>
</Button>
<Button disabled={busy || !detail.hour.unread} onClick={() => void markRead()}>