From bc91fbdfa1a08709ddc8220ee92db9ca0e8595c6 Mon Sep 17 00:00:00 2001 From: dbin0123 Date: Wed, 17 Jun 2026 00:21:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(generate):=20=E7=A1=AE=E4=BF=9D=E4=BF=9D?= =?UTF-8?q?=E5=AD=98/=E5=8A=A0=E8=BD=BD=E9=85=8D=E7=BD=AE=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E6=89=80=E6=9C=89=E7=94=9F=E6=88=90=E5=99=A8=E5=8F=82?= =?UTF-8?q?=E6=95=B0=EF=BC=8C=E5=B9=B6=E4=BF=AE=E5=A4=8D=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E8=A2=AB=E6=84=8F=E5=A4=96=E6=B8=85=E7=A9=BA=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: highlight selected SQL matches * fix(core): detect column extra changes (auto_increment, on_update) in table structure SQL generation - Add has_column_extra_change check to build_column_sql skip condition - Add is_column_extra_empty to handle None/Some(false)/empty ColumnExtra - Fix false positives when extra is parsed but no effective change * feat(generate): fix date range persistence, add i18n for data generation * fix:Remove invalid references * fix(generate): ensure config save/load covers all generator params - Remove auto-reset watch in GeneratorParamsPanel that cleared params when loading a profile with different generatorKey - Extend defaultGeneratorParams() to provide explicit defaults for all 35+ generators (full_name, phone, email, barcode, url, file_path, foreign_key, image, regex, product_name, etc.) so profiles persist and restore correctly through JSON serialize/deserialize round-trip --------- Co-authored-by: t8y2 <1156263951@qq.com> --- .../generate/DataGenerateDialog.vue | 220 ++++++- .../generate/params/GeneratorParamsPanel.vue | 10 +- .../generate/params/IdNumberParams.vue | 250 +++++++ apps/desktop/src/i18n/locales/en.ts | 17 +- apps/desktop/src/i18n/locales/es.ts | 17 +- apps/desktop/src/i18n/locales/it.ts | 17 +- apps/desktop/src/i18n/locales/ja.ts | 15 + apps/desktop/src/i18n/locales/pt-BR.ts | 17 +- apps/desktop/src/i18n/locales/zh-CN.ts | 17 +- apps/desktop/src/i18n/locales/zh-TW.ts | 17 +- apps/desktop/src/lib/dataGenerate.ts | 623 +++++++++++++++++- 11 files changed, 1167 insertions(+), 53 deletions(-) create mode 100644 apps/desktop/src/components/generate/params/IdNumberParams.vue diff --git a/apps/desktop/src/components/generate/DataGenerateDialog.vue b/apps/desktop/src/components/generate/DataGenerateDialog.vue index 94e07ca0e..26e2904c5 100644 --- a/apps/desktop/src/components/generate/DataGenerateDialog.vue +++ b/apps/desktop/src/components/generate/DataGenerateDialog.vue @@ -4,8 +4,9 @@ import { useI18n } from "vue-i18n"; import { useConnectionStore } from "@/stores/connectionStore"; import * as api from "@/lib/api"; import type { TableGenerateConfig } from "@/lib/dataGenerate"; -import { findGeneratorKey, generateTableData } from "@/lib/dataGenerate"; +import { findGeneratorKey, generateTableData, defaultGeneratorParams } from "@/lib/dataGenerate"; import { quoteTableIdentifier } from "@/lib/tableSelectSql"; +import { isTauriRuntime } from "@/lib/tauriRuntime"; import { effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect"; import GeneratorParamsPanel from "./params/GeneratorParamsPanel.vue"; import type { ColumnInfo, TableInfo } from "@/types/database"; @@ -111,14 +112,29 @@ async function loadSchemas() { schema: targetSchema, database: db, rowCount: 1000, - columns: cols.map((c: ColumnInfo) => ({ - columnName: c.name, - dataType: c.data_type, - rowCount: 1000, - generatorKey: findGeneratorKey(c.name, c.data_type), - generatorParams: {}, - isAutoIncrement: c.extra === "auto_increment" || (c.column_default?.includes("nextval") ?? false), - })), + columns: cols.map((c: ColumnInfo) => { + const isAI = c.extra === "auto_increment" || (c.column_default?.includes("nextval") ?? false); + const gKey = findGeneratorKey(c.name, c.data_type, isAI); + return { + columnName: c.name, + dataType: c.data_type, + rowCount: 1000, + generatorKey: gKey, + generatorParams: defaultGeneratorParams( + c.name, + { + dataType: c.data_type, + isAutoIncrement: isAI, + columnDefault: c.column_default, + numericPrecision: c.numeric_precision, + numericScale: c.numeric_scale, + characterMaximumLength: c.character_maximum_length, + }, + gKey, + ), + isAutoIncrement: isAI, + }; + }), }; checkedTables[key] = true; for (const c of cols) { @@ -188,14 +204,29 @@ async function loadColumns(schema: string, table: string) { schema, database: props.prefillDatabase, rowCount: 1000, - columns: cols.map((c: ColumnInfo) => ({ - columnName: c.name, - dataType: c.data_type, - rowCount: 1000, - generatorKey: findGeneratorKey(c.name, c.data_type), - generatorParams: {}, - isAutoIncrement: c.extra === "auto_increment" || (c.column_default?.includes("nextval") ?? false), - })), + columns: cols.map((c: ColumnInfo) => { + const isAI = c.extra === "auto_increment" || (c.column_default?.includes("nextval") ?? false); + const gKey = findGeneratorKey(c.name, c.data_type, isAI); + return { + columnName: c.name, + dataType: c.data_type, + rowCount: 1000, + generatorKey: gKey, + generatorParams: defaultGeneratorParams( + c.name, + { + dataType: c.data_type, + isAutoIncrement: isAI, + columnDefault: c.column_default, + numericPrecision: c.numeric_precision, + numericScale: c.numeric_scale, + characterMaximumLength: c.character_maximum_length, + }, + gKey, + ), + isAutoIncrement: isAI, + }; + }), }; configs[key] = cfg; for (const c of cols) { @@ -523,6 +554,154 @@ watch(open, (val) => { void loadSchemas(); } }); + +interface GenerateProfileJson { + version: 1; + connectionId?: string; + database?: string; + savedAt: string; + configs: Record; + checkedTables: Record; + checkedColumns: Record; + expandedTables: Record; + expandedSchemas: Record; + tableOrder: string[]; +} + +const fileInputRef = ref(null); + +function buildProfilePayload(): GenerateProfileJson { + return { + version: 1, + connectionId: props.prefillConnectionId, + database: props.prefillDatabase, + savedAt: new Date().toISOString(), + configs: JSON.parse(JSON.stringify(configs)), + checkedTables: { ...checkedTables }, + checkedColumns: { ...checkedColumns }, + expandedTables: { ...expandedTables }, + expandedSchemas: { ...expandedSchemas }, + tableOrder: [...tableOrder.value], + }; +} + +function defaultProfileFilename(): string { + const payload = buildProfilePayload(); + const tableNames = Object.keys(payload.configs); + const firstTableName = tableNames[0]?.split(".").pop() || "profile"; + const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); + return tableNames.length === 1 ? `${firstTableName}-${timestamp}.json` : `data-generate-${timestamp}.json`; +} + +function downloadJsonFallback(data: GenerateProfileJson, filename: string) { + const json = JSON.stringify(data, null, 2); + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +async function saveProfile() { + const payload = buildProfilePayload(); + const json = JSON.stringify(payload, null, 2); + const defaultPath = defaultProfileFilename(); + + if (isTauriRuntime()) { + try { + const { save } = await import("@tauri-apps/plugin-dialog"); + const { writeTextFile } = await import("@tauri-apps/plugin-fs"); + const path = await save({ + defaultPath, + filters: [{ name: "JSON", extensions: ["json"] }], + }); + if (path) { + await writeTextFile(path, json); + } + } catch (e: any) { + console.warn("profile save error (tauri):", e?.message ?? e); + } + } else { + downloadJsonFallback(payload, defaultPath); + } +} + +function applyProfileData(data: Partial & { configs?: Record }) { + if (!data.configs || typeof data.configs !== "object") { + throw new Error("Invalid profile file: missing configs"); + } + Object.keys(configs).forEach((k) => delete configs[k]); + Object.keys(checkedTables).forEach((k) => delete checkedTables[k]); + Object.keys(checkedColumns).forEach((k) => delete checkedColumns[k]); + Object.keys(expandedTables).forEach((k) => delete expandedTables[k]); + + for (const [key, cfg] of Object.entries(data.configs)) { + if (cfg && cfg.tableName) configs[key] = cfg; + } + if (data.checkedTables && typeof data.checkedTables === "object") { + for (const [k, v] of Object.entries(data.checkedTables)) { + checkedTables[k] = !!v; + } + } + if (data.checkedColumns && typeof data.checkedColumns === "object") { + for (const [k, v] of Object.entries(data.checkedColumns)) { + checkedColumns[k] = !!v; + } + } + if (data.expandedTables && typeof data.expandedTables === "object") { + for (const [k, v] of Object.entries(data.expandedTables)) { + expandedTables[k] = !!v; + } + } + if (data.expandedSchemas && typeof data.expandedSchemas === "object") { + for (const [k, v] of Object.entries(data.expandedSchemas)) { + expandedSchemas[k] = !!v; + } + } + if (Array.isArray(data.tableOrder)) { + tableOrder.value = [...data.tableOrder]; + } +} + +async function triggerLoadProfile() { + if (isTauriRuntime()) { + try { + const { open } = await import("@tauri-apps/plugin-dialog"); + const { readTextFile } = await import("@tauri-apps/plugin-fs"); + const path = await open({ + multiple: false, + filters: [{ name: "JSON", extensions: ["json"] }], + }); + if (!path) return; + const text = await readTextFile(path as string); + const data = JSON.parse(text); + applyProfileData(data); + } catch (e: any) { + console.warn("profile load error (tauri):", e?.message ?? e); + } + } else { + fileInputRef.value?.click(); + } +} + +async function onFileSelected(event: Event) { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + if (!file) return; + try { + const text = await file.text(); + const data = JSON.parse(text); + applyProfileData(data); + } catch (e: any) { + console.warn("profile load error:", e?.message ?? e); + } finally { + input.value = ""; + } +}