feat(docs): add offline driver downloads

This commit is contained in:
t8y2 committed 2026-06-08 21:50:00 +08:00
1 parent 87b8f1f462
commit 19139a9b34
9 files changed
+741 -97

No files matched your search

@@ -3,9 +3,11 @@ import { ref, onMounted, onUnmounted, computed, watch } from "vue";
import { useI18n } from "vue-i18n";
import {
Activity,
ExternalLink,
Cpu,
FolderOpen,
MemoryStick,
Search,
Square,
Trash2,
Download,
@@ -74,6 +76,7 @@ const driverStoreTab = ref("agent");
// ──────────── Agent drivers ────────────
const drivers = ref<AgentDriverInfo[]>([]);
const agentDriverSearch = ref("");
const installing = ref<string | null>(null);
const upgradingAll = ref(false);
const upgradingCurrent = ref("");
@@ -93,6 +96,7 @@ const runtimeError = ref("");
const runtimeBusy = ref<string | null>(null);
let runtimeTimer: ReturnType<typeof setInterval> | null = null;
const DRIVER_RUNTIME_POLL_MS = 5000;
const OFFLINE_DRIVER_DOWNLOAD_URL = "https://dbxio.com/cn/drivers";
let unlisten: (() => void) | null = null;
@@ -189,6 +193,15 @@ function canInstallOrUpdateDriver(dbType: string): boolean {
return Boolean(driver && (!driver.installed || driver.update_available));
}
async function openOfflineDriverDownload() {
if (isWeb) {
window.open(OFFLINE_DRIVER_DOWNLOAD_URL, "_blank", "noopener,noreferrer");
return;
}
const { open } = await import("@tauri-apps/plugin-shell");
await open(OFFLINE_DRIVER_DOWNLOAD_URL);
}
function queueDriverInstall(dbType: string) {
queuedDriverInstalls.value = addDriverInstallQueue(queuedDriverInstalls.value, dbType, installing.value);
}
@@ -470,12 +483,33 @@ function formatSize(bytes: number): string {
// ──────────── JDBC drivers ────────────
const jdbcDrivers = ref<JdbcDriverInfo[]>([]);
const jdbcDriverSearch = ref("");
const isLoadingJdbcDrivers = ref(false);
const jdbcPluginStatus = ref<JdbcPluginStatus | null>(null);
const isInstallingJdbcPlugin = ref(false);
const isUninstallingJdbcPlugin = ref(false);
const jdbcDriverPathInput = ref("");
const filteredAgentDrivers = computed(() => {
const query = agentDriverSearch.value.trim().toLowerCase();
if (!query) return drivers.value;
return drivers.value.filter((driver) =>
[driver.label, driver.db_type, driver.version, driver.installed_version, driver.jre]
.filter(Boolean)
.join(" ")
.toLowerCase()
.includes(query),
);
});
const filteredJdbcDrivers = computed(() => {
const query = jdbcDriverSearch.value.trim().toLowerCase();
if (!query) return jdbcDrivers.value;
return jdbcDrivers.value.filter((driver) =>
[driver.name, driver.path, String(driver.size)].join(" ").toLowerCase().includes(query),
);
});
function formatBytes(bytes: number) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -803,6 +837,22 @@ watch(driverStoreTab, (tab) => {
<div class="mt-0.5 text-xs font-medium">{{ formatBytes(item.bytes) }}</div>
</div>
</div>
<div class="mt-3 rounded-lg border bg-background/50 px-2.5 py-2">
<div class="flex items-center justify-between gap-3">
<div class="min-w-0 truncate text-xs text-muted-foreground">
{{ t("driverStore.offlineDownloadHint") }}
</div>
<Button
variant="outline"
size="sm"
class="h-7 shrink-0 rounded-full text-xs gap-1 whitespace-nowrap"
@click="openOfflineDriverDownload"
>
<ExternalLink class="h-3.5 w-3.5" />
{{ t("driverStore.offlineDownloadLink") }}
</Button>
</div>
</div>
</div>
<div class="flex items-center justify-between">
@@ -845,22 +895,43 @@ watch(driverStoreTab, (tab) => {
<!-- Agent Tab -->
<TabsContent value="agent" class="mt-5 space-y-5">
<!-- Java Runtime Mode -->
<!-- Java Runtime -->
<div class="rounded-xl border bg-muted/20 p-4 space-y-3">
<div class="flex flex-wrap items-end gap-3">
<div class="min-w-[220px] flex-1 space-y-1.5">
<Label>{{ t("driverStore.javaRuntime") }}</Label>
<Select :model-value="javaRuntimeConfig.mode" @update:model-value="setJavaRuntimeMode">
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="managed">{{ t("driverStore.javaRuntimeManaged") }}</SelectItem>
<SelectItem value="system">{{ t("driverStore.javaRuntimeSystem") }}</SelectItem>
<SelectItem value="custom">{{ t("driverStore.javaRuntimeCustom") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex flex-wrap items-center gap-2">
<Label class="shrink-0">{{ t("driverStore.javaRuntime") }}</Label>
<Select :model-value="javaRuntimeConfig.mode" @update:model-value="setJavaRuntimeMode">
<SelectTrigger class="h-8 min-w-[112px] text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="managed">{{ t("driverStore.javaRuntimeManaged") }}</SelectItem>
<SelectItem value="system">{{ t("driverStore.javaRuntimeSystem") }}</SelectItem>
<SelectItem value="custom">{{ t("driverStore.javaRuntimeCustom") }}</SelectItem>
</SelectContent>
</Select>
<Input
v-if="javaRuntimeConfig.mode === 'custom'"
v-model="customJavaPath"
class="h-8 min-w-[180px] flex-1 text-xs"
:placeholder="t('driverStore.customJavaPathPlaceholder')"
@keydown.enter.prevent="saveJavaRuntimeConfig"
/>
<span v-else class="min-w-0 flex-1 truncate text-xs text-muted-foreground">
{{
javaRuntimeConfig.mode === "system"
? t("driverStore.systemJavaHint")
: t("driverStore.jreRuntimeAutoDownloadHint")
}}
</span>
<Button
v-if="javaRuntimeConfig.mode === 'custom'"
variant="outline"
class="h-8 shrink-0 rounded-full text-xs"
@click="chooseCustomJavaPath"
>
<FolderOpen class="h-3.5 w-3.5" />
{{ t("driverStore.choose") }}
</Button>
<Button
class="h-8 shrink-0 rounded-full text-xs"
:disabled="savingJavaRuntime || (javaRuntimeConfig.mode === 'custom' && !customJavaPath.trim())"
@@ -869,90 +940,87 @@ watch(driverStoreTab, (tab) => {
{{ savingJavaRuntime ? t("driverStore.saving") : t("settings.save") }}
</Button>
</div>
<div v-if="javaRuntimeConfig.mode === 'custom'" class="flex items-center gap-2">
<Input
v-model="customJavaPath"
class="h-8 flex-1 text-xs"
:placeholder="t('driverStore.customJavaPathPlaceholder')"
@keydown.enter.prevent="saveJavaRuntimeConfig"
/>
<Button variant="outline" class="h-8 shrink-0 rounded-full text-xs" @click="chooseCustomJavaPath">
<FolderOpen class="h-3.5 w-3.5" />
{{ t("driverStore.choose") }}
</Button>
</div>
<p v-else-if="javaRuntimeConfig.mode === 'system'" class="text-xs text-muted-foreground">
{{ t("driverStore.systemJavaHint") }}
</p>
</div>
<!-- JRE Runtime -->
<div v-if="installedJres.length > 0" class="rounded-xl border bg-muted/20 p-4 space-y-2.5">
<div v-for="jre in installedJres" :key="jre.key" class="flex items-center justify-between gap-3">
<div class="min-w-0">
<div class="text-sm font-medium">{{ t("driverStore.jreRuntimeTitle", { jre: jre.key }) }}</div>
</div>
<div class="flex shrink-0 items-center gap-3">
<span
v-if="jreUsageLabel(jre.key)"
class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground"
>
{{ jreUsageLabel(jre.key) }}
</span>
<Check v-if="jre.installed" class="h-4 w-4 text-green-600" />
<span v-else class="text-xs text-muted-foreground">{{ t("driverStore.notInstalled") }}</span>
<DriverInstallProgressCircle
v-if="reinstallingJre === jre.key"
:percent="progressNumber"
:title="progressTitle(jre.installed ? t('driverStore.reinstalling') : t('driverStore.installing'))"
/>
<Button
v-else-if="!jre.installed"
type="button"
variant="default"
size="sm"
class="h-8 rounded-full text-xs"
:disabled="reinstallingJre !== null || installing !== null"
@click="reinstallJre(jre.key)"
>
<Download class="h-3.5 w-3.5 mr-1" />
{{ t("driverStore.install") }}
</Button>
<Button
v-else-if="jre.installed"
type="button"
variant="outline"
size="sm"
class="h-8 rounded-full text-xs"
:disabled="reinstallingJre !== null || installing !== null"
@click="reinstallJre(jre.key)"
>
<RotateCcw class="h-3.5 w-3.5 mr-1" />
{{ t("driverStore.reinstall") }}
</Button>
<Button
v-if="jre.installed"
type="button"
variant="ghost"
size="sm"
class="h-8 rounded-full text-xs text-muted-foreground hover:text-destructive"
:disabled="reinstallingJre !== null || installing !== null"
@click="uninstallJre(jre.key)"
>
{{ t("driverStore.uninstall") }}
</Button>
<div v-if="installedJres.length > 0" class="divide-y rounded-lg border bg-background/50">
<div
v-for="jre in installedJres"
:key="jre.key"
class="flex items-center justify-between gap-3 px-3 py-2.5"
>
<div class="min-w-0">
<div class="text-sm font-medium">{{ t("driverStore.jreRuntimeTitle", { jre: jre.key }) }}</div>
</div>
<div class="flex shrink-0 items-center gap-3">
<span
v-if="jreUsageLabel(jre.key)"
class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground"
>
{{ jreUsageLabel(jre.key) }}
</span>
<Check v-if="jre.installed" class="h-4 w-4 text-green-600" />
<span v-else class="text-xs text-muted-foreground">{{ t("driverStore.notInstalled") }}</span>
<DriverInstallProgressCircle
v-if="reinstallingJre === jre.key"
:percent="progressNumber"
:title="
progressTitle(jre.installed ? t('driverStore.reinstalling') : t('driverStore.installing'))
"
/>
<Button
v-else-if="!jre.installed"
type="button"
variant="default"
size="sm"
class="h-8 rounded-full text-xs"
:disabled="reinstallingJre !== null || installing !== null"
@click="reinstallJre(jre.key)"
>
<Download class="h-3.5 w-3.5 mr-1" />
{{ t("driverStore.install") }}
</Button>
<Button
v-else-if="jre.installed"
type="button"
variant="outline"
size="sm"
class="h-8 rounded-full text-xs"
:disabled="reinstallingJre !== null || installing !== null"
@click="reinstallJre(jre.key)"
>
<RotateCcw class="h-3.5 w-3.5 mr-1" />
{{ t("driverStore.reinstall") }}
</Button>
<Button
v-if="jre.installed"
type="button"
variant="ghost"
size="sm"
class="h-8 rounded-full text-xs text-muted-foreground hover:text-destructive"
:disabled="reinstallingJre !== null || installing !== null"
@click="uninstallJre(jre.key)"
>
{{ t("driverStore.uninstall") }}
</Button>
</div>
</div>
</div>
</div>
<div v-else class="rounded-xl border bg-muted/20 p-4">
<div class="text-sm font-medium">{{ t("driverStore.jreRuntime") }}</div>
<p class="text-xs text-muted-foreground mt-0.5">{{ t("driverStore.jreRuntimeAutoDownloadHint") }}</p>
</div>
<!-- Driver List -->
<div class="relative">
<Search class="absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input
v-model="agentDriverSearch"
class="h-8 pl-8 text-xs"
:placeholder="t('driverStore.searchDrivers')"
/>
</div>
<div v-if="drivers.length === 0" class="py-12 text-center text-sm text-muted-foreground">
{{ t("common.loading") }}
</div>
<div v-else-if="filteredAgentDrivers.length === 0" class="py-12 text-center text-sm text-muted-foreground">
{{ t("driverStore.noMatchingDrivers") }}
</div>
<div v-else class="rounded-md border divide-y">
<div v-if="updatableCount > 0" class="flex items-center justify-between px-4 py-2 bg-muted/30">
<span class="text-xs text-muted-foreground">{{
@@ -974,7 +1042,7 @@ watch(driverStoreTab, (tab) => {
</Button>
</div>
<div
v-for="driver in drivers"
v-for="driver in filteredAgentDrivers"
:key="driver.db_type"
class="flex items-center gap-3 px-4 py-2.5 transition hover:bg-muted/30"
>
@@ -1180,6 +1248,14 @@ watch(driverStoreTab, (tab) => {
<div class="space-y-1">
<Label>{{ t("settings.jdbcDrivers") }}</Label>
</div>
<div class="relative">
<Search class="absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input
v-model="jdbcDriverSearch"
class="h-8 pl-8 text-xs"
:placeholder="t('driverStore.searchJdbcDrivers')"
/>
</div>
<div class="flex items-center gap-2">
<Input
v-model="jdbcDriverPathInput"
@@ -1209,8 +1285,11 @@ watch(driverStoreTab, (tab) => {
<div v-else-if="jdbcDrivers.length === 0" class="p-4 text-sm text-muted-foreground">
{{ t("settings.jdbcNoDrivers") }}
</div>
<div v-else-if="filteredJdbcDrivers.length === 0" class="p-4 text-sm text-muted-foreground">
{{ t("driverStore.noMatchingDrivers") }}
</div>
<div v-else class="divide-y">
<div v-for="driver in jdbcDrivers" :key="driver.path" class="flex items-center gap-3 p-3">
<div v-for="driver in filteredJdbcDrivers" :key="driver.path" class="flex items-center gap-3 p-3">
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium">{{ driver.name }}</div>
<div class="truncate text-xs text-muted-foreground">{{ driver.path }}</div>
+6
View File
@@ -1976,6 +1976,12 @@ export default {
usageAgentDrivers: "Built-in driver agents",
usageJdbcPlugin: "JDBC plugin",
usageJdbcDriverJars: "JDBC driver JARs",
offlineDownloadHint:
"For air-gapped environments, download offline driver packages on an internet-connected machine, then import them here.",
offlineDownloadLink: "Offline driver downloads",
searchDrivers: "Search driver name, type, version...",
searchJdbcDrivers: "Search JDBC driver name or path...",
noMatchingDrivers: "No matching drivers.",
javaRuntimeSaved: "Java runtime settings saved",
javaRuntimeSaveFailed: "Failed to save Java runtime settings: {error}",
chooseJavaExecutable: "Choose Java executable",
+5
View File
@@ -1929,6 +1929,11 @@ export default {
usageAgentDrivers: "内置驱动 Agent",
usageJdbcPlugin: "JDBC 插件",
usageJdbcDriverJars: "JDBC 驱动 JAR",
offlineDownloadHint: "内网环境可先在有网机器下载离线驱动包,再回到这里导入。",
offlineDownloadLink: "离线驱动下载",
searchDrivers: "搜索驱动名称、类型、版本...",
searchJdbcDrivers: "搜索 JDBC 驱动名称或路径...",
noMatchingDrivers: "没有匹配的驱动。",
javaRuntimeSaved: "Java 运行时设置已保存",
javaRuntimeSaveFailed: "Java 运行时设置失败: {error}",
chooseJavaExecutable: "选择 Java 可执行文件",