feat: unify task and rule workbenches
This commit is contained in:
@@ -357,10 +357,13 @@ test('keeps mobile rule selection focused and opens editing in a Semi bottom Sid
|
|||||||
|
|
||||||
fireEvent.click(ruleItem);
|
fireEvent.click(ruleItem);
|
||||||
const editor = await screen.findByRole('dialog', { name: '告警规则编辑' });
|
const editor = await screen.findByRole('dialog', { name: '告警规则编辑' });
|
||||||
expect(document.querySelector('.v2-alert-rule-editor-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
expect(document.querySelector('.v2-alert-rule-editor-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-editor-sidesheet');
|
||||||
expect(document.querySelector('.v2-alert-rule-editor-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(92dvh, 820px)' });
|
expect(document.querySelector('.v2-alert-rule-editor-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(92dvh, 820px)' });
|
||||||
expect(within(editor).getByDisplayValue('测试超速规则')).toBeInTheDocument();
|
expect(within(editor).getByDisplayValue('测试超速规则')).toBeInTheDocument();
|
||||||
expect(within(editor).getByRole('button', { name: '保存规则' })).toBeInTheDocument();
|
expect(within(editor).getByRole('button', { name: '保存规则' })).toBeInTheDocument();
|
||||||
|
expect(within(editor).getByRole('heading', { name: '触发条件' })).toBeInTheDocument();
|
||||||
|
expect(within(editor).getByRole('heading', { name: '恢复与提醒' })).toBeInTheDocument();
|
||||||
|
expect(within(editor).getByRole('heading', { name: '适用范围与说明' })).toBeInTheDocument();
|
||||||
expect(within(editor).queryByRole('region', { name: '告警规则模板' })).not.toBeInTheDocument();
|
expect(within(editor).queryByRole('region', { name: '告警规则模板' })).not.toBeInTheDocument();
|
||||||
expect(ruleItem).toHaveAttribute('aria-expanded', 'true');
|
expect(ruleItem).toHaveAttribute('aria-expanded', 'true');
|
||||||
|
|
||||||
@@ -397,6 +400,9 @@ test('creates auditable drafts from simple offline and hydrogen rule templates',
|
|||||||
expect(view.container.querySelector('.v2-alert-rule-editor')).toHaveClass('semi-card');
|
expect(view.container.querySelector('.v2-alert-rule-editor')).toHaveClass('semi-card');
|
||||||
expect(view.container.querySelector('.v2-alert-rule-list-heading')).toHaveClass('v2-workspace-panel-header');
|
expect(view.container.querySelector('.v2-alert-rule-list-heading')).toHaveClass('v2-workspace-panel-header');
|
||||||
expect(view.container.querySelector('.v2-alert-rule-editor-heading')).toHaveClass('v2-workspace-panel-header');
|
expect(view.container.querySelector('.v2-alert-rule-editor-heading')).toHaveClass('v2-workspace-panel-header');
|
||||||
|
expect(screen.getByRole('heading', { name: '触发条件' }).closest('.v2-rule-form-section')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('heading', { name: '恢复与提醒' }).closest('.v2-rule-form-section')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('heading', { name: '适用范围与说明' }).closest('.v2-rule-form-section')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('region', { name: '告警规则列表,可左右滚动选择' })).toHaveAttribute('tabindex', '0');
|
expect(screen.getByRole('region', { name: '告警规则列表,可左右滚动选择' })).toHaveAttribute('tabindex', '0');
|
||||||
expect(ruleItem).toHaveClass('semi-button', 'v2-alert-rule-item');
|
expect(ruleItem).toHaveClass('semi-button', 'v2-alert-rule-item');
|
||||||
await waitFor(() => expect(ruleItem).toHaveAttribute('aria-pressed', 'true'));
|
await waitFor(() => expect(ruleItem).toHaveAttribute('aria-pressed', 'true'));
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IconAlarm, IconBell, IconChevronRight, IconClose, IconFilter, IconPlus, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
|
import { IconAlarm, IconBell, IconChevronRight, IconClose, IconFilter, IconPlus, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
|
||||||
import { Button, Card, CardGroup, Collapse, Descriptions, Empty, Input, Radio, Select, SideSheet, Spin, Table, Tag, TextArea, Timeline, Typography } from '@douyinfe/semi-ui';
|
import { Button, Card, CardGroup, Collapse, Descriptions, Empty, Input, Radio, Select, SideSheet, Spin, Table, Tag, TextArea, Timeline, Typography } from '@douyinfe/semi-ui';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { FormEvent, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import { FormEvent, memo, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
import type { AlertEvent, AlertQuery, AlertRule, AlertRuleInput, AlertStatus, MetricDefinition, Page } from '../../api/types';
|
import type { AlertEvent, AlertQuery, AlertRule, AlertRuleInput, AlertStatus, MetricDefinition, Page } from '../../api/types';
|
||||||
@@ -17,6 +17,7 @@ import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
|||||||
import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet';
|
import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet';
|
||||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||||
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
||||||
|
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
|
||||||
import { detailTriggerRow } from '../shared/detailTriggerRow';
|
import { detailTriggerRow } from '../shared/detailTriggerRow';
|
||||||
import { usePlatformSession } from '../auth/AuthGate';
|
import { usePlatformSession } from '../auth/AuthGate';
|
||||||
import { canAdminister, canOperate } from '../auth/session';
|
import { canAdminister, canOperate } from '../auth/session';
|
||||||
@@ -369,6 +370,13 @@ function ruleDraft(rule: AlertRule): AlertRuleInput {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RuleFormSection({ id, title, description, children }: { id: string; title: string; description: string; children: ReactNode }) {
|
||||||
|
return <section className="v2-rule-form-section" aria-labelledby={id}>
|
||||||
|
<header><span><Typography.Title heading={6} id={id}>{title}</Typography.Title><small>{description}</small></span></header>
|
||||||
|
<div className="v2-rule-form-grid">{children}</div>
|
||||||
|
</section>;
|
||||||
|
}
|
||||||
|
|
||||||
function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: MetricDefinition[] }) {
|
function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: MetricDefinition[] }) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const mobileLayout = useMobileLayout();
|
const mobileLayout = useMobileLayout();
|
||||||
@@ -378,7 +386,6 @@ function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: Metri
|
|||||||
useEffect(() => { if (!selectedID && rules[0]) { setSelectedID(rules[0].id); setDraft(ruleDraft(rules[0])); } }, [rules, selectedID]);
|
useEffect(() => { if (!selectedID && rules[0]) { setSelectedID(rules[0].id); setDraft(ruleDraft(rules[0])); } }, [rules, selectedID]);
|
||||||
const save = useMutation({ mutationFn: api.saveAlertRuleV2, onSuccess: async (rule) => { setSelectedID(rule.id); setDraft(ruleDraft(rule)); await queryClient.invalidateQueries({ queryKey: ['alert-rules-v2'] }); } });
|
const save = useMutation({ mutationFn: api.saveAlertRuleV2, onSuccess: async (rule) => { setSelectedID(rule.id); setDraft(ruleDraft(rule)); await queryClient.invalidateQueries({ queryKey: ['alert-rules-v2'] }); } });
|
||||||
const toggle = useMutation({ mutationFn: (rule: AlertRule) => api.setAlertRuleEnabledV2(rule.id, { version: rule.version, enabled: !rule.enabled }), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['alert-rules-v2'] }); } });
|
const toggle = useMutation({ mutationFn: (rule: AlertRule) => api.setAlertRuleEnabledV2(rule.id, { version: rule.version, enabled: !rule.enabled }), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['alert-rules-v2'] }); } });
|
||||||
useSideSheetA11y(mobileLayout && editorOpen, '.v2-alert-rule-editor-sidesheet', 'v2-alert-rule-editor', '告警规则编辑', '关闭告警规则编辑');
|
|
||||||
const mutationError = save.error ?? toggle.error;
|
const mutationError = save.error ?? toggle.error;
|
||||||
const operators = draft.valueType === 'boolean' ? BOOLEAN_OPERATORS : NUMERIC_OPERATORS;
|
const operators = draft.valueType === 'boolean' ? BOOLEAN_OPERATORS : NUMERIC_OPERATORS;
|
||||||
const availableMetrics = metrics.filter((metric) => metric.alertable && metric.valueType === draft.valueType);
|
const availableMetrics = metrics.filter((metric) => metric.alertable && metric.valueType === draft.valueType);
|
||||||
@@ -417,7 +424,6 @@ function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: Metri
|
|||||||
actions={toggleButton}
|
actions={toggleButton}
|
||||||
/> : <div className={`v2-alert-rule-sheet-status${draft.version ? '' : ' is-new'}`}>
|
/> : <div className={`v2-alert-rule-sheet-status${draft.version ? '' : ' is-new'}`}>
|
||||||
<span>
|
<span>
|
||||||
<Tag size="small" color={draft.version ? draft.enabled ? 'green' : 'grey' : 'blue'} type="light">{draft.version ? draft.enabled ? '已启用' : '已停用' : '新规则'}</Tag>
|
|
||||||
<small>{draft.version ? `v${draft.version} · 配置变更写入版本审计` : '保存后生成首个可审计版本'}</small>
|
<small>{draft.version ? `v${draft.version} · 配置变更写入版本审计` : '保存后生成首个可审计版本'}</small>
|
||||||
</span>
|
</span>
|
||||||
{toggleButton}
|
{toggleButton}
|
||||||
@@ -440,24 +446,30 @@ function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: Metri
|
|||||||
</Button>;
|
</Button>;
|
||||||
})}</nav>
|
})}</nav>
|
||||||
</section> : null}
|
</section> : null}
|
||||||
<div className="v2-rule-form-grid">
|
<div className="v2-rule-form-sections">
|
||||||
<label><span>规则名称</span><Input required maxLength={80} value={draft.name} onChange={(value) => setDraft({ ...draft, name: value })} /></label>
|
<RuleFormSection id="v2-rule-trigger-heading" title="触发条件" description="定义监控指标、判断方式与持续时间">
|
||||||
<label><span>严重程度</span><Select value={draft.severity} onChange={(value) => setDraft({ ...draft, severity: String(value) as AlertRuleInput['severity'] })} optionList={[{ value: 'critical', label: '紧急' }, { value: 'major', label: '重要' }, { value: 'minor', label: '一般' }]} /></label>
|
<label><span>规则名称</span><Input required maxLength={80} value={draft.name} onChange={(value) => setDraft({ ...draft, name: value })} /></label>
|
||||||
<label><span>值类型</span><Select value={draft.valueType} onChange={(value) => { const valueType = String(value) as AlertRuleInput['valueType']; const metric = metrics.find((item) => item.alertable && item.valueType === valueType)?.key ?? ''; setDraft({ ...draft, valueType, operator: valueType === 'boolean' ? 'eq' : 'gt', metric }); }} optionList={[{ value: 'numeric', label: '数值' }, { value: 'boolean', label: '布尔' }]} /></label>
|
<label><span>严重程度</span><Select value={draft.severity} onChange={(value) => setDraft({ ...draft, severity: String(value) as AlertRuleInput['severity'] })} optionList={[{ value: 'critical', label: '紧急' }, { value: 'major', label: '重要' }, { value: 'minor', label: '一般' }]} /></label>
|
||||||
<label><span>指标</span><Select disabled={!availableMetrics.length} value={draft.metric} onChange={(value) => setDraft({ ...draft, metric: String(value) })} optionList={availableMetrics.map((metric) => ({ value: metric.key, label: `${metric.label}${metric.unit ? ` (${metric.unit})` : ''}` }))} /></label>
|
<label><span>值类型</span><Select value={draft.valueType} onChange={(value) => { const valueType = String(value) as AlertRuleInput['valueType']; const metric = metrics.find((item) => item.alertable && item.valueType === valueType)?.key ?? ''; setDraft({ ...draft, valueType, operator: valueType === 'boolean' ? 'eq' : 'gt', metric }); }} optionList={[{ value: 'numeric', label: '数值' }, { value: 'boolean', label: '布尔' }]} /></label>
|
||||||
<label><span>触发比较符</span><Select value={draft.operator} onChange={(value) => { const operator = String(value); setDraft({ ...draft, operator, durationSec: operator === 'changed' ? 0 : draft.durationSec }); }} optionList={operators.map((value) => ({ value, label: operatorLabels[value] }))} /></label>
|
<label><span>指标</span><Select disabled={!availableMetrics.length} value={draft.metric} onChange={(value) => setDraft({ ...draft, metric: String(value) })} optionList={availableMetrics.map((metric) => ({ value: metric.key, label: `${metric.label}${metric.unit ? ` (${metric.unit})` : ''}` }))} /></label>
|
||||||
{draft.operator === 'changed' ? <label><span>变化语义</span><Input value="false ↔ true" disabled /></label> : draft.valueType === 'boolean' ? <label><span>目标值</span><Select value={draft.booleanThreshold ? 'true' : 'false'} onChange={(value) => setDraft({ ...draft, booleanThreshold: value === 'true' })} optionList={[{ value: 'true', label: '是' }, { value: 'false', label: '否' }]} /></label> : <label><span>{draft.operator === 'between' || draft.operator === 'outside' ? '区间下限' : '触发阈值'}</span><Input required type="number" step="0.01" placeholder={draft.metric === 'hydrogen_concentration_percent' ? '请按厂家标准填写百分比' : undefined} value={Number.isFinite(draft.threshold) ? String(draft.threshold) : ''} onChange={(value) => setDraft({ ...draft, threshold: value === '' ? Number.NaN : Number(value) })} /></label>}
|
<label><span>触发比较符</span><Select value={draft.operator} onChange={(value) => { const operator = String(value); setDraft({ ...draft, operator, durationSec: operator === 'changed' ? 0 : draft.durationSec }); }} optionList={operators.map((value) => ({ value, label: operatorLabels[value] }))} /></label>
|
||||||
{draft.operator === 'between' || draft.operator === 'outside' ? <label><span>区间上限</span><Input type="number" step="0.1" value={String(draft.thresholdHigh)} onChange={(value) => setDraft({ ...draft, thresholdHigh: Number(value) })} /></label> : null}
|
{draft.operator === 'changed' ? <label><span>变化语义</span><Input value="false ↔ true" disabled /></label> : draft.valueType === 'boolean' ? <label><span>目标值</span><Select value={draft.booleanThreshold ? 'true' : 'false'} onChange={(value) => setDraft({ ...draft, booleanThreshold: value === 'true' })} optionList={[{ value: 'true', label: '是' }, { value: 'false', label: '否' }]} /></label> : <label><span>{draft.operator === 'between' || draft.operator === 'outside' ? '区间下限' : '触发阈值'}</span><Input required type="number" step="0.01" placeholder={draft.metric === 'hydrogen_concentration_percent' ? '请按厂家标准填写百分比' : undefined} value={Number.isFinite(draft.threshold) ? String(draft.threshold) : ''} onChange={(value) => setDraft({ ...draft, threshold: value === '' ? Number.NaN : Number(value) })} /></label>}
|
||||||
<label><span>持续时间(秒)</span><Input type="number" min="0" max="86400" disabled={draft.operator === 'changed'} value={String(draft.durationSec)} onChange={(value) => setDraft({ ...draft, durationSec: Number(value) })} /></label>
|
{draft.operator === 'between' || draft.operator === 'outside' ? <label><span>区间上限</span><Input type="number" step="0.1" value={String(draft.thresholdHigh)} onChange={(value) => setDraft({ ...draft, thresholdHigh: Number(value) })} /></label> : null}
|
||||||
<label><span>恢复比较符</span><Select value={draft.recoveryOperator} onChange={(value) => setDraft({ ...draft, recoveryOperator: String(value) })} optionList={[{ value: '', label: '未配置' }, ...['gt', 'gte', 'lt', 'lte', 'eq', 'neq'].map((value) => ({ value, label: operatorLabels[value] }))]} /></label>
|
<label><span>持续时间(秒)</span><Input type="number" min="0" max="86400" disabled={draft.operator === 'changed'} value={String(draft.durationSec)} onChange={(value) => setDraft({ ...draft, durationSec: Number(value) })} /></label>
|
||||||
<label><span>恢复阈值</span><Input type="number" step="0.1" value={String(draft.recoveryThreshold)} onChange={(value) => setDraft({ ...draft, recoveryThreshold: Number(value) })} /></label>
|
</RuleFormSection>
|
||||||
<label><span>重复间隔(秒)</span><Input type="number" min="0" max="604800" value={String(draft.repeatIntervalSec)} onChange={(value) => setDraft({ ...draft, repeatIntervalSec: Number(value) })} /></label>
|
<RuleFormSection id="v2-rule-recovery-heading" title="恢复与提醒" description="控制恢复条件和重复通知频率">
|
||||||
<label className="is-wide"><span>协议范围(逗号分隔;空为全部)</span><Input value={draft.scopeProtocols.join(',')} onChange={(value) => setList('scopeProtocols', value)} /></label>
|
<label><span>恢复比较符</span><Select value={draft.recoveryOperator} onChange={(value) => setDraft({ ...draft, recoveryOperator: String(value) })} optionList={[{ value: '', label: '未配置' }, ...['gt', 'gte', 'lt', 'lte', 'eq', 'neq'].map((value) => ({ value, label: operatorLabels[value] }))]} /></label>
|
||||||
<label className="is-wide"><span>车辆 VIN 范围(逗号分隔;空为全部)</span><Input value={draft.scopeVins.join(',')} onChange={(value) => setList('scopeVins', value)} /></label>
|
<label><span>恢复阈值</span><Input type="number" step="0.1" value={String(draft.recoveryThreshold)} onChange={(value) => setDraft({ ...draft, recoveryThreshold: Number(value) })} /></label>
|
||||||
<label className="is-wide"><span>厂家范围(逗号分隔;空为全部)</span><Input value={draft.scopeOems.join(',')} onChange={(value) => setList('scopeOems', value)} /></label>
|
<label><span>重复间隔(秒)</span><Input type="number" min="0" max="604800" value={String(draft.repeatIntervalSec)} onChange={(value) => setDraft({ ...draft, repeatIntervalSec: Number(value) })} /></label>
|
||||||
<label className="is-wide"><span>车型范围(来自车辆主档;空为全部)</span><Input value={draft.scopeModels.join(',')} onChange={(value) => setList('scopeModels', value)} /></label>
|
</RuleFormSection>
|
||||||
<label className="is-wide"><span>企业范围(来自车辆主档;空为全部)</span><Input value={draft.scopeCompanies.join(',')} onChange={(value) => setList('scopeCompanies', value)} /></label>
|
<RuleFormSection id="v2-rule-scope-heading" title="适用范围与说明" description="按协议、车辆与主数据范围精确生效">
|
||||||
<label className="is-wide"><span>说明</span><TextArea maxCount={500} autosize={{ minRows: 3, maxRows: 7 }} value={draft.description} onChange={(value) => setDraft({ ...draft, description: value })} /></label>
|
<label className="is-wide"><span>协议范围(逗号分隔;空为全部)</span><Input value={draft.scopeProtocols.join(',')} onChange={(value) => setList('scopeProtocols', value)} /></label>
|
||||||
|
<label className="is-wide"><span>车辆 VIN 范围(逗号分隔;空为全部)</span><Input value={draft.scopeVins.join(',')} onChange={(value) => setList('scopeVins', value)} /></label>
|
||||||
|
<label className="is-wide"><span>厂家范围(逗号分隔;空为全部)</span><Input value={draft.scopeOems.join(',')} onChange={(value) => setList('scopeOems', value)} /></label>
|
||||||
|
<label className="is-wide"><span>车型范围(来自车辆主档;空为全部)</span><Input value={draft.scopeModels.join(',')} onChange={(value) => setList('scopeModels', value)} /></label>
|
||||||
|
<label className="is-wide"><span>企业范围(来自车辆主档;空为全部)</span><Input value={draft.scopeCompanies.join(',')} onChange={(value) => setList('scopeCompanies', value)} /></label>
|
||||||
|
<label className="is-wide"><span>说明</span><TextArea maxCount={500} autosize={{ minRows: 3, maxRows: 7 }} value={draft.description} onChange={(value) => setDraft({ ...draft, description: value })} /></label>
|
||||||
|
</RuleFormSection>
|
||||||
</div>
|
</div>
|
||||||
<footer><div><b>通知通道</b><span>站内通知已启用;短信、邮件、企微为预留 / 未启用。</span></div>{mutationError ? <em>{mutationError.message}</em> : null}<Button className="v2-primary-button" theme="solid" htmlType="submit" disabled={save.isPending || toggle.isPending}>{save.isPending ? '保存中' : '保存规则'}</Button></footer>
|
<footer><div><b>通知通道</b><span>站内通知已启用;短信、邮件、企微为预留 / 未启用。</span></div>{mutationError ? <em>{mutationError.message}</em> : null}<Button className="v2-primary-button" theme="solid" htmlType="submit" disabled={save.isPending || toggle.isPending}>{save.isPending ? '保存中' : '保存规则'}</Button></footer>
|
||||||
</form>
|
</form>
|
||||||
@@ -492,17 +504,21 @@ function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: Metri
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
{!mobileLayout ? editor : null}
|
{!mobileLayout ? editor : null}
|
||||||
{mobileLayout ? <SideSheet
|
{mobileLayout ? <WorkspaceSideSheet
|
||||||
className="v2-alert-rule-editor-sidesheet"
|
className="v2-alert-rule-editor-sidesheet"
|
||||||
|
variant="editor"
|
||||||
visible={editorOpen}
|
visible={editorOpen}
|
||||||
aria-label="告警规则编辑"
|
ariaLabel="告警规则编辑"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
height="min(92dvh, 820px)"
|
height="min(92dvh, 820px)"
|
||||||
title={<div className="v2-alert-sheet-title"><strong>{draft.version ? '编辑规则' : '新建规则'}</strong><span>{draft.name || '配置触发条件、范围与通知策略'}</span></div>}
|
title={draft.version ? '编辑规则' : '新建规则'}
|
||||||
|
description={draft.name || '配置触发条件、范围与通知策略'}
|
||||||
|
badge={draft.version ? draft.enabled ? '已启用' : '已停用' : '新规则'}
|
||||||
|
badgeColor={draft.version ? draft.enabled ? 'green' : 'grey' : 'blue'}
|
||||||
onCancel={() => setEditorOpen(false)}
|
onCancel={() => setEditorOpen(false)}
|
||||||
>
|
>
|
||||||
{editorOpen ? editor : null}
|
{editorOpen ? editor : null}
|
||||||
</SideSheet> : null}
|
</WorkspaceSideSheet> : null}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ test('renders only selectable Semi history cards on mobile', async () => {
|
|||||||
await waitFor(() => expect(toolsButton).toHaveAttribute('aria-expanded', 'true'));
|
await waitFor(() => expect(toolsButton).toHaveAttribute('aria-expanded', 'true'));
|
||||||
fireEvent.click(screen.getByRole('menuitem', { name: /导出任务/ }));
|
fireEvent.click(screen.getByRole('menuitem', { name: /导出任务/ }));
|
||||||
expect(await screen.findByRole('dialog', { name: '历史数据导出任务' })).toBeInTheDocument();
|
expect(await screen.findByRole('dialog', { name: '历史数据导出任务' })).toBeInTheDocument();
|
||||||
expect(document.querySelector('.v2-history-export-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
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(76dvh, 680px)' });
|
||||||
await waitFor(() => expect(mocks.historyExports).toHaveBeenCalledTimes(1));
|
await waitFor(() => expect(mocks.historyExports).toHaveBeenCalledTimes(1));
|
||||||
expect(screen.getByText('暂无导出任务')).toBeInTheDocument();
|
expect(screen.getByText('暂无导出任务')).toBeInTheDocument();
|
||||||
@@ -388,6 +388,7 @@ test('shows only the authenticated customer export workspace with owner and scop
|
|||||||
fireEvent.click(await screen.findByRole('button', { name: '查看导出任务' }));
|
fireEvent.click(await screen.findByRole('button', { name: '查看导出任务' }));
|
||||||
expect(await screen.findByText(/customer-a · 1 辆/)).toBeInTheDocument();
|
expect(await screen.findByText(/customer-a · 1 辆/)).toBeInTheDocument();
|
||||||
expect(screen.getByRole('dialog', { name: '历史数据导出任务' })).toBeInTheDocument();
|
expect(screen.getByRole('dialog', { name: '历史数据导出任务' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('list', { name: '导出任务状态概览' })).toHaveTextContent('生成中0排队与执行可下载1已完成任务需处理0失败任务');
|
||||||
expect(screen.getByRole('button', { name: /创建导出/ })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /创建导出/ })).toBeInTheDocument();
|
||||||
expect(mocks.historyExports).toHaveBeenCalled();
|
expect(mocks.historyExports).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { IconChevronRight, IconClose, IconDownload, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
|
import { IconChevronRight, IconClose, IconDownload, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
|
||||||
import { Button, Card, CardGroup, Checkbox, Descriptions, Dropdown, Empty, Input, Progress, Select, SideSheet, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
|
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 { FormEvent, KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
@@ -144,11 +144,22 @@ function ExportJobsPanel({ showHeader = true }: { showHeader?: boolean }) {
|
|||||||
const jobs = query.data ?? [];
|
const jobs = query.data ?? [];
|
||||||
const statusLabel = (status: string) => status === 'queued' ? '排队中' : status === 'running' ? '执行中' : status === 'completed' ? '已完成' : '失败';
|
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 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}
|
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-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><small>{job.ownerUsername || job.ownerName || '历史任务'} · {job.vehicleVins?.length ?? job.keywords.length} 辆</small><small>{job.dateFrom && job.dateTo ? `${job.dateFrom.replace('T', ' ')} 至 ${job.dateTo.replace('T', ' ')}` : job.createdAt}</small></div>
|
<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>
|
||||||
|
</div>
|
||||||
{job.status === 'running' ? <Progress className="v2-export-job-progress" percent={job.progress} showInfo /> : null}
|
{job.status === 'running' ? <Progress className="v2-export-job-progress" percent={job.progress} showInfo /> : null}
|
||||||
{job.downloadUrl ? <ExportDownloadButton id={job.id} /> : null}
|
|
||||||
</Card>)}</CardGroup>}</div>
|
</Card>)}</CardGroup>}</div>
|
||||||
</Card>;
|
</Card>;
|
||||||
}
|
}
|
||||||
@@ -562,17 +573,21 @@ export default function HistoryPage() {
|
|||||||
>
|
>
|
||||||
{mobileLayout && selectedRow ? <EvidencePanel row={selectedRow} metrics={visibleMetrics} onClose={closeSelectedRow} showHeader={false} /> : null}
|
{mobileLayout && selectedRow ? <EvidencePanel row={selectedRow} metrics={visibleMetrics} onClose={closeSelectedRow} showHeader={false} /> : null}
|
||||||
</WorkspaceDetailSideSheet>
|
</WorkspaceDetailSideSheet>
|
||||||
<SideSheet
|
<WorkspaceSideSheet
|
||||||
className="v2-history-export-sidesheet"
|
className="v2-history-export-sidesheet"
|
||||||
|
variant="task"
|
||||||
visible={exportAllowed && exportJobsOpen}
|
visible={exportAllowed && exportJobsOpen}
|
||||||
aria-label="历史数据导出任务"
|
ariaLabel="历史数据导出任务"
|
||||||
placement={mobileLayout ? 'bottom' : 'right'}
|
placement={mobileLayout ? 'bottom' : 'right'}
|
||||||
width={mobileLayout ? undefined : 520}
|
width={mobileLayout ? undefined : 520}
|
||||||
height={mobileLayout ? 'min(76dvh, 680px)' : undefined}
|
height={mobileLayout ? 'min(76dvh, 680px)' : undefined}
|
||||||
title={<div className="v2-history-mobile-sheet-title"><strong>导出任务</strong><span>查看生成进度与下载记录</span></div>}
|
title="导出任务"
|
||||||
|
description="查看后台生成进度与可追溯下载记录"
|
||||||
|
badge="后台任务"
|
||||||
|
badgeColor="blue"
|
||||||
onCancel={() => setExportJobsOpen(false)}
|
onCancel={() => setExportJobsOpen(false)}
|
||||||
>
|
>
|
||||||
{exportJobsOpen ? <ExportJobsPanel showHeader={false} /> : null}
|
{exportJobsOpen ? <ExportJobsPanel showHeader={false} /> : null}
|
||||||
</SideSheet>
|
</WorkspaceSideSheet>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,8 @@ describe('V2 production entry', () => {
|
|||||||
expect(corePageSources.HistoryPage).toContain('<Input');
|
expect(corePageSources.HistoryPage).toContain('<Input');
|
||||||
expect(corePageSources.HistoryPage).toContain('<Select');
|
expect(corePageSources.HistoryPage).toContain('<Select');
|
||||||
expect(corePageSources.HistoryPage).toContain('<Checkbox');
|
expect(corePageSources.HistoryPage).toContain('<Checkbox');
|
||||||
expect(corePageSources.HistoryPage).toContain('<SideSheet');
|
expect(corePageSources.HistoryPage).toContain('<WorkspaceSideSheet');
|
||||||
|
expect(corePageSources.HistoryPage).toContain('variant="task"');
|
||||||
expect(corePageSources.HistoryPage).toContain('v2-history-column-sidesheet');
|
expect(corePageSources.HistoryPage).toContain('v2-history-column-sidesheet');
|
||||||
expect(corePageSources.HistoryPage).toContain('<Card key={row.id} className={`v2-history-mobile-card');
|
expect(corePageSources.HistoryPage).toContain('<Card key={row.id} className={`v2-history-mobile-card');
|
||||||
expect(corePageSources.HistoryPage).toContain('v2-history-trend');
|
expect(corePageSources.HistoryPage).toContain('v2-history-trend');
|
||||||
@@ -185,7 +186,11 @@ describe('V2 production entry', () => {
|
|||||||
expect(corePageSources.AlertsPage).toContain('className="v2-alert-navigation v2-alert-command-bar"');
|
expect(corePageSources.AlertsPage).toContain('className="v2-alert-navigation v2-alert-command-bar"');
|
||||||
expect(corePageSources.AlertsPage).toContain('<SideSheet\n className="v2-alert-filter-sidesheet"');
|
expect(corePageSources.AlertsPage).toContain('<SideSheet\n className="v2-alert-filter-sidesheet"');
|
||||||
expect(corePageSources.AlertsPage).toContain('<WorkspaceDetailSideSheet\n className="v2-alert-detail-sidesheet"');
|
expect(corePageSources.AlertsPage).toContain('<WorkspaceDetailSideSheet\n className="v2-alert-detail-sidesheet"');
|
||||||
expect(corePageSources.AlertsPage).toContain('<SideSheet\n className="v2-alert-rule-editor-sidesheet"');
|
expect(corePageSources.AlertsPage).toContain('<WorkspaceSideSheet\n className="v2-alert-rule-editor-sidesheet"');
|
||||||
|
expect(corePageSources.AlertsPage).toContain('variant="editor"');
|
||||||
|
expect(corePageSources.AlertsPage).toContain('<RuleFormSection id="v2-rule-trigger-heading"');
|
||||||
|
expect(corePageSources.AlertsPage).toContain('<RuleFormSection id="v2-rule-recovery-heading"');
|
||||||
|
expect(corePageSources.AlertsPage).toContain('<RuleFormSection id="v2-rule-scope-heading"');
|
||||||
expect(corePageSources.AlertsPage).toContain('className={`v2-alert-event-table');
|
expect(corePageSources.AlertsPage).toContain('className={`v2-alert-event-table');
|
||||||
expect(corePageSources.AlertsPage).toContain('detailTriggerRow({');
|
expect(corePageSources.AlertsPage).toContain('detailTriggerRow({');
|
||||||
expect(corePageSources.AlertsPage).not.toContain('<table><thead><tr><th />');
|
expect(corePageSources.AlertsPage).not.toContain('<table><thead><tr><th />');
|
||||||
|
|||||||
@@ -38,3 +38,35 @@ test('renders one accessible Semi configuration sheet with shared summary and ac
|
|||||||
expect(onComplete).toHaveBeenCalledTimes(1);
|
expect(onComplete).toHaveBeenCalledTimes(1);
|
||||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('exposes task and editor variants without changing the accessible dialog contract', () => {
|
||||||
|
const view = render(<>
|
||||||
|
<WorkspaceSideSheet
|
||||||
|
visible
|
||||||
|
variant="task"
|
||||||
|
ariaLabel="后台任务"
|
||||||
|
title="任务"
|
||||||
|
description="查看生成进度"
|
||||||
|
onCancel={() => undefined}
|
||||||
|
>
|
||||||
|
<span>任务内容</span>
|
||||||
|
</WorkspaceSideSheet>
|
||||||
|
<WorkspaceSideSheet
|
||||||
|
visible
|
||||||
|
variant="editor"
|
||||||
|
ariaLabel="策略编辑"
|
||||||
|
title="编辑策略"
|
||||||
|
description="保存后生效"
|
||||||
|
onCancel={() => undefined}
|
||||||
|
>
|
||||||
|
<span>编辑内容</span>
|
||||||
|
</WorkspaceSideSheet>
|
||||||
|
</>);
|
||||||
|
|
||||||
|
expect(screen.getByRole('dialog', { name: '后台任务' }).closest('.semi-sidesheet')).toHaveClass('v2-workspace-task-sidesheet');
|
||||||
|
expect(screen.getByRole('dialog', { name: '策略编辑' }).closest('.semi-sidesheet')).toHaveClass('v2-workspace-editor-sidesheet');
|
||||||
|
expect(screen.getByText('任务内容')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('编辑内容')).toBeInTheDocument();
|
||||||
|
expect(view.baseElement).toHaveTextContent('查看生成进度');
|
||||||
|
expect(view.baseElement).toHaveTextContent('保存后生效');
|
||||||
|
});
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export type WorkspaceSideSheetBadgeColor = 'blue' | 'green' | 'orange' | 'red' |
|
|||||||
|
|
||||||
export type WorkspaceSideSheetProps = {
|
export type WorkspaceSideSheetProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
variant?: 'config' | 'detail';
|
variant?: 'config' | 'detail' | 'task' | 'editor';
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
closeLabel?: string;
|
closeLabel?: string;
|
||||||
|
|||||||
@@ -21068,3 +21068,330 @@
|
|||||||
padding: 7px 8px;
|
padding: 7px 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shared task and editor SideSheets.
|
||||||
|
* Task sheets prioritise progress and traceable output; editor sheets preserve
|
||||||
|
* a stable title/status boundary while the form owns its save action.
|
||||||
|
*/
|
||||||
|
:is(.v2-workspace-task-sidesheet, .v2-workspace-editor-sidesheet) .semi-sidesheet-inner {
|
||||||
|
overflow: hidden;
|
||||||
|
border-left: 1px solid #d9e3ef;
|
||||||
|
background: #f4f7fb;
|
||||||
|
box-shadow: -22px 0 64px rgba(24, 43, 68, .16);
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-task-sidesheet, .v2-workspace-editor-sidesheet) .semi-sidesheet-header {
|
||||||
|
min-height: 78px;
|
||||||
|
border-bottom: 1px solid #dfe7f0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 10% 0%, rgba(18, 104, 243, .075), transparent 36%),
|
||||||
|
linear-gradient(180deg, #fff 0%, #fbfcfe 100%);
|
||||||
|
padding: 13px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-task-sidesheet, .v2-workspace-editor-sidesheet) .semi-sidesheet-title {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-task-sidesheet, .v2-workspace-editor-sidesheet) .semi-sidesheet-close {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-task-sidesheet, .v2-workspace-editor-sidesheet) .semi-sidesheet-body {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f4f7fb;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-task-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-editor-sidesheet .v2-workspace-config-content {
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-editor-sidesheet .v2-workspace-config-body,
|
||||||
|
.v2-workspace-editor-sidesheet .v2-workspace-config-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-editor-sidesheet .v2-workspace-config-content > .semi-card {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-jobs.semi-card {
|
||||||
|
min-height: 100%;
|
||||||
|
border-color: #dce5ef;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 9px 28px rgba(31, 53, 80, .065);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-export-job-overview {
|
||||||
|
display: grid;
|
||||||
|
min-height: 88px;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 1px;
|
||||||
|
border-bottom: 1px solid #e1e8f1;
|
||||||
|
background: #e1e8f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-export-job-overview > span {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
align-content: center;
|
||||||
|
gap: 3px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 12px 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-export-job-overview > span::before {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto 0 0;
|
||||||
|
width: 2px;
|
||||||
|
background: transparent;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-export-job-overview > span.is-active::before { background: #1268f3; }
|
||||||
|
.v2-export-job-overview > span.is-completed::before { background: #18a76f; }
|
||||||
|
.v2-export-job-overview > span.is-failed::before { background: #e95a52; }
|
||||||
|
|
||||||
|
.v2-export-job-overview small {
|
||||||
|
color: #7f8ea1;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-export-job-overview strong {
|
||||||
|
color: #243950;
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 760;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-export-job-overview > span.is-active strong { color: #1268f3; }
|
||||||
|
.v2-export-job-overview > span.is-completed strong { color: #14865c; }
|
||||||
|
.v2-export-job-overview > span.is-failed strong { color: #c8423b; }
|
||||||
|
|
||||||
|
.v2-export-job-overview em {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #98a3b2;
|
||||||
|
font-size: 8px;
|
||||||
|
font-style: normal;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-list {
|
||||||
|
max-height: none;
|
||||||
|
padding: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-list > .semi-card-group {
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-card.semi-card {
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 5px 16px rgba(30, 50, 74, .045);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-card > .semi-card-header {
|
||||||
|
min-height: 44px;
|
||||||
|
padding-inline: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-name {
|
||||||
|
max-width: 310px;
|
||||||
|
color: #32475f;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-card > .semi-card-header .semi-tag {
|
||||||
|
min-height: 23px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-card > .semi-card-body {
|
||||||
|
gap: 8px;
|
||||||
|
padding: 11px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-summary {
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-summary > span {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-summary strong {
|
||||||
|
color: #354b63;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-summary small {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-card .v2-export-download.semi-button {
|
||||||
|
min-width: 66px;
|
||||||
|
height: 30px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #edf5ff;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-sections {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 14px 15px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-section {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #dce5ef;
|
||||||
|
border-radius: 11px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 6px 20px rgba(31, 53, 80, .045);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-section > header {
|
||||||
|
min-height: 54px;
|
||||||
|
border-bottom: 1px solid #e5ebf2;
|
||||||
|
background: linear-gradient(180deg, #fff 0%, #fbfcfe 100%);
|
||||||
|
padding: 10px 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-section > header > span {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-section > header h6.semi-typography {
|
||||||
|
margin: 0;
|
||||||
|
color: #2a4058;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 720;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-section > header small {
|
||||||
|
color: #8390a2;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-section > .v2-rule-form-grid {
|
||||||
|
gap: 13px 15px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
:is(.v2-workspace-task-sidesheet, .v2-workspace-editor-sidesheet).semi-sidesheet-bottom .semi-sidesheet-inner {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
border: 1px solid #d8e2ed;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-radius: 18px 18px 0 0;
|
||||||
|
box-shadow: 0 -18px 48px rgba(23, 43, 68, .18);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-task-sidesheet, .v2-workspace-editor-sidesheet).semi-sidesheet-bottom .semi-sidesheet-header {
|
||||||
|
min-height: 70px;
|
||||||
|
padding: 10px 12px 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-task-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-export-job-overview {
|
||||||
|
min-height: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-export-job-overview > span {
|
||||||
|
gap: 2px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-export-job-overview small {
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-export-job-overview strong {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-export-job-overview em {
|
||||||
|
font-size: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-list {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-name {
|
||||||
|
max-width: 220px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-summary strong {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-history-export-sidesheet .v2-export-job-summary small {
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-sections {
|
||||||
|
gap: 9px;
|
||||||
|
padding: 9px 9px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-section {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-section > header {
|
||||||
|
min-height: 50px;
|
||||||
|
padding: 9px 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-section > header h6.semi-typography {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-section > header small {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-section > .v2-rule-form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 11px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-rule-form-section > .v2-rule-form-grid label.is-wide {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user