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:
1 parent
0830e03f89
commit
bc91fbdfa1
11 files changed
+1167
-53
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user