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:
dbin0123andt8y2 authored and GitHub committed 2026-06-17 00:21:35 +08:00
1 parent 0830e03f89
commit bc91fbdfa1
11 files changed
+1167 -53

No files matched your search

@@ -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>