From 43643d1a83acdac8be72ca918457a315ecb62992 Mon Sep 17 00:00:00 2001 From: tisonhuang Date: Wed, 4 Mar 2026 20:08:18 +0800 Subject: [PATCH] feat(export): simplify export panel and page-scroll contacts list --- src/pages/ExportPage.scss | 315 +----------------------- src/pages/ExportPage.tsx | 487 +------------------------------------- 2 files changed, 15 insertions(+), 787 deletions(-) diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index 3471090..76778f5 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -386,267 +386,6 @@ } } -.export-card-diagnostics-section { - border: 1px solid var(--border-color); - border-radius: 12px; - background: var(--card-bg); - padding: 10px; - display: flex; - flex-direction: column; - gap: 10px; -} - -.diag-panel-header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 10px; - flex-wrap: wrap; -} - -.diag-panel-title { - display: flex; - flex-direction: column; - gap: 2px; - font-size: 13px; - color: var(--text-primary); - font-weight: 600; -} - -.diag-panel-subtitle { - font-size: 11px; - color: var(--text-tertiary); - font-weight: 500; -} - -.diag-panel-actions { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.diag-overview-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - gap: 8px; -} - -.diag-overview-item { - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 8px; - display: grid; - gap: 4px; - background: var(--bg-secondary); - font-size: 11px; - color: var(--text-secondary); - - strong { - font-size: 16px; - line-height: 1; - color: var(--text-primary); - } - - .warn { - color: #ff4d4f; - } -} - -.diag-step-chain { - border: 1px dashed var(--border-color); - border-radius: 10px; - padding: 10px; - background: var(--bg-secondary); - display: grid; - gap: 8px; -} - -.diag-step-chain-title { - font-size: 12px; - color: var(--text-primary); - font-weight: 600; -} - -.diag-step-list { - display: grid; - gap: 6px; -} - -.diag-step-item { - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 7px 8px; - background: var(--bg-primary); - display: flex; - align-items: flex-start; - gap: 8px; - - &.running { - border-color: rgba(var(--primary-rgb), 0.4); - } - - &.done { - border-color: rgba(82, 196, 26, 0.4); - } - - &.failed, - &.timeout, - &.stalled { - border-color: rgba(255, 77, 79, 0.45); - } -} - -.diag-step-order { - min-width: 18px; - height: 18px; - border-radius: 999px; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - font-size: 11px; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--text-secondary); - line-height: 1; -} - -.diag-step-main { - min-width: 0; - display: grid; - gap: 3px; -} - -.diag-step-name { - font-size: 12px; - color: var(--text-primary); - font-weight: 600; -} - -.diag-step-meta { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; - font-size: 11px; - color: var(--text-secondary); - - .warn { - color: #ff4d4f; - font-weight: 600; - } -} - -.diag-log-toolbar { - display: flex; - align-items: center; - gap: 6px; - flex-wrap: wrap; -} - -.diag-filter-btn { - border: 1px solid var(--border-color); - border-radius: 999px; - background: var(--bg-secondary); - color: var(--text-secondary); - font-size: 11px; - padding: 5px 10px; - cursor: pointer; - - &.active { - border-color: var(--primary); - color: var(--primary); - background: rgba(var(--primary-rgb), 0.12); - } -} - -.diag-log-list { - border: 1px solid var(--border-color); - border-radius: 10px; - background: var(--bg-secondary); - max-height: 320px; - overflow-y: auto; - padding: 8px; - display: grid; - gap: 6px; -} - -.diag-log-item { - border: 1px solid var(--border-color); - border-radius: 8px; - background: var(--bg-primary); - padding: 6px 8px; - display: grid; - gap: 4px; - - &.warn { - border-color: rgba(250, 173, 20, 0.5); - } - - &.error { - border-color: rgba(255, 77, 79, 0.45); - } -} - -.diag-log-top { - display: flex; - align-items: center; - gap: 6px; - flex-wrap: wrap; -} - -.diag-log-time { - font-size: 11px; - color: var(--text-tertiary); -} - -.diag-log-tag { - border-radius: 999px; - border: 1px solid var(--border-color); - background: var(--bg-secondary); - color: var(--text-secondary); - font-size: 10px; - padding: 1px 6px; - line-height: 1.4; - - &.warn, - &.timeout { - border-color: rgba(250, 173, 20, 0.55); - color: #d48806; - } - - &.error, - &.failed { - border-color: rgba(255, 77, 79, 0.55); - color: #ff4d4f; - } - - &.done { - border-color: rgba(82, 196, 26, 0.5); - color: #52c41a; - } -} - -.diag-log-message { - font-size: 12px; - color: var(--text-primary); - line-height: 1.45; -} - -.diag-log-meta { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; - font-size: 11px; - color: var(--text-tertiary); -} - -.diag-empty { - color: var(--text-secondary); - font-size: 12px; -} - .count-loading { color: var(--text-tertiary); font-size: 12px; @@ -974,12 +713,12 @@ border-radius: 12px; background: var(--card-bg); padding: 12px; - flex: 1 0 420px; + flex: 0 0 auto; min-height: 420px; display: flex; flex-direction: column; gap: 10px; - overflow: hidden; + overflow: visible; } .table-stage-hint { @@ -1105,7 +844,6 @@ .session-table-layout { display: flex; - flex: 1; min-height: 0; .table-wrap { @@ -1117,12 +855,12 @@ .table-wrap { --contacts-message-col-width: 120px; --contacts-action-col-width: 280px; - overflow: hidden; + overflow: visible; border: 1px solid var(--border-color); border-radius: 10px; min-height: 320px; - height: 100%; - flex: 1; + height: auto; + flex: 0 0 auto; display: flex; flex-direction: column; } @@ -1271,35 +1009,16 @@ } .contacts-list { - flex: 1; - min-height: 0; - overflow-y: auto; + flex: 0 0 auto; + min-height: auto; + overflow: visible; padding: 0 12px 12px; - position: relative; - - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-thumb { - background: var(--text-tertiary); - border-radius: 3px; - opacity: 0.3; - } - } - - .contacts-list-virtual { - position: relative; - min-height: 100%; } .contact-row { - position: absolute; - left: 0; - right: 0; - height: 76px; + position: static; + height: auto; padding-bottom: 4px; - will-change: transform; } .contact-item { @@ -2460,20 +2179,6 @@ font-size: 12px; } - .diag-panel-header { - flex-direction: column; - align-items: stretch; - } - - .diag-panel-actions { - width: 100%; - } - - .diag-panel-actions .secondary-btn { - flex: 1; - justify-content: center; - } - .export-dialog-overlay { padding: 10px; } diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 9ef8fec..707646e 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useEffect, useMemo, useRef, useState, type UIEvent } from 'react' +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useLocation } from 'react-router-dom' import { TableVirtuoso } from 'react-virtuoso' import { createPortal } from 'react-dom' @@ -471,16 +471,10 @@ const matchesContactTab = (contact: ContactInfo, tab: ConversationTab): boolean } const createTaskId = (): string => `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` -const createExportDiagTraceId = (): string => `export-card-${Date.now()}-${Math.random().toString(36).slice(2, 9)}` const CONTACT_ENRICH_TIMEOUT_MS = 7000 const EXPORT_SNS_STATS_CACHE_STALE_MS = 12 * 60 * 60 * 1000 const EXPORT_AVATAR_ENRICH_BATCH_SIZE = 80 -const CONTACTS_LIST_VIRTUAL_ROW_HEIGHT = 76 -const CONTACTS_LIST_VIRTUAL_OVERSCAN = 10 const DEFAULT_CONTACTS_LOAD_TIMEOUT_MS = 3000 -const EXPORT_CARD_DIAG_MAX_FRONTEND_LOGS = 1500 -const EXPORT_CARD_DIAG_STALL_MS = 3200 -const EXPORT_CARD_DIAG_POLL_INTERVAL_MS = 1200 const EXPORT_REENTER_SESSION_SOFT_REFRESH_MS = 5 * 60 * 1000 const EXPORT_REENTER_CONTACTS_SOFT_REFRESH_MS = 5 * 60 * 1000 const EXPORT_REENTER_SNS_SOFT_REFRESH_MS = 3 * 60 * 1000 @@ -568,64 +562,6 @@ interface SessionExportCacheMeta { source: 'memory' | 'disk' | 'fresh' } -type ExportCardDiagFilter = 'all' | 'frontend' | 'main' | 'backend' | 'worker' | 'warn' | 'error' - -type ExportCardDiagSource = 'frontend' | 'main' | 'backend' | 'worker' -type ExportCardDiagLevel = 'debug' | 'info' | 'warn' | 'error' -type ExportCardDiagStatus = 'running' | 'done' | 'failed' | 'timeout' - -interface ExportCardDiagLogEntry { - id: string - ts: number - source: ExportCardDiagSource - level: ExportCardDiagLevel - message: string - traceId?: string - stepId?: string - stepName?: string - status?: ExportCardDiagStatus - durationMs?: number - data?: Record -} - -interface ExportCardDiagActiveStep { - traceId: string - stepId: string - stepName: string - source: ExportCardDiagSource - elapsedMs: number - stallMs: number - startedAt: number - lastUpdatedAt: number - message?: string -} - -interface ExportCardDiagSnapshotState { - logs: ExportCardDiagLogEntry[] - activeSteps: ExportCardDiagActiveStep[] - summary: { - totalLogs: number - activeStepCount: number - errorCount: number - warnCount: number - timeoutCount: number - lastUpdatedAt: number - } -} - -const defaultExportCardDiagSnapshot: ExportCardDiagSnapshotState = { - logs: [], - activeSteps: [], - summary: { - totalLogs: 0, - activeStepCount: 0, - errorCount: 0, - warnCount: 0, - timeoutCount: 0, - lastUpdatedAt: 0 - } -} - const withTimeout = async (promise: Promise, timeoutMs: number): Promise => { let timer: ReturnType | null = null try { @@ -890,8 +826,6 @@ function ExportPage() { const [sessionMessageCounts, setSessionMessageCounts] = useState>({}) const [isLoadingSessionCounts, setIsLoadingSessionCounts] = useState(false) const [sessionContentMetrics, setSessionContentMetrics] = useState>({}) - const [contactsListScrollTop, setContactsListScrollTop] = useState(0) - const [contactsListViewportHeight, setContactsListViewportHeight] = useState(480) const [contactsLoadTimeoutMs, setContactsLoadTimeoutMs] = useState(DEFAULT_CONTACTS_LOAD_TIMEOUT_MS) const [contactsLoadSession, setContactsLoadSession] = useState(null) const [contactsLoadIssue, setContactsLoadIssue] = useState(null) @@ -950,11 +884,6 @@ function ExportPage() { totalFriends: 0 }) const [hasSeededSnsStats, setHasSeededSnsStats] = useState(false) - const [showCardDiagnostics, setShowCardDiagnostics] = useState(false) - const [diagFilter, setDiagFilter] = useState('all') - const [frontendDiagLogs, setFrontendDiagLogs] = useState([]) - const [backendDiagSnapshot, setBackendDiagSnapshot] = useState(defaultExportCardDiagSnapshot) - const [isExportCardDiagSyncing, setIsExportCardDiagSyncing] = useState(false) const [nowTick, setNowTick] = useState(Date.now()) const tabCounts = useContactTypeCountsStore(state => state.tabCounts) const isSharedTabCountsLoading = useContactTypeCountsStore(state => state.isLoading) @@ -975,7 +904,6 @@ function ExportPage() { const contactsLoadTimeoutTimerRef = useRef(null) const contactsLoadTimeoutMsRef = useRef(DEFAULT_CONTACTS_LOAD_TIMEOUT_MS) const contactsAvatarCacheRef = useRef>({}) - const contactsListRef = useRef(null) const detailRequestSeqRef = useRef(0) const sessionsRef = useRef([]) const contactsListSizeRef = useRef(0) @@ -988,63 +916,6 @@ function ExportPage() { const sessionCountRequestIdRef = useRef(0) const activeTabRef = useRef('private') - const appendFrontendDiagLog = useCallback((entry: ExportCardDiagLogEntry) => { - setFrontendDiagLogs(prev => { - const next = [...prev, entry] - if (next.length > EXPORT_CARD_DIAG_MAX_FRONTEND_LOGS) { - return next.slice(next.length - EXPORT_CARD_DIAG_MAX_FRONTEND_LOGS) - } - return next - }) - }, []) - - const logFrontendDiag = useCallback((input: { - source?: ExportCardDiagSource - level?: ExportCardDiagLevel - message: string - traceId?: string - stepId?: string - stepName?: string - status?: ExportCardDiagStatus - durationMs?: number - data?: Record - }) => { - const ts = Date.now() - appendFrontendDiagLog({ - id: `frontend-diag-${ts}-${Math.random().toString(36).slice(2, 8)}`, - ts, - source: input.source || 'frontend', - level: input.level || 'info', - message: input.message, - traceId: input.traceId, - stepId: input.stepId, - stepName: input.stepName, - status: input.status, - durationMs: input.durationMs, - data: input.data - }) - }, [appendFrontendDiagLog]) - - const fetchExportCardDiagnosticsSnapshot = useCallback(async (limit = 1200) => { - setIsExportCardDiagSyncing(true) - try { - const snapshot = await window.electronAPI.diagnostics.getExportCardLogs({ limit }) - if (!snapshot || typeof snapshot !== 'object') return - setBackendDiagSnapshot(snapshot as ExportCardDiagSnapshotState) - } catch (error) { - logFrontendDiag({ - level: 'warn', - message: '拉取后端诊断日志失败', - stepId: 'frontend-sync-backend-diag', - stepName: '同步后端诊断日志', - status: 'failed', - data: { error: String(error) } - }) - } finally { - setIsExportCardDiagSyncing(false) - } - }, [logFrontendDiag]) - const ensureExportCacheScope = useCallback(async (): Promise => { if (exportCacheScopeReadyRef.current) { return exportCacheScopeRef.current @@ -1967,15 +1838,6 @@ function ExportPage() { return () => window.clearTimeout(timer) }, [isExportRoute, ensureSharedTabCountsLoaded, loadBaseConfig, loadSessions, loadSnsStats]) - useEffect(() => { - if (!isExportRoute || !showCardDiagnostics) return - void fetchExportCardDiagnosticsSnapshot(1600) - const timer = window.setInterval(() => { - void fetchExportCardDiagnosticsSnapshot(1600) - }, EXPORT_CARD_DIAG_POLL_INTERVAL_MS) - return () => window.clearInterval(timer) - }, [isExportRoute, showCardDiagnostics, fetchExportCardDiagnosticsSnapshot]) - useEffect(() => { if (isExportRoute) return // 导出页隐藏时停止后台联系人补齐请求,避免与通讯录页面查询抢占。 @@ -2831,147 +2693,6 @@ function ExportPage() { return [...sessionCards, snsCard] }, [sessions, lastExportByContent, snsStats, lastSnsExportPostCount]) - const mergedCardDiagLogs = useMemo(() => { - const merged = [...backendDiagSnapshot.logs, ...frontendDiagLogs] - merged.sort((a, b) => (b.ts - a.ts) || a.id.localeCompare(b.id)) - return merged - }, [backendDiagSnapshot.logs, frontendDiagLogs]) - - const latestCardDiagTraceId = useMemo(() => { - for (const item of mergedCardDiagLogs) { - const traceId = String(item.traceId || '').trim() - if (traceId) return traceId - } - return '' - }, [mergedCardDiagLogs]) - - const cardDiagTraceSteps = useMemo(() => { - if (!latestCardDiagTraceId) return [] as Array<{ - traceId: string - stepId: string - stepName: string - source: ExportCardDiagSource - status: ExportCardDiagStatus - startedAt: number - endedAt?: number - durationMs?: number - lastUpdatedAt: number - message: string - stalled: boolean - }> - - const traceLogs = mergedCardDiagLogs - .filter(item => item.traceId === latestCardDiagTraceId && item.stepId && item.stepName) - .sort((a, b) => a.ts - b.ts) - - const stepMap = new Map() - - for (const item of traceLogs) { - const stepId = String(item.stepId || '').trim() - if (!stepId) continue - const prev = stepMap.get(stepId) - const nextStatus: ExportCardDiagStatus = item.status || prev?.status || 'running' - const startedAt = prev?.startedAt || item.ts - const endedAt = nextStatus === 'done' || nextStatus === 'failed' || nextStatus === 'timeout' - ? item.ts - : prev?.endedAt - const durationMs = typeof item.durationMs === 'number' - ? item.durationMs - : endedAt - ? Math.max(0, endedAt - startedAt) - : undefined - stepMap.set(stepId, { - traceId: latestCardDiagTraceId, - stepId, - stepName: String(item.stepName || stepId), - source: item.source, - status: nextStatus, - startedAt, - endedAt, - durationMs, - lastUpdatedAt: item.ts, - message: item.message - }) - } - - const now = Date.now() - return Array.from(stepMap.values()).map(step => ({ - ...step, - stalled: step.status === 'running' && now - step.lastUpdatedAt >= EXPORT_CARD_DIAG_STALL_MS - })) - }, [mergedCardDiagLogs, latestCardDiagTraceId]) - - const cardDiagRunningStepCount = useMemo( - () => cardDiagTraceSteps.filter(step => step.status === 'running').length, - [cardDiagTraceSteps] - ) - const cardDiagStalledStepCount = useMemo( - () => cardDiagTraceSteps.filter(step => step.stalled).length, - [cardDiagTraceSteps] - ) - - const filteredCardDiagLogs = useMemo(() => { - return mergedCardDiagLogs.filter((item) => { - if (diagFilter === 'all') return true - if (diagFilter === 'warn') return item.level === 'warn' - if (diagFilter === 'error') return item.level === 'error' || item.status === 'failed' || item.status === 'timeout' - return item.source === diagFilter - }) - }, [mergedCardDiagLogs, diagFilter]) - - const clearCardDiagnostics = useCallback(async () => { - setFrontendDiagLogs([]) - setBackendDiagSnapshot(defaultExportCardDiagSnapshot) - try { - await window.electronAPI.diagnostics.clearExportCardLogs() - } catch (error) { - logFrontendDiag({ - level: 'warn', - message: '清空后端诊断日志失败', - stepId: 'frontend-clear-diagnostics', - stepName: '清空诊断日志', - status: 'failed', - data: { error: String(error) } - }) - } - }, [logFrontendDiag]) - - const exportCardDiagnosticsLogs = useCallback(async () => { - const now = new Date() - const stamp = `${now.getFullYear()}${`${now.getMonth() + 1}`.padStart(2, '0')}${`${now.getDate()}`.padStart(2, '0')}-${`${now.getHours()}`.padStart(2, '0')}${`${now.getMinutes()}`.padStart(2, '0')}${`${now.getSeconds()}`.padStart(2, '0')}` - const defaultDir = exportFolder || await window.electronAPI.app.getDownloadsPath() - const saveResult = await window.electronAPI.dialog.saveFile({ - title: '导出导出卡片诊断日志', - defaultPath: `${defaultDir}/weflow-export-card-diagnostics-${stamp}.jsonl`, - filters: [ - { name: 'JSON Lines', extensions: ['jsonl'] }, - { name: 'Text', extensions: ['txt'] } - ] - }) - if (saveResult.canceled || !saveResult.filePath) return - - const result = await window.electronAPI.diagnostics.exportExportCardLogs({ - filePath: saveResult.filePath, - frontendLogs: frontendDiagLogs - }) - if (result.success) { - window.alert(`导出成功\\n日志:${result.filePath}\\n摘要:${result.summaryPath || '未生成'}\\n总条数:${result.count || 0}`) - } else { - window.alert(`导出失败:${result.error || '未知错误'}`) - } - }, [exportFolder, frontendDiagLogs]) - const activeTabLabel = useMemo(() => { if (activeTab === 'private') return '私聊' if (activeTab === 'group') return '群聊' @@ -3404,54 +3125,6 @@ function ExportPage() { contact.avatarUrl ? count + 1 : count ), 0) }, [contactsList]) - useEffect(() => { - if (!contactsListRef.current) return - contactsListRef.current.scrollTop = 0 - setContactsListScrollTop(0) - }, [activeTab, searchKeyword]) - - useEffect(() => { - const node = contactsListRef.current - if (!node) return - const updateViewportHeight = () => { - setContactsListViewportHeight(Math.max(node.clientHeight, CONTACTS_LIST_VIRTUAL_ROW_HEIGHT)) - } - updateViewportHeight() - const observer = new ResizeObserver(() => updateViewportHeight()) - observer.observe(node) - return () => observer.disconnect() - }, [filteredContacts.length, isContactsListLoading]) - - useEffect(() => { - const maxScroll = Math.max(0, filteredContacts.length * CONTACTS_LIST_VIRTUAL_ROW_HEIGHT - contactsListViewportHeight) - if (contactsListScrollTop <= maxScroll) return - setContactsListScrollTop(maxScroll) - if (contactsListRef.current) { - contactsListRef.current.scrollTop = maxScroll - } - }, [filteredContacts.length, contactsListViewportHeight, contactsListScrollTop]) - - const { startIndex: contactStartIndex, endIndex: contactEndIndex } = useMemo(() => { - if (filteredContacts.length === 0) { - return { startIndex: 0, endIndex: 0 } - } - const baseStart = Math.floor(contactsListScrollTop / CONTACTS_LIST_VIRTUAL_ROW_HEIGHT) - const visibleCount = Math.ceil(contactsListViewportHeight / CONTACTS_LIST_VIRTUAL_ROW_HEIGHT) - const nextStart = Math.max(0, baseStart - CONTACTS_LIST_VIRTUAL_OVERSCAN) - const nextEnd = Math.min(filteredContacts.length, nextStart + visibleCount + CONTACTS_LIST_VIRTUAL_OVERSCAN * 2) - return { - startIndex: nextStart, - endIndex: nextEnd - } - }, [filteredContacts.length, contactsListViewportHeight, contactsListScrollTop]) - - const visibleContacts = useMemo(() => { - return filteredContacts.slice(contactStartIndex, contactEndIndex) - }, [filteredContacts, contactStartIndex, contactEndIndex]) - - const onContactsListScroll = useCallback((event: UIEvent) => { - setContactsListScrollTop(event.currentTarget.scrollTop) - }, []) const contactsIssueElapsedMs = useMemo(() => { if (!contactsLoadIssue) return 0 @@ -3912,155 +3585,13 @@ function ExportPage() { openContentExport(card.type) }} > - {isCardRunning ? '导出中' : '导出'} + {isCardRunning ? '批量导出中' : '批量导出'} ) })} -
-
-
- 卡片统计诊断日志 - 仅用于当前 6 个卡片排查 -
-
- - {showCardDiagnostics && ( - <> - - - - - )} -
-
- - {showCardDiagnostics && ( - <> -
-
- 日志总数 - {backendDiagSnapshot.summary.totalLogs + frontendDiagLogs.length} -
-
- 活跃步骤 - {backendDiagSnapshot.activeSteps.length} -
-
- 当前运行步骤 - {cardDiagRunningStepCount} -
-
- 疑似卡住 - 0 ? 'warn' : ''}>{cardDiagStalledStepCount} -
-
- 最近告警 - {backendDiagSnapshot.summary.warnCount} -
-
- 最近错误 - 0 ? 'warn' : ''}>{backendDiagSnapshot.summary.errorCount} -
-
- -
-
- 当前链路 - {latestCardDiagTraceId ? ` · trace=${latestCardDiagTraceId}` : ''} -
- {cardDiagTraceSteps.length === 0 ? ( -
暂无链路步骤,请先触发一次卡片统计。
- ) : ( -
- {cardDiagTraceSteps.map((step, index) => ( -
- {index + 1} -
-
{step.stepName}
-
- {step.source} - {step.status} - 耗时 {step.durationMs ?? Math.max(0, Date.now() - step.startedAt)}ms - {step.stalled && 卡住 {Math.max(0, Date.now() - step.lastUpdatedAt)}ms} -
-
-
- ))} -
- )} -
- -
- {([ - { value: 'all', label: '全部' }, - { value: 'frontend', label: '前端' }, - { value: 'main', label: '主进程' }, - { value: 'backend', label: '后端' }, - { value: 'worker', label: 'Worker' }, - { value: 'warn', label: '告警' }, - { value: 'error', label: '错误' } - ] as Array<{ value: ExportCardDiagFilter; label: string }>).map(item => ( - - ))} -
- -
- {filteredCardDiagLogs.length === 0 ? ( -
暂无日志
- ) : ( - filteredCardDiagLogs.slice(0, 260).map(log => { - const ms = `${log.ts % 1000}`.padStart(3, '0') - const timeLabel = `${new Date(log.ts).toLocaleTimeString('zh-CN', { hour12: false })}.${ms}` - return ( -
-
- {timeLabel} - {log.source} - {log.level} - {log.status && {log.status}} - {typeof log.durationMs === 'number' && 耗时 {log.durationMs}ms} -
-
{log.message}
- {(log.stepName || log.traceId) && ( -
- {log.stepName && {log.stepName}} - {log.traceId && trace={log.traceId}} -
- )} -
- ) - }) - )} -
- - )} -
-
@@ -4182,14 +3713,8 @@ function ExportPage() { 总消息数 操作
-
-
- {visibleContacts.map((contact, idx) => { - const absoluteIndex = contactStartIndex + idx - const top = absoluteIndex * CONTACTS_LIST_VIRTUAL_ROW_HEIGHT +
+ {filteredContacts.map((contact) => { const matchedSession = sessionRowByUsername.get(contact.username) const canExport = Boolean(matchedSession?.hasSession) const isRunning = canExport && runningSessionIds.has(contact.username) @@ -4207,7 +3732,6 @@ function ExportPage() {
@@ -4264,7 +3788,7 @@ function ExportPage() { 导出中 - ) : !canExport ? '暂无会话' : isQueued ? '排队中' : '导出'} + ) : !canExport ? '暂无会话' : isQueued ? '排队中' : '单会话导出'}
{recent && {recent}} @@ -4273,7 +3797,6 @@ function ExportPage() {
) })} -
)}