From 98c62fd6bd0c9fe657f93d16ea5edccc745dd764 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 30 Apr 2026 13:35:07 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20style(driver):=20=E9=87=8D?= =?UTF-8?q?=E5=81=9A=E9=A9=B1=E5=8A=A8=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=B8=83=E5=B1=80=E4=B8=8E=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 页面结构:将驱动表格改为卡片列表,移除横向滚动依赖 - 信息展示:新增顶部状态统计,清晰区分全部、已启用、需重装、未启用 - 重装提示:将长原因文案收敛为摘要展示,并支持展开查看完整原因 - 操作优化:集中展示版本、进度、安装、重装、移除、本地导入和日志入口 - 响应式适配:窄屏下驱动卡片自动堆叠,避免内容挤压 --- frontend/src/App.css | 230 ++++- .../src/components/DriverManagerModal.tsx | 792 +++++++----------- 2 files changed, 504 insertions(+), 518 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index 1bd7e3b..7f20cfa 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -326,35 +326,194 @@ body[data-theme='light'] .redis-viewer-workbench .ant-radio-button-wrapper-check color: #fff !important; } -/* 驱动管理:统一关闭 antd sticky 横向条,仅保留自定义独立横向条 */ -.driver-manager-table .ant-table-sticky-scroll { - display: none !important; +.driver-manager-modal .ant-modal-body { + background: var(--ant-color-bg-layout, #f5f5f5); } -/* 仅在独立横向条激活时隐藏表格自身横向滚动条,避免出现双横向条 */ -.driver-manager-table-wrap.driver-manager-table-wrap-external-active .driver-manager-table .ant-table-content, -.driver-manager-table-wrap.driver-manager-table-wrap-external-active .driver-manager-table .ant-table-body { - overflow-x: auto !important; - -ms-overflow-style: none; - scrollbar-width: none; -} - -.driver-manager-table-wrap.driver-manager-table-wrap-external-active .driver-manager-table .ant-table-content::-webkit-scrollbar:horizontal, -.driver-manager-table-wrap.driver-manager-table-wrap-external-active .driver-manager-table .ant-table-body::-webkit-scrollbar:horizontal { - height: 0 !important; -} - -.driver-manager-table-wrap { - width: 100%; - max-width: 100%; - overflow-x: hidden; -} - -.driver-manager-footer { - width: 100%; +.driver-manager-shell { display: flex; flex-direction: column; + gap: 14px; +} + +.driver-manager-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 16px; + align-items: stretch; + padding: 14px 16px; + border: 1px solid rgba(5, 5, 5, 0.08); + border-radius: 8px; + background: var(--ant-color-bg-container, #fff); +} + +.driver-manager-heading { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.driver-manager-stats { + display: grid; + grid-template-columns: repeat(4, minmax(64px, 1fr)); gap: 8px; + min-width: 360px; +} + +.driver-manager-stat { + display: flex; + flex-direction: column; + gap: 2px; + justify-content: center; + min-height: 58px; + padding: 8px 10px; + border: 1px solid rgba(5, 5, 5, 0.08); + border-radius: 8px; + background: rgba(5, 5, 5, 0.02); +} + +.driver-manager-stat span:first-child { + font-size: 20px; + font-weight: 700; + line-height: 1.2; +} + +.driver-manager-stat-warning span:first-child { + color: #d48806; +} + +.driver-manager-directory-panel { + border: 1px solid rgba(5, 5, 5, 0.08); + border-radius: 8px; + background: var(--ant-color-bg-container, #fff); +} + +.driver-manager-toolbar { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + justify-content: space-between; +} + +.driver-manager-search { + min-width: 280px; + flex: 1 1 360px; +} + +.driver-manager-toolbar-actions { + justify-content: flex-end; +} + +.driver-manager-list-head { + display: flex; + justify-content: space-between; + gap: 12px; + min-height: 24px; +} + +.driver-manager-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.driver-manager-card { + border: 1px solid rgba(5, 5, 5, 0.08); + border-radius: 8px; + background: var(--ant-color-bg-container, #fff); + overflow: hidden; +} + +.driver-manager-card-warning { + border-color: rgba(250, 173, 20, 0.35); +} + +.driver-manager-card-ready { + border-color: rgba(82, 196, 26, 0.22); +} + +.driver-manager-card-main { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(300px, 38%); + gap: 16px; + padding: 16px; +} + +.driver-manager-card-info { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; +} + +.driver-manager-title-row, +.driver-manager-meta-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + min-width: 0; +} + +.driver-manager-driver-name { + font-size: 16px; +} + +.driver-manager-meta-row { + row-gap: 4px; +} + +.driver-manager-update-note { + display: grid; + gap: 4px; + padding: 10px 12px; + border-radius: 8px; + background: rgba(250, 173, 20, 0.1); +} + +.driver-manager-note-text, +.driver-manager-muted-message { + margin-bottom: 0 !important; +} + +.driver-manager-muted-message { + color: var(--ant-color-text-secondary); +} + +.driver-manager-card-controls { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; +} + +.driver-manager-control-block { + display: grid; + gap: 4px; +} + +.driver-manager-control-label, +.driver-manager-small-text { + font-size: 12px; +} + +.driver-manager-version-control { + display: grid; + gap: 4px; +} + +.driver-manager-version-lock { + line-height: 24px; +} + +.driver-manager-card-actions { + justify-content: flex-end; +} + +.driver-manager-card-actions .ant-btn { + min-width: 88px; } .driver-manager-footer-actions { @@ -363,17 +522,20 @@ body[data-theme='light'] .redis-viewer-workbench .ant-radio-button-wrapper-check justify-content: flex-end; } -.driver-manager-hscroll { - width: 100%; - height: 12px; - overflow-x: auto; - overflow-y: hidden; - scrollbar-gutter: stable; - background: transparent; -} +@media (max-width: 900px) { + .driver-manager-header, + .driver-manager-card-main { + grid-template-columns: 1fr; + } -.driver-manager-hscroll-inner { - height: 1px; + .driver-manager-stats { + min-width: 0; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .driver-manager-card-actions { + justify-content: flex-start; + } } .security-update-action-btn.ant-btn, diff --git a/frontend/src/components/DriverManagerModal.tsx b/frontend/src/components/DriverManagerModal.tsx index a4551d7..d37e228 100644 --- a/frontend/src/components/DriverManagerModal.tsx +++ b/frontend/src/components/DriverManagerModal.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Alert, Button, Collapse, Input, Modal, Progress, Select, Space, Switch, Table, Tag, Typography, message } from 'antd'; +import { Alert, Button, Collapse, Empty, Input, Modal, Progress, Select, Space, Switch, Tag, Typography, message } from 'antd'; import { DeleteOutlined, DownloadOutlined, FileSearchOutlined, FolderOpenOutlined, InfoCircleFilled, ReloadOutlined } from '@ant-design/icons'; import { EventsOn } from '../../wailsjs/runtime/runtime'; import { useStore } from '../store'; @@ -113,7 +113,6 @@ type DriverVersionOption = { const buildVersionOptionKey = (option: DriverVersionOption) => `${option.version}@@${option.downloadUrl}`; const buildVersionSizeLoadingKey = (driverType: string, optionKey: string) => `${driverType}@@${optionKey}`; -const DRIVER_TABLE_SCROLL_X = 1450; const DRIVER_STATUS_CACHE_TTL_MS = 60 * 1000; const DRIVER_NETWORK_CACHE_TTL_MS = 5 * 60 * 1000; const normalizeDriverSearchText = (value: string) => String(value || '').trim().toLowerCase(); @@ -179,11 +178,6 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG const darkMode = theme === 'dark'; const resolvedAppearance = resolveAppearanceValues(appearance); const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity); - const modalContentRef = useRef(null); - const tableContainerRef = useRef(null); - const tableScrollTargetsRef = useRef([]); - const externalHScrollRef = useRef(null); - const horizontalSyncSourceRef = useRef<'table' | 'external' | ''>(''); const [loading, setLoading] = useState(false); const [downloadDir, setDownloadDir] = useState(''); const [networkChecking, setNetworkChecking] = useState(false); @@ -201,7 +195,6 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG const [selectedVersionMap, setSelectedVersionMap] = useState>({}); const [versionLoadingMap, setVersionLoadingMap] = useState>({}); const [versionSizeLoadingMap, setVersionSizeLoadingMap] = useState>({}); - const [horizontalScrollWidth, setHorizontalScrollWidth] = useState(DRIVER_TABLE_SCROLL_X); const downloadDirRef = useRef(downloadDir); useEffect(() => { @@ -254,76 +247,6 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG }); }, []); - const refreshHorizontalScrollState = useCallback(() => { - const tableContainer = tableContainerRef.current; - const targets = tableContainer - ? [ - ...new Set( - [ - ...Array.from(tableContainer.querySelectorAll('.ant-table-content')), - ...Array.from(tableContainer.querySelectorAll('.ant-table-body')), - ].filter((node): node is HTMLElement => node instanceof HTMLElement), - ), - ] - : tableScrollTargetsRef.current; - if (!targets || targets.length === 0) { - setHorizontalScrollWidth(DRIVER_TABLE_SCROLL_X); - return; - } - - const nextWidth = Math.max( - DRIVER_TABLE_SCROLL_X, - ...targets.map((target) => Math.max(0, target.scrollWidth)), - ); - setHorizontalScrollWidth((prev) => (prev === nextWidth ? prev : nextWidth)); - - const externalScroll = externalHScrollRef.current; - if (!externalScroll || horizontalSyncSourceRef.current === 'external') { - return; - } - const preferredTarget = - targets.find((target) => target.scrollWidth > target.clientWidth + 1) || - targets[0]; - const targetScrollLeft = preferredTarget?.scrollLeft || 0; - if (Math.abs(externalScroll.scrollLeft - targetScrollLeft) > 1) { - externalScroll.scrollLeft = targetScrollLeft; - } - }, []); - - const applyExternalScrollToTableTargets = useCallback(() => { - const tableContainer = tableContainerRef.current; - const externalScroll = externalHScrollRef.current; - if (!(tableContainer instanceof HTMLElement) || !(externalScroll instanceof HTMLDivElement)) { - return; - } - if (horizontalSyncSourceRef.current === 'table') { - return; - } - - const liveTargets = [ - ...new Set( - [ - ...Array.from(tableContainer.querySelectorAll('.ant-table-content')), - ...Array.from(tableContainer.querySelectorAll('.ant-table-body')), - ].filter((node): node is HTMLElement => node instanceof HTMLElement), - ), - ]; - if (liveTargets.length === 0) { - return; - } - - horizontalSyncSourceRef.current = 'external'; - liveTargets.forEach((target) => { - if (target.scrollWidth <= target.clientWidth + 1) { - return; - } - if (Math.abs(target.scrollLeft - externalScroll.scrollLeft) > 1) { - target.scrollLeft = externalScroll.scrollLeft; - } - }); - horizontalSyncSourceRef.current = ''; - }, []); - const refreshStatus = useCallback(async ( toastOnError = true, options?: { showLoading?: boolean }, @@ -601,8 +524,6 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG useEffect(() => { if (!open) { - setHorizontalScrollWidth(DRIVER_TABLE_SCROLL_X); - tableScrollTargetsRef.current = []; return; } @@ -630,117 +551,6 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG } }, [checkNetworkStatus, open, refreshStatus]); - useEffect(() => { - if (!open) { - return; - } - const tableContainer = tableContainerRef.current; - const externalScroll = externalHScrollRef.current; - if (!(tableContainer instanceof HTMLElement) || !(externalScroll instanceof HTMLDivElement)) { - return; - } - - let currentTargets: HTMLElement[] = []; - let rafId: number | null = null; - let bodyResizeObserver: ResizeObserver | null = null; - let containerResizeObserver: ResizeObserver | null = null; - - const pickSyncTarget = () => { - if (currentTargets.length === 0) { - return null; - } - return currentTargets.find((target) => target.scrollWidth > target.clientWidth + 1) || currentTargets[0]; - }; - - const syncFromTableTarget = (event?: Event) => { - const source = event?.currentTarget instanceof HTMLElement ? event.currentTarget : null; - const activeTarget = source || pickSyncTarget(); - if (!activeTarget) { - return; - } - if (horizontalSyncSourceRef.current === 'external') { - return; - } - horizontalSyncSourceRef.current = 'table'; - if (Math.abs(externalScroll.scrollLeft - activeTarget.scrollLeft) > 1) { - externalScroll.scrollLeft = activeTarget.scrollLeft; - } - horizontalSyncSourceRef.current = ''; - }; - - const bindCurrentTableTargets = () => { - const nextTargets = [ - ...new Set( - [ - ...Array.from(tableContainer.querySelectorAll('.ant-table-content')), - ...Array.from(tableContainer.querySelectorAll('.ant-table-body')), - ].filter((node): node is HTMLElement => node instanceof HTMLElement), - ), - ]; - - const sameTargets = - nextTargets.length === currentTargets.length && - nextTargets.every((target, index) => target === currentTargets[index]); - if (sameTargets) { - return; - } - - currentTargets.forEach((target) => { - target.removeEventListener('scroll', syncFromTableTarget); - bodyResizeObserver?.unobserve(target); - }); - - currentTargets = nextTargets; - tableScrollTargetsRef.current = nextTargets; - currentTargets.forEach((target) => { - target.addEventListener('scroll', syncFromTableTarget, { passive: true }); - bodyResizeObserver?.observe(target); - }); - - refreshHorizontalScrollState(); - syncFromTableTarget(); - }; - - const scheduleRefresh = () => { - if (rafId !== null) { - cancelAnimationFrame(rafId); - } - rafId = requestAnimationFrame(() => { - bindCurrentTableTargets(); - refreshHorizontalScrollState(); - }); - }; - - const mutationObserver = new MutationObserver(scheduleRefresh); - mutationObserver.observe(tableContainer, { childList: true, subtree: true }); - - bodyResizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(scheduleRefresh) : null; - containerResizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(scheduleRefresh) : null; - containerResizeObserver?.observe(tableContainer); - if (typeof ResizeObserver !== 'undefined') { - modalContentRef.current && containerResizeObserver?.observe(modalContentRef.current); - } - window.addEventListener('resize', scheduleRefresh); - - scheduleRefresh(); - return () => { - mutationObserver.disconnect(); - window.removeEventListener('resize', scheduleRefresh); - currentTargets.forEach((target) => { - target.removeEventListener('scroll', syncFromTableTarget); - }); - if (bodyResizeObserver) { - bodyResizeObserver.disconnect(); - } - if (containerResizeObserver) { - containerResizeObserver.disconnect(); - } - if (rafId !== null) { - cancelAnimationFrame(rafId); - } - }; - }, [open, refreshHorizontalScrollState]); - useEffect(() => { if (!open) { return; @@ -1011,221 +821,155 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG } }, [appendOperationLog, downloadDir, refreshStatus]); - const columns = useMemo(() => { - return [ - { - title: '数据源', - dataIndex: 'name', - key: 'name', - width: 220, - render: (_: string, row: DriverStatusRow) => ( -
- {row.name} - {row.message ? ( - - {row.message} - - ) : null} -
- ), - }, - { - title: '安装包大小', - dataIndex: 'packageSizeText', - key: 'packageSizeText', - width: 120, - render: (_: string | undefined, row: DriverStatusRow) => { - if (row.builtIn) { - return row.packageSizeText || '-'; - } - const options = versionMap[row.type] || []; - const selectedKey = selectedVersionMap[row.type]; - const loadingKey = buildVersionSizeLoadingKey(row.type, selectedKey || ''); - const selectedOption = - options.find((item) => buildVersionOptionKey(item) === selectedKey) || - options.find((item) => item.recommended) || - options[0]; - const anyKnownSize = options.find((item) => String(item.packageSizeText || '').trim())?.packageSizeText; - if (selectedKey && versionSizeLoadingMap[loadingKey]) { - return '计算中...'; - } - return selectedOption?.packageSizeText || anyKnownSize || row.packageSizeText || '-'; - }, - }, - { - title: '状态', - key: 'status', - width: 140, - render: (_: string, row: DriverStatusRow) => { - if (row.builtIn) { - return 内置可用; - } - const progress = progressMap[row.type]; - if (progress && (progress.status === 'start' || progress.status === 'downloading')) { - return 安装中 {Math.round(progress.percent)}%; - } - if (row.needsUpdate) { - return 强烈建议重装; - } - if (row.connectable) { - return 已启用; - } - if (row.packageInstalled) { - return 已安装; - } - return 未启用; - }, - }, - { - title: '安装进度', - key: 'progress', - width: 170, - render: (_: string, row: DriverStatusRow) => { - if (row.builtIn) { - return -; - } + const resolvePackageSizeText = (row: DriverStatusRow): string => { + if (row.builtIn) { + return row.packageSizeText || '-'; + } + const options = versionMap[row.type] || []; + const selectedKey = selectedVersionMap[row.type]; + const loadingKey = buildVersionSizeLoadingKey(row.type, selectedKey || ''); + const selectedOption = + options.find((item) => buildVersionOptionKey(item) === selectedKey) || + options.find((item) => item.recommended) || + options[0]; + const anyKnownSize = options.find((item) => String(item.packageSizeText || '').trim())?.packageSizeText; + if (selectedKey && versionSizeLoadingMap[loadingKey]) { + return '计算中...'; + } + return selectedOption?.packageSizeText || anyKnownSize || row.packageSizeText || '-'; + }; - const progress = progressMap[row.type]; - let percent = 0; - let status: 'normal' | 'exception' | 'active' | 'success' = 'normal'; + const resolveDriverStatusTag = (row: DriverStatusRow) => { + if (row.builtIn) { + return 内置可用; + } + const progress = progressMap[row.type]; + if (progress && (progress.status === 'start' || progress.status === 'downloading')) { + return 安装中 {Math.round(progress.percent)}%; + } + if (row.needsUpdate) { + return 建议重装; + } + if (row.connectable) { + return 已启用; + } + if (row.packageInstalled) { + return 已安装未启用; + } + return 未启用; + }; - if (progress?.status === 'error') { - percent = Math.max(0, Math.min(100, Math.round(progress.percent || 0))); - status = 'exception'; - } else if (progress && (progress.status === 'start' || progress.status === 'downloading')) { - percent = Math.max(1, Math.min(99, Math.round(progress.percent || 0))); - status = 'active'; - } else if (row.connectable || row.packageInstalled) { - percent = 100; - status = 'success'; - } + const resolveDriverProgress = (row: DriverStatusRow) => { + const progress = progressMap[row.type]; + let percent = 0; + let status: 'normal' | 'exception' | 'active' | 'success' = 'normal'; - return ; - }, - }, - { - title: '驱动版本', - key: 'driverVersion', - width: 230, - render: (_: string, row: DriverStatusRow) => { - if (row.builtIn) { - return -; - } - const versionLocked = row.packageInstalled || row.connectable; - if (versionLocked) { - const installedVersion = String(row.installedVersion || '').trim(); - const revisionHint = row.needsUpdate ? ',需重装' : ''; - if (installedVersion) { - return {installedVersion}(已安装{revisionHint},移除后可更换); + if (progress?.status === 'error') { + percent = Math.max(0, Math.min(100, Math.round(progress.percent || 0))); + status = 'exception'; + } else if (progress && (progress.status === 'start' || progress.status === 'downloading')) { + percent = Math.max(1, Math.min(99, Math.round(progress.percent || 0))); + status = 'active'; + } else if (row.connectable || row.packageInstalled) { + percent = 100; + status = 'success'; + } + + return { percent, status }; + }; + + const renderVersionControl = (row: DriverStatusRow) => { + if (row.builtIn) { + return 内置驱动无需安装; + } + + const versionLocked = row.packageInstalled || row.connectable; + if (versionLocked) { + const installedVersion = String(row.installedVersion || '').trim(); + const revisionHint = row.needsUpdate ? ',需重装' : ''; + return ( + + {installedVersion ? `${installedVersion}(已安装${revisionHint})` : `已安装${row.needsUpdate ? ',需重装' : ''}`} + + ); + } + + const options = versionMap[row.type] || []; + const selectedKey = selectedVersionMap[row.type]; + const selectOptions = buildVersionSelectOptions(options); + const mongoHint = row.type === 'mongodb' + ? '当前仅支持 MongoDB 1.17.x 和 2.x;更老 1.x 暂不提供安装。' + : ''; + return ( +
+ 0 ? '选择驱动版本' : '点击展开加载版本'} - value={selectedKey} - options={selectOptions as any} - onOpenChange={(open) => { - if (open && options.length === 0 && !versionLoadingMap[row.type]) { - void loadVersionOptions(row, true); - return; - } - if (open && selectedKey) { - void loadVersionPackageSize(row, selectedKey); - } - }} - onChange={(value) => { - setSelectedVersionMap((prev) => ({ ...prev, [row.type]: value })); - void loadVersionPackageSize(row, value); - }} - /> - {mongoHint ? {mongoHint} : null} -
- ); - }, - }, - { - title: '操作', - key: 'actions', - width: 320, - render: (_: string, row: DriverStatusRow) => { - if (row.builtIn) { - return -; - } - const isSlimBuildUnavailable = (row.message || '').includes('精简构建'); - const loadingInstallOrRemove = - actionState.driverType === row.type && (actionState.kind === 'install' || actionState.kind === 'remove'); - const loadingLocal = actionState.driverType === row.type && actionState.kind === 'local'; - if (isSlimBuildUnavailable && !row.packageInstalled) { - return 需 Full 版; - } + if (open && selectedKey) { + void loadVersionPackageSize(row, selectedKey); + } + }} + onChange={(value) => { + setSelectedVersionMap((prev) => ({ ...prev, [row.type]: value })); + void loadVersionPackageSize(row, value); + }} + /> + {mongoHint ? {mongoHint} : null} + + ); + }; - const logs = operationLogMap[row.type] || []; - const hasLogs = logs.length > 0; + const renderDriverActions = (row: DriverStatusRow) => { + if (row.builtIn) { + return null; + } + const isSlimBuildUnavailable = (row.message || '').includes('精简构建'); + const loadingInstallOrRemove = + actionState.driverType === row.type && (actionState.kind === 'install' || actionState.kind === 'remove'); + const loadingLocal = actionState.driverType === row.type && actionState.kind === 'local'; + const logs = operationLogMap[row.type] || []; + const hasLogs = logs.length > 0; - const mainAction = row.needsUpdate ? ( - - ) : row.connectable ? ( - - ) : ( - - ); + if (isSlimBuildUnavailable && !row.packageInstalled) { + return 当前精简版不可安装,请使用 Full 版; + } - return ( - - {mainAction} - - - - ); - }, - }, - ]; - }, [actionState, installDriver, installDriverFromLocalFile, loadVersionOptions, loadVersionPackageSize, openDriverLog, operationLogMap, progressMap, removeDriver, selectedVersionMap, versionLoadingMap, versionMap, versionSizeLoadingMap]); + const mainAction = row.needsUpdate ? ( + + ) : row.connectable ? ( + + ) : ( + + ); + + return ( + + {mainAction} + + + + ); + }; const activeLogRow = useMemo(() => { if (!logDriverType) { @@ -1259,6 +1003,87 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG } return `共 ${rows.length} 个驱动`; }, [filteredRows.length, normalizedSearchKeyword, rows.length]); + const statusSummary = useMemo(() => { + const optionalRows = rows.filter((row) => !row.builtIn); + return { + total: rows.length, + enabled: optionalRows.filter((row) => row.connectable).length, + needsUpdate: optionalRows.filter((row) => row.needsUpdate).length, + notEnabled: optionalRows.filter((row) => !row.connectable && !row.packageInstalled).length, + }; + }, [rows]); + + const renderDriverCard = (row: DriverStatusRow) => { + const progress = resolveDriverProgress(row); + const hasActiveProgress = !!progressMap[row.type] || row.connectable || row.packageInstalled; + const issueText = String(row.updateReason || row.message || '').trim(); + const affectedText = row.affectedConnections && row.affectedConnections > 0 + ? `影响 ${row.affectedConnections} 个已保存连接` + : ''; + + return ( +
+
+
+
+ {row.name} + {row.type} + {resolveDriverStatusTag(row)} +
+
+ 大小:{resolvePackageSizeText(row)} + 版本:{row.installedVersion || row.pinnedVersion || '-'} + {affectedText ? {affectedText} : null} +
+ {row.needsUpdate && issueText ? ( +
+ 需要重装 + + {issueText} + +
+ ) : issueText ? ( + + {issueText} + + ) : null} +
+ +
+
+ 驱动版本 + {renderVersionControl(row)} +
+
+ 状态进度 + {row.builtIn ? ( + 无需安装 + ) : hasActiveProgress ? ( + + ) : ( + + )} +
+ {renderDriverActions(row)} +
+
+
+ ); + }; const activeDriverLogs = operationLogMap[logDriverType] || []; const activeDriverLogLines = activeDriverLogs.map((item) => `[${item.time}] ${item.text}`); @@ -1286,8 +1111,9 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG title="驱动管理" open={open} onCancel={onClose} - width={980} + width={1120} style={{ top: 24 }} + className="driver-manager-modal" styles={{ body: { maxHeight: 'calc(100vh - 220px)', @@ -1298,32 +1124,46 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG }} destroyOnHidden footer={( -
-
-
-
- - - - - -
+ + + + + )} > -
- - 除 MySQL / Redis / Oracle / PostgreSQL 外,其他数据源需先安装启用后再连接。 +
+
+
+ 除 MySQL / Redis / Oracle / PostgreSQL 外,其他数据源需先安装启用后再连接。 + 驱动代理独立运行,GoNavi 升级后如提示重装,请重新安装对应驱动以应用新的 agent 逻辑。 +
+
+
+ {statusSummary.total} + 全部 +
+
+ {statusSummary.enabled} + 已启用 +
+
+ {statusSummary.needsUpdate} + 需重装 +
+
+ {statusSummary.notEnabled} + 未启用 +
+
+
+ + {networkStatus ? ( networkUnreachable ? ( void; onOpenG /> )} - - 自动下载和手动导入的驱动都会落盘到以下目录;后续版本升级可重复复用已下载驱动。 - {DRIVER_LOCAL_IMPORT_DIRECTORY_HELP} - {DRIVER_LOCAL_IMPORT_SINGLE_FILE_HELP} - - 驱动根目录:{downloadDir || '-'} +
+ + 自动下载和手动导入的驱动都会落盘到以下目录;后续版本升级可重复复用已下载驱动。 + {DRIVER_LOCAL_IMPORT_DIRECTORY_HELP} + {DRIVER_LOCAL_IMPORT_SINGLE_FILE_HELP} + + 驱动根目录:{downloadDir || '-'} + + {networkStatus?.logPath ? ( + + 运行日志文件:{networkStatus.logPath} - - {networkStatus?.logPath ? ( - - 运行日志文件:{networkStatus.logPath} - - ) : null} - - ), - }, - ]} - /> - )} - /> + ) : null} + + ), + }, + ]} + /> +
-
+
setSearchKeyword(event.target.value)} - style={{ minWidth: 300, flex: '1 1 360px' }} + className="driver-manager-search" /> - + 覆盖已安装 void; onOpenG
- {filterSummaryText} - -
- +
+ {filterSummaryText} + {loading ? 正在刷新状态... : null}
- + +
+ {filteredRows.length > 0 ? ( + filteredRows.map(renderDriverCard) + ) : ( + + )} +
+