feat(safety): 增加危险删除操作二次确认 (#11)
将现有 SQL 危险确认弹窗扩展为可复用确认组件。 覆盖 SQL UPDATE/MERGE/REPLACE 等危险语句识别,并在识别前去除注释。 为表格行删除、Redis key/hash/list/set 删除、MongoDB 文档和字段删除接入确认弹窗。 补充中英文危险确认文案,保持本次提交只聚焦确认流程。
This commit is contained in:
parent
84ea2a6e13
commit
68430d16c1
11
src/App.vue
11
src/App.vue
|
|
@ -201,10 +201,17 @@ async function openConnectionQuery(connectionId: string) {
|
|||
queryStore.createTab(connectionId, database);
|
||||
}
|
||||
|
||||
const DANGER_RE = /^\s*(DROP|DELETE|TRUNCATE|ALTER)\b/i;
|
||||
const DANGER_RE = /\b(DROP|DELETE|TRUNCATE|ALTER|UPDATE|MERGE|REPLACE)\b/i;
|
||||
|
||||
function stripSqlComments(sql: string): string {
|
||||
return sql
|
||||
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
||||
.replace(/--.*$/gm, " ")
|
||||
.replace(/#.*$/gm, " ");
|
||||
}
|
||||
|
||||
function isDangerousSql(sql: string): boolean {
|
||||
return DANGER_RE.test(sql);
|
||||
return DANGER_RE.test(stripSqlComments(sql));
|
||||
}
|
||||
|
||||
function tryExecute() {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,19 @@ const { t } = useI18n();
|
|||
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
|
||||
defineProps<{
|
||||
sql: string;
|
||||
}>();
|
||||
withDefaults(defineProps<{
|
||||
sql?: string;
|
||||
title?: string;
|
||||
message?: string;
|
||||
details?: string;
|
||||
confirmLabel?: string;
|
||||
}>(), {
|
||||
sql: "",
|
||||
title: "",
|
||||
message: "",
|
||||
details: "",
|
||||
confirmLabel: "",
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [];
|
||||
|
|
@ -30,18 +40,18 @@ function onConfirm() {
|
|||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2 text-destructive">
|
||||
<AlertTriangle class="h-5 w-5" />
|
||||
{{ t('dangerDialog.title') }}
|
||||
{{ title || t('dangerDialog.title') }}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="py-4">
|
||||
<p class="text-sm text-muted-foreground mb-3">{{ t('dangerDialog.message') }}</p>
|
||||
<pre class="text-xs bg-muted p-3 rounded overflow-auto max-h-40 font-mono whitespace-pre-wrap">{{ sql }}</pre>
|
||||
<p class="text-sm text-muted-foreground mb-3">{{ message || t('dangerDialog.message') }}</p>
|
||||
<pre v-if="details || sql" class="text-xs bg-muted p-3 rounded overflow-auto max-h-40 font-mono whitespace-pre-wrap">{{ details || sql }}</pre>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="open = false">{{ t('dangerDialog.cancel') }}</Button>
|
||||
<Button variant="destructive" @click="onConfirm">{{ t('dangerDialog.confirm') }}</Button>
|
||||
<Button variant="destructive" @click="onConfirm">{{ confirmLabel || t('dangerDialog.confirm') }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import type { QueryResult, ColumnInfo, DatabaseType } from "@/types/database";
|
||||
import { save as savePath } from "@tauri-apps/plugin-dialog";
|
||||
import { writeTextFile } from "@tauri-apps/plugin-fs";
|
||||
|
|
@ -422,7 +423,7 @@ function addRow() {
|
|||
});
|
||||
}
|
||||
|
||||
function deleteRow(rowId: number) {
|
||||
function applyDeleteRow(rowId: number) {
|
||||
const item = getRowItem(rowId);
|
||||
if (!item) return;
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
|
|
@ -434,6 +435,25 @@ function deleteRow(rowId: number) {
|
|||
if (editingCell.value?.rowId === rowId) editingCell.value = null;
|
||||
}
|
||||
|
||||
const showDeleteRowConfirm = ref(false);
|
||||
const pendingDeleteRowId = ref<number | null>(null);
|
||||
const deleteRowDetails = computed(() =>
|
||||
props.tableMeta?.tableName
|
||||
? t("dangerDialog.deleteRowDetails", { table: props.tableMeta.tableName })
|
||||
: t("dangerDialog.deleteRowDetailsNoTable")
|
||||
);
|
||||
|
||||
function requestDeleteRow(rowId: number) {
|
||||
pendingDeleteRowId.value = rowId;
|
||||
showDeleteRowConfirm.value = true;
|
||||
}
|
||||
|
||||
function confirmDeleteRow() {
|
||||
if (pendingDeleteRowId.value === null) return;
|
||||
applyDeleteRow(pendingDeleteRowId.value);
|
||||
pendingDeleteRowId.value = null;
|
||||
}
|
||||
|
||||
function restoreRow(rowId: number) {
|
||||
const item = getRowItem(rowId);
|
||||
if (item?.sourceIndex !== undefined) {
|
||||
|
|
@ -443,7 +463,7 @@ function restoreRow(rowId: number) {
|
|||
|
||||
function deleteSelectedRow() {
|
||||
if (!contextCell.value) return;
|
||||
deleteRow(contextCell.value.rowId);
|
||||
requestDeleteRow(contextCell.value.rowId);
|
||||
}
|
||||
|
||||
function escapeVal(v: CellValue): string {
|
||||
|
|
@ -793,7 +813,7 @@ function escapeAndHighlightKeywords(s: string): string {
|
|||
size="icon"
|
||||
class="h-5 w-5 shrink-0 text-destructive"
|
||||
:title="t('grid.deleteRow')"
|
||||
@click.stop="deleteRow(item.id)"
|
||||
@click.stop="requestDeleteRow(item.id)"
|
||||
>
|
||||
<Trash2 class="w-3 h-3" />
|
||||
</Button>
|
||||
|
|
@ -955,6 +975,14 @@ function escapeAndHighlightKeywords(s: string): string {
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<DangerConfirmDialog
|
||||
v-model:open="showDeleteRowConfirm"
|
||||
:message="t('dangerDialog.deleteRowMessage')"
|
||||
:details="deleteRowDetails"
|
||||
:confirm-label="t('grid.deleteRow')"
|
||||
@confirm="confirmDeleteRow"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Plus, Trash2 } from "lucide-vue-next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import type { EditNode, EditNodeKind } from "@/types/editor";
|
||||
|
||||
defineOptions({ name: "JsonEditNode" });
|
||||
|
|
@ -23,6 +24,8 @@ const emit = defineEmits<{
|
|||
const { t } = useI18n();
|
||||
|
||||
const isContainer = computed(() => props.node.kind !== "value");
|
||||
const showChildDeleteConfirm = ref(false);
|
||||
const pendingChildDeleteIdx = ref<number | null>(null);
|
||||
|
||||
const childKeyWidth = computed(() => {
|
||||
const longest = props.node.children.reduce((max, child) => {
|
||||
|
|
@ -31,6 +34,17 @@ const childKeyWidth = computed(() => {
|
|||
return `${Math.min(Math.max(longest + 4, 8), 36)}ch`;
|
||||
});
|
||||
|
||||
const childDeleteDetails = computed(() => {
|
||||
const idx = pendingChildDeleteIdx.value;
|
||||
if (idx === null) return "";
|
||||
const child = props.node.children[idx];
|
||||
if (!child) return "";
|
||||
if (props.node.kind === "array") {
|
||||
return t("dangerDialog.mongoArrayItemDetails", { index: child.keyName });
|
||||
}
|
||||
return t("dangerDialog.mongoFieldDetails", { field: child.keyName || t("mongo.field") });
|
||||
});
|
||||
|
||||
function fieldRows(value: string): number {
|
||||
const estimatedRows = value.split(/\r?\n/).reduce((sum, line) => {
|
||||
return sum + Math.max(1, Math.ceil(Array.from(line).length / 110));
|
||||
|
|
@ -76,6 +90,17 @@ function removeChild(idx: number) {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
function requestRemoveChild(idx: number) {
|
||||
pendingChildDeleteIdx.value = idx;
|
||||
showChildDeleteConfirm.value = true;
|
||||
}
|
||||
|
||||
function confirmRemoveChild() {
|
||||
if (pendingChildDeleteIdx.value === null) return;
|
||||
removeChild(pendingChildDeleteIdx.value);
|
||||
pendingChildDeleteIdx.value = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -138,7 +163,7 @@ function removeChild(idx: number) {
|
|||
:node="child"
|
||||
:parent-kind="node.kind"
|
||||
:removable="!child.readonlyValue || node.kind === 'array'"
|
||||
@remove="removeChild(idx)"
|
||||
@remove="requestRemoveChild(idx)"
|
||||
/>
|
||||
|
||||
<Button variant="ghost" size="sm" class="json-edit-add" @click="addChild">
|
||||
|
|
@ -149,6 +174,14 @@ function removeChild(idx: number) {
|
|||
{{ node.kind === 'array' ? ']' : '}' }}<span class="json-edit-comma">,</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DangerConfirmDialog
|
||||
v-model:open="showChildDeleteConfirm"
|
||||
:message="t('dangerDialog.deleteMessage')"
|
||||
:details="childDeleteDetails"
|
||||
:confirm-label="t('mongo.deleteField')"
|
||||
@confirm="confirmRemoveChild"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useI18n } from "vue-i18n";
|
|||
import { RefreshCw, Trash2, Plus, Save, ChevronLeft, ChevronRight } from "lucide-vue-next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import * as api from "@/lib/tauri";
|
||||
import JsonEditNode from "./JsonEditNode.vue";
|
||||
import type { EditNode } from "@/types/editor";
|
||||
|
|
@ -31,6 +32,13 @@ const isEditing = ref(false);
|
|||
const isNew = ref(false);
|
||||
const error = ref("");
|
||||
const editFields = ref<EditNode[]>([]);
|
||||
const showDeleteConfirm = ref(false);
|
||||
|
||||
type PendingDelete =
|
||||
| { kind: "document"; index: number }
|
||||
| { kind: "field"; index: number; name: string };
|
||||
|
||||
const pendingDelete = ref<PendingDelete | null>(null);
|
||||
|
||||
const selectedDoc = computed(() => {
|
||||
if (selectedIdx.value === null) return null;
|
||||
|
|
@ -44,6 +52,16 @@ const editKeyWidth = computed(() => {
|
|||
return `${Math.min(Math.max(longest + 4, 8), 36)}ch`;
|
||||
});
|
||||
|
||||
const deleteDetails = computed(() => {
|
||||
const pending = pendingDelete.value;
|
||||
if (!pending) return "";
|
||||
if (pending.kind === "document") {
|
||||
const id = documents.value[pending.index]?._id ?? "";
|
||||
return t("dangerDialog.mongoDocumentDetails", { collection: props.collection, id: String(id) });
|
||||
}
|
||||
return t("dangerDialog.mongoFieldDetails", { field: pending.name || t("mongo.field") });
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
|
|
@ -150,11 +168,18 @@ function addField() {
|
|||
editFields.value.push(createEditNode("", "", false, false));
|
||||
}
|
||||
|
||||
function removeField(idx: number) {
|
||||
function applyRemoveField(idx: number) {
|
||||
if (editFields.value[idx]?.readonlyValue) return;
|
||||
editFields.value.splice(idx, 1);
|
||||
}
|
||||
|
||||
function requestRemoveField(idx: number) {
|
||||
const field = editFields.value[idx];
|
||||
if (!field || field.readonlyValue) return;
|
||||
pendingDelete.value = { kind: "field", index: idx, name: field.keyName };
|
||||
showDeleteConfirm.value = true;
|
||||
}
|
||||
|
||||
function formatForEdit(value: unknown): string {
|
||||
if (value === undefined) return "";
|
||||
if (value === null) return "null";
|
||||
|
|
@ -222,7 +247,7 @@ async function saveDoc() {
|
|||
}
|
||||
}
|
||||
|
||||
async function deleteDoc(idx: number) {
|
||||
async function applyDeleteDoc(idx: number) {
|
||||
const doc = documents.value[idx];
|
||||
const id = doc._id;
|
||||
if (!id) return;
|
||||
|
|
@ -236,6 +261,22 @@ async function deleteDoc(idx: number) {
|
|||
}
|
||||
}
|
||||
|
||||
function requestDeleteDoc(idx: number) {
|
||||
pendingDelete.value = { kind: "document", index: idx };
|
||||
showDeleteConfirm.value = true;
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
const pending = pendingDelete.value;
|
||||
if (!pending) return;
|
||||
if (pending.kind === "document") {
|
||||
await applyDeleteDoc(pending.index);
|
||||
} else {
|
||||
applyRemoveField(pending.index);
|
||||
}
|
||||
pendingDelete.value = null;
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (page.value <= 0) return;
|
||||
page.value--;
|
||||
|
|
@ -297,7 +338,7 @@ onMounted(load);
|
|||
@click="selectDoc(idx)"
|
||||
>
|
||||
<span class="truncate flex-1">{{ docPreview(doc) }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100 text-destructive shrink-0" @click.stop="deleteDoc(idx)">
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100 text-destructive shrink-0" @click.stop="requestDeleteDoc(idx)">
|
||||
<Trash2 class="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -346,7 +387,7 @@ onMounted(load);
|
|||
:node="field"
|
||||
parent-kind="root"
|
||||
:removable="!field.readonlyValue"
|
||||
@remove="removeField(idx)"
|
||||
@remove="requestRemoveField(idx)"
|
||||
/>
|
||||
|
||||
<Button variant="ghost" size="sm" class="json-edit-add" @click="addField">
|
||||
|
|
@ -371,6 +412,13 @@ onMounted(load);
|
|||
<div v-if="error" class="px-3 py-1.5 border-t bg-destructive/10 text-destructive text-xs shrink-0">
|
||||
{{ error }}
|
||||
</div>
|
||||
<DangerConfirmDialog
|
||||
v-model:open="showDeleteConfirm"
|
||||
:message="t('dangerDialog.deleteMessage')"
|
||||
:details="deleteDetails"
|
||||
:confirm-label="t('dangerDialog.deleteConfirm')"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { computed, ref, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Copy, Trash2, Save, RefreshCw, Plus } from "lucide-vue-next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import * as api from "@/lib/tauri";
|
||||
import type { RedisValue } from "@/lib/tauri";
|
||||
|
||||
|
|
@ -23,6 +24,24 @@ const editValue = ref("");
|
|||
const isEditing = ref(false);
|
||||
const newField = ref("");
|
||||
const newValue = ref("");
|
||||
const showDeleteConfirm = ref(false);
|
||||
|
||||
type PendingDelete =
|
||||
| { kind: "key" }
|
||||
| { kind: "hash"; field: string }
|
||||
| { kind: "list"; index: number }
|
||||
| { kind: "set"; member: string };
|
||||
|
||||
const pendingDelete = ref<PendingDelete | null>(null);
|
||||
|
||||
const deleteDetails = computed(() => {
|
||||
const pending = pendingDelete.value;
|
||||
if (!pending) return "";
|
||||
if (pending.kind === "key") return t("dangerDialog.redisKeyDetails", { key: props.keyName });
|
||||
if (pending.kind === "hash") return t("dangerDialog.redisHashFieldDetails", { key: props.keyName, field: pending.field });
|
||||
if (pending.kind === "list") return t("dangerDialog.redisListItemDetails", { key: props.keyName, index: pending.index });
|
||||
return t("dangerDialog.redisSetMemberDetails", { key: props.keyName, member: pending.member });
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
|
|
@ -42,11 +61,16 @@ async function saveString() {
|
|||
await load();
|
||||
}
|
||||
|
||||
async function deleteKey() {
|
||||
async function applyDeleteKey() {
|
||||
await api.redisDeleteKey(props.connectionId, props.keyName);
|
||||
emit("deleted");
|
||||
}
|
||||
|
||||
function requestDeleteKey() {
|
||||
pendingDelete.value = { kind: "key" };
|
||||
showDeleteConfirm.value = true;
|
||||
}
|
||||
|
||||
function copyValue() {
|
||||
if (!data.value) return;
|
||||
const text = typeof data.value.value === "string" ? data.value.value : JSON.stringify(data.value.value, null, 2);
|
||||
|
|
@ -61,10 +85,14 @@ async function hashSet() {
|
|||
newValue.value = "";
|
||||
await load();
|
||||
}
|
||||
async function hashDel(field: string) {
|
||||
async function applyHashDel(field: string) {
|
||||
await api.redisHashDel(props.connectionId, props.keyName, field);
|
||||
await load();
|
||||
}
|
||||
function requestHashDel(field: string) {
|
||||
pendingDelete.value = { kind: "hash", field };
|
||||
showDeleteConfirm.value = true;
|
||||
}
|
||||
|
||||
// List
|
||||
async function listPush() {
|
||||
|
|
@ -73,10 +101,14 @@ async function listPush() {
|
|||
newValue.value = "";
|
||||
await load();
|
||||
}
|
||||
async function listRemove(index: number) {
|
||||
async function applyListRemove(index: number) {
|
||||
await api.redisListRemove(props.connectionId, props.keyName, index);
|
||||
await load();
|
||||
}
|
||||
function requestListRemove(index: number) {
|
||||
pendingDelete.value = { kind: "list", index };
|
||||
showDeleteConfirm.value = true;
|
||||
}
|
||||
|
||||
// Set
|
||||
async function setAdd() {
|
||||
|
|
@ -85,10 +117,24 @@ async function setAdd() {
|
|||
newValue.value = "";
|
||||
await load();
|
||||
}
|
||||
async function setRemove(member: string) {
|
||||
async function applySetRemove(member: string) {
|
||||
await api.redisSetRemove(props.connectionId, props.keyName, member);
|
||||
await load();
|
||||
}
|
||||
function requestSetRemove(member: string) {
|
||||
pendingDelete.value = { kind: "set", member };
|
||||
showDeleteConfirm.value = true;
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
const pending = pendingDelete.value;
|
||||
if (!pending) return;
|
||||
if (pending.kind === "key") await applyDeleteKey();
|
||||
else if (pending.kind === "hash") await applyHashDel(pending.field);
|
||||
else if (pending.kind === "list") await applyListRemove(pending.index);
|
||||
else await applySetRemove(pending.member);
|
||||
pendingDelete.value = null;
|
||||
}
|
||||
|
||||
function formatValue(val: any): string {
|
||||
if (typeof val === "string") return val;
|
||||
|
|
@ -114,7 +160,7 @@ onMounted(load);
|
|||
<span class="flex-1" />
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="load"><RefreshCw class="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="copyValue"><Copy class="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="deleteKey"><Trash2 class="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="requestDeleteKey"><Trash2 class="h-3.5 w-3.5" /></Button>
|
||||
</div>
|
||||
|
||||
<!-- String -->
|
||||
|
|
@ -138,7 +184,7 @@ onMounted(load);
|
|||
<div v-for="(item, idx) in data.value" :key="idx" class="px-4 py-1.5 border-b text-sm font-mono hover:bg-accent/50 flex items-center gap-2 group">
|
||||
<span class="text-muted-foreground text-xs w-8 shrink-0">{{ idx }}</span>
|
||||
<span class="truncate flex-1">{{ item }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100 text-destructive" @click="listRemove(Number(idx))"><Trash2 class="w-3 h-3" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100 text-destructive" @click="requestListRemove(Number(idx))"><Trash2 class="w-3 h-3" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -154,7 +200,7 @@ onMounted(load);
|
|||
<div class="flex-1 overflow-y-auto">
|
||||
<div v-for="(item, idx) in data.value" :key="idx" class="px-4 py-1.5 border-b text-sm font-mono hover:bg-accent/50 flex items-center gap-2 group">
|
||||
<span class="truncate flex-1">{{ item }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100 text-destructive" @click="setRemove(String(item))"><Trash2 class="w-3 h-3" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100 text-destructive" @click="requestSetRemove(String(item))"><Trash2 class="w-3 h-3" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -172,7 +218,7 @@ onMounted(load);
|
|||
<div v-for="(val, field) in data.value" :key="String(field)" class="px-4 py-1.5 border-b text-sm font-mono hover:bg-accent/50 flex items-center gap-3 group">
|
||||
<span class="text-blue-500 shrink-0 min-w-24">{{ field }}</span>
|
||||
<span class="truncate text-muted-foreground flex-1">{{ val }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100 text-destructive" @click="hashDel(String(field))"><Trash2 class="w-3 h-3" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100 text-destructive" @click="requestHashDel(String(field))"><Trash2 class="w-3 h-3" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -193,5 +239,13 @@ onMounted(load);
|
|||
<pre class="font-mono text-sm whitespace-pre-wrap">{{ formatValue(data.value) }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<DangerConfirmDialog
|
||||
v-model:open="showDeleteConfirm"
|
||||
:message="t('dangerDialog.deleteMessage')"
|
||||
:details="deleteDetails"
|
||||
:confirm-label="t('dangerDialog.deleteConfirm')"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -196,6 +196,18 @@ export default {
|
|||
dangerDialog: {
|
||||
title: "Dangerous Operation",
|
||||
message: "This SQL statement may modify or delete data irreversibly. Are you sure you want to execute it?",
|
||||
deleteMessage: "This delete operation may be irreversible. Continue?",
|
||||
deleteConfirm: "Confirm Delete",
|
||||
deleteRowMessage: "This row will be marked for deletion and removed from the database after saving. Continue?",
|
||||
deleteRowDetails: "Table: {table}",
|
||||
deleteRowDetailsNoTable: "Current result row",
|
||||
redisKeyDetails: "Redis key: {key}",
|
||||
redisHashFieldDetails: "Redis hash: {key}\nField: {field}",
|
||||
redisListItemDetails: "Redis list: {key}\nIndex: {index}",
|
||||
redisSetMemberDetails: "Redis set: {key}\nMember: {member}",
|
||||
mongoDocumentDetails: "MongoDB collection: {collection}\nDocument _id: {id}",
|
||||
mongoFieldDetails: "Field: {field}",
|
||||
mongoArrayItemDetails: "Array item index: {index}",
|
||||
cancel: "Cancel",
|
||||
confirm: "Execute",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -194,6 +194,18 @@ export default {
|
|||
dangerDialog: {
|
||||
title: "危险操作",
|
||||
message: "此 SQL 语句可能不可逆地修改或删除数据,确认要执行吗?",
|
||||
deleteMessage: "此删除操作可能不可逆,确认要继续吗?",
|
||||
deleteConfirm: "确认删除",
|
||||
deleteRowMessage: "此行将被标记为删除,保存后会从数据库删除,确认要继续吗?",
|
||||
deleteRowDetails: "数据表:{table}",
|
||||
deleteRowDetailsNoTable: "当前结果集中的行",
|
||||
redisKeyDetails: "Redis key:{key}",
|
||||
redisHashFieldDetails: "Redis hash:{key}\n字段:{field}",
|
||||
redisListItemDetails: "Redis list:{key}\n索引:{index}",
|
||||
redisSetMemberDetails: "Redis set:{key}\n成员:{member}",
|
||||
mongoDocumentDetails: "MongoDB 集合:{collection}\n文档 _id:{id}",
|
||||
mongoFieldDetails: "字段:{field}",
|
||||
mongoArrayItemDetails: "数组项索引:{index}",
|
||||
cancel: "取消",
|
||||
confirm: "执行",
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue