mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-31 00:22:30 +00:00
feat: multi-row selection with batch clone, delete, and copy support
This commit is contained in:
@@ -246,7 +246,7 @@ onMounted(() => store.load());
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ selectedEntry ? entryTitle(selectedEntry) : t("history.viewDetails") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div v-if="selectedEntry" class="space-y-4">
|
||||
<div v-if="selectedEntry" class="space-y-4 overflow-y-auto max-h-[60vh]">
|
||||
<div class="grid grid-cols-[120px_1fr] gap-x-3 gap-y-2 text-sm">
|
||||
<template v-for="[label, value] in detailsRows(selectedEntry)" :key="label">
|
||||
<div class="text-muted-foreground">{{ label }}</div>
|
||||
|
||||
@@ -927,6 +927,9 @@ const {
|
||||
requestDeleteRow,
|
||||
confirmDeleteRow,
|
||||
restoreRow,
|
||||
pendingDeleteRowIds,
|
||||
requestDeleteRows,
|
||||
cloneRows,
|
||||
saveChanges,
|
||||
discardChanges,
|
||||
rowDataWithChanges,
|
||||
@@ -1040,10 +1043,6 @@ const deleteRowDetails = computed(() =>
|
||||
: t("dangerDialog.deleteRowDetailsNoTable"),
|
||||
);
|
||||
|
||||
function deleteSelectedRow() {
|
||||
if (!contextCell.value) return;
|
||||
requestDeleteRow(contextCell.value.rowId);
|
||||
}
|
||||
const hasVisibleRows = computed(() => displayItems.value.length > 0);
|
||||
const hasActiveFilter = computed(
|
||||
() => !!deferredClientSearchText.value || rowStatusFilter.value !== "all" || hasLocalColumnFilters.value,
|
||||
@@ -1074,17 +1073,27 @@ const {
|
||||
hasCellSelection,
|
||||
clearCellSelection,
|
||||
selectSingleCell,
|
||||
selectRow,
|
||||
finishCellSelection,
|
||||
beginCellSelection,
|
||||
extendCellSelection,
|
||||
cellIsSelected,
|
||||
selectedRangeStart,
|
||||
selectedRowIds,
|
||||
hasRowSelection,
|
||||
selectedRowCount,
|
||||
clearRowSelection,
|
||||
handleRowClick,
|
||||
handleDataCellMousedown,
|
||||
isRowSelected,
|
||||
} = selection;
|
||||
|
||||
const selectionSummary = computed(() => t("grid.selectedCells", { count: selectedCellCount.value }));
|
||||
const selectionSummary = computed(() => {
|
||||
if (hasRowSelection.value) return t("grid.selectedRows", { count: selectedRowCount.value });
|
||||
return t("grid.selectedCells", { count: selectedCellCount.value });
|
||||
});
|
||||
|
||||
function isRowActive(index: number): boolean {
|
||||
const item = displayItems.value[index];
|
||||
if (item && isRowSelected(item.id)) return true;
|
||||
const range = selectedRange.value;
|
||||
if (!range) return false;
|
||||
return index >= range.startRow && index <= range.endRow;
|
||||
@@ -1407,6 +1416,8 @@ const {
|
||||
getRowItem: (rowId: number) => visibleDisplayItems.value.find((item) => item.id === rowId),
|
||||
quoteIdent,
|
||||
escapeVal,
|
||||
selectedRowIds,
|
||||
hasRowSelection,
|
||||
});
|
||||
|
||||
// --- Cell selection and detail ---
|
||||
@@ -1541,6 +1552,7 @@ watch(
|
||||
resetGridVerticalScroll();
|
||||
}
|
||||
clearCellSelection();
|
||||
clearRowSelection();
|
||||
showCellDetail.value = false;
|
||||
detailCell.value = null;
|
||||
showTranspose.value = false;
|
||||
@@ -1552,6 +1564,10 @@ watch(
|
||||
// --- Context menu handlers ---
|
||||
function onCellContext(rowId: number, rowIndex: number, colIdx: number, visibleColIdx: number) {
|
||||
contextCell.value = { rowId, rowIndex, col: colIdx };
|
||||
if (hasRowSelection.value && isRowSelected(rowId)) {
|
||||
return;
|
||||
}
|
||||
clearRowSelection();
|
||||
if (!cellIsSelected(rowIndex, visibleColIdx)) {
|
||||
selectSingleCell(rowIndex, visibleColIdx);
|
||||
}
|
||||
@@ -1559,7 +1575,11 @@ function onCellContext(rowId: number, rowIndex: number, colIdx: number, visibleC
|
||||
|
||||
function onRowContext(rowId: number, rowIndex: number) {
|
||||
contextCell.value = { rowId, rowIndex, col: -1 };
|
||||
selectRow(rowIndex);
|
||||
if (!isRowSelected(rowId)) {
|
||||
clearCellSelection();
|
||||
selectedRowIds.value = new Set([rowId]);
|
||||
selection.lastClickedRowIndex.value = rowIndex;
|
||||
}
|
||||
}
|
||||
|
||||
const sqlOneLiner = computed(() => props.sql?.replace(/\s+/g, " ").trim() || "");
|
||||
@@ -2229,9 +2249,18 @@ defineExpose({
|
||||
>
|
||||
<div
|
||||
class="shrink-0 px-2 py-1 border-r border-border text-center select-none cursor-default hover:bg-accent/50"
|
||||
:class="rowNumberStatusClass(item)"
|
||||
:class="[
|
||||
rowNumberStatusClass(item),
|
||||
{
|
||||
'text-primary font-semibold !bg-primary/15':
|
||||
isRowSelected(item.id) &&
|
||||
item.status !== 'new' &&
|
||||
item.status !== 'edited' &&
|
||||
item.status !== 'deleted',
|
||||
},
|
||||
]"
|
||||
:style="{ width: 'var(--row-num-w)' }"
|
||||
@click="selectRow(index)"
|
||||
@click="handleRowClick(index, item.id, $event)"
|
||||
@contextmenu="onRowContext(item.id, index)"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
@@ -2249,7 +2278,7 @@ defineExpose({
|
||||
'cursor-text hover:bg-accent/50': editable && !item.isDeleted,
|
||||
'line-through': item.isDeleted,
|
||||
}"
|
||||
@mousedown="beginCellSelection(index, visibleColIdx, $event)"
|
||||
@mousedown="handleDataCellMousedown(index, visibleColIdx, item.id, $event)"
|
||||
@mouseenter="extendCellSelection(index, visibleColIdx)"
|
||||
@dblclick="editable && !item.isDeleted && startEdit(item.id, actualColIdx)"
|
||||
@contextmenu="onCellContext(item.id, index, actualColIdx, visibleColIdx)"
|
||||
@@ -2540,8 +2569,20 @@ defineExpose({
|
||||
<ContextMenuSubTrigger> <Copy class="w-3.5 h-3.5 mr-2" /> {{ t("grid.copy") }} </ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent class="w-max max-w-[min(80vw,18rem)]">
|
||||
<ContextMenuItem v-if="contextColumn" @click="copyCell">{{ t("grid.copyCell") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyRow">{{ t("grid.copyRow") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyRowAsInsert">{{ t("grid.copyRowInsert") }}</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyRow">
|
||||
{{
|
||||
hasRowSelection && selectedRowCount > 1
|
||||
? t("grid.copyRows", { count: selectedRowCount })
|
||||
: t("grid.copyRow")
|
||||
}}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyRowAsInsert">
|
||||
{{
|
||||
hasRowSelection && selectedRowCount > 1
|
||||
? t("grid.copyRowsInsert", { count: selectedRowCount })
|
||||
: t("grid.copyRowInsert")
|
||||
}}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="copyAll">{{ t("grid.copyAll") }}</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
@@ -2563,14 +2604,36 @@ defineExpose({
|
||||
</ContextMenuSub>
|
||||
<ContextMenuSeparator />
|
||||
<template v-if="editable && contextRowItem">
|
||||
<ContextMenuItem @click="cloneRow(contextRowItem.id)">
|
||||
<CopyPlus class="w-3.5 h-3.5 mr-2" /> {{ t("grid.cloneRow") }}
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
hasRowSelection && selectedRowCount > 1 ? cloneRows([...selectedRowIds]) : cloneRow(contextRowItem.id)
|
||||
"
|
||||
>
|
||||
<CopyPlus class="w-3.5 h-3.5 mr-2" />
|
||||
{{
|
||||
hasRowSelection && selectedRowCount > 1
|
||||
? t("grid.cloneRows", { count: selectedRowCount })
|
||||
: t("grid.cloneRow")
|
||||
}}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-if="contextRowItem.isDeleted" @click="restoreRow(contextRowItem.id)">
|
||||
<Undo2 class="w-3.5 h-3.5 mr-2" /> {{ t("grid.restoreRow") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-else class="text-destructive" @click="deleteSelectedRow">
|
||||
<Trash2 class="w-3.5 h-3.5 mr-2" /> {{ t("grid.deleteRow") }}
|
||||
<ContextMenuItem
|
||||
v-else
|
||||
class="text-destructive"
|
||||
@click="
|
||||
hasRowSelection && selectedRowCount > 1
|
||||
? requestDeleteRows([...selectedRowIds])
|
||||
: requestDeleteRow(contextRowItem.id)
|
||||
"
|
||||
>
|
||||
<Trash2 class="w-3.5 h-3.5 mr-2" />
|
||||
{{
|
||||
hasRowSelection && selectedRowCount > 1
|
||||
? t("grid.deleteRows", { count: selectedRowCount })
|
||||
: t("grid.deleteRow")
|
||||
}}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
</template>
|
||||
@@ -2676,9 +2739,17 @@ defineExpose({
|
||||
|
||||
<DangerConfirmDialog
|
||||
v-model:open="showDeleteRowConfirm"
|
||||
:message="t('dangerDialog.deleteRowMessage')"
|
||||
:message="
|
||||
pendingDeleteRowIds.length > 1
|
||||
? t('dangerDialog.deleteRowsMessage', { count: pendingDeleteRowIds.length })
|
||||
: t('dangerDialog.deleteRowMessage')
|
||||
"
|
||||
:details="deleteRowDetails"
|
||||
:confirm-label="t('grid.deleteRow')"
|
||||
:confirm-label="
|
||||
pendingDeleteRowIds.length > 1
|
||||
? t('grid.deleteRows', { count: pendingDeleteRowIds.length })
|
||||
: t('grid.deleteRow')
|
||||
"
|
||||
@confirm="confirmDeleteRow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -384,6 +384,21 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
function cloneRows(rowIds: number[]) {
|
||||
const primaryKeys = tableMeta.value?.primaryKeys ?? [];
|
||||
const columns = result.value.columns;
|
||||
rowStatusFilter.value = rowStatusFilterAfterAddingRow(rowStatusFilter.value);
|
||||
for (const rowId of rowIds) {
|
||||
const item = getRowItem(rowId);
|
||||
if (!item) continue;
|
||||
const clonedData = item.data.map((val, i) => (primaryKeys.includes(columns[i]) ? null : val));
|
||||
newRows.value.push(clonedData);
|
||||
}
|
||||
if (useTransaction.value && !transactionActive.value) {
|
||||
enterTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
function applyDeleteRow(rowId: number) {
|
||||
const item = getRowItem(rowId);
|
||||
if (!item) return;
|
||||
@@ -401,13 +416,26 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
|
||||
const showDeleteRowConfirm = ref(false);
|
||||
const pendingDeleteRowId = ref<number | null>(null);
|
||||
const pendingDeleteRowIds = ref<number[]>([]);
|
||||
|
||||
function requestDeleteRow(rowId: number) {
|
||||
pendingDeleteRowId.value = rowId;
|
||||
showDeleteRowConfirm.value = true;
|
||||
}
|
||||
|
||||
function requestDeleteRows(rowIds: number[]) {
|
||||
pendingDeleteRowIds.value = rowIds;
|
||||
showDeleteRowConfirm.value = true;
|
||||
}
|
||||
|
||||
function confirmDeleteRow() {
|
||||
if (pendingDeleteRowIds.value.length > 0) {
|
||||
for (const rowId of pendingDeleteRowIds.value) {
|
||||
applyDeleteRow(rowId);
|
||||
}
|
||||
pendingDeleteRowIds.value = [];
|
||||
return;
|
||||
}
|
||||
if (pendingDeleteRowId.value === null) return;
|
||||
applyDeleteRow(pendingDeleteRowId.value);
|
||||
pendingDeleteRowId.value = null;
|
||||
@@ -642,10 +670,13 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
||||
onEditKeydown,
|
||||
addRow,
|
||||
cloneRow,
|
||||
cloneRows,
|
||||
applyDeleteRow,
|
||||
showDeleteRowConfirm,
|
||||
pendingDeleteRowId,
|
||||
pendingDeleteRowIds,
|
||||
requestDeleteRow,
|
||||
requestDeleteRows,
|
||||
confirmDeleteRow,
|
||||
restoreRow,
|
||||
deleteSelectedRow,
|
||||
|
||||
@@ -39,6 +39,8 @@ export interface UseDataGridExportOptions {
|
||||
getRowItem: (rowId: number) => RowItem | undefined;
|
||||
quoteIdent: (name: string) => string;
|
||||
escapeVal: (value: CellValue) => string;
|
||||
selectedRowIds: Ref<Set<number>> | ComputedRef<Set<number>>;
|
||||
hasRowSelection: ComputedRef<boolean>;
|
||||
}
|
||||
|
||||
export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
@@ -57,6 +59,8 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
getRowItem,
|
||||
quoteIdent,
|
||||
escapeVal,
|
||||
selectedRowIds,
|
||||
hasRowSelection,
|
||||
} = options;
|
||||
|
||||
function copyText(text: string) {
|
||||
@@ -94,6 +98,18 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
}
|
||||
|
||||
function copyRow() {
|
||||
if (hasRowSelection.value && selectedRowIds.value.size > 0) {
|
||||
const items = displayItems.value.filter((item) => selectedRowIds.value.has(item.id));
|
||||
const objects = items.map((item) => {
|
||||
const obj: Record<string, unknown> = {};
|
||||
columns.value.forEach((col, i) => {
|
||||
obj[col] = item.data[i];
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
copyText(JSON.stringify(objects, null, 2));
|
||||
return;
|
||||
}
|
||||
if (!contextCell.value) return;
|
||||
const item = getRowItem(contextCell.value.rowId);
|
||||
if (!item) return;
|
||||
@@ -109,6 +125,17 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
? (tableMeta.value.schema ? `${quoteIdent(tableMeta.value.schema)}.` : "") + quoteIdent(tableMeta.value.tableName)
|
||||
: "table_name";
|
||||
const cols = columns.value.map((c) => quoteIdent(c)).join(", ");
|
||||
|
||||
if (hasRowSelection.value && selectedRowIds.value.size > 0) {
|
||||
const items = displayItems.value.filter((item) => selectedRowIds.value.has(item.id));
|
||||
const statements = items.map((item) => {
|
||||
const vals = item.data.map((v) => escapeVal(v)).join(", ");
|
||||
return `INSERT INTO ${table} (${cols}) VALUES (${vals});`;
|
||||
});
|
||||
copyText(statements.join("\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
const range = selectedRange.value;
|
||||
if (range && range.startRow !== range.endRow) {
|
||||
const items = displayItems.value.slice(range.startRow, range.endRow + 1);
|
||||
@@ -119,6 +146,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
copyText(statements.join("\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!contextCell.value) return;
|
||||
const item = getRowItem(contextCell.value.rowId);
|
||||
if (!item) return;
|
||||
|
||||
@@ -37,6 +37,11 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
const selectionFocus = ref<CellPosition | null>(null);
|
||||
const isSelectingCells = ref(false);
|
||||
|
||||
const selectedRowIds = ref<Set<number>>(new Set());
|
||||
const lastClickedRowIndex = ref<number | null>(null);
|
||||
const hasRowSelection = computed(() => selectedRowIds.value.size > 0);
|
||||
const selectedRowCount = computed(() => selectedRowIds.value.size);
|
||||
|
||||
const selectedRange = computed<CellSelectionRange | null>(() => {
|
||||
if (!selectionAnchor.value || !selectionFocus.value) return null;
|
||||
return normalizeSelectionRange(selectionAnchor.value, selectionFocus.value);
|
||||
@@ -57,6 +62,11 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
isSelectingCells.value = false;
|
||||
}
|
||||
|
||||
function clearRowSelection() {
|
||||
selectedRowIds.value = new Set();
|
||||
lastClickedRowIndex.value = null;
|
||||
}
|
||||
|
||||
function selectSingleCell(rowIndex: number, colIndex: number) {
|
||||
const cell = { rowIndex, colIndex };
|
||||
selectionAnchor.value = cell;
|
||||
@@ -69,6 +79,36 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
selectionFocus.value = { rowIndex, colIndex: columns.value.length - 1 };
|
||||
}
|
||||
|
||||
function handleRowClick(rowIndex: number, rowId: number, event: MouseEvent) {
|
||||
const isMeta = event.metaKey || event.ctrlKey;
|
||||
const isShift = event.shiftKey;
|
||||
|
||||
clearCellSelection();
|
||||
|
||||
if (isMeta) {
|
||||
const next = new Set(selectedRowIds.value);
|
||||
if (next.has(rowId)) {
|
||||
next.delete(rowId);
|
||||
} else {
|
||||
next.add(rowId);
|
||||
}
|
||||
selectedRowIds.value = next;
|
||||
lastClickedRowIndex.value = rowIndex;
|
||||
} else if (isShift && lastClickedRowIndex.value !== null) {
|
||||
const start = Math.min(lastClickedRowIndex.value, rowIndex);
|
||||
const end = Math.max(lastClickedRowIndex.value, rowIndex);
|
||||
const next = new Set(selectedRowIds.value);
|
||||
for (let i = start; i <= end; i++) {
|
||||
const item = displayItems.value[i];
|
||||
if (item) next.add(item.id);
|
||||
}
|
||||
selectedRowIds.value = next;
|
||||
} else {
|
||||
selectedRowIds.value = new Set([rowId]);
|
||||
lastClickedRowIndex.value = rowIndex;
|
||||
}
|
||||
}
|
||||
|
||||
function finishCellSelection() {
|
||||
isSelectingCells.value = false;
|
||||
document.removeEventListener("mouseup", finishCellSelection);
|
||||
@@ -94,6 +134,28 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
selectionFocus.value = { rowIndex, colIndex };
|
||||
}
|
||||
|
||||
function handleDataCellMousedown(rowIndex: number, colIndex: number, rowId: number, event: MouseEvent) {
|
||||
if (event.button !== 0) return;
|
||||
if (editingCell.value) return;
|
||||
|
||||
const isMeta = event.metaKey || event.ctrlKey;
|
||||
const isShift = event.shiftKey;
|
||||
|
||||
if (isMeta || isShift) {
|
||||
event.preventDefault();
|
||||
focusGridWithoutScrolling();
|
||||
handleRowClick(rowIndex, rowId, event);
|
||||
return;
|
||||
}
|
||||
|
||||
clearRowSelection();
|
||||
beginCellSelection(rowIndex, colIndex, event);
|
||||
}
|
||||
|
||||
function isRowSelected(rowId: number): boolean {
|
||||
return selectedRowIds.value.has(rowId);
|
||||
}
|
||||
|
||||
function cellIsSelected(rowIndex: number, colIndex: number): boolean {
|
||||
return isCellInSelection(rowIndex, colIndex, selectedRange.value);
|
||||
}
|
||||
@@ -120,5 +182,13 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
extendCellSelection,
|
||||
cellIsSelected,
|
||||
selectedRangeStart,
|
||||
selectedRowIds,
|
||||
lastClickedRowIndex,
|
||||
hasRowSelection,
|
||||
selectedRowCount,
|
||||
clearRowSelection,
|
||||
handleRowClick,
|
||||
handleDataCellMousedown,
|
||||
isRowSelected,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -286,6 +286,11 @@ export default {
|
||||
addRow: "Add Row",
|
||||
cloneRow: "Clone Row",
|
||||
deleteRow: "Delete Row",
|
||||
deleteRows: "Delete {count} Rows",
|
||||
cloneRows: "Clone {count} Rows",
|
||||
copyRows: "Copy {count} Rows (JSON)",
|
||||
copyRowsInsert: "Copy {count} Rows as INSERT",
|
||||
selectedRows: "{count} rows selected",
|
||||
restoreRow: "Restore Row",
|
||||
statusClean: "Clean",
|
||||
statusNew: "New Items",
|
||||
@@ -800,6 +805,8 @@ export default {
|
||||
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?",
|
||||
deleteRowsMessage:
|
||||
"These {count} rows will be marked for deletion and removed from the database after saving. Continue?",
|
||||
deleteRowDetails: "Table: {table}",
|
||||
deleteRowDetailsNoTable: "Current result row",
|
||||
redisKeyDetails: "Redis key: {key}",
|
||||
|
||||
@@ -287,6 +287,11 @@ export default {
|
||||
addRow: "Agregar fila",
|
||||
cloneRow: "Clonar fila",
|
||||
deleteRow: "Eliminar fila",
|
||||
deleteRows: "Eliminar {count} filas",
|
||||
cloneRows: "Clonar {count} filas",
|
||||
copyRows: "Copiar {count} filas (JSON)",
|
||||
copyRowsInsert: "Copiar {count} filas como INSERT",
|
||||
selectedRows: "{count} filas seleccionadas",
|
||||
restoreRow: "Restaurar fila",
|
||||
statusClean: "Sin cambios",
|
||||
statusNew: "Elementos nuevos",
|
||||
@@ -803,6 +808,8 @@ export default {
|
||||
deleteConfirm: "Confirmar eliminación",
|
||||
deleteRowMessage:
|
||||
"Esta fila quedará marcada para eliminación y se borrará de la base de datos al guardar. ¿Continuar?",
|
||||
deleteRowsMessage:
|
||||
"Estas {count} filas se marcarán para eliminación y se eliminarán de la base de datos después de guardar. ¿Continuar?",
|
||||
deleteRowDetails: "Tabla: {table}",
|
||||
deleteRowDetailsNoTable: "Fila actual del resultado",
|
||||
redisKeyDetails: "Clave Redis: {key}",
|
||||
|
||||
@@ -285,6 +285,11 @@ export default {
|
||||
addRow: "新增行",
|
||||
cloneRow: "克隆行",
|
||||
deleteRow: "删除行",
|
||||
deleteRows: "删除 {count} 行",
|
||||
cloneRows: "克隆 {count} 行",
|
||||
copyRows: "复制 {count} 行 (JSON)",
|
||||
copyRowsInsert: "复制 {count} 行为 INSERT",
|
||||
selectedRows: "已选 {count} 行",
|
||||
restoreRow: "恢复行",
|
||||
statusClean: "未改",
|
||||
statusNew: "新增项",
|
||||
@@ -786,6 +791,7 @@ export default {
|
||||
deleteMessage: "此删除操作可能不可逆,确认要继续吗?",
|
||||
deleteConfirm: "确认删除",
|
||||
deleteRowMessage: "此行将被标记为删除,保存后会从数据库删除,确认要继续吗?",
|
||||
deleteRowsMessage: "这 {count} 行将被标记为删除,保存后会从数据库删除,确认要继续吗?",
|
||||
deleteRowDetails: "数据表:{table}",
|
||||
deleteRowDetailsNoTable: "当前结果集中的行",
|
||||
redisKeyDetails: "Redis key:{key}",
|
||||
|
||||
Reference in New Issue
Block a user