fix(generate): 确保保存/加载配置覆盖所有生成器参数,并修复参数被意外清空的问题
* 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>
This commit is contained in:
parent
0830e03f89
commit
bc91fbdfa1
|
|
@ -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<string, TableGenerateConfig>;
|
||||
checkedTables: Record<string, boolean>;
|
||||
checkedColumns: Record<string, boolean>;
|
||||
expandedTables: Record<string, boolean>;
|
||||
expandedSchemas: Record<string, boolean>;
|
||||
tableOrder: string[];
|
||||
}
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement | null>(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<GenerateProfileJson> & { configs?: Record<string, TableGenerateConfig> }) {
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -653,7 +832,7 @@ watch(open, (val) => {
|
|||
<div />
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<div class="rounded-md border w-full overflow-hidden">
|
||||
<div v-if="generatedResults.length === 0" class="flex h-[420px] items-center justify-center text-xs text-muted-foreground">{{ t("dataGenerate.noData") }}</div>
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-between px-3 py-2 border-b bg-muted/10">
|
||||
|
|
@ -724,11 +903,11 @@ watch(open, (val) => {
|
|||
<DialogFooter class="flex items-center justify-between border-t pt-3 sm:justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<template v-if="currentStep === 'config'">
|
||||
<Button variant="outline" size="sm" class="h-7 text-xs">
|
||||
<Button variant="outline" size="sm" class="h-7 text-xs" @click="saveProfile">
|
||||
<Save class="mr-1 h-3 w-3" />
|
||||
{{ t("dataGenerate.saveProfile") }}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" class="h-7 text-xs">
|
||||
<Button variant="outline" size="sm" class="h-7 text-xs" @click="triggerLoadProfile">
|
||||
<Upload class="mr-1 h-3 w-3" />
|
||||
{{ t("dataGenerate.loadProfile") }}
|
||||
</Button>
|
||||
|
|
@ -768,6 +947,7 @@ watch(open, (val) => {
|
|||
</template>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
<input ref="fileInputRef" type="file" accept="application/json,.json" class="hidden" @change="onFileSelected" />
|
||||
</DialogScrollContent>
|
||||
|
||||
<Dialog v-model:open="optionsDialogOpen">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, watch } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RotateCcw } from "@lucide/vue";
|
||||
|
|
@ -48,6 +48,7 @@ import FileNameParams from "./FileNameParams.vue";
|
|||
import FileExtensionParams from "./FileExtensionParams.vue";
|
||||
import UrlParams from "./UrlParams.vue";
|
||||
import HostnameParams from "./HostnameParams.vue";
|
||||
import IdNumberParams from "./IdNumberParams.vue";
|
||||
import DefaultParams from "./DefaultParams.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
@ -62,12 +63,6 @@ function initParams() {
|
|||
|
||||
const key = computed(() => props.config.generatorKey);
|
||||
|
||||
watch(key, (newKey, oldKey) => {
|
||||
if (newKey && newKey !== oldKey) {
|
||||
resetParams();
|
||||
}
|
||||
});
|
||||
|
||||
function resetParams() {
|
||||
props.config.generatorParams = {} as GeneratorParams;
|
||||
}
|
||||
|
|
@ -116,6 +111,7 @@ const componentMap: Record<string, any> = {
|
|||
file_extension: FileExtensionParams,
|
||||
url: UrlParams,
|
||||
hostname: HostnameParams,
|
||||
id_number: IdNumberParams,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,250 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw } from "@lucide/vue";
|
||||
import type { GeneratorParams } from "@/lib/dataGenerate";
|
||||
import CommonOptions from "./CommonOptions.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
function tr(key: string, fallback: string): string {
|
||||
const v = t("dataGenerate.idTypes." + key);
|
||||
return v === "dataGenerate.idTypes." + key ? fallback : v;
|
||||
}
|
||||
|
||||
const props = defineProps<{ params: GeneratorParams }>();
|
||||
|
||||
const idTypeKeys = ["id_card", "passport", "hk_macau_pass", "taiwan_pass", "uscc", "bank_card", "drivers_license", "custom"];
|
||||
|
||||
if (!props.params.idTypes) {
|
||||
props.params.idTypes = ["id_card"];
|
||||
}
|
||||
|
||||
function pick<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
function toggleType(val: string) {
|
||||
if (!props.params.idTypes) props.params.idTypes = [];
|
||||
const idx = props.params.idTypes.indexOf(val);
|
||||
if (idx >= 0) props.params.idTypes.splice(idx, 1);
|
||||
else props.params.idTypes.push(val);
|
||||
}
|
||||
|
||||
const hasCustom = computed(() => (props.params.idTypes ?? []).includes("custom"));
|
||||
|
||||
const areaCodes = [
|
||||
"110101",
|
||||
"110105",
|
||||
"110108",
|
||||
"120101",
|
||||
"120103",
|
||||
"130102",
|
||||
"130105",
|
||||
"210102",
|
||||
"210105",
|
||||
"210203",
|
||||
"310101",
|
||||
"310104",
|
||||
"310115",
|
||||
"320102",
|
||||
"320105",
|
||||
"330102",
|
||||
"330106",
|
||||
"340102",
|
||||
"340104",
|
||||
"350102",
|
||||
"360102",
|
||||
"370102",
|
||||
"370202",
|
||||
"410102",
|
||||
"410105",
|
||||
"420102",
|
||||
"420106",
|
||||
"430102",
|
||||
"440103",
|
||||
"440106",
|
||||
"440303",
|
||||
"440305",
|
||||
"450102",
|
||||
"460105",
|
||||
"500103",
|
||||
"510104",
|
||||
"510107",
|
||||
"520102",
|
||||
"530102",
|
||||
"540102",
|
||||
"610103",
|
||||
"620102",
|
||||
"630102",
|
||||
"640104",
|
||||
"650102",
|
||||
];
|
||||
|
||||
function generateIdCard(): string {
|
||||
const area = pick(areaCodes);
|
||||
const year = (1970 + Math.floor(Math.random() * 35)).toString();
|
||||
const month = (Math.floor(Math.random() * 12) + 1).toString().padStart(2, "0");
|
||||
const day = (Math.floor(Math.random() * 28) + 1).toString().padStart(2, "0");
|
||||
const seq = Math.floor(Math.random() * 1000)
|
||||
.toString()
|
||||
.padStart(3, "0");
|
||||
const first17 = `${area}${year}${month}${day}${seq}`;
|
||||
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||||
const checks = ["1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 17; i++) sum += parseInt(first17[i]) * weights[i];
|
||||
const check = checks[sum % 11];
|
||||
return `${first17}${check}`;
|
||||
}
|
||||
|
||||
function randDigits(n: number): string {
|
||||
return Array.from({ length: n }, () => Math.floor(Math.random() * 10).toString()).join("");
|
||||
}
|
||||
|
||||
function generatePassport(): string {
|
||||
return `${pick(["E", "G", "E", "H", "P"])}${randDigits(7)}`;
|
||||
}
|
||||
|
||||
function generateHKMacauPass(): string {
|
||||
return `${pick(["C", "W", "H"])}${randDigits(8)}`;
|
||||
}
|
||||
|
||||
function generateTaiwanPass(): string {
|
||||
return `T${randDigits(8)}`;
|
||||
}
|
||||
|
||||
function generateUSCC(): string {
|
||||
const randChars = (n: number) => Array.from({ length: n }, () => "ABCDEFGHJKLMNPQRSTUVWXY23456789".charAt(Math.floor(Math.random() * "ABCDEFGHJKLMNPQRSTUVWXY23456789".length))).join("");
|
||||
return `91310115MA${randChars(6)}${randDigits(4)}`;
|
||||
}
|
||||
|
||||
function luhnCalculateCheckDigit(numStr: string): string {
|
||||
let sum = 0;
|
||||
const digits = numStr.split("").map(Number);
|
||||
for (let i = 0; i < digits.length; i++) {
|
||||
let d = digits[digits.length - 1 - i];
|
||||
if (i % 2 === 0) {
|
||||
d *= 2;
|
||||
if (d > 9) d -= 9;
|
||||
}
|
||||
sum += d;
|
||||
}
|
||||
return ((10 - (sum % 10)) % 10).toString();
|
||||
}
|
||||
|
||||
function generateBankCard(): string {
|
||||
const bins = ["622202", "622700", "621700", "622848", "621559", "621798", "622208", "622280", "621661", "621785", "622588", "621286", "622696"];
|
||||
const bin = pick(bins);
|
||||
const middle = randDigits(10);
|
||||
const first15 = `${bin}${middle}`;
|
||||
return `${first15}${luhnCalculateCheckDigit(first15)}`;
|
||||
}
|
||||
|
||||
function randFromCharSet(charSet: string, n: number): string {
|
||||
return Array.from({ length: n }, () => charSet.charAt(Math.floor(Math.random() * charSet.length))).join("");
|
||||
}
|
||||
|
||||
function generateFromPattern(pattern: string): string {
|
||||
const letterCharSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const digitCharSet = "0123456789";
|
||||
let result = "";
|
||||
const tokens = pattern.split(/([{}[\]-])/).filter(Boolean);
|
||||
let i = 0;
|
||||
while (i < tokens.length) {
|
||||
const t = tokens[i];
|
||||
if (t === "(" || t === ")") {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (t === "[") {
|
||||
const charSetToken = tokens[i + 1];
|
||||
i += 2;
|
||||
let charSet = "";
|
||||
if (charSetToken?.includes("A-F") || charSetToken?.includes("A-Z")) charSet = letterCharSet;
|
||||
else if (charSetToken?.includes("0-9")) charSet = digitCharSet;
|
||||
else charSet = letterCharSet + digitCharSet;
|
||||
i++;
|
||||
let count = 1;
|
||||
if (tokens[i] === "{") {
|
||||
i++;
|
||||
count = parseInt(tokens[i], 10) || 1;
|
||||
i += 2;
|
||||
}
|
||||
result += randFromCharSet(charSet, count);
|
||||
continue;
|
||||
}
|
||||
if (t === "-") {
|
||||
result += "-";
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (!result) {
|
||||
return randDigits(10);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function generateByType(type: string): string {
|
||||
switch (type) {
|
||||
case "id_card":
|
||||
return generateIdCard();
|
||||
case "passport":
|
||||
return generatePassport();
|
||||
case "hk_macau_pass":
|
||||
return generateHKMacauPass();
|
||||
case "taiwan_pass":
|
||||
return generateTaiwanPass();
|
||||
case "uscc":
|
||||
return generateUSCC();
|
||||
case "bank_card":
|
||||
return generateBankCard();
|
||||
case "drivers_license":
|
||||
return generateIdCard();
|
||||
case "custom":
|
||||
return generateFromPattern(props.params.idCustomPattern ?? "([A-Z]{2})-([0-9]{8})");
|
||||
default:
|
||||
return generateIdCard();
|
||||
}
|
||||
}
|
||||
|
||||
const previewKey = ref(0);
|
||||
const previewVal = computed(() => {
|
||||
void previewKey.value;
|
||||
const selected = props.params.idTypes?.length ? props.params.idTypes : ["id_card"];
|
||||
return generateByType(pick(selected));
|
||||
});
|
||||
|
||||
function refresh() {
|
||||
previewKey.value++;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-2">{{ tr("title", "证件类型") }}</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<button v-for="k in idTypeKeys" :key="k" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.idTypes ?? []).includes(k) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleType(k)">{{ tr(k, k) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasCustom" class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="text-xs text-muted-foreground mb-1">{{ tr("customPattern", "自定义模式") }}</div>
|
||||
<input v-model="params.idCustomPattern" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono" :placeholder="tr('customPatternPlaceholder', '如 ([A-Z]{2})-([0-9]{8})')" />
|
||||
<div class="text-[10px] text-muted-foreground mt-1">{{ tr("customPatternHint", "支持 [A-Z] 字母、[0-9] 数字、{n} 重复、连字符等简单模式") }}</div>
|
||||
</div>
|
||||
<div class="rounded-md border bg-muted/10 p-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
|
||||
<span :key="previewKey" class="font-mono text-sm">{{ previewVal }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<CommonOptions :params="params" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export default {
|
||||
export default {
|
||||
app: {
|
||||
name: "DBX",
|
||||
},
|
||||
|
|
@ -1825,6 +1825,21 @@
|
|||
file_extension: "File Extension",
|
||||
url: "URL",
|
||||
hostname: "Hostname",
|
||||
id_number: "ID Number",
|
||||
},
|
||||
idTypes: {
|
||||
title: "ID Type",
|
||||
customPattern: "Custom Pattern",
|
||||
customPatternPlaceholder: "e.g. ([A-Z]{2})-([0-9]{8})",
|
||||
customPatternHint: "Supports simple patterns like [A-Z] letters, [0-9] digits, {n} repetitions, hyphens",
|
||||
id_card: "Chinese ID Card",
|
||||
passport: "Passport",
|
||||
hk_macau_pass: "HK/Macau Permit",
|
||||
taiwan_pass: "Taiwan Permit",
|
||||
uscc: "Unified Social Credit Code",
|
||||
bank_card: "Bank Card",
|
||||
drivers_license: "Driver's License",
|
||||
custom: "Custom",
|
||||
},
|
||||
},
|
||||
tableToolbox: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export default {
|
||||
export default {
|
||||
app: {
|
||||
name: "DBX",
|
||||
},
|
||||
|
|
@ -1563,6 +1563,21 @@
|
|||
file_extension: "File Extension",
|
||||
url: "URL",
|
||||
hostname: "Hostname",
|
||||
id_number: "Número de documento",
|
||||
},
|
||||
idTypes: {
|
||||
title: "Tipo de documento",
|
||||
customPattern: "Patrón personalizado",
|
||||
customPatternPlaceholder: "p. ej. ([A-Z]{2})-([0-9]{8})",
|
||||
customPatternHint: "Admite patrones simples como [A-Z] letras, [0-9] dígitos, {n} repeticiones, guiones",
|
||||
id_card: "DNI chino",
|
||||
passport: "Pasaporte",
|
||||
hk_macau_pass: "Permiso HK/Macao",
|
||||
taiwan_pass: "Permiso Taiwán",
|
||||
uscc: "Código social unificado",
|
||||
bank_card: "Tarjeta bancaria",
|
||||
drivers_license: "Carnet de conducir",
|
||||
custom: "Personalizado",
|
||||
},
|
||||
},
|
||||
tableToolbox: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export default {
|
||||
export default {
|
||||
app: {
|
||||
name: "DBX",
|
||||
},
|
||||
|
|
@ -1686,6 +1686,21 @@
|
|||
file_extension: "File Extension",
|
||||
url: "URL",
|
||||
hostname: "Hostname",
|
||||
id_number: "Numero documento",
|
||||
},
|
||||
idTypes: {
|
||||
title: "Tipo documento",
|
||||
customPattern: "Pattern personalizzato",
|
||||
customPatternPlaceholder: "es. ([A-Z]{2})-([0-9]{8})",
|
||||
customPatternHint: "Supporta pattern semplici come [A-Z] lettere, [0-9] cifre, {n} ripetizioni, trattini",
|
||||
id_card: "Carta d'identità cinese",
|
||||
passport: "Passaporto",
|
||||
hk_macau_pass: "Permesso HK/Macao",
|
||||
taiwan_pass: "Permesso Taiwan",
|
||||
uscc: "Codice sociale unificato",
|
||||
bank_card: "Carta bancaria",
|
||||
drivers_license: "Patente di guida",
|
||||
custom: "Personalizzato",
|
||||
},
|
||||
},
|
||||
tableToolbox: {
|
||||
|
|
|
|||
|
|
@ -1815,6 +1815,21 @@ export default {
|
|||
file_extension: "ファイル拡張子",
|
||||
url: "URL",
|
||||
hostname: "ホスト名",
|
||||
id_number: "証明書番号",
|
||||
},
|
||||
idTypes: {
|
||||
title: "証明書の種類",
|
||||
customPattern: "カスタムパターン",
|
||||
customPatternPlaceholder: "例: ([A-Z]{2})-([0-9]{8})",
|
||||
customPatternHint: "[A-Z] 英字、[0-9] 数字、{n} 繰り返し、ハイフンなどの簡単なパターンに対応",
|
||||
id_card: "中国身分証",
|
||||
passport: "パスポート",
|
||||
hk_macau_pass: "港澳通行証",
|
||||
taiwan_pass: "台湾通行証",
|
||||
uscc: "統一社会信用コード",
|
||||
bank_card: "銀行カード",
|
||||
drivers_license: "運転免許証",
|
||||
custom: "カスタム",
|
||||
},
|
||||
},
|
||||
tableToolbox: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export default {
|
||||
export default {
|
||||
app: {
|
||||
name: "DBX",
|
||||
},
|
||||
|
|
@ -1697,6 +1697,21 @@
|
|||
file_extension: "File Extension",
|
||||
url: "URL",
|
||||
hostname: "Hostname",
|
||||
id_number: "Número de documento",
|
||||
},
|
||||
idTypes: {
|
||||
title: "Tipo de documento",
|
||||
customPattern: "Padrão personalizado",
|
||||
customPatternPlaceholder: "ex.: ([A-Z]{2})-([0-9]{8})",
|
||||
customPatternHint: "Suporta padrões simples como [A-Z] letras, [0-9] dígitos, {n} repetições, hífens",
|
||||
id_card: "CPF chinês",
|
||||
passport: "Passaporte",
|
||||
hk_macau_pass: "Permissão HK/Macau",
|
||||
taiwan_pass: "Permissão Taiwan",
|
||||
uscc: "Código social unificado",
|
||||
bank_card: "Cartão bancário",
|
||||
drivers_license: "Carteira de motorista",
|
||||
custom: "Personalizado",
|
||||
},
|
||||
},
|
||||
tableToolbox: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export default {
|
||||
export default {
|
||||
app: {
|
||||
name: "DBX",
|
||||
},
|
||||
|
|
@ -1824,6 +1824,21 @@
|
|||
file_extension: "文件扩展名",
|
||||
url: "网址",
|
||||
hostname: "主机名",
|
||||
id_number: "证件号",
|
||||
},
|
||||
idTypes: {
|
||||
title: "证件类型",
|
||||
customPattern: "自定义模式",
|
||||
customPatternPlaceholder: "如 ([A-Z]{2})-([0-9]{8})",
|
||||
customPatternHint: "支持 [A-Z] 字母、[0-9] 数字、{n} 重复、连字符等简单模式",
|
||||
id_card: "身份证号",
|
||||
passport: "护照号",
|
||||
hk_macau_pass: "港澳通行证",
|
||||
taiwan_pass: "台湾通行证",
|
||||
uscc: "统一社会信用代码",
|
||||
bank_card: "银行卡号",
|
||||
drivers_license: "驾驶证号",
|
||||
custom: "自定义",
|
||||
},
|
||||
},
|
||||
tableToolbox: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export default {
|
||||
export default {
|
||||
app: {
|
||||
name: "DBX",
|
||||
},
|
||||
|
|
@ -1674,6 +1674,21 @@
|
|||
file_extension: "檔案副檔名",
|
||||
url: "網址",
|
||||
hostname: "主機名稱",
|
||||
id_number: "證件號",
|
||||
},
|
||||
idTypes: {
|
||||
title: "證件類型",
|
||||
customPattern: "自訂模式",
|
||||
customPatternPlaceholder: "如 ([A-Z]{2})-([0-9]{8})",
|
||||
customPatternHint: "支援 [A-Z] 字母、[0-9] 數字、{n} 重複、連字號等簡單模式",
|
||||
id_card: "身份證號",
|
||||
passport: "護照號",
|
||||
hk_macau_pass: "港澳通行證",
|
||||
taiwan_pass: "台灣通行證",
|
||||
uscc: "統一社會信用代碼",
|
||||
bank_card: "銀行卡號",
|
||||
drivers_license: "駕駛證號",
|
||||
custom: "自訂",
|
||||
},
|
||||
},
|
||||
tableToolbox: {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export const GeneratorHierarchy: GeneratorNode[] = [
|
|||
{ key: "marital_status", label: "婚姻状况" },
|
||||
{ key: "phone", label: "电话号码" },
|
||||
{ key: "email", label: "电子邮箱" },
|
||||
{ key: "id_number", label: "证件号" },
|
||||
{ key: "job_title", label: "职位名称" },
|
||||
{ key: "social_id", label: "社交网络ID" },
|
||||
],
|
||||
|
|
@ -107,6 +108,7 @@ export interface GeneratorParams {
|
|||
// text
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
textFormat?: "lorem" | "alphanumeric" | "hex" | "bcrypt" | "version" | "language" | "currency" | "slug" | "tag_list" | "level" | "channel";
|
||||
// date/time
|
||||
allDay?: boolean;
|
||||
startTime?: string;
|
||||
|
|
@ -160,6 +162,9 @@ export interface GeneratorParams {
|
|||
// region / country
|
||||
regionFormat?: string;
|
||||
regionLang?: string;
|
||||
// id numbers
|
||||
idTypes?: string[];
|
||||
idCustomPattern?: string;
|
||||
textTransform?: string;
|
||||
// product
|
||||
productKeywords?: string;
|
||||
|
|
@ -554,6 +559,154 @@ function generateIPv6(): string {
|
|||
return groups.join(":");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ID Numbers (证件号)
|
||||
// ============================================================
|
||||
|
||||
const areaCodes = [
|
||||
"110101",
|
||||
"110105",
|
||||
"110108",
|
||||
"120101",
|
||||
"120103",
|
||||
"130102",
|
||||
"130105",
|
||||
"210102",
|
||||
"210105",
|
||||
"210203",
|
||||
"310101",
|
||||
"310104",
|
||||
"310115",
|
||||
"320102",
|
||||
"320105",
|
||||
"330102",
|
||||
"330106",
|
||||
"340102",
|
||||
"340104",
|
||||
"350102",
|
||||
"360102",
|
||||
"370102",
|
||||
"370202",
|
||||
"410102",
|
||||
"410105",
|
||||
"420102",
|
||||
"420106",
|
||||
"430102",
|
||||
"440103",
|
||||
"440106",
|
||||
"440303",
|
||||
"440305",
|
||||
"450102",
|
||||
"460105",
|
||||
"500103",
|
||||
"510104",
|
||||
"510107",
|
||||
"520102",
|
||||
"530102",
|
||||
"540102",
|
||||
"610103",
|
||||
"620102",
|
||||
"630102",
|
||||
"640104",
|
||||
"650102",
|
||||
];
|
||||
|
||||
function generateIdCard(): string {
|
||||
const area = pick(areaCodes);
|
||||
const year = randInt(1960, 2005).toString();
|
||||
const month = randInt(1, 12).toString().padStart(2, "0");
|
||||
const day = randInt(1, 28).toString().padStart(2, "0");
|
||||
const seq = randInt(1, 999).toString().padStart(3, "0");
|
||||
const first17 = `${area}${year}${month}${day}${seq}`;
|
||||
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||||
const checks = ["1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 17; i++) sum += parseInt(first17[i]) * weights[i];
|
||||
const check = checks[sum % 11];
|
||||
return `${first17}${check}`;
|
||||
}
|
||||
|
||||
function generatePassport(): string {
|
||||
const prefixes = ["E", "G", "E", "E", "E", "H", "P"];
|
||||
const prefix = pick(prefixes);
|
||||
const digits = Array.from({ length: 7 }, () => Math.floor(Math.random() * 10).toString()).join("");
|
||||
return `${prefix}${digits}`;
|
||||
}
|
||||
|
||||
function generateHKMacauPass(): string {
|
||||
const prefixes = ["C", "W", "H"];
|
||||
const prefix = pick(prefixes);
|
||||
const digits = Array.from({ length: 8 }, () => Math.floor(Math.random() * 10).toString()).join("");
|
||||
return `${prefix}${digits}`;
|
||||
}
|
||||
|
||||
function generateTaiwanPass(): string {
|
||||
const digits = Array.from({ length: 8 }, () => Math.floor(Math.random() * 10).toString()).join("");
|
||||
return `T${digits}`;
|
||||
}
|
||||
|
||||
function generateUSCC(): string {
|
||||
const firstPart = "91310115MA";
|
||||
const randChars = randFromCharSet("ABCDEFGHJKLMNPQRSTUVWXY23456789", 6);
|
||||
const randDigits = Array.from({ length: 4 }, () => Math.floor(Math.random() * 10).toString()).join("");
|
||||
return `${firstPart}${randChars}${randDigits}`;
|
||||
}
|
||||
|
||||
const bankBins = ["622202", "622700", "621700", "622848", "621559", "621798", "622208", "622280", "621661", "621785", "622838", "622510", "622698", "622690", "622556", "622588", "621286", "622218", "622506", "622622", "622696", "621483", "621299", "622155", "622156"];
|
||||
|
||||
function luhnCalculateCheckDigit(numStr: string): string {
|
||||
let sum = 0;
|
||||
const digits = numStr.split("").map(Number);
|
||||
for (let i = 0; i < digits.length; i++) {
|
||||
let d = digits[digits.length - 1 - i];
|
||||
if (i % 2 === 0) {
|
||||
d *= 2;
|
||||
if (d > 9) d -= 9;
|
||||
}
|
||||
sum += d;
|
||||
}
|
||||
const check = (10 - (sum % 10)) % 10;
|
||||
return check.toString();
|
||||
}
|
||||
|
||||
function generateBankCard(): string {
|
||||
const bin = pick(bankBins);
|
||||
const middle = Array.from({ length: 10 }, () => Math.floor(Math.random() * 10).toString()).join("");
|
||||
const first15 = `${bin}${middle}`;
|
||||
const check = luhnCalculateCheckDigit(first15);
|
||||
return `${first15}${check}`;
|
||||
}
|
||||
|
||||
function generateDriversLicense(): string {
|
||||
return generateIdCard();
|
||||
}
|
||||
|
||||
function generateIdNumber(params?: GeneratorParams): string {
|
||||
const types = params?.idTypes?.length ? params.idTypes : ["id_card"];
|
||||
const type = pick(types);
|
||||
switch (type) {
|
||||
case "id_card":
|
||||
return generateIdCard();
|
||||
case "passport":
|
||||
return generatePassport();
|
||||
case "hk_macau_pass":
|
||||
return generateHKMacauPass();
|
||||
case "taiwan_pass":
|
||||
return generateTaiwanPass();
|
||||
case "uscc":
|
||||
return generateUSCC();
|
||||
case "bank_card":
|
||||
return generateBankCard();
|
||||
case "drivers_license":
|
||||
return generateDriversLicense();
|
||||
case "custom":
|
||||
if (params?.idCustomPattern) return generateSkuFromPattern(params.idCustomPattern);
|
||||
return Array.from({ length: 10 }, () => Math.floor(Math.random() * 10).toString()).join("");
|
||||
default:
|
||||
return generateIdCard();
|
||||
}
|
||||
}
|
||||
|
||||
// Extension categories
|
||||
const extensionCategoryMap: Record<string, string[]> = {
|
||||
image: [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".tiff"],
|
||||
|
|
@ -820,6 +973,7 @@ const GeneratorFunctions: Record<string, (params?: GeneratorParams) => string> =
|
|||
return generateBarcode(pick(selected));
|
||||
},
|
||||
sku: (params) => generateSkuFromPattern(params?.pattern ?? "([A-F]{2}[-]){2}([0-9]{4}[-])([A-Z])"),
|
||||
id_number: generateIdNumber,
|
||||
ip_address: (params) => (params?.ipType === "ipv6" ? generateIPv6() : generateIP()),
|
||||
mac_address: generateMAC,
|
||||
file_path: (params) => generateFilePath(params),
|
||||
|
|
@ -831,44 +985,419 @@ const GeneratorFunctions: Record<string, (params?: GeneratorParams) => string> =
|
|||
|
||||
const KnownColumnPatterns: Array<{ pattern: RegExp; generatorKey: string }> = [
|
||||
{ pattern: /email|e-?mail|mail/i, generatorKey: "email" },
|
||||
{ pattern: /phone|tel|mobile|cell|fax/i, generatorKey: "phone" },
|
||||
{ pattern: /phone|tel|mobile|cell|fax|telephone/i, generatorKey: "phone" },
|
||||
{ pattern: /id.?card|idcard|身份证|id.?number|证件号|证件|passport|护照|social.?credit|统一社会|信用代码/i, generatorKey: "id_number" },
|
||||
{ pattern: /bank.?card|银行卡|bank.?account/i, generatorKey: "id_number" },
|
||||
{ pattern: /driver.?license|drivers?|驾驶证|驾照/i, generatorKey: "id_number" },
|
||||
{ pattern: /first.?name|fname|given.?name/i, generatorKey: "full_name" },
|
||||
{ pattern: /last.?name|lname|surname|family.?name/i, generatorKey: "full_name" },
|
||||
{ pattern: /full.?name|name|user.?name/i, generatorKey: "full_name" },
|
||||
{ pattern: /full.?name|name|user.?name|username|nickname|nick.?name/i, generatorKey: "full_name" },
|
||||
{ pattern: /gender|sex/i, generatorKey: "gender" },
|
||||
{ pattern: /city/i, generatorKey: "city" },
|
||||
{ pattern: /country/i, generatorKey: "city" },
|
||||
{ pattern: /city|town|municipality/i, generatorKey: "city" },
|
||||
{ pattern: /country|province|state|region/i, generatorKey: "city" },
|
||||
{ pattern: /zip|postal.?code|postcode/i, generatorKey: "number" },
|
||||
{ pattern: /address|street/i, generatorKey: "address" },
|
||||
{ pattern: /url|website|link/i, generatorKey: "url" },
|
||||
{ pattern: /ip/i, generatorKey: "ip_address" },
|
||||
{ pattern: /mac/i, generatorKey: "mac_address" },
|
||||
{ pattern: /address|street|district|county/i, generatorKey: "address" },
|
||||
{ pattern: /url|website|link|homepage|href|weburl/i, generatorKey: "url" },
|
||||
{ pattern: /ip$/i, generatorKey: "ip_address" },
|
||||
{ pattern: /ip.?address|ipv4|ipv6/i, generatorKey: "ip_address" },
|
||||
{ pattern: /mac.?address|mac$/i, generatorKey: "mac_address" },
|
||||
{ pattern: /uuid|guid/i, generatorKey: "uuid" },
|
||||
{ pattern: /company|corp|organization|org/i, generatorKey: "company_name" },
|
||||
{ pattern: /department|dept/i, generatorKey: "department" },
|
||||
{ pattern: /company|corp|organization|org|brand/i, generatorKey: "company_name" },
|
||||
{ pattern: /department|dept|division/i, generatorKey: "department" },
|
||||
{ pattern: /color|colour/i, generatorKey: "color" },
|
||||
{ pattern: /title|position/i, generatorKey: "job_title" },
|
||||
{ pattern: /description|comment|note|summary|bio|content/i, generatorKey: "text" },
|
||||
{ pattern: /title|position|job.?title|role/i, generatorKey: "job_title" },
|
||||
{ pattern: /description|comment|note|summary|bio|content|body|detail|remark|memo|intro|introduction|overview/i, generatorKey: "text" },
|
||||
{ pattern: /status|state/i, generatorKey: "enum" },
|
||||
{ pattern: /barcode|upc|ean/i, generatorKey: "barcode" },
|
||||
{ pattern: /barcode|upc|ean|qr.?code/i, generatorKey: "barcode" },
|
||||
{ pattern: /sku/i, generatorKey: "sku" },
|
||||
{ pattern: /product.?name|product$/i, generatorKey: "product_name" },
|
||||
{ pattern: /category|type|kind|classification/i, generatorKey: "product_category" },
|
||||
{ pattern: /size$/i, generatorKey: "size" },
|
||||
{ pattern: /weight|weight.?unit|unit.?weight|weight.?unit|weight.?type/i, generatorKey: "weight_unit" },
|
||||
{ pattern: /file.?path|filePath|directory|dir$/i, generatorKey: "file_path" },
|
||||
{ pattern: /file.?name|filename|original.?name|file.?original.?name/i, generatorKey: "file_name" },
|
||||
{ pattern: /file.?extension|ext$|filetype|file.?type/i, generatorKey: "file_extension" },
|
||||
{ pattern: /hostname|host$/i, generatorKey: "hostname" },
|
||||
{ pattern: /version|ver$/i, generatorKey: "text" },
|
||||
{ pattern: /language|locale$/i, generatorKey: "text" },
|
||||
{ pattern: /currency|currency.?code|currency.?name/i, generatorKey: "text" },
|
||||
{ pattern: /slug|permalink|alias|url.?slug|key$/i, generatorKey: "text" },
|
||||
{ pattern: /tag|keyword|labels|tags$/i, generatorKey: "text" },
|
||||
{ pattern: /level|tier|stage|phase|channel|source|platform/i, generatorKey: "text" },
|
||||
{ pattern: /owner|creator|author|created_by|updated_by|modified_by|operator|user$/i, generatorKey: "full_name" },
|
||||
];
|
||||
|
||||
export function findGeneratorKey(columnName: string, dataType: string): string {
|
||||
export function findGeneratorKey(columnName: string, dataType: string, isAutoIncrement?: boolean): string {
|
||||
if (isAutoIncrement) return "sequence";
|
||||
const type = dataType.toLowerCase();
|
||||
const isNumeric = type.includes("int") || type === "smallint" || type === "bigint" || type.includes("bool") || type.includes("decimal") || type.includes("numeric") || type.includes("float") || type.includes("double") || type === "real";
|
||||
const isDateTime = type.includes("date") || type.includes("timestamp") || type === "time";
|
||||
const isBinary = type.includes("binary") || type.includes("blob") || type.includes("bytea");
|
||||
const isBoolType = type === "bool" || type === "boolean" || type === "bit" || type === "tinyint(1)";
|
||||
const isBoolName = /^(is|has|had|can|did|enable|disable|allow|use|visible|deleted|active|flag)[_\-]?/i.test(columnName);
|
||||
if (isNumeric || isDateTime || isBinary) {
|
||||
if (type.includes("serial")) return "sequence";
|
||||
if (isBoolType || (isNumeric && isBoolName)) return "enum";
|
||||
if (/status|state/i.test(columnName)) return "enum";
|
||||
if (isNumeric) return "number";
|
||||
if (type === "time") return "time";
|
||||
if (isDateTime) return "datetime";
|
||||
if (isBinary) return "text";
|
||||
}
|
||||
for (const { pattern, generatorKey } of KnownColumnPatterns) {
|
||||
if (pattern.test(columnName)) return generatorKey;
|
||||
}
|
||||
const type = dataType.toLowerCase();
|
||||
if (type.includes("int") || type.includes("serial") || type === "bigint" || type === "smallint") return "number";
|
||||
if (type.includes("bool")) return "number";
|
||||
if (type.includes("decimal") || type.includes("numeric") || type.includes("float") || type.includes("double") || type === "real") return "number";
|
||||
if (type.includes("date") || type.includes("timestamp")) return "datetime";
|
||||
if (type === "time") return "time";
|
||||
if (type.includes("char") || type.includes("text") || type.includes("varchar") || type === "clob") return "text";
|
||||
if (type.includes("uuid") || type.includes("guid")) return "uuid";
|
||||
return "text";
|
||||
}
|
||||
|
||||
export interface ColumnAttrs {
|
||||
dataType: string;
|
||||
isAutoIncrement?: boolean;
|
||||
columnDefault?: string | null;
|
||||
numericPrecision?: number | null;
|
||||
numericScale?: number | null;
|
||||
characterMaximumLength?: number | null;
|
||||
}
|
||||
|
||||
export function defaultGeneratorParams(_columnName: string, attrs: ColumnAttrs, generatorKey: string): GeneratorParams {
|
||||
const params: GeneratorParams = {};
|
||||
const type = attrs.dataType.toLowerCase();
|
||||
const precision = attrs.numericPrecision ?? null;
|
||||
const scale = attrs.numericScale ?? null;
|
||||
const charLen = attrs.characterMaximumLength ?? null;
|
||||
|
||||
if (generatorKey === "sequence") {
|
||||
params.startValue = 1;
|
||||
params.increment = 1;
|
||||
return params;
|
||||
}
|
||||
|
||||
if (generatorKey === "number") {
|
||||
const isDecimal = type.includes("decimal") || type.includes("numeric") || type.includes("float") || type.includes("double") || type === "real";
|
||||
const col = _columnName.toLowerCase();
|
||||
const isAmount = /price|amount|total|cost|fee|balance|salary|income|revenue|tax|discount|money|payment|price/i.test(col);
|
||||
const isBigAmount = /salary|income|revenue|balance|total/i.test(col);
|
||||
const isPrice = /price|fee|cost|payment|amount|discount/i.test(col);
|
||||
const isPercent = /percent|percentage|pct|rate|score|rating|progress/i.test(col);
|
||||
|
||||
if (isDecimal) {
|
||||
params.numberType = "decimal";
|
||||
params.decimalPlaces = scale ?? 2;
|
||||
if (isPercent) {
|
||||
params.min = 0;
|
||||
params.max = 100;
|
||||
} else if (isBigAmount) {
|
||||
params.min = 100;
|
||||
params.max = 999999;
|
||||
} else if (isPrice) {
|
||||
params.min = 1;
|
||||
params.max = 9999.99;
|
||||
} else {
|
||||
const effectivePrecision = precision ?? 10;
|
||||
const intDigits = Math.max(1, effectivePrecision - (scale ?? 0));
|
||||
const maxVal = Math.pow(10, intDigits) - 1;
|
||||
params.min = 0;
|
||||
params.max = Math.max(1, Math.min(maxVal, 999999));
|
||||
}
|
||||
} else {
|
||||
params.numberType = "integer";
|
||||
if (type.includes("tinyint") || (precision !== null && precision <= 3)) {
|
||||
params.min = 0;
|
||||
params.max = 127;
|
||||
} else if (type === "smallint" || (precision !== null && precision <= 5)) {
|
||||
params.min = 0;
|
||||
params.max = 32767;
|
||||
} else if (type === "bigint" || (precision !== null && precision >= 19)) {
|
||||
params.min = 1;
|
||||
params.max = 999999;
|
||||
} else if (isPercent) {
|
||||
params.min = 0;
|
||||
params.max = 100;
|
||||
} else if (isAmount) {
|
||||
params.min = 1;
|
||||
params.max = 999999;
|
||||
} else {
|
||||
params.min = 1;
|
||||
params.max = precision ? Math.min(Math.pow(10, Math.min(precision, 6)) - 1, 999999) : 1000;
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
if (generatorKey === "text") {
|
||||
const col = _columnName.toLowerCase();
|
||||
if (/^password|^pwd|password$|pwd$/.test(col)) {
|
||||
params.textFormat = "bcrypt";
|
||||
params.minLength = 60;
|
||||
params.maxLength = 60;
|
||||
return params;
|
||||
}
|
||||
if (/token/.test(col)) {
|
||||
params.textFormat = "hex";
|
||||
params.minLength = 32;
|
||||
params.maxLength = 32;
|
||||
return params;
|
||||
}
|
||||
if (/^hash|hash$/.test(col)) {
|
||||
params.textFormat = "hex";
|
||||
params.minLength = 64;
|
||||
params.maxLength = 64;
|
||||
return params;
|
||||
}
|
||||
if (/secret|api[_-]?key/.test(col)) {
|
||||
params.textFormat = "alphanumeric";
|
||||
params.minLength = 32;
|
||||
params.maxLength = 32;
|
||||
return params;
|
||||
}
|
||||
if (/^version|version$|^ver$/i.test(_columnName)) {
|
||||
params.textFormat = "version";
|
||||
params.minLength = 3;
|
||||
params.maxLength = 10;
|
||||
return params;
|
||||
}
|
||||
if (/language|^locale$/i.test(_columnName)) {
|
||||
params.textFormat = "language";
|
||||
params.minLength = 5;
|
||||
params.maxLength = 5;
|
||||
return params;
|
||||
}
|
||||
if (/currency/i.test(_columnName)) {
|
||||
params.textFormat = "currency";
|
||||
params.minLength = 3;
|
||||
params.maxLength = 3;
|
||||
return params;
|
||||
}
|
||||
if (/slug|permalink|alias/i.test(_columnName)) {
|
||||
params.textFormat = "slug";
|
||||
params.minLength = 8;
|
||||
params.maxLength = 40;
|
||||
return params;
|
||||
}
|
||||
if (/tag|keyword|labels/i.test(_columnName)) {
|
||||
params.textFormat = "tag_list";
|
||||
params.minLength = 3;
|
||||
params.maxLength = 30;
|
||||
return params;
|
||||
}
|
||||
if (/level|tier|stage|phase/i.test(_columnName)) {
|
||||
params.textFormat = "level";
|
||||
params.minLength = 4;
|
||||
params.maxLength = 15;
|
||||
return params;
|
||||
}
|
||||
if (/channel|source|platform/i.test(_columnName)) {
|
||||
params.textFormat = "channel";
|
||||
params.minLength = 3;
|
||||
params.maxLength = 20;
|
||||
return params;
|
||||
}
|
||||
if (charLen !== null && charLen > 0) {
|
||||
const effectiveLen = Math.min(charLen, 2000);
|
||||
if (effectiveLen <= 20) {
|
||||
params.minLength = Math.max(1, Math.floor(effectiveLen * 0.6));
|
||||
params.maxLength = effectiveLen;
|
||||
} else {
|
||||
params.minLength = Math.max(10, Math.floor(effectiveLen * 0.3));
|
||||
params.maxLength = effectiveLen;
|
||||
}
|
||||
} else {
|
||||
params.minLength = 50;
|
||||
params.maxLength = 500;
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
if (generatorKey === "id_number") {
|
||||
if (/passport|护照/.test(_columnName)) {
|
||||
params.idTypes = ["passport"];
|
||||
} else if (/bank.?card|银行卡|credit.?card/.test(_columnName)) {
|
||||
params.idTypes = ["bank_card"];
|
||||
} else if (/driver|驾照|驾驶证/.test(_columnName)) {
|
||||
params.idTypes = ["drivers_license"];
|
||||
} else if (/social.?credit|统一社会|信用.?代码|uscc/.test(_columnName)) {
|
||||
params.idTypes = ["uscc"];
|
||||
} else if (/hk|macau|港澳|港澳通行/.test(_columnName)) {
|
||||
params.idTypes = ["hk_macau_pass"];
|
||||
} else if (/taiwan|台湾|台胞/.test(_columnName)) {
|
||||
params.idTypes = ["taiwan_pass"];
|
||||
} else {
|
||||
params.idTypes = ["id_card"];
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
if (generatorKey === "datetime" || generatorKey === "date") {
|
||||
params.start = "2020-01-01";
|
||||
params.end = "2030-12-31";
|
||||
return params;
|
||||
}
|
||||
|
||||
if (generatorKey === "time") {
|
||||
params.startTime = "00";
|
||||
params.endTime = "23";
|
||||
return params;
|
||||
}
|
||||
|
||||
if (generatorKey === "uuid") {
|
||||
params.uuidHyphens = true;
|
||||
return params;
|
||||
}
|
||||
|
||||
if (generatorKey === "enum") {
|
||||
const t = attrs.dataType.toLowerCase();
|
||||
const isNum = t.includes("int") || t.includes("bool") || t.includes("decimal") || t.includes("numeric") || t.includes("float") || t.includes("double") || t === "real";
|
||||
const isBoolType = t === "bool" || t === "boolean" || t === "bit" || t === "tinyint(1)";
|
||||
const isBoolName = /^(is|has|had|can|did|enable|disable|allow|use|visible|deleted|active|flag)[_\-]?/i.test(_columnName);
|
||||
if (isBoolType || (isNum && isBoolName)) {
|
||||
params.values = "0\n1";
|
||||
} else if (isNum) {
|
||||
params.values = "0\n1\n2";
|
||||
} else {
|
||||
params.values = "A\nB\nC\nD\nE";
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
// --- 其他生成器的默认参数(确保保存/加载配置时所有参数都被覆盖) ---
|
||||
if (generatorKey === "full_name") {
|
||||
params.nameFormat = "full";
|
||||
params.languages = ["en"];
|
||||
return params;
|
||||
}
|
||||
if (
|
||||
generatorKey === "gender" ||
|
||||
generatorKey === "title" ||
|
||||
generatorKey === "marital_status" ||
|
||||
generatorKey === "job_title" ||
|
||||
generatorKey === "company_name" ||
|
||||
generatorKey === "department" ||
|
||||
generatorKey === "industry" ||
|
||||
generatorKey === "product_category" ||
|
||||
generatorKey === "color" ||
|
||||
generatorKey === "size"
|
||||
) {
|
||||
params.languages = ["en"];
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "social_id") {
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "phone") {
|
||||
params.phoneFormat = "domestic";
|
||||
params.phoneSeparator = true;
|
||||
params.phoneRegions = ["us"];
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "email") {
|
||||
params.emailDomains = "gmail.com\nyahoo.com\noutlook.com\nexample.com\ntest.org";
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "payment_method") {
|
||||
params.values = "Credit Card\nPayPal\nApple Pay\nGoogle Pay\nBank Transfer\nCash\nCryptocurrency";
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "credit_card_type" || generatorKey === "credit_card_number") {
|
||||
params.cardTypes = ["visa", "mastercard"];
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "credit_card_date") {
|
||||
params.ccDateFormat = "MM/YY";
|
||||
params.ccYearOffsetMin = 0;
|
||||
params.ccYearOffsetMax = 5;
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "address") {
|
||||
params.addressType = "line1";
|
||||
params.regions = ["us"];
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "city") {
|
||||
params.regions = ["us"];
|
||||
params.languages = ["en"];
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "region") {
|
||||
params.regionFormat = "name";
|
||||
params.regionLang = "en";
|
||||
params.textTransform = "none";
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "product_name") {
|
||||
params.productKeywords = "Premium\nBasic\nUltra\nStandard\nPro\nWidget\nGadget\nTool\nDevice\nKit\nPack\nBundle\nEdition\nPlus\nMax\nMini\nLite\nCore\nSelect\nAdvanced\nSmart";
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "weight_unit") {
|
||||
params.values = "g\nkg\nlb\noz\nt\nmg\nct";
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "barcode") {
|
||||
params.barcodeTypes = ["ean13"];
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "sku") {
|
||||
params.pattern = "([A-F]{2}[-]){2}([0-9]{4}[-])([A-Z])";
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "ip_address") {
|
||||
params.ipType = "ipv4";
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "mac_address") {
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "file_path") {
|
||||
params.pathTypes = ["linux"];
|
||||
params.includeFileName = true;
|
||||
params.extensionCategory = "image";
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "file_name") {
|
||||
params.includeExtension = true;
|
||||
params.extensionCategory = "image";
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "file_extension") {
|
||||
params.extensionCategory = "image";
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "url") {
|
||||
params.urlSubdomains = "auth.\ndrive.\nimage.\nwww.\napi.\nmail.\nshop.\nblog.";
|
||||
params.urlTlds = ".biz\n.co.jp\n.com\n.cn\n.org\n.net\n.io\n.dev\n.xyz";
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "hostname") {
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "foreign_key") {
|
||||
params.fkSchema = "";
|
||||
params.fkTable = "";
|
||||
params.fkField = "";
|
||||
params.fkMode = "random";
|
||||
params.min = 1;
|
||||
params.max = 500;
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "image") {
|
||||
params.imageMode = "generate";
|
||||
params.imageWidth = 200;
|
||||
params.imageHeight = 200;
|
||||
params.imageFormat = "JPEG";
|
||||
params.folderPath = "";
|
||||
params.fileExtensions = "";
|
||||
return params;
|
||||
}
|
||||
if (generatorKey === "regex") {
|
||||
params.pattern = "";
|
||||
params.rawPattern = false;
|
||||
return params;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export function getGeneratorCategoryAndLabel(key: string): { category: string; categoryLabel: string; label: string } {
|
||||
const genKey = key.startsWith("general/") ? key.slice(8) : key;
|
||||
for (const cat of GeneratorHierarchy) {
|
||||
|
|
@ -953,6 +1482,60 @@ export function generateValue(columnName: string, dataType: string, generatorKey
|
|||
return vals[Math.floor(Math.random() * vals.length)];
|
||||
}
|
||||
if (key === "text") {
|
||||
const fmt = params?.textFormat ?? "lorem";
|
||||
if (fmt === "bcrypt") {
|
||||
const b64chars = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const b64 = (n: number) => Array.from({ length: n }, () => b64chars[Math.floor(Math.random() * b64chars.length)]).join("");
|
||||
return `$2a$10$${b64(22)}${b64(31)}`;
|
||||
}
|
||||
if (fmt === "hex") {
|
||||
const hex = "0123456789abcdef";
|
||||
const minLen = params?.minLength ?? 32;
|
||||
const maxLen = params?.maxLength ?? 32;
|
||||
const len = Math.floor(Math.random() * (maxLen - minLen + 1)) + minLen;
|
||||
return Array.from({ length: len }, () => hex[Math.floor(Math.random() * 16)]).join("");
|
||||
}
|
||||
if (fmt === "alphanumeric") {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const minLen = params?.minLength ?? 32;
|
||||
const maxLen = params?.maxLength ?? 32;
|
||||
const len = Math.floor(Math.random() * (maxLen - minLen + 1)) + minLen;
|
||||
return Array.from({ length: len }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
|
||||
}
|
||||
if (fmt === "version") {
|
||||
const v = `${randInt(0, 3)}.${randInt(0, 20)}.${randInt(0, 99)}`;
|
||||
return Math.random() < 0.3 ? `v${v}` : v;
|
||||
}
|
||||
if (fmt === "language") {
|
||||
const langs = ["en-US", "en-GB", "zh-CN", "zh-TW", "ja-JP", "ko-KR", "fr-FR", "de-DE", "es-ES", "pt-BR", "it-IT", "ru-RU", "ar-SA", "hi-IN", "nl-NL", "pl-PL"];
|
||||
return pick(langs);
|
||||
}
|
||||
if (fmt === "currency") {
|
||||
const codes = ["USD", "CNY", "EUR", "JPY", "GBP", "HKD", "SGD", "AUD", "CAD", "CHF", "THB", "TWD", "KRW", "INR", "RUB", "BRL"];
|
||||
return pick(codes);
|
||||
}
|
||||
if (fmt === "slug") {
|
||||
const words = ["product", "item", "category", "page", "service", "plan", "feature", "edition", "version", "type", "mode", "option", "setting", "config"];
|
||||
const count = randInt(2, 4);
|
||||
const parts: string[] = [];
|
||||
for (let i = 0; i < count; i++) parts.push(pick(words).toLowerCase());
|
||||
return parts.join("-");
|
||||
}
|
||||
if (fmt === "tag_list") {
|
||||
const pool = ["new", "featured", "hot", "sale", "discount", "promo", "vip", "limited", "bestseller", "recommended", "popular", "trending", "special", "exclusive", "premium", "basic"];
|
||||
const count = randInt(1, 3);
|
||||
const selected = new Set<string>();
|
||||
while (selected.size < count) selected.add(pick(pool));
|
||||
return Array.from(selected).join(",");
|
||||
}
|
||||
if (fmt === "level") {
|
||||
const tiers = ["basic", "standard", "premium", "enterprise", "professional", "ultimate", "starter", "advanced", "lite", "pro"];
|
||||
return pick(tiers);
|
||||
}
|
||||
if (fmt === "channel") {
|
||||
const channels = ["web", "mobile", "api", "desktop", "ios", "android", "mini-program", "wechat", "official", "direct", "partner", "affiliate", "referral", "organic", "email"];
|
||||
return pick(channels);
|
||||
}
|
||||
const texts = [
|
||||
"Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat",
|
||||
"The quick brown fox jumps over the lazy dog near the bank of the river A gentle breeze carried the scent of wildflowers across the meadow as birds sang melodies from the treetops",
|
||||
|
|
|
|||
Loading…
Reference in New Issue