diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx
index 8de4ad89..69b5925a 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx
@@ -13,11 +13,14 @@ const mocks = vi.hoisted(() => ({
historyExports: vi.fn(),
createHistoryExport: vi.fn()
}));
+const auth = vi.hoisted(() => ({ role: 'admin' }));
vi.mock('../../api/client', () => ({ api: mocks }));
+vi.mock('../auth/AuthGate', () => ({ usePlatformSession: () => ({ session: { role: auth.role } }) }));
afterEach(() => {
cleanup();
+ auth.role = 'admin';
Object.values(mocks).forEach((mock) => mock.mockReset());
});
@@ -121,3 +124,18 @@ test('opens a working column visibility panel and preserves non-hideable identit
fireEvent.click(screen.getByRole('button', { name: '关闭列显示设置' }));
expect(screen.queryByRole('dialog', { name: '列显示设置' })).not.toBeInTheDocument();
});
+
+test('keeps history readable without mounting operator-only export requests for a viewer', async () => {
+ auth.role = 'viewer';
+ mocks.historyMetricCatalog.mockResolvedValue({ categories: [{ key: 'location', label: '位置数据' }], metrics: [metric] });
+ mocks.historyData.mockResolvedValue(historyData('OLDVIN', '旧车牌', 'old-as-of'));
+ mocks.historySeries.mockResolvedValue(historySeries('OLDVIN', '旧车牌', 'old-as-of'));
+ renderPage();
+
+ expect((await screen.findAllByText('旧车牌')).length).toBeGreaterThan(0);
+ expect(screen.getByText('只读 · 导出需操作员权限')).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /创建导出/ })).not.toBeInTheDocument();
+ expect(screen.queryByText('导出任务')).not.toBeInTheDocument();
+ expect(mocks.historyExports).not.toHaveBeenCalled();
+ expect(mocks.createHistoryExport).not.toHaveBeenCalled();
+});
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx
index 5f91332e..ac3e2a8f 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx
@@ -7,6 +7,8 @@ import type { HistoryDataResponse, HistoryDataRow, HistoryExportRequest, History
import { buildHistorySeriesPanels, formatExportFileSize, formatHistoryValue, formatSeriesGrain, historyExportPollInterval, parseHistoryKeywords } from '../domain/history';
import { InlineError } from '../shared/AsyncState';
import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
+import { usePlatformSession } from '../auth/AuthGate';
+import { canOperate } from '../auth/session';
function HistoryTrend({ response, category, loading, error }: { response?: HistorySeriesResponse; category: string; loading: boolean; error?: string }) {
const panels = useMemo(() => buildHistorySeriesPanels(response), [response]);
@@ -80,6 +82,8 @@ function ColumnVisibilityPanel({ metrics, visibleKeys, onToggle, onShowAll, onRe
}
export default function HistoryPage() {
+ const { session } = usePlatformSession();
+ const exportAllowed = canOperate(session);
const [searchParams, setSearchParams] = useSearchParams();
const today = useMemo(currentHistoryWindow, []);
const initial = { keywords: searchParams.get('vin') || searchParams.get('keywords') || '', dateFrom: searchParams.get('dateFrom') || today.dateFrom, dateTo: searchParams.get('dateTo') || today.dateTo, category: searchParams.get('category') || 'location', protocol: searchParams.get('protocol') || '' };
@@ -161,7 +165,7 @@ export default function HistoryPage() {
-
| 设备时间 | 服务时间 | 车牌 | VIN | 协议 | {visibleMetrics.map((metric) =>{metric.label}{metric.unit ? ` (${metric.unit})` : ''} | )}质量 | 操作 | |
|---|---|---|---|---|---|---|---|---|
| setSelectedRowRef(selectedRow?.id === row.id ? undefined : { scope: dataScope, id: row.id })} aria-label={`选择 ${row.plate || row.vin} ${row.deviceTime}`} /> | {row.deviceTime} | {row.serverTime} | {row.plate || '—'} | {row.vin} | {row.protocol} | {visibleMetrics.map((metric) =>{formatHistoryValue(row.values[metric.key], metric)} | )}{row.quality === 'normal' ? '正常' : row.quality} |