This commit is contained in:
@@ -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" /> },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
Reference in New Issue
Block a user