feat: unify export task workspace
This commit is contained in:
@@ -283,8 +283,11 @@ test('renders only selectable Semi history cards on mobile', async () => {
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /导出任务/ }));
|
||||
expect(await screen.findByRole('dialog', { name: '历史数据导出任务' })).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-history-export-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-task-sidesheet');
|
||||
expect(document.querySelector('.v2-history-export-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(76dvh, 680px)' });
|
||||
expect(document.querySelector('.v2-history-export-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(82dvh, 720px)' });
|
||||
await waitFor(() => expect(mocks.historyExports).toHaveBeenCalledTimes(1));
|
||||
expect(screen.getByRole('list', { name: '历史数据导出任务摘要' })).toHaveTextContent('生成中0排队与执行可下载0已完成任务需处理0失败任务');
|
||||
expect(screen.getByRole('button', { name: '刷新导出任务' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '完成' })).toBeInTheDocument();
|
||||
expect(screen.getByText('暂无导出任务')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭历史数据导出任务' }));
|
||||
expect(toolsButton).toHaveAttribute('aria-expanded', 'false');
|
||||
@@ -388,9 +391,12 @@ test('shows only the authenticated customer export workspace with owner and scop
|
||||
fireEvent.click(await screen.findByRole('button', { name: '查看导出任务' }));
|
||||
expect(await screen.findByText(/customer-a · 1 辆/)).toBeInTheDocument();
|
||||
expect(screen.getByRole('dialog', { name: '历史数据导出任务' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('list', { name: '导出任务状态概览' })).toHaveTextContent('生成中0排队与执行可下载1已完成任务需处理0失败任务');
|
||||
expect(screen.getByRole('list', { name: '历史数据导出任务摘要' })).toHaveTextContent('生成中0排队与执行可下载1已完成任务需处理0失败任务');
|
||||
expect(screen.getByText('最近更新')).toBeInTheDocument();
|
||||
expect(screen.getByText('2026-07-16 13:00:01')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '刷新导出任务' }));
|
||||
await waitFor(() => expect(mocks.historyExports).toHaveBeenCalledTimes(2));
|
||||
expect(screen.getByRole('button', { name: /创建导出/ })).toBeInTheDocument();
|
||||
expect(mocks.historyExports).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('uses selected chartable fields for trends and omits empty RAW evidence UI', async () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';
|
||||
import { IconChevronRight, IconClose, IconDownload, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
|
||||
import { Button, Card, CardGroup, Checkbox, Descriptions, Dropdown, Empty, Input, Progress, Select, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { FormEvent, KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { api } from '../../api/client';
|
||||
import type { HistoryDataResponse, HistoryDataRow, HistoryExportRequest, HistoryMetricDefinition, HistorySeriesResponse } from '../../api/types';
|
||||
import type { HistoryDataResponse, HistoryDataRow, HistoryExportJob, HistoryExportRequest, HistoryMetricDefinition, HistorySeriesResponse } from '../../api/types';
|
||||
import { buildHistorySeriesPanels, formatExportFileSize, formatHistoryValue, formatSeriesGrain, historyExportPollInterval, parseHistoryKeywords } from '../domain/history';
|
||||
import { downloadBlob } from '../domain/download';
|
||||
import { formatShanghaiDateTime } from '../domain/formatters';
|
||||
@@ -27,7 +27,6 @@ import { usePlatformSession } from '../auth/AuthGate';
|
||||
import { canOperate } from '../auth/session';
|
||||
import { monitorReturnFromParams, preserveMonitorReturn } from '../routing/monitorContext';
|
||||
import { useMobileLayout } from '../hooks/useMobileLayout';
|
||||
import { useSideSheetA11y } from '../hooks/useSideSheetA11y';
|
||||
|
||||
const DESKTOP_HISTORY_PAGE_SIZE = 50;
|
||||
const MOBILE_HISTORY_PAGE_SIZE = 20;
|
||||
@@ -133,37 +132,81 @@ function ExportDownloadButton({ id }: { id: string }) {
|
||||
return <Button className="v2-export-download" theme="borderless" icon={<IconDownload />} disabled={mutation.isPending} title={mutation.error instanceof Error ? mutation.error.message : '下载当前账号有权访问的导出文件'} onClick={() => mutation.mutate()}>{mutation.isPending ? '下载中' : mutation.isError ? '重试' : '下载'}</Button>;
|
||||
}
|
||||
|
||||
function ExportJobsPanel({ showHeader = true }: { showHeader?: boolean }) {
|
||||
const query = useQuery({
|
||||
function useHistoryExportJobs(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: ['history-exports'],
|
||||
queryFn: ({ signal }) => api.historyExports(signal),
|
||||
enabled,
|
||||
refetchInterval: (current) => historyExportPollInterval(current.state.data),
|
||||
...LIVE_QUERY_POLICY,
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime
|
||||
});
|
||||
}
|
||||
|
||||
function ExportJobsPanel({ query, showHeader = true }: { query: UseQueryResult<HistoryExportJob[], Error>; showHeader?: boolean }) {
|
||||
const jobs = query.data ?? [];
|
||||
const statusLabel = (status: string) => status === 'queued' ? '排队中' : status === 'running' ? '执行中' : status === 'completed' ? '已完成' : '失败';
|
||||
const statusColor = (status: string) => status === 'completed' ? 'green' : status === 'running' ? 'blue' : status === 'failed' ? 'red' : 'grey';
|
||||
const activeJobs = jobs.filter((job) => job.status === 'queued' || job.status === 'running').length;
|
||||
const completedJobs = jobs.filter((job) => job.status === 'completed').length;
|
||||
const failedJobs = jobs.filter((job) => job.status === 'failed').length;
|
||||
return <Card className="v2-export-jobs" bodyStyle={{ padding: 0 }}>{showHeader ? <WorkspacePanelHeader title="导出任务" description="异步生成并保留可追溯下载记录" meta={`${jobs.length.toLocaleString('zh-CN')} 个任务`} /> : null}
|
||||
{query.isPending || query.isError || !jobs.length ? null : <div className="v2-export-job-overview" role="list" aria-label="导出任务状态概览">
|
||||
<span className={activeJobs ? 'is-active' : ''} role="listitem"><small>生成中</small><strong>{activeJobs}</strong><em>排队与执行</em></span>
|
||||
<span className="is-completed" role="listitem"><small>可下载</small><strong>{completedJobs}</strong><em>已完成任务</em></span>
|
||||
<span className={failedJobs ? 'is-failed' : ''} role="listitem"><small>需处理</small><strong>{failedJobs}</strong><em>失败任务</em></span>
|
||||
</div>}
|
||||
<div className="v2-export-job-list">{query.isPending ? <div className="v2-history-side-loading"><Spin size="middle" tip="正在读取导出任务" /></div> : query.isError ? <Empty className="v2-history-side-empty" title="导出任务加载失败" description="请稍后刷新重试。" /> : !jobs.length ? <Empty className="v2-history-side-empty" title="暂无导出任务" description="创建任务后可在这里查看进度并下载。" /> : <CardGroup type="grid" spacing={0}>{jobs.slice(0, 6).map((job) => <Card className={`v2-export-job-card is-${job.status}`} key={job.id} title={<span className="v2-export-job-name" title={job.name}>{job.name}</span>} headerExtraContent={<Tag color={statusColor(job.status)} type="light" size="small">{statusLabel(job.status)}</Tag>} headerLine bodyStyle={{ padding: 0 }}>
|
||||
<div className="v2-export-job-summary">
|
||||
<strong>{job.status === 'queued' ? '等待单并发执行' : job.status === 'running' ? `${job.processedRows.toLocaleString('zh-CN')} / ${job.totalRows.toLocaleString('zh-CN')} 行` : job.status === 'completed' ? `${job.rowCount.toLocaleString('zh-CN')} 行 · ${formatExportFileSize(job.fileSizeBytes)}` : job.error || '任务失败'}</strong>
|
||||
<span><small>{job.ownerUsername || job.ownerName || '历史任务'} · {job.vehicleVins?.length ?? job.keywords.length} 辆</small>{job.downloadUrl ? <ExportDownloadButton id={job.id} /> : null}</span>
|
||||
<small>{job.dateFrom && job.dateTo ? `${job.dateFrom.replace('T', ' ')} 至 ${job.dateTo.replace('T', ' ')}` : job.createdAt}</small>
|
||||
<small>{job.dateFrom && job.dateTo ? `${formatHistoryDateTime(job.dateFrom)} 至 ${formatHistoryDateTime(job.dateTo)}` : <>创建于 <PlatformTime value={job.createdAt} /></>}</small>
|
||||
</div>
|
||||
{job.status === 'running' ? <Progress className="v2-export-job-progress" percent={job.progress} showInfo /> : null}
|
||||
</Card>)}</CardGroup>}</div>
|
||||
</Card>;
|
||||
}
|
||||
|
||||
function ExportJobsWorkspace({ visible, mobileLayout, onClose }: { visible: boolean; mobileLayout: boolean; onClose: () => void }) {
|
||||
const query = useHistoryExportJobs(visible);
|
||||
const jobs = query.data ?? [];
|
||||
const activeJobs = jobs.filter((job) => job.status === 'queued' || job.status === 'running').length;
|
||||
const completedJobs = jobs.filter((job) => job.status === 'completed').length;
|
||||
const failedJobs = jobs.filter((job) => job.status === 'failed').length;
|
||||
const latestUpdatedAt = jobs.reduce<string | undefined>((latest, job) => {
|
||||
if (!latest) return job.updatedAt;
|
||||
return Date.parse(job.updatedAt) > Date.parse(latest) ? job.updatedAt : latest;
|
||||
}, undefined);
|
||||
const badge = query.isFetching ? '同步中' : `${jobs.length.toLocaleString('zh-CN')} 个任务`;
|
||||
const badgeColor = failedJobs ? 'red' : activeJobs ? 'blue' : completedJobs ? 'green' : 'grey';
|
||||
|
||||
return <WorkspaceSideSheet
|
||||
className="v2-history-export-sidesheet"
|
||||
variant="task"
|
||||
visible={visible}
|
||||
ariaLabel="历史数据导出任务"
|
||||
closeLabel="关闭历史数据导出任务"
|
||||
dialogId="v2-history-export-jobs"
|
||||
placement={mobileLayout ? 'bottom' : 'right'}
|
||||
width={mobileLayout ? undefined : 520}
|
||||
height={mobileLayout ? 'min(82dvh, 720px)' : undefined}
|
||||
title="导出任务"
|
||||
description="查看后台生成进度与可追溯下载记录"
|
||||
icon={<IconDownload />}
|
||||
badge={badge}
|
||||
badgeColor={badgeColor}
|
||||
summaryItems={[
|
||||
{ label: '生成中', value: activeJobs, detail: '排队与执行', tone: activeJobs ? 'primary' : 'neutral' },
|
||||
{ label: '可下载', value: completedJobs, detail: '已完成任务', tone: 'success' },
|
||||
{ label: '需处理', value: failedJobs, detail: '失败任务', tone: failedJobs ? 'danger' : 'neutral' }
|
||||
]}
|
||||
footerNote={query.isError ? '任务状态读取失败,请手动刷新' : latestUpdatedAt ? <>最近更新 <PlatformTime value={latestUpdatedAt} /></> : '打开后自动同步任务状态'}
|
||||
secondaryActions={[{
|
||||
label: '刷新任务',
|
||||
ariaLabel: '刷新导出任务',
|
||||
icon: <IconRefresh />,
|
||||
loading: query.isFetching,
|
||||
onClick: () => { void query.refetch(); }
|
||||
}]}
|
||||
primaryAction={{ label: '完成', onClick: onClose }}
|
||||
onCancel={onClose}
|
||||
>
|
||||
{visible ? <ExportJobsPanel query={query} showHeader={false} /> : null}
|
||||
</WorkspaceSideSheet>;
|
||||
}
|
||||
|
||||
function EvidencePanel({ row, metrics, onClose, showHeader = true }: { row?: HistoryDataRow; metrics: HistoryMetricDefinition[]; onClose: () => void; showHeader?: boolean }) {
|
||||
return <Card className="v2-history-evidence" bodyStyle={{ padding: 0 }}>{showHeader ? <WorkspacePanelHeader title="数据详情" description={row ? `${row.plate || '未绑定车牌'} · ${row.protocol}` : '选择明细后核对来源与质量'} actions={row ? <Button theme="borderless" type="tertiary" icon={<IconClose />} onClick={onClose} aria-label="关闭数据详情" /> : null} /> : null}
|
||||
{row ? <><div className={`v2-history-evidence-status${isHealthyHistoryQuality(row.quality) ? ' is-healthy' : ' is-attention'}`}>
|
||||
@@ -368,7 +411,6 @@ export default function HistoryPage() {
|
||||
setSelectedRowRef({ scope: dataScope, id: '' });
|
||||
}, [dataScope]);
|
||||
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
|
||||
useSideSheetA11y(exportAllowed && exportJobsOpen, '.v2-history-export-sidesheet', 'v2-history-export-jobs', '历史数据导出任务', '关闭历史数据导出任务');
|
||||
|
||||
const applyDraft = () => {
|
||||
const parsed = parseHistoryKeywords(draft.keywords);
|
||||
@@ -575,21 +617,10 @@ export default function HistoryPage() {
|
||||
>
|
||||
{mobileLayout && selectedRow ? <EvidencePanel row={selectedRow} metrics={visibleMetrics} onClose={closeSelectedRow} showHeader={false} /> : null}
|
||||
</WorkspaceDetailSideSheet>
|
||||
<WorkspaceSideSheet
|
||||
className="v2-history-export-sidesheet"
|
||||
variant="task"
|
||||
visible={exportAllowed && exportJobsOpen}
|
||||
ariaLabel="历史数据导出任务"
|
||||
placement={mobileLayout ? 'bottom' : 'right'}
|
||||
width={mobileLayout ? undefined : 520}
|
||||
height={mobileLayout ? 'min(76dvh, 680px)' : undefined}
|
||||
title="导出任务"
|
||||
description="查看后台生成进度与可追溯下载记录"
|
||||
badge="后台任务"
|
||||
badgeColor="blue"
|
||||
onCancel={() => setExportJobsOpen(false)}
|
||||
>
|
||||
{exportJobsOpen ? <ExportJobsPanel showHeader={false} /> : null}
|
||||
</WorkspaceSideSheet>
|
||||
{exportAllowed ? <ExportJobsWorkspace
|
||||
visible={exportJobsOpen}
|
||||
mobileLayout={mobileLayout}
|
||||
onClose={() => setExportJobsOpen(false)}
|
||||
/> : null}
|
||||
</div>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user