fix(i18n): translate backend errors at UI boundaries

This commit is contained in:
Rahul authored and GitHub committed 2026-07-29 02:40:45 +08:00
1 parent 4422ac8e01
commit 2f44d36018
82 files changed
+1862 -335

No files matched your search

+17 -2
View File
@@ -6,6 +6,7 @@ import PasswordInput from "@/components/ui/PasswordInput.vue";
import { Lock, Loader2, ShieldCheck } from "@lucide/vue";
import AppLogo from "@/components/icons/AppLogo.vue";
import { apiUrl } from "@/lib/common/webPath";
import { translateBackendError } from "@/i18n/backend-errors";
const props = withDefaults(
defineProps<{
@@ -22,6 +23,21 @@ const confirmPassword = ref("");
const error = ref("");
const loading = ref(false);
// The auth routes report failures as `{"error": "..."}`, so unwrap that before
// translating; anything else is treated as a plain-text message.
async function readAuthError(res: Response): Promise<string> {
const text = (await res.text()).trim();
if (!text) return t("auth.loginFailed");
let message = text;
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed.error === "string") message = parsed.error;
} catch {
// not JSON — fall through with the raw body
}
return translateBackendError(t, message) || t("auth.loginFailed");
}
async function submit() {
if (props.setupMode && password.value !== confirmPassword.value) {
error.value = t("auth.passwordMismatch");
@@ -40,8 +56,7 @@ async function submit() {
if (res.ok) {
emit("authenticated");
} else {
const text = await res.text();
error.value = text || t("auth.loginFailed");
error.value = await readAuthError(res);
}
} catch (e: any) {
error.value = e?.message || t("auth.connectFailed");
@@ -12,6 +12,7 @@ import { ChevronDown, ChevronRight, DatabaseBackup, FolderOpen, Loader2, Pencil,
import * as api from "@/lib/backend/api";
import { useScheduledDatabaseBackups } from "@/composables/useScheduledDatabaseBackups";
import { useToast } from "@/composables/useToast";
import { translateBackendError } from "@/i18n/backend-errors";
import { generateDatabaseExportId } from "@/lib/export/databaseExport";
import { nextDatabaseBackupRunAt, normalizeDatabaseBackupTablePatterns, supportsScheduledDatabaseBackup, type DatabaseBackupFile, type DatabaseBackupRun, type DatabaseBackupSchedule } from "@/lib/backup/scheduledDatabaseBackup";
import { useConnectionStore } from "@/stores/connectionStore";
@@ -213,7 +214,7 @@ async function runNow(schedule: DatabaseBackupSchedule) {
if (!run) return;
if (run.status === "success") toast(t("databaseBackup.runSuccess", { count: run.files.length }), 3000);
else if (run.status === "cancelled") toast(t("databaseBackup.runCancelled"), 3000);
else toast(t("databaseBackup.runFailed", { error: run.error || t("databaseBackup.unknownError") }), 5000);
else toast(t("databaseBackup.runFailed", { error: run.error ? translateBackendError(t, run.error) : t("databaseBackup.unknownError") }), 5000);
} catch (error: any) {
toast(error?.message || String(error), 5000);
}
@@ -261,7 +262,7 @@ async function revealBackup(file: DatabaseBackupFile) {
try {
await api.revealPathInFileManager(file.filePath);
} catch (error: any) {
toast(error?.message || String(error), 5000);
toast(translateBackendError(t, error), 5000);
}
}
@@ -345,7 +346,7 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {
<span>{{ run.connectionName || connectionName(run.connectionId) }}</span>
<span>{{ formatDate(run.startedAt) }}</span>
<span>{{ t("databaseBackup.fileCount", { count: run.files.length }) }}</span>
<span v-if="run.error" class="break-all text-destructive">{{ run.error }}</span>
<span v-if="run.error" class="break-all text-destructive">{{ translateBackendError(t, run.error) }}</span>
</div>
</div>
<div class="flex items-center justify-end gap-1">
@@ -32,6 +32,7 @@ import {
import { PRESTOSQL_DRIVER_DB_TYPE, prestoSqlBuiltinDriverRow, prestoSqlMavenBundle } from "@/lib/database/prestoSqlBuiltinDriver";
import type { DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint";
import { isOfflineDriverPackage, webDriverImportAccept } from "@/lib/driverStore/driverImportSelection";
import { translateBackendError } from "@/i18n/backend-errors";
import { DRIVER_CATEGORIES, getCategoryForAgentDriver, assertAgentDriverCategoriesComplete } from "@/lib/connection/driver-category-definitions";
import { selectUpdatableDrivers, selectStableDrivers, hasAnyUpdatableDriverMatching } from "@/lib/connection/driverListFilter";
@@ -39,6 +40,13 @@ const { t } = useI18n();
const { toast } = useToast();
const isWeb = !isTauriRuntime();
// Backend errors arrive as plain strings, so translate them before they are
// interpolated into an already-localized wrapper message.
function backendError(e: unknown): string {
const message = e instanceof Error ? e.message : ((e as { message?: string } | null)?.message ?? String(e));
return translateBackendError(t, message);
}
const props = withDefaults(
defineProps<{
updateNotificationsEnabled?: boolean;
@@ -166,7 +174,7 @@ async function applyDriverStoreDir(kind: DriverStoreDirKind, newDir: string | nu
const { relaunch } = await import("@tauri-apps/plugin-process");
relaunch();
} catch (e: any) {
toast(t("driverStore.driverStoreDirMigrationFailed", { error: e?.message || String(e) }), 5000);
toast(t("driverStore.driverStoreDirMigrationFailed", { error: backendError(e) }), 5000);
} finally {
driverStoreDirMigrating.value = null;
}
@@ -427,7 +435,7 @@ async function saveJavaRuntimeConfig() {
customJavaPath.value = config.custom_java_path ?? "";
toast(t("driverStore.javaRuntimeSaved"));
} catch (e: any) {
toast(t("driverStore.javaRuntimeSaveFailed", { error: e }));
toast(t("driverStore.javaRuntimeSaveFailed", { error: backendError(e) }));
} finally {
savingJavaRuntime.value = false;
}
@@ -480,7 +488,7 @@ async function runDriverInstall(dbType: string) {
await refreshAgents();
toast(t("driverStore.driverInstallSuccess", { label }));
} catch (e: any) {
toast(t("driverStore.driverInstallFailed", { label, error: e }));
toast(t("driverStore.driverInstallFailed", { label, error: backendError(e) }));
} finally {
installing.value = null;
activeAgentOperationId.value = null;
@@ -522,7 +530,7 @@ async function upgradeAll() {
toast(t("driverStore.upgradeAllSuccess", { count: result.upgraded }));
}
} catch (e: any) {
toast(t("driverStore.upgradeAllFailed", { error: e }));
toast(t("driverStore.upgradeAllFailed", { error: backendError(e) }));
} finally {
upgradingAll.value = false;
activeAgentOperationId.value = null;
@@ -548,7 +556,7 @@ async function uninstallDriver(dbType: string) {
await refreshAgents();
toast(t("driverStore.driverUninstallSuccess", { label }));
} catch (e: any) {
toast(t("driverStore.driverUninstallFailed", { label, error: e }));
toast(t("driverStore.driverUninstallFailed", { label, error: backendError(e) }));
}
}
@@ -615,7 +623,7 @@ async function importOfflineZip() {
await refreshAgents();
toast(t("driverStore.offlineImportSuccess", { count }));
} catch (e: any) {
toast(t("driverStore.offlineImportFailed", { error: e }));
toast(t("driverStore.offlineImportFailed", { error: backendError(e) }));
} finally {
importingZip.value = false;
activeAgentOperationId.value = null;
@@ -663,7 +671,7 @@ async function importDriverFile(driver: AgentDriverInfo) {
try {
await installSelectedFile(file);
} catch (e: any) {
toast(t("driverStore.driverImportFailed", { label, error: e }));
toast(t("driverStore.driverImportFailed", { label, error: backendError(e) }));
}
return;
}
@@ -677,7 +685,7 @@ async function importDriverFile(driver: AgentDriverInfo) {
try {
await installSelectedFile(selected);
} catch (e: any) {
toast(t("driverStore.driverImportFailed", { label, error: e }));
toast(t("driverStore.driverImportFailed", { label, error: backendError(e) }));
}
}
@@ -690,7 +698,7 @@ async function reinstallJre(jreKey: string) {
await refreshAgents();
toast(t("driverStore.jreReinstallSuccess", { jre: jreKey }));
} catch (e: any) {
toast(t("driverStore.jreReinstallFailed", { jre: jreKey, error: e }));
toast(t("driverStore.jreReinstallFailed", { jre: jreKey, error: backendError(e) }));
} finally {
reinstallingJre.value = null;
activeAgentOperationId.value = null;
@@ -1005,7 +1013,7 @@ async function stopRuntime(runtime: DriverRuntimeInfo) {
await loadDriverRuntimeSummary(false);
toast(t("driverStore.runtimeStopSuccess", { label: runtime.label }));
} catch (e: any) {
toast(t("driverStore.runtimeStopFailed", { label: runtime.label, error: e }));
toast(t("driverStore.runtimeStopFailed", { label: runtime.label, error: backendError(e) }));
} finally {
runtimeBusy.value = null;
}
@@ -1018,7 +1026,7 @@ async function restartRuntime(runtime: DriverRuntimeInfo) {
await loadDriverRuntimeSummary(false);
toast(t("driverStore.runtimeRestartSuccess", { label: runtime.label }));
} catch (e: any) {
toast(t("driverStore.runtimeRestartFailed", { label: runtime.label, error: e }));
toast(t("driverStore.runtimeRestartFailed", { label: runtime.label, error: backendError(e) }));
} finally {
runtimeBusy.value = null;
}
@@ -1060,7 +1068,7 @@ async function clearDownloadCache() {
await loadDriverStoreUsage();
toast(t("driverStore.downloadCacheClearSuccess"));
} catch (e: any) {
toast(t("driverStore.downloadCacheClearFailed", { error: e?.message || String(e) }), 5000);
toast(t("driverStore.downloadCacheClearFailed", { error: backendError(e) }), 5000);
} finally {
clearingDownloadCache.value = false;
}
@@ -1475,7 +1475,7 @@ function finishAgentDriverInstall() {
function failAgentDriverInstall(error: unknown) {
agentInstallOperationId.value = null;
agentInstallRunning.value = false;
agentInstallError.value = errorMessage(error);
agentInstallError.value = translateBackendError(t, errorMessage(error));
showAgentInstallDialog.value = true;
}
@@ -2780,11 +2780,11 @@ const testResultMessage = computed(() => {
const agentInstallPercent = computed(() => driverInstallProgressPercent(agentInstallProgress.value));
const agentInstallProgressLabel = computed(() => {
const progress = agentInstallProgress.value;
if (agentInstallError.value) return "安装失败";
if (!agentInstallRunning.value) return "等待安装";
if (!progress) return "准备安装驱动...";
if (progress.step === "jre-extract") return "解压 JRE...";
const label = progress.step === "jre" ? "下载 JRE" : progress.step === "driver" ? "下载驱动" : progress.step || "安装驱动";
if (agentInstallError.value) return t("connection.driverInstall.statusFailed");
if (!agentInstallRunning.value) return t("connection.driverInstall.statusWaiting");
if (!progress) return t("connection.driverInstall.statusPreparing");
if (progress.step === "jre-extract") return t("connection.driverInstall.statusExtractingJre");
const label = progress.step === "jre" ? t("connection.driverInstall.stepJre") : progress.step === "driver" ? t("connection.driverInstall.stepDriver") : progress.step || t("connection.driverInstall.stepDefault");
if (!progress.total) return `${label}...`;
return `${label} ${formatInstallSize(progress.downloaded ?? 0)} / ${formatInstallSize(progress.total)} (${agentInstallPercent.value ?? 0}%)`;
});
@@ -4874,7 +4874,7 @@ function openExternalUrl(url: string) {
<PopoverContent class="w-auto p-2">
<div class="flex items-center gap-2">
<input type="color" :value="form.color" @input="handleCustomColorPicked(($event.target as HTMLInputElement).value)" class="h-6 w-6 cursor-pointer rounded border-0 p-0" />
<Input type="text" :value="customColorInput || form.color" @input="handleCustomColorInput(($event.target as HTMLInputElement).value)" class="w-28 h-7 text-xs font-mono" :placeholder="'#ff0000 或 rgba(…)'" />
<Input type="text" :value="customColorInput || form.color" @input="handleCustomColorInput(($event.target as HTMLInputElement).value)" class="w-28 h-7 text-xs font-mono" :placeholder="t('connection.customColorPlaceholder')" />
</div>
</PopoverContent>
</Popover>
@@ -5104,11 +5104,11 @@ function openExternalUrl(url: string) {
</div>
<template v-if="form.db_type === 'h2' || form.db_type === 'access'">
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.user") }}{{ form.db_type === "access" ? "(可选)" : "" }}</Label>
<Label :class="connectionLabelClass">{{ t("connection.user") }}{{ form.db_type === "access" ? t("connection.optionalSuffix") : "" }}</Label>
<Input v-model="form.username" class="col-span-3" :placeholder="form.db_type === 'access' ? '' : 'sa'" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.password") }}{{ form.db_type === "access" ? "(可选)" : "" }}</Label>
<Label :class="connectionLabelClass">{{ t("connection.password") }}{{ form.db_type === "access" ? t("connection.optionalSuffix") : "" }}</Label>
<PasswordInput v-model="form.password" class="col-span-3" />
</div>
</template>
@@ -5817,12 +5817,12 @@ function openExternalUrl(url: string) {
<template v-else-if="form.db_type === 'turso'">
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.host") }}</Label>
<Input v-model="form.host" class="col-span-3" placeholder="your-database.turso.io 或 libsql://your-database.turso.io" />
<Input v-model="form.host" class="col-span-3" :placeholder="t('connection.tursoHostPlaceholder')" />
</div>
<div class="grid grid-cols-4 items-start gap-4">
<span />
<p class="col-span-3 text-xs text-muted-foreground">支持 libsql:// 或 https:// 协议,也可以只填主机名(自动使用 HTTPS)</p>
<p class="col-span-3 text-xs text-muted-foreground">{{ t("connection.tursoHostHint") }}</p>
</div>
<div class="grid grid-cols-4 items-center gap-4">
@@ -5832,12 +5832,12 @@ function openExternalUrl(url: string) {
<div class="grid grid-cols-4 items-start gap-4">
<span />
<p class="col-span-3 text-xs text-muted-foreground">使用 <code class="px-1 py-0.5 rounded bg-muted text-xs">turso db tokens create &lt;database-name&gt;</code> 创建 token</p>
<p class="col-span-3 text-xs text-muted-foreground">{{ t("connection.tursoTokenHint") }} <code class="px-1 py-0.5 rounded bg-muted text-xs">turso db tokens create &lt;database-name&gt;</code></p>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.urlParams") }}</Label>
<Input v-model="form.url_params" class="col-span-3" placeholder="authToken=xxx(可选,优先使用上面的 Token 字段)" />
<Input v-model="form.url_params" class="col-span-3" :placeholder="t('connection.tursoUrlParamsPlaceholder')" />
</div>
</template>
@@ -6893,7 +6893,7 @@ function openExternalUrl(url: string) {
<Dialog :open="showAgentInstallDialog" @update:open="setAgentInstallDialogOpen">
<DialogContent class="sm:max-w-[520px]" @interact-outside.prevent @escape-key-down.prevent>
<DialogHeader>
<DialogTitle>{{ agentInstallError ? "驱动安装失败" : "正在安装驱动" }}</DialogTitle>
<DialogTitle>{{ agentInstallError ? t("connection.driverInstall.failedTitle") : t("connection.driverInstall.installingTitle") }}</DialogTitle>
</DialogHeader>
<div class="space-y-4">
@@ -6911,7 +6911,7 @@ function openExternalUrl(url: string) {
</div>
<div v-if="agentInstallError" class="space-y-2">
<div class="text-sm font-medium text-destructive">完整错误</div>
<div class="text-sm font-medium text-destructive">{{ t("connection.driverInstall.fullError") }}</div>
<pre class="max-h-56 overflow-auto whitespace-pre-wrap break-words rounded-md border bg-muted/30 p-3 text-xs leading-5 text-destructive">{{ agentInstallError }}</pre>
</div>
</div>
@@ -6919,10 +6919,10 @@ function openExternalUrl(url: string) {
<DialogFooter class="gap-2">
<Button v-if="agentInstallError" variant="outline" @click="copyAgentInstallError">
<Copy class="mr-1.5 h-3.5 w-3.5" />
复制错误
{{ t("connection.copyError") }}
</Button>
<Button :disabled="!canCloseAgentInstallDialog" @click="showAgentInstallDialog = false">
{{ agentInstallError ? "关闭" : "安装中..." }}
{{ agentInstallError ? t("common.close") : t("connection.driverInstall.installingButton") }}
</Button>
</DialogFooter>
</DialogContent>
@@ -6931,11 +6931,11 @@ function openExternalUrl(url: string) {
<Dialog v-model:open="showConnectionErrorDialog">
<DialogContent class="sm:max-w-[560px]">
<DialogHeader>
<DialogTitle>连接失败</DialogTitle>
<DialogTitle>{{ t("connection.connectFailedTitle") }}</DialogTitle>
</DialogHeader>
<div class="space-y-2">
<div class="text-sm text-muted-foreground">完整错误信息</div>
<div class="text-sm text-muted-foreground">{{ t("connection.fullErrorMessage") }}</div>
<pre class="max-h-72 overflow-auto whitespace-pre-wrap break-words rounded-md border bg-muted/30 p-3 text-xs leading-5 text-destructive">{{ connectionErrorDetail }}</pre>
</div>
@@ -6946,9 +6946,9 @@ function openExternalUrl(url: string) {
</Button>
<Button variant="outline" @click="copyConnectionErrorDetail">
<Copy class="mr-1.5 h-3.5 w-3.5" />
复制错误
{{ t("connection.copyError") }}
</Button>
<Button @click="showConnectionErrorDialog = false">关闭</Button>
<Button @click="showConnectionErrorDialog = false">{{ t("common.close") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -2304,7 +2304,7 @@ const aiEndpointHint = computed(() => {
return t("ai.anthropicMessagesHint");
}
if (aiEditProvider.value === "openai-compatible" || aiEditProvider.value === "custom") {
return "大多数 OpenAI 兼容 API 需要 /v1 路径前缀";
return t("ai.openAiCompatibleEndpointHint");
}
return "";
});
@@ -4525,7 +4525,7 @@ onUnmounted(cleanupPreviewEditor);
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="github">GitHub Gist</SelectItem>
<SelectItem value="gitee">Gitee 代码片段</SelectItem>
<SelectItem value="gitee">{{ t("settings.syncSnippetProviderGitee") }}</SelectItem>
</SelectContent>
</Select>
</div>
@@ -7,6 +7,7 @@ import { Loader2, CheckCircle2, XCircle, AlertCircle, FolderOpen, X } from "@luc
import { useToast } from "@/composables/useToast";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import * as api from "@/lib/backend/api";
import { translateBackendError } from "@/i18n/backend-errors";
const { t } = useI18n();
const { toast } = useToast();
@@ -30,6 +31,7 @@ const emit = defineEmits<{
"update:open": [value: boolean];
}>();
const translatedErrorMessage = computed(() => (props.errorMessage ? translateBackendError(t, props.errorMessage) : ""));
const isActive = computed(() => props.status === "Running" || props.status === "Writing");
const isFinished = computed(() => props.status === "Done" || props.status === "Error" || props.status === "Cancelled");
const canRevealFile = computed(() => props.status === "Done" && !!props.filePath && isTauriRuntime());
@@ -53,8 +55,7 @@ async function revealExportFile() {
try {
await api.revealPathInFileManager(props.filePath);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
toast(t("exportProgress.openFolderFailed", { message }), 5000);
toast(t("exportProgress.openFolderFailed", { message: translateBackendError(t, error) }), 5000);
} finally {
isRevealing.value = false;
}
@@ -106,7 +107,7 @@ async function revealExportFile() {
</template>
<template v-else-if="status === 'Error'">
<XCircle class="h-4 w-4 text-destructive" />
<span class="text-destructive">{{ errorMessage || t("exportProgress.error") }}</span>
<span class="text-destructive">{{ translatedErrorMessage || t("exportProgress.error") }}</span>
</template>
<template v-else-if="status === 'Cancelled'">
<AlertCircle class="h-4 w-4 text-yellow-500" />
@@ -5,6 +5,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { Button } from "@/components/ui/button";
import { Loader2, CheckCircle2, XCircle, AlertCircle, X, FileDown, DatabaseBackup, FileCode2, ArrowRightLeft, ChevronRight } from "@lucide/vue";
import { formatDataTransferDuration, useExportTracker, type ExportTask } from "@/composables/useExportTracker";
import { translateBackendError } from "@/i18n/backend-errors";
const { t } = useI18n();
const { tasks, activeCount, hasActive, clearFinished, cancelTask, removeTask } = useExportTracker();
@@ -185,8 +186,8 @@ function failureDetailCount(task: ExportTask) {
<div class="min-w-0 text-muted-foreground">
<span class="break-words tabular-nums">{{ rowsText(task) }}</span>
<span v-if="task.status === 'Error' && task.errorMessage" class="mt-1 block whitespace-normal break-words text-destructive" :title="task.errorMessage">
{{ task.errorMessage }}
<span v-if="task.status === 'Error' && task.errorMessage" class="mt-1 block whitespace-normal break-words text-destructive" :title="translateBackendError(t, task.errorMessage)">
{{ translateBackendError(t, task.errorMessage) }}
</span>
<template v-if="task.kind === 'data-transfer' && failureDetailCount(task) > 0">
<button class="mt-1.5 flex items-center gap-1 text-xs font-medium text-foreground hover:text-primary" :aria-expanded="failureDetailsExpanded(task.exportId)" @click="toggleFailureDetails(task.exportId)">
@@ -925,12 +925,12 @@ async function onFileSelected(event: Event) {
<template v-else>
<div class="flex items-center justify-between px-3 py-2 border-b bg-muted/10">
<div class="flex items-center gap-2">
<span class="text-xs text-muted-foreground">{{ t("dataGenerate.target") }}:</span>
<span class="text-xs text-muted-foreground">{{ t("dataGenerate.target") }}:</span>
<select v-if="generatedResults.length > 1" v-model="previewTableIndex" class="h-7 rounded border bg-background px-2 text-xs">
<option v-for="(r, i) in generatedResults" :key="i" :value="i">{{ r.tableName }}</option>
</select>
<span v-else class="text-sm font-medium">{{ generatedResults[0].tableName }}</span>
<span class="text-xs text-muted-foreground">{{ currentPreview.rows.length }} 行</span>
<span class="text-xs text-muted-foreground">{{ t("dataGenerate.previewRowCount", { count: currentPreview.rows.length }) }}</span>
</div>
<Button variant="outline" size="sm" class="h-7 text-xs" @click="regenerate">{{ t("dataGenerate.regenerate") }}</Button>
</div>
@@ -1041,7 +1041,7 @@ async function onFileSelected(event: Event) {
<Dialog v-model:open="optionsDialogOpen">
<DialogContent class="max-w-sm">
<DialogHeader>
<DialogTitle class="text-sm">生成选项</DialogTitle>
<DialogTitle class="text-sm">{{ t("dataGenerate.generateOptions") }}</DialogTitle>
</DialogHeader>
<div class="space-y-3 py-2">
<label class="flex items-center gap-3 cursor-pointer">
@@ -4,21 +4,24 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const addressTypes = [
{ value: "line1", label: "第1行地址" },
{ value: "line2", label: "第2行地址" },
{ value: "full", label: "完整地址" },
];
const { t } = useI18n();
const regions = [
{ value: "us", label: "美国" },
{ value: "uk", label: "英国" },
{ value: "cn", label: "中国 (English)" },
{ value: "jp", label: "日本 (English)" },
];
const addressTypes = computed(() => [
{ value: "line1", label: t("dataGenerate.addressTypes.line1") },
{ value: "line2", label: t("dataGenerate.addressTypes.line2") },
{ value: "full", label: t("dataGenerate.addressTypes.full") },
]);
const regions = computed(() => [
{ value: "us", label: t("dataGenerate.countries.us") },
{ value: "uk", label: t("dataGenerate.countries.uk") },
{ value: "cn", label: t("dataGenerate.countries.cnEnglish") },
{ value: "jp", label: t("dataGenerate.countries.jpEnglish") },
]);
if (!props.params.addressType) props.params.addressType = "line1";
if (!props.params.regions) props.params.regions = ["us"];
@@ -90,14 +93,14 @@ function refresh() {
<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">类型</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.typeLabel") }}</div>
<div class="flex items-center gap-2 text-xs">
<button v-for="t in addressTypes" :key="t.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.addressType || 'line1') === t.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.addressType = t.value">{{ t.label }}</button>
<button v-for="a in addressTypes" :key="a.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.addressType || 'line1') === a.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.addressType = a.value">{{ a.label }}</button>
</div>
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-2">地区</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.regionLabel") }}</div>
<div class="flex flex-wrap gap-1.5">
<button v-for="r in regions" :key="r.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.regions ?? []).includes(r.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleRegion(r.value)">{{ r.label }}</button>
</div>
@@ -105,7 +108,7 @@ function refresh() {
<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">预览</span>
<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" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const barcodeTypes = [
{ value: "ean8", label: "EAN8", regex: "[0-9]{8}" },
{ value: "ean13", label: "EAN13", regex: "[0-9]{13}" },
@@ -78,18 +81,18 @@ function refresh() {
<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">类型</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.typeLabel") }}</div>
<div class="flex flex-wrap gap-1.5">
<button v-for="b in barcodeTypes" :key="b.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.barcodeTypes ?? []).includes(b.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleBarcode(b.value)">{{ b.label }}</button>
</div>
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-1">正则表达式</div>
<div class="text-xs text-muted-foreground mb-1">{{ t("dataGenerate.regex") }}</div>
<div class="font-mono text-xs bg-background rounded border px-2 py-1 break-all">{{ currentRegex }}</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">预览</span>
<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" />
@@ -4,20 +4,23 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const regions = [
{ value: "us", label: "美国" },
{ value: "uk", label: "英国" },
{ value: "cn", label: "中国" },
{ value: "jp", label: "日本" },
];
const { t } = useI18n();
const languages = [
{ value: "en", label: "English" },
{ value: "native", label: "本地语言" },
];
const regions = computed(() => [
{ value: "us", label: t("dataGenerate.countries.us") },
{ value: "uk", label: t("dataGenerate.countries.uk") },
{ value: "cn", label: t("dataGenerate.countries.cn") },
{ value: "jp", label: t("dataGenerate.countries.jp") },
]);
const languages = computed(() => [
{ value: "en", label: t("dataGenerate.cityLanguages.english") },
{ value: "native", label: t("dataGenerate.cityLanguages.native") },
]);
if (!props.params.regions) props.params.regions = ["us"];
if (!props.params.languages) props.params.languages = ["en"];
@@ -96,14 +99,14 @@ function refresh() {
<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">地区</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.regionLabel") }}</div>
<div class="flex flex-wrap gap-1.5">
<button v-for="r in regions" :key="r.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.regions ?? []).includes(r.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleRegion(r.value)">{{ r.label }}</button>
</div>
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-2">语言</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
<div class="flex items-center gap-2 text-xs">
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages?.[0] || 'en') === l.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="setLang(l.value)">{{ l.label }}</button>
</div>
@@ -111,7 +114,7 @@ function refresh() {
<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">预览</span>
<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" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const dataMap: Record<string, string[]> = {
en: ["Red", "Blue", "Green", "Black", "White", "Yellow", "Purple", "Orange", "Pink", "Brown", "Gray", "Cyan", "Jasmine", "Teal", "Maroon", "Navy", "Olive", "Coral"],
zh: ["红色", "蓝色", "绿色", "黑色", "白色", "黄色", "紫色", "橙色", "粉色", "棕色", "灰色", "青色", "茉莉色", "蓝绿色", "褐红色", "藏青", "橄榄", "珊瑚色"],
@@ -33,7 +36,7 @@ function refresh() {
<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">语言</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
<div class="flex flex-wrap gap-1.5">
<button
v-for="l in [
@@ -54,7 +57,7 @@ function refresh() {
</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">预览</span>
<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" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const languageOptions = [
{ value: "en", label: "English" },
{ value: "zh_pinyin", label: "Chinese (Pinyin)" },
@@ -75,7 +78,7 @@ function refresh() {
<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">语言选择</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
<div class="flex flex-wrap gap-1.5">
<button v-for="l in languageOptions" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages ?? []).includes(l.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleLang(l.value)">{{ l.label }}</button>
</div>
@@ -83,7 +86,7 @@ function refresh() {
<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">预览</span>
<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" />
@@ -6,9 +6,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const dateFormats = [
{ value: "MM/YY", label: "MM/YY" },
{ value: "MM/YYYY", label: "MM/YYYY" },
@@ -59,25 +62,25 @@ function refresh() {
<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">日期类型</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.dateType") }}</div>
<div class="flex items-center gap-2 text-xs">
<button v-for="f in dateFormats" :key="f.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.ccDateFormat || 'MM/YY') === f.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.ccDateFormat = f.value">{{ f.label }}</button>
</div>
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-2">年份范围(相对于当前年的偏移量)</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.yearOffsetRange") }}</div>
<div class="flex items-center gap-2 text-xs">
<Label class="text-muted-foreground">最小</Label>
<Label class="text-muted-foreground">{{ t("dataGenerate.min") }}</Label>
<Input v-model.number="params.ccYearOffsetMin" type="number" class="h-6 w-20 text-xs" />
<Label class="text-muted-foreground">最大</Label>
<Label class="text-muted-foreground">{{ t("dataGenerate.max") }}</Label>
<Input v-model.number="params.ccYearOffsetMax" type="number" class="h-6 w-20 text-xs" />
</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">预览</span>
<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" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const cardTypes = [
{ value: "american_express", label: "American Express" },
{ value: "jcb", label: "JCB" },
@@ -95,7 +98,7 @@ function refresh() {
<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">类型选择</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.typeLabel") }}</div>
<div class="flex flex-wrap gap-1.5">
<button v-for="c in cardTypes" :key="c.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.cardTypes ?? []).includes(c.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleCardType(c.value)">{{ c.label }}</button>
</div>
@@ -103,7 +106,7 @@ function refresh() {
<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">预览</span>
<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" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const cardTypes = [
{ value: "american_express", label: "American Express" },
{ value: "jcb", label: "JCB" },
@@ -47,7 +50,7 @@ function refresh() {
<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">类型选择</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.typeLabel") }}</div>
<div class="flex flex-wrap gap-1.5">
<button v-for="c in cardTypes" :key="c.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.cardTypes ?? []).includes(c.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleCardType(c.value)">{{ c.label }}</button>
</div>
@@ -55,7 +58,7 @@ function refresh() {
<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">预览</span>
<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" />
@@ -26,7 +26,7 @@ const previewVal = computed(() => {
return `${year}-${month}-${day}`;
});
const weekdayLabels = ["日", "一", "二", "三", "四", "五", "六"];
const weekdayLabels = computed(() => (["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const).map((d) => t(`dataGenerate.weekdayShort.${d}`)));
function toggleWeekday(d: number) {
if (!props.params.weekdays) props.params.weekdays = [];
@@ -77,11 +77,11 @@ function refresh() {
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-2">星期</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.weekday") }}</div>
<div class="flex items-center gap-2 text-xs">
<button type="button" class="px-2 py-1 rounded border text-xs" :class="!params.weekdayMode || params.weekdayMode === 'all' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.weekdayMode = 'all'">全部</button>
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.weekdayMode === 'weekday' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.weekdayMode = 'weekday'">工作日</button>
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.weekdayMode === 'custom' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.weekdayMode = 'custom'">自定义</button>
<button type="button" class="px-2 py-1 rounded border text-xs" :class="!params.weekdayMode || params.weekdayMode === 'all' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.weekdayMode = 'all'">{{ t("dataGenerate.all") }}</button>
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.weekdayMode === 'weekday' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.weekdayMode = 'weekday'">{{ t("dataGenerate.weekdayOnly") }}</button>
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.weekdayMode === 'custom' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.weekdayMode = 'custom'">{{ t("dataGenerate.custom") }}</button>
</div>
<div v-if="params.weekdayMode === 'custom'" class="flex gap-1 mt-2">
<button v-for="(_, i) in 7" :key="i" type="button" class="w-7 h-7 rounded text-xs border" :class="(params.weekdays ?? []).includes(i) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleWeekday(i)">{{ weekdayLabels[i] }}</button>
@@ -38,7 +38,7 @@ const previewVal = computed(() => {
return `${formatLocalDate(d)} ${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
});
const weekdayLabels = ["日", "一", "二", "三", "四", "五", "六"];
const weekdayLabels = computed(() => (["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const).map((d) => t(`dataGenerate.weekdayShort.${d}`)));
function toggleWeekday(d: number) {
if (!props.params.weekdays) props.params.weekdays = [];
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const languages = [
{ value: "en", label: "English" },
{ value: "zh", label: "简体中文" },
@@ -50,7 +53,7 @@ function refresh() {
<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">语言选择</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
<div class="flex flex-wrap gap-1.5">
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages ?? []).includes(l.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleLang(l.value)">{{ l.label }}</button>
</div>
@@ -58,7 +61,7 @@ function refresh() {
<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">预览</span>
<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" />
@@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const defaultDomains = "gmail.com\noutlook.com\nyahoo.com\nexample.com\ntest.org";
function pick<T>(arr: T[]): T {
@@ -27,7 +30,7 @@ const previewVal = computed(() => {
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
if (domains.length === 0) return "(无域名)";
if (domains.length === 0) return t("dataGenerate.noDomain");
return `${genName()}@${pick(domains)}`;
});
@@ -39,13 +42,13 @@ function refresh() {
<template>
<div class="space-y-3">
<div class="rounded-md border bg-muted/10 p-3">
<Label class="text-xs text-muted-foreground mb-1 block">域(Domain)列表</Label>
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.domainList") }}</Label>
<textarea v-model="params.emailDomains" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="defaultDomains" />
</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">预览</span>
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto shrink-0" @click="refresh">
<RefreshCw class="h-3 w-3" />
@@ -30,7 +30,7 @@ function refresh() {
<div class="space-y-3">
<div class="rounded-md border bg-muted/10 p-3">
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.values") }}</Label>
<textarea v-model="params.values" rows="6" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个值&#10;first&#10;second&#10;third" />
<textarea v-model="params.values" rows="6" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.enumValues')" />
</div>
<div class="rounded-md border bg-muted/10 p-3">
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const extensionCategoryMap: Record<string, string[]> = {
image: [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".tiff"],
document: [".txt", ".pdf", ".doc", ".docx", ".rtf", ".odt"],
@@ -20,18 +23,7 @@ const extensionCategoryMap: Record<string, string[]> = {
database: [".db", ".sqlite", ".sql", ".mdb", ".accdb"],
};
const categoryLabels: Record<string, string> = {
image: "图像",
document: "文档",
spreadsheet: "表格",
presentation: "演示文稿",
audio: "音频",
video: "视频",
code: "代码",
archive: "压缩包",
web: "网页",
database: "数据库",
};
const categoryLabels = computed<Record<string, string>>(() => Object.fromEntries((["image", "document", "spreadsheet", "presentation", "audio", "video", "code", "archive", "web", "database"] as const).map((key) => [key, t(`dataGenerate.extensionCategories.${key}`)])));
if (!props.params.extensionCategory) {
props.params.extensionCategory = "image";
@@ -56,18 +48,18 @@ function refresh() {
<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">扩展名类型</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.extensionType") }}</div>
<select v-model="params.extensionCategory" class="w-full rounded border bg-background px-2 py-1 text-xs">
<option v-for="(label, key) in categoryLabels" :key="key" :value="key">{{ label }}</option>
</select>
<div class="mt-2 text-xs text-muted-foreground">扩展名列表:</div>
<div class="mt-2 text-xs text-muted-foreground">{{ t("dataGenerate.extensionList") }}</div>
<div class="mt-1 flex flex-wrap gap-1">
<span v-for="ext in extensionCategoryMap[params.extensionCategory || 'image'] || []" :key="ext" class="px-1.5 py-0.5 rounded bg-background border text-xs font-mono">{{ ext }}</span>
</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">预览</span>
<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" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const extensionCategoryMap: Record<string, string[]> = {
image: [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".tiff"],
document: [".txt", ".pdf", ".doc", ".docx", ".rtf", ".odt"],
@@ -20,18 +23,7 @@ const extensionCategoryMap: Record<string, string[]> = {
database: [".db", ".sqlite", ".sql", ".mdb", ".accdb"],
};
const categoryLabels: Record<string, string> = {
image: "图像",
document: "文档",
spreadsheet: "表格",
presentation: "演示文稿",
audio: "音频",
video: "视频",
code: "代码",
archive: "压缩包",
web: "网页",
database: "数据库",
};
const categoryLabels = computed<Record<string, string>>(() => Object.fromEntries((["image", "document", "spreadsheet", "presentation", "audio", "video", "code", "archive", "web", "database"] as const).map((key) => [key, t(`dataGenerate.extensionCategories.${key}`)])));
if (!props.params.includeExtension) {
props.params.includeExtension = true;
@@ -66,22 +58,22 @@ function refresh() {
<div class="rounded-md border bg-muted/10 p-3">
<label class="text-xs text-muted-foreground flex items-center gap-2">
<input type="checkbox" v-model="params.includeExtension" class="rounded" />
包含扩展名
{{ t("dataGenerate.includeExtension") }}
</label>
</div>
<div class="rounded-md border bg-muted/10 p-3" v-if="params.includeExtension">
<div class="text-xs text-muted-foreground mb-2">扩展名类型</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.extensionType") }}</div>
<select v-model="params.extensionCategory" class="w-full rounded border bg-background px-2 py-1 text-xs">
<option v-for="(label, key) in categoryLabels" :key="key" :value="key">{{ label }}</option>
</select>
<div class="mt-2 text-xs text-muted-foreground">可用扩展名:</div>
<div class="mt-2 text-xs text-muted-foreground">{{ t("dataGenerate.availableExtensions") }}</div>
<div class="mt-1 flex flex-wrap gap-1">
<span v-for="ext in extensionCategoryMap[params.extensionCategory || 'image'] || []" :key="ext" class="px-1.5 py-0.5 rounded bg-background border text-xs font-mono">{{ ext }}</span>
</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">预览</span>
<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" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const extensionCategoryMap: Record<string, string[]> = {
image: [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".tiff"],
document: [".txt", ".pdf", ".doc", ".docx", ".rtf", ".odt"],
@@ -20,18 +23,7 @@ const extensionCategoryMap: Record<string, string[]> = {
database: [".db", ".sqlite", ".sql", ".mdb", ".accdb"],
};
const categoryLabels: Record<string, string> = {
image: "图像",
document: "文档",
spreadsheet: "表格",
presentation: "演示文稿",
audio: "音频",
video: "视频",
code: "代码",
archive: "压缩包",
web: "网页",
database: "数据库",
};
const categoryLabels = computed<Record<string, string>>(() => Object.fromEntries((["image", "document", "spreadsheet", "presentation", "audio", "video", "code", "archive", "web", "database"] as const).map((key) => [key, t(`dataGenerate.extensionCategories.${key}`)])));
if (!props.params.pathTypes) {
props.params.pathTypes = ["windows", "macos", "linux"];
@@ -104,7 +96,7 @@ function refresh() {
<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">路径类型</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.pathType") }}</div>
<div class="flex flex-wrap gap-1.5">
<button
v-for="pt in [
@@ -125,22 +117,22 @@ function refresh() {
<div class="rounded-md border bg-muted/10 p-3">
<label class="text-xs text-muted-foreground flex items-center gap-2">
<input type="checkbox" v-model="params.includeFileName" class="rounded" />
包含文件名称
{{ t("dataGenerate.includeFileName") }}
</label>
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-2">扩展名类型</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.extensionType") }}</div>
<select v-model="params.extensionCategory" class="w-full rounded border bg-background px-2 py-1 text-xs">
<option v-for="(label, key) in categoryLabels" :key="key" :value="key">{{ label }}</option>
</select>
<div class="mt-2 text-xs text-muted-foreground">可用扩展名:</div>
<div class="mt-2 text-xs text-muted-foreground">{{ t("dataGenerate.availableExtensions") }}</div>
<div class="mt-1 flex flex-wrap gap-1">
<span v-for="ext in extensionCategoryMap[params.extensionCategory || 'image'] || []" :key="ext" class="px-1.5 py-0.5 rounded bg-background border text-xs font-mono">{{ ext }}</span>
</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">预览</span>
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
<RefreshCw class="h-3 w-3" />
@@ -1,11 +1,14 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ref, onMounted, computed } from "vue";
import { Label } from "@/components/ui/label";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import * as api from "@/lib/backend/api";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams; config: any; connectionId?: string; database?: string }>();
const { t } = useI18n();
const schemas = ref<string[]>([]);
const tables = ref<{ name: string }[]>([]);
const columns = ref<{ name: string }[]>([]);
@@ -13,11 +16,11 @@ const loadingSchemas = ref(false);
const loadingTables = ref(false);
const loadingColumns = ref(false);
const fkModes = [
{ value: "random", label: "随机" },
{ value: "unique", label: "不重复" },
{ value: "repeat", label: "重复每个值" },
];
const fkModes = computed(() => [
{ value: "random", label: t("dataGenerate.genModes.random") },
{ value: "unique", label: t("dataGenerate.genModes.unique") },
{ value: "repeat", label: t("dataGenerate.genModes.repeat") },
]);
onMounted(async () => {
await loadSchemas();
@@ -84,30 +87,30 @@ function onTableChange(val: string) {
<div class="space-y-3">
<div class="rounded-md border bg-muted/10 p-3 space-y-2">
<div class="grid grid-cols-[60px_1fr] items-center gap-2 text-xs">
<Label class="text-muted-foreground">模式</Label>
<Label class="text-muted-foreground">{{ t("dataGenerate.schema") }}</Label>
<select v-model="props.params.fkSchema" class="h-7 rounded border bg-background px-2 text-xs" @change="onSchemaChange(($event.target as HTMLSelectElement).value)">
<option v-if="loadingSchemas" disabled>加载中...</option>
<option v-if="loadingSchemas" disabled>{{ t("dataGenerate.loading") }}</option>
<option v-for="s in schemas" :key="s" :value="s">{{ s }}</option>
</select>
</div>
<div class="grid grid-cols-[60px_1fr] items-center gap-2 text-xs">
<Label class="text-muted-foreground">表</Label>
<Label class="text-muted-foreground">{{ t("dataGenerate.tableLabel") }}</Label>
<select v-model="props.params.fkTable" class="h-7 rounded border bg-background px-2 text-xs" @change="onTableChange(($event.target as HTMLSelectElement).value)">
<option v-if="loadingTables" disabled>加载中...</option>
<option v-for="t in tables" :key="t.name" :value="t.name">{{ t.name }}</option>
<option v-if="loadingTables" disabled>{{ t("dataGenerate.loading") }}</option>
<option v-for="tbl in tables" :key="tbl.name" :value="tbl.name">{{ tbl.name }}</option>
</select>
</div>
<div class="grid grid-cols-[60px_1fr] items-center gap-2 text-xs">
<Label class="text-muted-foreground">字段</Label>
<Label class="text-muted-foreground">{{ t("dataGenerate.columnLabel") }}</Label>
<select v-model="props.params.fkField" class="h-7 rounded border bg-background px-2 text-xs">
<option v-if="loadingColumns" disabled>加载中...</option>
<option v-if="loadingColumns" disabled>{{ t("dataGenerate.loading") }}</option>
<option v-for="c in columns" :key="c.name" :value="c.name">{{ c.name }}</option>
</select>
</div>
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-2">生成模式</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.genMode") }}</div>
<div class="flex items-center gap-2 text-xs flex-wrap">
<button v-for="m in fkModes" :key="m.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.fkMode || 'random') === m.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.fkMode = m.value as 'random' | 'unique' | 'repeat'">
{{ m.label }}
@@ -4,14 +4,17 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const nameFormats: Array<{ value: "full" | "last" | "first"; label: string }> = [
{ value: "full", label: "全名" },
{ value: "last", label: "仅姓氏" },
{ value: "first", label: "仅名字" },
];
const { t } = useI18n();
const nameFormats = computed<Array<{ value: "full" | "last" | "first"; label: string }>>(() => [
{ value: "full", label: t("dataGenerate.nameFormats.full") },
{ value: "last", label: t("dataGenerate.nameFormats.last") },
{ value: "first", label: t("dataGenerate.nameFormats.first") },
]);
const languageOptions = [
{ value: "en", label: "English" },
@@ -78,14 +81,14 @@ function refresh() {
<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">格式类型</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.nameFormat") }}</div>
<div class="flex items-center gap-2 text-xs">
<button v-for="f in nameFormats" :key="f.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.nameFormat || 'full') === f.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.nameFormat = f.value">{{ f.label }}</button>
</div>
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-2">语言选择</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
<div class="flex flex-wrap gap-1.5">
<button v-for="l in languageOptions" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages ?? []).includes(l.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleLang(l.value)">{{ l.label }}</button>
</div>
@@ -93,7 +96,7 @@ function refresh() {
<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">预览</span>
<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" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const languages = [
{ value: "en", label: "English" },
{ value: "zh", label: "简体中文" },
@@ -43,7 +46,7 @@ function refresh() {
<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">语言选择</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
<div class="flex items-center gap-2 text-xs">
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages?.[0] || 'en') === l.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="setLang(l.value)">{{ l.label }}</button>
</div>
@@ -51,7 +54,7 @@ function refresh() {
<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">预览</span>
<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" />
@@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
if (!props.params.urlSubdomains) {
props.params.urlSubdomains = "auth.\ndrive.\nimage.\nwww.\napi.\nmail.\nshop.\nblog.";
}
@@ -47,16 +50,16 @@ function refresh() {
<template>
<div class="space-y-3">
<div class="rounded-md border bg-muted/10 p-3">
<Label class="text-xs text-muted-foreground mb-1 block">子域</Label>
<textarea v-model="params.urlSubdomains" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个子域&#10;auth.&#10;www.&#10;api." />
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.subdomains") }}</Label>
<textarea v-model="params.urlSubdomains" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.subdomains')" />
</div>
<div class="rounded-md border bg-muted/10 p-3">
<Label class="text-xs text-muted-foreground mb-1 block">顶级域</Label>
<textarea v-model="params.urlTlds" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个TLD&#10;.com&#10;.cn&#10;.io" />
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.tlds") }}</Label>
<textarea v-model="params.urlTlds" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.tlds')" />
</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">预览</span>
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
<RefreshCw class="h-3 w-3" />
@@ -49,7 +49,7 @@ const isGenerate = computed(() => props.params.imageMode !== "folder");
<Input v-model="params.folderPath" type="text" placeholder="/path/to/images" class="h-7 text-xs" />
</div>
<div class="text-xs text-muted-foreground mt-2 mb-1">{{ t("dataGenerate.filterByExtension") }}</div>
<textarea v-model="params.fileExtensions" rows="3" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个扩展名&#10;png&#10;jpg&#10;txt" />
<textarea v-model="params.fileExtensions" rows="3" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.fileExtensions')" />
</div>
</template>
</div>
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const languages = [
{ value: "en", label: "English" },
{ value: "zh", label: "简体中文" },
@@ -50,7 +53,7 @@ function refresh() {
<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">语言选择</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
<div class="flex flex-wrap gap-1.5">
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages ?? []).includes(l.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleLang(l.value)">{{ l.label }}</button>
</div>
@@ -58,7 +61,7 @@ function refresh() {
<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">预览</span>
<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" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const ipTypes = [
{ value: "ipv4", label: "IPv4", regex: "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" },
{ value: "ipv6", label: "IPv6", regex: "^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$" },
@@ -47,18 +50,18 @@ function refresh() {
<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">IP地址类型</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.ipType") }}</div>
<div class="flex items-center gap-2 text-xs">
<button v-for="t in ipTypes" :key="t.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.ipType || 'ipv4') === t.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.ipType = t.value">{{ t.label }}</button>
<button v-for="x in ipTypes" :key="x.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.ipType || 'ipv4') === x.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.ipType = x.value">{{ x.label }}</button>
</div>
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-1">正则表达式</div>
<div class="text-xs text-muted-foreground mb-1">{{ t("dataGenerate.regex") }}</div>
<div class="font-mono text-xs bg-background rounded border px-2 py-1 break-all">{{ currentRegex }}</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">预览</span>
<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" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const languages = [
{ value: "en", label: "English" },
{ value: "zh", label: "简体中文" },
@@ -43,7 +46,7 @@ function refresh() {
<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">语言选择</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
<div class="flex items-center gap-2 text-xs">
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages?.[0] || 'en') === l.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="setLang(l.value)">{{ l.label }}</button>
</div>
@@ -51,7 +54,7 @@ function refresh() {
<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">预览</span>
<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" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
if (!props.params.pattern) {
props.params.pattern = "([0-9a-f]{2}[:]){5}([0-9a-f]{2})";
}
@@ -33,12 +36,12 @@ function refresh() {
<template>
<div class="space-y-3">
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-1">正则表达式</div>
<div class="text-xs text-muted-foreground mb-1">{{ t("dataGenerate.regex") }}</div>
<input v-model="params.pattern" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono" />
</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">预览</span>
<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" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const languages = [
{ value: "en", label: "English" },
{ value: "zh", label: "简体中文" },
@@ -43,7 +46,7 @@ function refresh() {
<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">语言选择</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
<div class="flex items-center gap-2 text-xs">
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages?.[0] || 'en') === l.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="setLang(l.value)">{{ l.label }}</button>
</div>
@@ -51,7 +54,7 @@ function refresh() {
<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">预览</span>
<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" />
@@ -52,14 +52,14 @@ function refresh() {
<div class="rounded-md border bg-muted/10 p-3">
<div class="flex items-center gap-3 text-xs">
<Label class="text-muted-foreground shrink-0">数字类型</Label>
<Label class="text-muted-foreground shrink-0">{{ t("dataGenerate.numberType") }}</Label>
<div class="flex gap-2">
<button type="button" class="px-2 py-1 rounded text-xs border" :class="params.numberType !== 'decimal' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="handleTypeChange('integer')">整数</button>
<button type="button" class="px-2 py-1 rounded text-xs border" :class="params.numberType === 'decimal' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="handleTypeChange('decimal')">小数</button>
<button type="button" class="px-2 py-1 rounded text-xs border" :class="params.numberType !== 'decimal' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="handleTypeChange('integer')">{{ t("dataGenerate.integer") }}</button>
<button type="button" class="px-2 py-1 rounded text-xs border" :class="params.numberType === 'decimal' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="handleTypeChange('decimal')">{{ t("dataGenerate.decimal") }}</button>
</div>
</div>
<div v-if="isDecimal" class="grid grid-cols-[80px_1fr] items-center gap-2 text-xs mt-2">
<Label class="text-muted-foreground">小数位数</Label>
<Label class="text-muted-foreground">{{ t("dataGenerate.decimalPlaces") }}</Label>
<Input v-model.number="params.decimalPlaces" type="number" min="0" max="10" class="h-7 w-20 text-xs" />
</div>
</div>
@@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
if (!props.params.values) {
props.params.values = "Credit Card\nPayPal\nApple Pay\nGoogle Pay\nBank Transfer\nCash\nCryptocurrency\nAlipay\nWeChat Pay";
}
@@ -18,7 +21,7 @@ const previewVal = computed(() => {
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
if (vals.length === 0) return "(无)";
if (vals.length === 0) return t("dataGenerate.noValue");
return vals[Math.floor(Math.random() * vals.length)];
});
@@ -31,13 +34,13 @@ function refresh() {
<template>
<div class="space-y-3">
<div class="rounded-md border bg-muted/10 p-3">
<Label class="text-xs text-muted-foreground mb-1 block">值列表</Label>
<textarea v-model="params.values" rows="6" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个支付方式&#10;Credit Card&#10;PayPal&#10;Apple Pay" />
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.valueList") }}</Label>
<textarea v-model="params.values" rows="6" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.paymentMethods')" />
</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">预览</span>
<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" />
@@ -11,13 +11,13 @@ const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const regions = [
{ value: "us", label: "美国" },
{ value: "uk", label: "英国" },
{ value: "cn", label: "中国" },
{ value: "jp", label: "日本" },
{ value: "other", label: "其他" },
];
const regions = computed(() => [
{ value: "us", label: t("dataGenerate.countries.us") },
{ value: "uk", label: t("dataGenerate.countries.uk") },
{ value: "cn", label: t("dataGenerate.countries.cn") },
{ value: "jp", label: t("dataGenerate.countries.jp") },
{ value: "other", label: t("dataGenerate.countries.other") },
]);
function randInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
@@ -77,22 +77,22 @@ function refresh() {
<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">格式</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.formatLabel") }}</div>
<div class="flex items-center gap-2 text-xs">
<button type="button" class="px-2 py-1 rounded border text-xs" :class="(params.phoneFormat || 'domestic') === 'domestic' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.phoneFormat = 'domestic'">国内</button>
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.phoneFormat === 'international' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.phoneFormat = 'international'">国际</button>
<button type="button" class="px-2 py-1 rounded border text-xs" :class="(params.phoneFormat || 'domestic') === 'domestic' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.phoneFormat = 'domestic'">{{ t("dataGenerate.phoneDomestic") }}</button>
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.phoneFormat === 'international' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.phoneFormat = 'international'">{{ t("dataGenerate.phoneInternational") }}</button>
</div>
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="flex items-center gap-2 text-xs">
<input id="phone-sep" type="checkbox" class="h-3.5 w-3.5 accent-primary" :checked="!!params.phoneSeparator" @change="params.phoneSeparator = !params.phoneSeparator" />
<Label for="phone-sep" class="text-muted-foreground">包含分隔符</Label>
<Label for="phone-sep" class="text-muted-foreground">{{ t("dataGenerate.includeSeparators") }}</Label>
</div>
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-2">地区选择</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.regionLabel") }}</div>
<div class="flex flex-wrap gap-1.5">
<button v-for="r in regions" :key="r.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.phoneRegions ?? []).includes(r.value) ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="toggleRegion(r.value)">{{ r.label }}</button>
</div>
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const dataMap: Record<string, string[]> = {
en: ["Electronics", "Clothing", "Food", "Books", "Home Goods", "Sports", "Toys", "Beauty", "Furniture", "Automotive", "Garden", "Pet Supplies", "Video Games", "Music Instruments", "Office Supplies"],
zh: ["电子", "服装", "食品", "图书", "家居", "运动", "玩具", "美妆", "家具", "汽车用品", "园艺", "宠物用品", "视频游戏", "乐器", "办公用品"],
@@ -33,7 +36,7 @@ function refresh() {
<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">语言</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
<div class="flex flex-wrap gap-1.5">
<button
v-for="l in [
@@ -54,7 +57,7 @@ function refresh() {
</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">预览</span>
<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" />
@@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
if (!props.params.productKeywords) {
props.params.productKeywords = "Apple\nCherry\nOrange\nMango\nBanana\nLemon\nGrape\nPeach\nPear\nMelon";
}
@@ -32,7 +35,7 @@ const previewVal = computed(() => {
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
if (keywords.length === 0) return "(无)";
if (keywords.length === 0) return t("dataGenerate.noValue");
const mode = Math.random();
if (mode < 0.4) {
return pick(keywords);
@@ -51,12 +54,12 @@ function refresh() {
<template>
<div class="space-y-3">
<div class="rounded-md border bg-muted/10 p-3">
<Label class="text-xs text-muted-foreground mb-1 block">使用关键字生成</Label>
<textarea v-model="params.productKeywords" rows="6" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个关键字&#10;Apple&#10;Cherry&#10;Orange" />
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.useKeywords") }}</Label>
<textarea v-model="params.productKeywords" rows="6" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.keywords')" />
</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">预览</span>
<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" />
@@ -6,14 +6,17 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const previewKey = ref(0);
const previewVal = computed(() => {
void previewKey.value;
const p = props.params.pattern ?? "";
if (!p) return "(未设置)";
if (!p) return t("dataGenerate.notSet");
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
try {
const regex = new RegExp("^" + p + "$");
@@ -23,9 +26,9 @@ const previewVal = computed(() => {
attempt = Array.from({ length: Math.min(len, 50) }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
if (regex.test(attempt)) return attempt;
}
return attempt || "(无法匹配)";
return attempt || t("dataGenerate.noMatch");
} catch {
return "(无效正则)";
return t("dataGenerate.invalidRegex");
}
});
@@ -38,18 +41,18 @@ function refresh() {
<div class="space-y-3">
<div class="rounded-md border bg-muted/10 p-3">
<div class="grid grid-cols-[80px_1fr] items-center gap-2 text-xs">
<Label class="text-muted-foreground">正则表达式</Label>
<Label class="text-muted-foreground">{{ t("dataGenerate.regex") }}</Label>
<Input v-model="params.pattern" type="text" placeholder="[A-Za-z0-9]{10}" class="h-7 text-xs font-mono" />
</div>
<div class="flex items-center gap-2 text-xs mt-2">
<input id="regex-raw" type="checkbox" class="h-3.5 w-3.5 accent-primary" :checked="!!params.rawPattern" @change="params.rawPattern = !params.rawPattern" />
<Label for="regex-raw" class="text-muted-foreground">原始数据模式</Label>
<Label for="regex-raw" class="text-muted-foreground">{{ t("dataGenerate.rawPattern") }}</Label>
</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">预览</span>
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto shrink-0" @click="refresh">
<RefreshCw class="h-3 w-3" />
@@ -4,21 +4,24 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const formats = [
{ value: "name", label: "名称" },
{ value: "code", label: "代码" },
{ value: "code_name", label: "代码+名称" },
];
const { t } = useI18n();
const textTransforms = [
{ value: "none", label: "原样" },
{ value: "upper", label: "全大写" },
{ value: "lower", label: "全小写" },
{ value: "title", label: "每个单词首字母大写" },
];
const formats = computed(() => [
{ value: "name", label: t("dataGenerate.regionFormats.name") },
{ value: "code", label: t("dataGenerate.regionFormats.code") },
{ value: "code_name", label: t("dataGenerate.regionFormats.codeName") },
]);
const textTransforms = computed(() => [
{ value: "none", label: t("dataGenerate.transforms.none") },
{ value: "upper", label: t("dataGenerate.transforms.upper") },
{ value: "lower", label: t("dataGenerate.transforms.lower") },
{ value: "title", label: t("dataGenerate.transforms.title") },
]);
const regionData = [
{ code: "US", en: "United States", zh: "美国", zh_hant: "美國", ja: "アメリカ合衆国" },
@@ -71,13 +74,13 @@ function refresh() {
<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">格式</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.formatLabel") }}</div>
<div class="flex items-center gap-2 text-xs">
<button v-for="f in formats" :key="f.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.regionFormat || 'name') === f.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.regionFormat = f.value">{{ f.label }}</button>
</div>
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-2">语言</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
<div class="flex flex-wrap gap-1.5">
<button
v-for="l in [
@@ -97,16 +100,16 @@ function refresh() {
</div>
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-2">将值转换为</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.transformTo") }}</div>
<div class="flex items-center gap-2 text-xs">
<button v-for="t in textTransforms" :key="t.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.textTransform || 'none') === t.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.textTransform = t.value">
{{ t.label }}
<button v-for="x in textTransforms" :key="x.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.textTransform || 'none') === x.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.textTransform = x.value">
{{ x.label }}
</button>
</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">预览</span>
<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" />
@@ -28,7 +28,7 @@ function refresh() {
<div class="space-y-3">
<div class="rounded-md border bg-muted/10 p-3">
<div class="grid grid-cols-[80px_1fr] items-center gap-2 text-xs">
<Label class="text-muted-foreground">开始</Label>
<Label class="text-muted-foreground">{{ t("dataGenerate.start") }}</Label>
<Input v-model.number="params.startValue" type="number" placeholder="1" class="h-7 text-xs" />
</div>
<div class="grid grid-cols-[80px_1fr] items-center gap-2 text-xs mt-2">
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const dataMap: Record<string, string[]> = {
en: ["XS", "S", "M", "L", "XL", "XXL", "XXXL", "One Size"],
zh: ["特小", "小", "中", "大", "加大", "特大", "超特大", "均码"],
@@ -33,7 +36,7 @@ function refresh() {
<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">语言</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
<div class="flex flex-wrap gap-1.5">
<button
v-for="l in [
@@ -54,7 +57,7 @@ function refresh() {
</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">预览</span>
<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" />
@@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
if (!props.params.pattern) {
props.params.pattern = "([A-F]{2}[-]){2}([0-9]{4}[-])([A-Z])";
}
@@ -72,12 +75,12 @@ function refresh() {
<template>
<div class="space-y-3">
<div class="rounded-md border bg-muted/10 p-3">
<Label class="text-xs text-muted-foreground mb-1 block">正则表达式</Label>
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.regex") }}</Label>
<input v-model="params.pattern" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono" placeholder="([A-F]{2}[-]){2}([0-9]{4}[-])([A-Z])" />
</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">预览</span>
<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" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const chars = "abcdefghijklmnopqrstuvwxyz0123456789_";
const previewKey = ref(0);
@@ -26,7 +29,7 @@ function refresh() {
<div class="space-y-3">
<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">预览</span>
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto shrink-0" @click="refresh">
<RefreshCw class="h-3 w-3" />
@@ -5,9 +5,12 @@ import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const lorem =
"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 Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est laborum";
@@ -33,7 +36,7 @@ function refresh() {
<div class="space-y-3">
<div class="rounded-md border bg-muted/10 p-3">
<div class="grid grid-cols-[80px_1fr] items-center gap-2 text-xs">
<Label class="text-muted-foreground">字符数</Label>
<Label class="text-muted-foreground">{{ t("dataGenerate.charCount") }}</Label>
<div class="flex items-center gap-2">
<Input v-model.number="params.minLength" type="number" min="1" placeholder="50" class="h-7 w-20 text-xs" />
<span class="text-muted-foreground">-</span>
@@ -44,7 +47,7 @@ function refresh() {
<div class="rounded-md border bg-muted/10 p-3">
<div class="flex items-start gap-2 text-xs">
<span class="text-muted-foreground shrink-0 mt-0.5">预览</span>
<span class="text-muted-foreground shrink-0 mt-0.5">{{ t("dataGenerate.preview") }}</span>
<p :key="previewKey" class="font-mono text-xs leading-relaxed break-all">{{ previewVal }}</p>
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto shrink-0 mt-0.5" @click="refresh">
<RefreshCw class="h-3 w-3" />
@@ -4,9 +4,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
const languages = [
{ value: "en", label: "English" },
{ value: "zh", label: "简体中文" },
@@ -43,7 +46,7 @@ function refresh() {
<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">语言选择</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.language") }}</div>
<div class="flex items-center gap-2 text-xs">
<button v-for="l in languages" :key="l.value" type="button" class="px-2 py-1 rounded border text-xs" :class="(params.languages?.[0] || 'en') === l.value ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="setLang(l.value)">{{ l.label }}</button>
</div>
@@ -51,7 +54,7 @@ function refresh() {
<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">预览</span>
<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" />
@@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
if (!props.params.urlSubdomains) {
props.params.urlSubdomains = "auth.\ndrive.\nimage.\nwww.\napi.\nmail.\nshop.\nblog.";
}
@@ -49,16 +52,16 @@ function refresh() {
<template>
<div class="space-y-3">
<div class="rounded-md border bg-muted/10 p-3">
<Label class="text-xs text-muted-foreground mb-1 block">子域</Label>
<textarea v-model="params.urlSubdomains" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个子域&#10;auth.&#10;www.&#10;api." />
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.subdomains") }}</Label>
<textarea v-model="params.urlSubdomains" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.subdomains')" />
</div>
<div class="rounded-md border bg-muted/10 p-3">
<Label class="text-xs text-muted-foreground mb-1 block">顶级域</Label>
<textarea v-model="params.urlTlds" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个TLD&#10;.com&#10;.cn&#10;.io" />
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.tlds") }}</Label>
<textarea v-model="params.urlTlds" rows="4" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.tlds')" />
</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">预览</span>
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="refresh">
<RefreshCw class="h-3 w-3" />
@@ -3,9 +3,12 @@ import { computed, ref } from "vue";
import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
function genUuid(hyphens: boolean): string {
const hex = "0123456789abcdef";
const parts = [8, 4, 4, 4, 12];
@@ -32,21 +35,21 @@ function refresh() {
<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">UUID格式</div>
<div class="text-xs text-muted-foreground mb-2">{{ t("dataGenerate.uuidFormat") }}</div>
<div class="flex items-center gap-2 text-xs">
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.uuidHyphens !== false ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.uuidHyphens = true">包含连字符</button>
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.uuidHyphens === false ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.uuidHyphens = false">无格式</button>
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.uuidHyphens !== false ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.uuidHyphens = true">{{ t("dataGenerate.uuidWithHyphens") }}</button>
<button type="button" class="px-2 py-1 rounded border text-xs" :class="params.uuidHyphens === false ? 'bg-primary text-primary-foreground border-primary' : 'bg-background'" @click="params.uuidHyphens = false">{{ t("dataGenerate.uuidNoHyphens") }}</button>
</div>
</div>
<div class="rounded-md border bg-muted/10 p-3">
<div class="text-xs text-muted-foreground mb-1">正则表达式</div>
<div class="text-xs text-muted-foreground mb-1">{{ t("dataGenerate.regex") }}</div>
<code class="block text-xs font-mono bg-background rounded px-2 py-1 break-all">{{ regex }}</code>
</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">预览</span>
<span class="text-muted-foreground shrink-0">{{ t("dataGenerate.preview") }}</span>
<span :key="previewKey" class="font-mono text-sm break-all">{{ previewVal }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto shrink-0" @click="refresh">
<RefreshCw class="h-3 w-3" />
@@ -5,9 +5,12 @@ import { Button } from "@/components/ui/button";
import { RefreshCw } from "@lucide/vue";
import type { GeneratorParams } from "@/lib/dataGrid/dataGenerate";
import CommonOptions from "./CommonOptions.vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{ params: GeneratorParams }>();
const { t } = useI18n();
if (!props.params.values) {
props.params.values = "g\nkg\nlb\noz\nt\nmg\nct";
}
@@ -23,7 +26,7 @@ const previewVal = computed(() => {
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
if (vals.length === 0) return "(无)";
if (vals.length === 0) return t("dataGenerate.noValue");
return pick(vals);
});
@@ -35,12 +38,12 @@ function refresh() {
<template>
<div class="space-y-3">
<div class="rounded-md border bg-muted/10 p-3">
<Label class="text-xs text-muted-foreground mb-1 block">值</Label>
<textarea v-model="params.values" rows="5" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" placeholder="每行一个单位&#10;g&#10;kg&#10;lb" />
<Label class="text-xs text-muted-foreground mb-1 block">{{ t("dataGenerate.values") }}</Label>
<textarea v-model="params.values" rows="5" class="w-full rounded border bg-background px-2 py-1 text-xs font-mono resize-y" :placeholder="t('dataGenerate.placeholders.weightUnits')" />
</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">预览</span>
<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" />
@@ -446,7 +446,7 @@ onBeforeUnmount(() => cleanupMap());
<!-- Label property selector -->
<select v-if="labelProperties.length" v-model="labelProperty" class="h-6 shrink-0 rounded border bg-background px-1.5 text-[11px] outline-none" @change="onLabelPropertyChange">
<option value="">— 标签 —</option>
<option value="">{{ t("grid.layerPreviewLabelNone") }}</option>
<option v-for="p in labelProperties" :key="p" :value="p">
{{ p }}
</option>
@@ -462,13 +462,13 @@ onBeforeUnmount(() => cleanupMap());
</select>
<!-- Save as image -->
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground" title="导出图片" :disabled="isExporting" @click="saveAsImage">
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground" :title="t('grid.layerPreviewExportImage')" :disabled="isExporting" @click="saveAsImage">
<Camera v-if="!isExporting" class="h-3.5 w-3.5" />
<Loader2 v-else class="h-3.5 w-3.5 animate-spin" />
</Button>
<!-- Maximise -->
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground" :title="isMaximized ? '还原' : '最大化'" @click="toggleMaximize">
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground" :title="isMaximized ? t('grid.layerPreviewRestore') : t('grid.layerPreviewMaximize')" @click="toggleMaximize">
<Maximize2 v-if="!isMaximized" class="h-3.5 w-3.5" />
<Minimize2 v-else class="h-3.5 w-3.5" />
</Button>
@@ -9,6 +9,7 @@ import { useQueryStore } from "@/stores/queryStore";
import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { translateBackendError } from "@/i18n/backend-errors";
import { resolveDefaultDatabase } from "@/lib/database/defaultDatabase";
import { copyToClipboard } from "@/lib/common/clipboard";
import { externalSqlFileOpenErrorMessage, formatSqlFileSize, isExternalSqlFileTooLargeError } from "@/lib/sql/sqlFileOpen";
@@ -156,7 +157,7 @@ async function revealInFileManager(path: string) {
try {
await api.revealPathInFileManager(path);
} catch (e: any) {
toast(t("sqlFileTree.revealFailed", { message: e?.message || String(e) }), 5000);
toast(t("sqlFileTree.revealFailed", { message: translateBackendError(t, e) }), 5000);
}
}
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { formatError } from "@/lib/backend/errorUtils";
import { translateBackendError } from "@/i18n/backend-errors";
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import type { ConsumerInfo, ProducerInfo, SubscriptionInfo, TopicInfo, TopicRef, TopicStats, MqSystemKind } from "@/types/mq";
@@ -147,7 +148,7 @@ async function loadTopics(force = false) {
try {
await topicSelectRef.value?.loadTopics();
} catch (e: unknown) {
error.value = formatError(e);
error.value = translateBackendError(t, formatError(e));
} finally {
topicsLoading.value = false;
}
@@ -260,7 +261,7 @@ async function loadRuntimeClients() {
}
} catch (e: unknown) {
if (isRuntimeLoadCurrent(loadSeq, currentKey)) {
error.value = formatError(e) || String(e);
error.value = translateBackendError(t, formatError(e)) || String(e);
}
} finally {
if (loadSeq === runtimeLoadSeq) {