feat: unify monitor action workbenches
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
IconChevronLeft, IconClose, IconFilter, IconList, IconMapPin,
|
IconChevronLeft, IconFilter, IconList, IconMapPin,
|
||||||
IconQrCode, IconRefresh, IconSearch
|
IconQrCode, IconRefresh, IconSearch
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { Button, Card, Empty, Input, Modal, Select, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui';
|
import { Button, Card, Empty, Input, Select, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { lazy, memo, Suspense, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import { lazy, memo, Suspense, useCallback, useDeferredValue, useEffect, useMemo, useRef, 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';
|
||||||
import type { Page, VehicleRealtimeRow } from '../../api/types';
|
import type { Page, VehicleRealtimeRow } from '../../api/types';
|
||||||
@@ -14,6 +14,7 @@ import { SegmentedTabs } from '../shared/SegmentedTabs';
|
|||||||
import { TablePagination } from '../shared/TablePagination';
|
import { TablePagination } from '../shared/TablePagination';
|
||||||
import { ProtocolTag } from '../shared/ProtocolTag';
|
import { ProtocolTag } from '../shared/ProtocolTag';
|
||||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||||
|
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
|
||||||
import { formatNumber, statusLabel, vehicleStatus } from '../domain/monitor';
|
import { formatNumber, statusLabel, vehicleStatus } from '../domain/monitor';
|
||||||
import { useMobileLayout } from '../hooks/useMobileLayout';
|
import { useMobileLayout } from '../hooks/useMobileLayout';
|
||||||
import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorFilterScope, monitorQueryParams, parseMonitorSearchTerms, useMonitorData, type MonitorViewport } from '../hooks/useMonitorData';
|
import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorFilterScope, monitorQueryParams, parseMonitorSearchTerms, useMonitorData, type MonitorViewport } from '../hooks/useMonitorData';
|
||||||
@@ -29,27 +30,41 @@ const MONITOR_VIEW_ITEMS = [
|
|||||||
] as const;
|
] as const;
|
||||||
const VehicleDetailCard = lazy(() => import('./MonitorVehicleDetailCard'));
|
const VehicleDetailCard = lazy(() => import('./MonitorVehicleDetailCard'));
|
||||||
|
|
||||||
function BatchVehicleSearchDialog({ initialValue, onApply, onClose }: { initialValue: string; onApply: (value: string) => void; onClose: () => void }) {
|
function BatchVehicleSearchDialog({ initialValue, mobile, onApply, onClose }: { initialValue: string; mobile: boolean; onApply: (value: string) => void; onClose: () => void }) {
|
||||||
const [draft, setDraft] = useState(initialValue);
|
const [draft, setDraft] = useState(initialValue);
|
||||||
const terms = useMemo(() => parseMonitorSearchTerms(draft), [draft]);
|
const terms = useMemo(() => parseMonitorSearchTerms(draft), [draft]);
|
||||||
|
|
||||||
return <Modal
|
return <WorkspaceSideSheet
|
||||||
className="v2-batch-search-modal"
|
className="v2-monitor-batch-sidesheet"
|
||||||
|
variant="editor"
|
||||||
visible
|
visible
|
||||||
centered
|
ariaLabel="批量搜索车辆"
|
||||||
closeOnEsc
|
closeLabel="关闭批量搜索车辆"
|
||||||
maskClosable
|
dialogId="v2-monitor-batch-search"
|
||||||
width={520}
|
placement={mobile ? 'bottom' : 'right'}
|
||||||
title={<div className="v2-batch-search-title"><strong id="batch-search-title">批量搜索车辆</strong><span>从 Excel、文本或聊天记录中直接粘贴车牌</span></div>}
|
width={mobile ? undefined : 520}
|
||||||
|
height={mobile ? 'min(86dvh, 700px)' : undefined}
|
||||||
|
title="批量搜索车辆"
|
||||||
|
description="从 Excel、文本或聊天记录中直接粘贴车牌"
|
||||||
|
icon={<IconSearch />}
|
||||||
|
badge={`${terms.length} 辆`}
|
||||||
|
badgeColor={terms.length ? 'blue' : 'grey'}
|
||||||
|
summaryItems={[
|
||||||
|
{ label: '已识别', value: terms.length.toLocaleString('zh-CN'), detail: '可直接应用到监控筛选', tone: terms.length ? 'primary' : 'neutral' },
|
||||||
|
{ label: '重复处理', value: '自动去重', detail: '相同车牌只保留一次', tone: 'success' },
|
||||||
|
{ label: '单次上限', value: `${MAX_MONITOR_SEARCH_TERMS} 辆`, detail: '超出部分不会进入查询' }
|
||||||
|
]}
|
||||||
|
footerNote="支持换行、空格、逗号或分号分隔。"
|
||||||
|
secondaryActions={[{ label: '取消', onClick: onClose }]}
|
||||||
|
primaryAction={{ label: `应用搜索(${terms.length})`, ariaLabel: `应用搜索(${terms.length})`, disabled: !terms.length, icon: <IconSearch />, onClick: () => onApply(terms.join(',')) }}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
footer={<div className="v2-batch-search-actions"><Button onClick={onClose}>取消</Button><Button theme="solid" disabled={!terms.length} onClick={() => onApply(terms.join(','))}>应用搜索({terms.length})</Button></div>}
|
|
||||||
>
|
>
|
||||||
<div className="v2-batch-search-dialog">
|
<div className="v2-batch-search-dialog">
|
||||||
<label htmlFor="batch-vehicle-search">每行一个车牌,也支持空格、逗号或分号分隔</label>
|
<label htmlFor="batch-vehicle-search">每行一个车牌,也支持空格、逗号或分号分隔</label>
|
||||||
<TextArea id="batch-vehicle-search" autoFocus value={draft} onChange={setDraft} autosize={{ minRows: 8, maxRows: 14 }} resize="vertical" placeholder={'粤A12345\n粤B67890\n粤C24680'} />
|
<TextArea id="batch-vehicle-search" autoFocus value={draft} onChange={setDraft} autosize={{ minRows: 8, maxRows: 14 }} resize="vertical" placeholder={'粤A12345\n粤B67890\n粤C24680'} />
|
||||||
<div className="v2-batch-search-summary"><span>已识别 <strong>{terms.length}</strong> 辆,重复项已自动去除</span><em>最多 {MAX_MONITOR_SEARCH_TERMS} 辆</em></div>
|
<div className="v2-batch-search-summary" role="status"><span>已识别 <strong>{terms.length}</strong> 辆,重复项已自动去除</span><em>最多 {MAX_MONITOR_SEARCH_TERMS} 辆</em></div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>;
|
</WorkspaceSideSheet>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddressCoordinate = { longitude: number; latitude: number; key: string };
|
type AddressCoordinate = { longitude: number; latitude: number; key: string };
|
||||||
@@ -135,15 +150,12 @@ function MonitorVehicleTable({ rows, total, page, totalPages, limit, loading, mo
|
|||||||
</Card>;
|
</Card>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MobileEntry({ onClose }: { onClose: () => void }) {
|
function MobileEntry({ mobile, onClose }: { mobile: boolean; onClose: () => void }) {
|
||||||
const [qr, setQr] = useState('');
|
const [qr, setQr] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'error'>('idle');
|
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'error'>('idle');
|
||||||
const [attempt, setAttempt] = useState(0);
|
const [attempt, setAttempt] = useState(0);
|
||||||
const url = `${window.location.origin}/monitor`;
|
const url = `${window.location.origin}/monitor`;
|
||||||
useLayoutEffect(() => {
|
|
||||||
document.querySelector('.v2-monitor-qr-modal .semi-modal-close')?.setAttribute('aria-label', '关闭手机端入口');
|
|
||||||
}, []);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (copyState === 'idle') return;
|
if (copyState === 'idle') return;
|
||||||
const timer = window.setTimeout(() => setCopyState('idle'), 1_800);
|
const timer = window.setTimeout(() => setCopyState('idle'), 1_800);
|
||||||
@@ -168,25 +180,40 @@ function MobileEntry({ onClose }: { onClose: () => void }) {
|
|||||||
setCopyState('error');
|
setCopyState('error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return <Modal
|
const qrState = qr ? '已生成' : error ? '生成失败' : '生成中';
|
||||||
className="v2-monitor-qr-modal"
|
const copyLabel = copyState === 'copied' ? '已复制访问地址' : copyState === 'error' ? '复制失败,重试' : '复制访问地址';
|
||||||
|
return <WorkspaceSideSheet
|
||||||
|
className="v2-monitor-qr-sidesheet"
|
||||||
|
variant="action"
|
||||||
visible
|
visible
|
||||||
centered
|
ariaLabel="手机端全局监控"
|
||||||
width={390}
|
closeLabel="关闭手机端入口"
|
||||||
|
dialogId="v2-monitor-mobile-entry"
|
||||||
|
placement={mobile ? 'bottom' : 'right'}
|
||||||
|
width={mobile ? undefined : 430}
|
||||||
|
height={mobile ? 'min(82dvh, 566px)' : undefined}
|
||||||
title="手机端全局监控"
|
title="手机端全局监控"
|
||||||
closeIcon={<IconClose aria-label="关闭手机端入口" />}
|
description="扫码后使用现有账号进入全局监控"
|
||||||
closeOnEsc
|
icon={<IconQrCode />}
|
||||||
maskClosable
|
badge="账号鉴权"
|
||||||
|
badgeColor="blue"
|
||||||
|
summaryItems={[
|
||||||
|
{ label: '入口页面', value: '全局监控', detail: '移动端响应式布局', tone: 'primary' },
|
||||||
|
{ label: '凭证安全', value: '不含 Token', detail: '扫码后仍需账号鉴权', tone: 'success' },
|
||||||
|
{ label: '二维码状态', value: qrState, detail: error ? '可在下方重新生成' : '仅在打开入口时生成', tone: error ? 'danger' : qr ? 'success' : 'neutral' }
|
||||||
|
]}
|
||||||
|
footerNote={url}
|
||||||
|
secondaryActions={[{ label: '关闭', onClick: onClose }]}
|
||||||
|
primaryAction={{ label: copyLabel, loading: !qr && !error, disabled: !qr && !error, onClick: () => void copyURL() }}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
footer={<Button block theme="solid" aria-live="polite" onClick={() => void copyURL()}>{copyState === 'copied' ? '已复制访问地址' : copyState === 'error' ? '复制失败,重试' : '复制访问地址'}</Button>}
|
|
||||||
>
|
>
|
||||||
<div className="v2-monitor-qr-content">
|
<div className="v2-monitor-qr-content">
|
||||||
<span className="v2-monitor-qr-icon"><IconQrCode /></span>
|
<p>扫码后使用现有账号鉴权,不在二维码中保存 Token。</p>
|
||||||
<p>扫码后使用现有账号鉴权,不在二维码中保存 Token</p>
|
|
||||||
{qr ? <img src={qr} alt="全局监控手机端二维码" /> : error ? <div className="v2-monitor-qr-error" role="alert"><span>{error}</span><Button theme="light" type="danger" onClick={() => setAttempt((value) => value + 1)}>重新生成</Button></div> : <span className="v2-spinner" role="status" aria-label="正在生成二维码" />}
|
{qr ? <img src={qr} alt="全局监控手机端二维码" /> : error ? <div className="v2-monitor-qr-error" role="alert"><span>{error}</span><Button theme="light" type="danger" onClick={() => setAttempt((value) => value + 1)}>重新生成</Button></div> : <span className="v2-spinner" role="status" aria-label="正在生成二维码" />}
|
||||||
<code>{url}</code>
|
<code>{url}</code>
|
||||||
|
<span className="v2-sr-only" aria-live="polite">{copyLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
</Modal>;
|
</WorkspaceSideSheet>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VehicleRow = memo(function VehicleRow({ vehicle, selected, onSelect }: { vehicle: VehicleRealtimeRow; selected: boolean; onSelect: (vin: string) => void }) {
|
const VehicleRow = memo(function VehicleRow({ vehicle, selected, onSelect }: { vehicle: VehicleRealtimeRow; selected: boolean; onSelect: (vin: string) => void }) {
|
||||||
@@ -380,7 +407,7 @@ export default function MonitorPage() {
|
|||||||
<Button className="v2-monitor-mobile-entry" theme="light" icon={<IconQrCode />} aria-label="打开手机端入口" onClick={() => setMobileEntryOpen(true)}><span className="v2-monitor-mobile-entry-label">手机端</span></Button>
|
<Button className="v2-monitor-mobile-entry" theme="light" icon={<IconQrCode />} aria-label="打开手机端入口" onClick={() => setMobileEntryOpen(true)}><span className="v2-monitor-mobile-entry-label">手机端</span></Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
{batchSearchOpen ? <BatchVehicleSearchDialog initialValue={searchTerms.join('\n')} onClose={() => setBatchSearchOpen(false)} onApply={(value) => { setKeyword(value); setListOffset(0); setBatchSearchOpen(false); }} /> : null}
|
{batchSearchOpen ? <BatchVehicleSearchDialog initialValue={searchTerms.join('\n')} mobile={mobileLayout} onClose={() => setBatchSearchOpen(false)} onApply={(value) => { setKeyword(value); setListOffset(0); setBatchSearchOpen(false); }} /> : null}
|
||||||
|
|
||||||
<Card className="v2-kpis" bodyStyle={{ padding: 0 }} aria-label="车辆整体统计">
|
<Card className="v2-kpis" bodyStyle={{ padding: 0 }} aria-label="车辆整体统计">
|
||||||
{[
|
{[
|
||||||
@@ -450,7 +477,7 @@ export default function MonitorPage() {
|
|||||||
</div>
|
</div>
|
||||||
<time dateTime={lastSyncAt?.toISOString()}><span><small>最近同步</small><b>{lastSyncAt ? lastSyncAt.toLocaleTimeString('zh-CN', { hour12: false }) : '等待数据'}</b></span></time>
|
<time dateTime={lastSyncAt?.toISOString()}><span><small>最近同步</small><b>{lastSyncAt ? lastSyncAt.toLocaleTimeString('zh-CN', { hour12: false }) : '等待数据'}</b></span></time>
|
||||||
</Card>
|
</Card>
|
||||||
{mobileEntryOpen ? <MobileEntry onClose={() => setMobileEntryOpen(false)} /> : null}
|
{mobileEntryOpen ? <MobileEntry mobile={mobileLayout} onClose={() => setMobileEntryOpen(false)} /> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -454,8 +454,10 @@ describe('V2 production entry', () => {
|
|||||||
expect(workspaceMetricRailSource).toContain('<CardGroup className={secondaryClassName}');
|
expect(workspaceMetricRailSource).toContain('<CardGroup className={secondaryClassName}');
|
||||||
expect(corePageSources.MonitorPage).toContain('<Input');
|
expect(corePageSources.MonitorPage).toContain('<Input');
|
||||||
expect(corePageSources.MonitorPage).toContain('<Select');
|
expect(corePageSources.MonitorPage).toContain('<Select');
|
||||||
expect(corePageSources.MonitorPage).toContain('<Modal');
|
expect(corePageSources.MonitorPage).toContain('<WorkspaceSideSheet');
|
||||||
expect(corePageSources.MonitorPage).toContain('v2-monitor-qr-modal');
|
expect(corePageSources.MonitorPage).toContain('v2-monitor-batch-sidesheet');
|
||||||
|
expect(corePageSources.MonitorPage).toContain('v2-monitor-qr-sidesheet');
|
||||||
|
expect(corePageSources.MonitorPage).not.toContain('<Modal');
|
||||||
expect(corePageSources.MonitorPage).not.toContain('v2-monitor-qr-backdrop');
|
expect(corePageSources.MonitorPage).not.toContain('v2-monitor-qr-backdrop');
|
||||||
expect(corePageSources.MonitorPage).toContain('<TextArea');
|
expect(corePageSources.MonitorPage).toContain('<TextArea');
|
||||||
expect(corePageSources.MonitorPage).toContain('<SegmentedTabs');
|
expect(corePageSources.MonitorPage).toContain('<SegmentedTabs');
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export type WorkspaceSideSheetProps = {
|
|||||||
placement?: 'left' | 'right' | 'top' | 'bottom';
|
placement?: 'left' | 'right' | 'top' | 'bottom';
|
||||||
width?: string | number;
|
width?: string | number;
|
||||||
height?: string | number;
|
height?: string | number;
|
||||||
|
closeOnEsc?: boolean;
|
||||||
summaryItems?: WorkspaceSideSheetSummaryItem[];
|
summaryItems?: WorkspaceSideSheetSummaryItem[];
|
||||||
footerNote?: ReactNode;
|
footerNote?: ReactNode;
|
||||||
secondaryActions?: WorkspaceSideSheetAction[];
|
secondaryActions?: WorkspaceSideSheetAction[];
|
||||||
@@ -59,6 +60,7 @@ export function WorkspaceSideSheet({
|
|||||||
placement = 'right',
|
placement = 'right',
|
||||||
width = 460,
|
width = 460,
|
||||||
height,
|
height,
|
||||||
|
closeOnEsc = true,
|
||||||
summaryItems = [],
|
summaryItems = [],
|
||||||
footerNote,
|
footerNote,
|
||||||
secondaryActions = [],
|
secondaryActions = [],
|
||||||
@@ -89,6 +91,7 @@ export function WorkspaceSideSheet({
|
|||||||
visible={visible}
|
visible={visible}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
placement={placement}
|
placement={placement}
|
||||||
|
closeOnEsc={closeOnEsc}
|
||||||
width={placement === 'left' || placement === 'right' ? width : undefined}
|
width={placement === 'left' || placement === 'right' ? width : undefined}
|
||||||
height={placement === 'top' || placement === 'bottom' ? height : undefined}
|
height={placement === 'top' || placement === 'bottom' ? height : undefined}
|
||||||
title={<div className="v2-workspace-config-title">
|
title={<div className="v2-workspace-config-title">
|
||||||
|
|||||||
@@ -220,6 +220,164 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Global monitor action workbenches.
|
||||||
|
* Batch vehicle input and the mobile handoff now share the same Semi UI
|
||||||
|
* identity, summary, scrolling and footer hierarchy as the rest of the app.
|
||||||
|
*/
|
||||||
|
.v2-monitor-batch-sidesheet .v2-workspace-config-content,
|
||||||
|
.v2-monitor-qr-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-batch-sidesheet .v2-batch-search-dialog {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
align-content: start;
|
||||||
|
gap: 10px;
|
||||||
|
border: 1px solid #dce5ef;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 14px;
|
||||||
|
box-shadow: 0 7px 22px rgba(31, 53, 80, .05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-batch-sidesheet .v2-batch-search-dialog > label {
|
||||||
|
margin: 0;
|
||||||
|
color: #4d627a;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 680;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-batch-sidesheet .v2-batch-search-dialog .semi-input-textarea-wrapper {
|
||||||
|
min-height: min(420px, 52dvh);
|
||||||
|
border-color: #d4dfeb;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fbfcfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-batch-sidesheet .v2-batch-search-dialog .semi-input-textarea-wrapper-focus {
|
||||||
|
border-color: #4386df;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(45, 116, 210, .09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-batch-sidesheet .v2-batch-search-dialog .semi-input-textarea {
|
||||||
|
min-height: min(418px, 52dvh);
|
||||||
|
padding: 13px 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-batch-sidesheet .v2-batch-search-summary {
|
||||||
|
display: flex;
|
||||||
|
min-height: 34px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f4f8fe;
|
||||||
|
padding: 7px 9px;
|
||||||
|
color: #66788e;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-batch-sidesheet .v2-batch-search-summary strong {
|
||||||
|
color: #1268f3;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-qr-sidesheet .v2-monitor-qr-content {
|
||||||
|
min-height: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #dce5ef;
|
||||||
|
border-radius: 12px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 50% 0%, rgba(18, 104, 243, .07), transparent 38%),
|
||||||
|
#fff;
|
||||||
|
padding: 15px;
|
||||||
|
box-shadow: 0 7px 22px rgba(31, 53, 80, .05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-qr-sidesheet .v2-monitor-qr-content > p {
|
||||||
|
max-width: 310px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: #66788e;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-qr-sidesheet .v2-monitor-qr-content > img {
|
||||||
|
width: min(240px, 64vw);
|
||||||
|
height: min(240px, 64vw);
|
||||||
|
border: 8px solid #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 9px 28px rgba(27, 50, 78, .09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-qr-sidesheet .v2-monitor-qr-content > code {
|
||||||
|
max-width: 100%;
|
||||||
|
margin-top: 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f7f9fc;
|
||||||
|
padding: 9px 11px;
|
||||||
|
color: #5c6d82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-qr-sidesheet .v2-monitor-qr-content > .v2-spinner {
|
||||||
|
margin: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.v2-monitor-batch-sidesheet .v2-workspace-config-content,
|
||||||
|
.v2-monitor-qr-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-batch-sidesheet .v2-batch-search-dialog {
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-batch-sidesheet .v2-batch-search-dialog .semi-input-textarea-wrapper,
|
||||||
|
.v2-monitor-batch-sidesheet .v2-batch-search-dialog .semi-input-textarea {
|
||||||
|
min-height: min(300px, 35dvh);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-batch-sidesheet .v2-batch-search-dialog .semi-input-textarea {
|
||||||
|
padding: 10px 11px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-qr-sidesheet .v2-workspace-config-summary > span:nth-child(2) strong {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-qr-sidesheet .v2-monitor-qr-content {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-qr-sidesheet .v2-monitor-qr-content > p {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-qr-sidesheet .v2-monitor-qr-content > img {
|
||||||
|
width: min(210px, 56vw);
|
||||||
|
height: min(210px, 56vw);
|
||||||
|
border-width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-monitor-qr-sidesheet .v2-monitor-qr-content > code {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-block: 7px;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Global short-landscape entry and navigation.
|
* Global short-landscape entry and navigation.
|
||||||
* Keep every authentication action in the first viewport, preserve practical
|
* Keep every authentication action in the first viewport, preserve practical
|
||||||
|
|||||||
Reference in New Issue
Block a user