mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-21 08:10:29 +08:00
✨ feat(driver-manager): 增强驱动管理本地导入并统一滚动交互体验
- 新增驱动目录批量导入入口,支持覆盖已安装开关与去重处理 - 行内本地导入聚焦单文件场景,目录导入与单文件导入流程统一 - 已安装驱动版本选择锁定,避免安装后误改版本 - 补充驱动下载网络检测与日志可见性,提升问题定位效率 - 重构驱动管理横向滚动条实现,修复双滚动条/消失/位置异常问题
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, Button, Collapse, Modal, Progress, Select, Space, Table, Tag, Typography, message } from 'antd';
|
||||
import { DeleteOutlined, DownloadOutlined, FileSearchOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Alert, Button, Collapse, Modal, Progress, Select, Space, Switch, Table, Tag, Typography, message } from 'antd';
|
||||
import { DeleteOutlined, DownloadOutlined, FileSearchOutlined, FolderOpenOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
import {
|
||||
CheckDriverNetworkStatus,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
GetDriverStatusList,
|
||||
InstallLocalDriverPackage,
|
||||
RemoveDriverPackage,
|
||||
SelectDriverPackageDirectory,
|
||||
SelectDriverPackageFile,
|
||||
} from '../../wailsjs/go/app/App';
|
||||
|
||||
@@ -46,6 +47,8 @@ type ProgressState = {
|
||||
percent: number;
|
||||
};
|
||||
|
||||
type DriverActionKind = '' | 'install' | 'remove' | 'local';
|
||||
|
||||
type DriverLogEntry = {
|
||||
time: string;
|
||||
text: string;
|
||||
@@ -84,6 +87,7 @@ 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 buildVersionSelectOptions = (options: DriverVersionOption[]) => {
|
||||
type SelectOption = { value: string; label: string };
|
||||
@@ -132,20 +136,28 @@ const buildVersionSelectOptions = (options: DriverVersionOption[]) => {
|
||||
};
|
||||
|
||||
const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, onClose }) => {
|
||||
const modalContentRef = useRef<HTMLDivElement | null>(null);
|
||||
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const tableScrollTargetsRef = useRef<HTMLElement[]>([]);
|
||||
const externalHScrollRef = useRef<HTMLDivElement | null>(null);
|
||||
const horizontalSyncSourceRef = useRef<'table' | 'external' | ''>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [downloadDir, setDownloadDir] = useState('');
|
||||
const [networkChecking, setNetworkChecking] = useState(false);
|
||||
const [networkStatus, setNetworkStatus] = useState<DriverNetworkStatus | null>(null);
|
||||
const [rows, setRows] = useState<DriverStatusRow[]>([]);
|
||||
const [actionDriver, setActionDriver] = useState('');
|
||||
const [actionState, setActionState] = useState<{ driverType: string; kind: DriverActionKind }>({ driverType: '', kind: '' });
|
||||
const [progressMap, setProgressMap] = useState<Record<string, ProgressState>>({});
|
||||
const [operationLogMap, setOperationLogMap] = useState<Record<string, DriverLogEntry[]>>({});
|
||||
const [logDriverType, setLogDriverType] = useState('');
|
||||
const [logModalOpen, setLogModalOpen] = useState(false);
|
||||
const [batchDirectoryImporting, setBatchDirectoryImporting] = useState(false);
|
||||
const [forceOverwriteInstalled, setForceOverwriteInstalled] = useState(false);
|
||||
const [versionMap, setVersionMap] = useState<Record<string, DriverVersionOption[]>>({});
|
||||
const [selectedVersionMap, setSelectedVersionMap] = useState<Record<string, string>>({});
|
||||
const [versionLoadingMap, setVersionLoadingMap] = useState<Record<string, boolean>>({});
|
||||
const [versionSizeLoadingMap, setVersionSizeLoadingMap] = useState<Record<string, boolean>>({});
|
||||
const [horizontalScrollWidth, setHorizontalScrollWidth] = useState(DRIVER_TABLE_SCROLL_X);
|
||||
|
||||
const appendOperationLog = useCallback((
|
||||
driverType: string,
|
||||
@@ -193,6 +205,76 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
});
|
||||
}, []);
|
||||
|
||||
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) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -429,12 +511,125 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setHorizontalScrollWidth(DRIVER_TABLE_SCROLL_X);
|
||||
tableScrollTargetsRef.current = [];
|
||||
return;
|
||||
}
|
||||
refreshStatus(false);
|
||||
checkNetworkStatus(false);
|
||||
}, [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;
|
||||
@@ -470,7 +665,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
}, [appendOperationLog, open]);
|
||||
|
||||
const installDriver = useCallback(async (row: DriverStatusRow) => {
|
||||
setActionDriver(row.type);
|
||||
setActionState({ driverType: row.type, kind: 'install' });
|
||||
setProgressMap((prev) => ({
|
||||
...prev,
|
||||
[row.type]: {
|
||||
@@ -505,25 +700,25 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
message.success(`${row.name}${versionTip} 已安装启用`);
|
||||
refreshStatus(false);
|
||||
} finally {
|
||||
setActionDriver('');
|
||||
setActionState({ driverType: '', kind: '' });
|
||||
}
|
||||
}, [appendOperationLog, downloadDir, loadVersionOptions, refreshStatus, selectedVersionMap, versionMap]);
|
||||
|
||||
const installDriverFromLocalFile = useCallback(async (row: DriverStatusRow) => {
|
||||
const fileRes = await SelectDriverPackageFile(downloadDir);
|
||||
if (!fileRes?.success) {
|
||||
if (String(fileRes?.message || '') !== 'Cancelled') {
|
||||
message.error(fileRes?.message || '选择本地驱动包失败');
|
||||
const installDriverFromLocalPath = useCallback(async (
|
||||
row: DriverStatusRow,
|
||||
sourcePath: string,
|
||||
sourceLabel: '文件' | '目录',
|
||||
options?: { silentToast?: boolean; skipRefresh?: boolean },
|
||||
) => {
|
||||
const pathText = String(sourcePath || '').trim();
|
||||
if (!pathText) {
|
||||
if (!options?.silentToast) {
|
||||
message.error(`未选择有效的本地导入${sourceLabel}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const filePath = String((fileRes?.data as any)?.path || '').trim();
|
||||
if (!filePath) {
|
||||
message.error('未选择有效的驱动包文件');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
setActionDriver(row.type);
|
||||
setActionState({ driverType: row.type, kind: 'local' });
|
||||
setProgressMap((prev) => ({
|
||||
...prev,
|
||||
[row.type]: {
|
||||
@@ -532,23 +727,122 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
percent: 0,
|
||||
},
|
||||
}));
|
||||
appendOperationLog(row.type, `[START] 开始本地导入:${filePath}`);
|
||||
appendOperationLog(row.type, `[START] 开始本地导入(${sourceLabel}):${pathText}`);
|
||||
try {
|
||||
const result = await InstallLocalDriverPackage(row.type, filePath, downloadDir);
|
||||
const result = await InstallLocalDriverPackage(row.type, pathText, downloadDir);
|
||||
if (!result?.success) {
|
||||
const errText = result?.message || `导入 ${row.name} 本地驱动包失败`;
|
||||
appendOperationLog(row.type, `[ERROR] ${errText}`);
|
||||
message.error(errText);
|
||||
return;
|
||||
if (!options?.silentToast) {
|
||||
message.error(errText);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
appendOperationLog(row.type, '[DONE] 本地导入安装完成');
|
||||
message.success(`${row.name} 本地驱动包已安装启用`);
|
||||
refreshStatus(false);
|
||||
if (!options?.silentToast) {
|
||||
message.success(`${row.name} 本地驱动包已安装启用`);
|
||||
}
|
||||
if (!options?.skipRefresh) {
|
||||
await refreshStatus(false);
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
setActionDriver('');
|
||||
setActionState({ driverType: '', kind: '' });
|
||||
}
|
||||
}, [appendOperationLog, downloadDir, refreshStatus]);
|
||||
|
||||
const installDriverFromLocalFile = useCallback(async (row: DriverStatusRow) => {
|
||||
const fileRes = await SelectDriverPackageFile(downloadDir);
|
||||
if (!fileRes?.success) {
|
||||
if (String(fileRes?.message || '') !== 'Cancelled') {
|
||||
message.error(fileRes?.message || '选择本地驱动包文件失败');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const filePath = String((fileRes?.data as any)?.path || '').trim();
|
||||
if (!filePath) {
|
||||
message.error('未选择有效的驱动包文件');
|
||||
return;
|
||||
}
|
||||
await installDriverFromLocalPath(row, filePath, '文件');
|
||||
}, [downloadDir, installDriverFromLocalPath]);
|
||||
|
||||
const installDriversFromDirectory = useCallback(async () => {
|
||||
const directoryRes = await SelectDriverPackageDirectory(downloadDir);
|
||||
if (!directoryRes?.success) {
|
||||
if (String(directoryRes?.message || '') !== 'Cancelled') {
|
||||
message.error(directoryRes?.message || '选择本地驱动包目录失败');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const directoryPath = String((directoryRes?.data as any)?.path || '').trim();
|
||||
if (!directoryPath) {
|
||||
message.error('未选择有效的驱动包目录');
|
||||
return;
|
||||
}
|
||||
const optionalRows = rows.filter((item) => !item.builtIn);
|
||||
if (optionalRows.length === 0) {
|
||||
message.info('当前没有可导入的外置驱动');
|
||||
return;
|
||||
}
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
let dedupeSkipCount = 0;
|
||||
let slimSkipCount = 0;
|
||||
|
||||
setBatchDirectoryImporting(true);
|
||||
try {
|
||||
for (const row of optionalRows) {
|
||||
const alreadyInstalled = row.packageInstalled || row.connectable;
|
||||
if (alreadyInstalled && !forceOverwriteInstalled) {
|
||||
dedupeSkipCount += 1;
|
||||
appendOperationLog(row.type, '[SKIP] 已检测到驱动已安装,目录导入去重跳过');
|
||||
continue;
|
||||
}
|
||||
if (alreadyInstalled && forceOverwriteInstalled) {
|
||||
appendOperationLog(row.type, '[INFO] 已启用覆盖已安装模式,执行重装导入');
|
||||
}
|
||||
const isSlimBuildUnavailable = (row.message || '').includes('精简构建') && !row.packageInstalled;
|
||||
if (isSlimBuildUnavailable) {
|
||||
slimSkipCount += 1;
|
||||
appendOperationLog(row.type, '[WARN] 当前发行包为精简构建,已跳过目录导入');
|
||||
continue;
|
||||
}
|
||||
const ok = await installDriverFromLocalPath(row, directoryPath, '目录', { silentToast: true, skipRefresh: true });
|
||||
if (ok) {
|
||||
successCount += 1;
|
||||
} else {
|
||||
failCount += 1;
|
||||
}
|
||||
}
|
||||
await refreshStatus(false);
|
||||
} finally {
|
||||
setBatchDirectoryImporting(false);
|
||||
}
|
||||
|
||||
const skipParts: string[] = [];
|
||||
if (dedupeSkipCount > 0) {
|
||||
skipParts.push(`去重跳过 ${dedupeSkipCount}`);
|
||||
}
|
||||
if (slimSkipCount > 0) {
|
||||
skipParts.push(`精简版跳过 ${slimSkipCount}`);
|
||||
}
|
||||
const skipTip = skipParts.length > 0 ? `,${skipParts.join(',')}` : '';
|
||||
|
||||
const forceTip = forceOverwriteInstalled ? '(覆盖已安装)' : '';
|
||||
if (failCount === 0) {
|
||||
message.success(`目录导入完成${forceTip}:成功 ${successCount}${skipTip}`);
|
||||
return;
|
||||
}
|
||||
if (successCount > 0) {
|
||||
message.warning(`目录导入完成${forceTip}:成功 ${successCount},失败 ${failCount}${skipTip}`);
|
||||
return;
|
||||
}
|
||||
message.error(`目录导入失败${forceTip}:失败 ${failCount}${skipTip}`);
|
||||
}, [appendOperationLog, downloadDir, forceOverwriteInstalled, installDriverFromLocalPath, refreshStatus, rows]);
|
||||
|
||||
const openDriverLog = useCallback((driverType: string) => {
|
||||
const normalized = String(driverType || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
@@ -559,7 +853,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
}, []);
|
||||
|
||||
const removeDriver = useCallback(async (row: DriverStatusRow) => {
|
||||
setActionDriver(row.type);
|
||||
setActionState({ driverType: row.type, kind: 'remove' });
|
||||
appendOperationLog(row.type, '[START] 开始移除驱动');
|
||||
try {
|
||||
const result = await RemoveDriverPackage(row.type, downloadDir);
|
||||
@@ -578,7 +872,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
});
|
||||
refreshStatus(false);
|
||||
} finally {
|
||||
setActionDriver('');
|
||||
setActionState({ driverType: '', kind: '' });
|
||||
}
|
||||
}, [appendOperationLog, downloadDir, refreshStatus]);
|
||||
|
||||
@@ -590,25 +884,6 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
key: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '安装位置',
|
||||
key: 'installPath',
|
||||
width: 260,
|
||||
render: (_: string, row: DriverStatusRow) => {
|
||||
if (row.builtIn) {
|
||||
return <Text type="secondary">内置</Text>;
|
||||
}
|
||||
const installPath = row.executablePath || row.installDir || '-';
|
||||
if (installPath === '-') {
|
||||
return <Text type="secondary">-</Text>;
|
||||
}
|
||||
return (
|
||||
<Text copyable={{ text: installPath }} style={{ fontSize: 12 }}>
|
||||
{installPath}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '安装包大小',
|
||||
dataIndex: 'packageSizeText',
|
||||
@@ -688,6 +963,14 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
if (row.builtIn) {
|
||||
return <Text type="secondary">-</Text>;
|
||||
}
|
||||
const versionLocked = row.packageInstalled || row.connectable;
|
||||
if (versionLocked) {
|
||||
const installedVersion = String(row.installedVersion || '').trim();
|
||||
if (installedVersion) {
|
||||
return <Text type="secondary">{installedVersion}(已安装,移除后可更换)</Text>;
|
||||
}
|
||||
return <Text type="secondary">已安装(移除后可更换)</Text>;
|
||||
}
|
||||
const options = versionMap[row.type] || [];
|
||||
const selectedKey = selectedVersionMap[row.type];
|
||||
const selectOptions = buildVersionSelectOptions(options);
|
||||
@@ -696,7 +979,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
size="small"
|
||||
style={{ width: '100%' }}
|
||||
loading={!!versionLoadingMap[row.type]}
|
||||
disabled={actionDriver === row.type}
|
||||
disabled={actionState.driverType === row.type}
|
||||
placeholder={options.length > 0 ? '选择驱动版本' : '点击展开加载版本'}
|
||||
value={selectedKey}
|
||||
options={selectOptions as any}
|
||||
@@ -726,7 +1009,9 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
return <Text type="secondary">-</Text>;
|
||||
}
|
||||
const isSlimBuildUnavailable = (row.message || '').includes('精简构建');
|
||||
const loadingAction = actionDriver === row.type;
|
||||
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 <Text type="secondary">需 Full 版</Text>;
|
||||
}
|
||||
@@ -738,7 +1023,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
loading={loadingAction}
|
||||
loading={loadingInstallOrRemove}
|
||||
onClick={() => removeDriver(row)}
|
||||
>
|
||||
移除
|
||||
@@ -747,7 +1032,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
loading={loadingAction}
|
||||
loading={loadingInstallOrRemove}
|
||||
onClick={() => installDriver(row)}
|
||||
>
|
||||
安装启用
|
||||
@@ -759,7 +1044,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
{mainAction}
|
||||
<Button
|
||||
icon={<FileSearchOutlined />}
|
||||
loading={loadingAction}
|
||||
loading={loadingLocal}
|
||||
onClick={() => installDriverFromLocalFile(row)}
|
||||
>
|
||||
本地导入
|
||||
@@ -776,7 +1061,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [actionDriver, installDriver, installDriverFromLocalFile, loadVersionOptions, loadVersionPackageSize, openDriverLog, operationLogMap, progressMap, removeDriver, selectedVersionMap, versionLoadingMap, versionMap, versionSizeLoadingMap]);
|
||||
}, [actionState, installDriver, installDriverFromLocalFile, loadVersionOptions, loadVersionPackageSize, openDriverLog, operationLogMap, progressMap, removeDriver, selectedVersionMap, versionLoadingMap, versionMap, versionSizeLoadingMap]);
|
||||
|
||||
const activeLogRow = useMemo(() => {
|
||||
if (!logDriverType) {
|
||||
@@ -805,18 +1090,31 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
},
|
||||
}}
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button key="refresh" icon={<ReloadOutlined />} onClick={() => refreshStatus(true)} loading={loading}>
|
||||
刷新
|
||||
</Button>,
|
||||
<Button key="network" onClick={() => checkNetworkStatus(true)} loading={networkChecking}>
|
||||
网络检测
|
||||
</Button>,
|
||||
<Button key="close" type="primary" onClick={onClose}>
|
||||
关闭
|
||||
</Button>,
|
||||
]}
|
||||
footer={(
|
||||
<div className="driver-manager-footer">
|
||||
<div
|
||||
ref={externalHScrollRef}
|
||||
className="driver-manager-hscroll"
|
||||
aria-hidden={false}
|
||||
onScroll={applyExternalScrollToTableTargets}
|
||||
>
|
||||
<div className="driver-manager-hscroll-inner" style={{ width: `${Math.max(horizontalScrollWidth, 1)}px` }} />
|
||||
</div>
|
||||
<Space className="driver-manager-footer-actions" size={8}>
|
||||
<Button key="refresh" icon={<ReloadOutlined />} onClick={() => refreshStatus(true)} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button key="network" onClick={() => checkNetworkStatus(true)} loading={networkChecking}>
|
||||
网络检测
|
||||
</Button>
|
||||
<Button key="close" type="primary" onClick={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div ref={modalContentRef}>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Text type="secondary">除 MySQL / Redis / Oracle / PostgreSQL 外,其他数据源需先安装启用后再连接。</Text>
|
||||
{networkStatus ? (
|
||||
@@ -868,7 +1166,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
description={(
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Text type="secondary">自动下载和手动导入的驱动都会落盘到以下目录;后续版本升级可重复复用已下载驱动。</Text>
|
||||
<Text type="secondary">手动导入支持单个驱动代理文件(如 `mariadb-driver-agent` / `mariadb-driver-agent.exe`)或驱动总包 `GoNavi-DriverAgents.zip`。</Text>
|
||||
<Text type="secondary">行内“本地导入”仅用于单个驱动文件/总包(如 `mariadb-driver-agent`、`mariadb-driver-agent.exe`、`GoNavi-DriverAgents.zip`);批量导入请使用上方“导入驱动目录”。</Text>
|
||||
<Paragraph copyable={{ text: downloadDir || '-' }} style={{ marginBottom: 0 }}>
|
||||
驱动根目录:{downloadDir || '-'}
|
||||
</Paragraph>
|
||||
@@ -881,16 +1179,42 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void }> = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<Table
|
||||
rowKey="type"
|
||||
loading={loading}
|
||||
columns={columns as any}
|
||||
dataSource={rows}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
scroll={{ x: 1450 }}
|
||||
/>
|
||||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<Space size={8}>
|
||||
<Text type="secondary">覆盖已安装</Text>
|
||||
<Switch
|
||||
checked={forceOverwriteInstalled}
|
||||
onChange={(checked) => setForceOverwriteInstalled(checked)}
|
||||
disabled={batchDirectoryImporting}
|
||||
/>
|
||||
</Space>
|
||||
<Button
|
||||
icon={<FolderOpenOutlined />}
|
||||
loading={batchDirectoryImporting}
|
||||
onClick={() => void installDriversFromDirectory()}
|
||||
>
|
||||
导入驱动目录
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<div
|
||||
ref={tableContainerRef}
|
||||
className="driver-manager-table-wrap driver-manager-table-wrap-external-active"
|
||||
>
|
||||
<Table
|
||||
className="driver-manager-table"
|
||||
rowKey="type"
|
||||
loading={loading}
|
||||
columns={columns as any}
|
||||
dataSource={rows}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
sticky={false}
|
||||
scroll={{ x: DRIVER_TABLE_SCROLL_X }}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
<Modal
|
||||
title={`驱动日志 - ${activeLogRow?.name || logDriverType}`}
|
||||
open={logModalOpen}
|
||||
|
||||
Reference in New Issue
Block a user