refactor: 数据库表使用虚拟表替换,提升数据量较大时的渲染速度

This commit is contained in:
meilin.huang
2023-11-17 13:31:28 +08:00
parent 43230267b6
commit a40ec21a05
5 changed files with 552 additions and 306 deletions

View File

@@ -366,7 +366,7 @@ const state = reactive({
activeName: '',
reloadStatus: false,
tabs,
dataTabsTableHeight: '600',
dataTabsTableHeight: 600,
editorHeight: '600',
tablesOpHeight: '600',
});
@@ -388,7 +388,7 @@ onBeforeUnmount(() => {
*/
const setHeight = () => {
state.editorHeight = window.innerHeight - 500 + 'px';
state.dataTabsTableHeight = window.innerHeight - 255 + 'px';
state.dataTabsTableHeight = window.innerHeight - 255;
state.tablesOpHeight = window.innerHeight - 220 + 'px';
};

View File

@@ -50,10 +50,7 @@
<div class="mt5">
<el-row>
<el-link v-if="table" @click="onDeleteData()" class="ml5" type="danger" icon="delete" :underline="false"></el-link>
<span v-if="execRes.data.length > 0">
<el-divider direction="vertical" border-style="dashed" />
<el-link type="success" :underline="false" @click="exportData"><span style="font-size: 12px">导出</span></el-link>
</span>
<span v-if="hasUpdatedFileds">
@@ -77,6 +74,7 @@
empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
@selection-change="onDataSelectionChange"
@change-updated-field="changeUpdatedField"
@data-delete="onDeleteData"
></db-table-data>
</div>
</div>
@@ -149,7 +147,7 @@ const state = reactive({
hasUpdatedFileds: false,
});
const { tableDataHeight, execRes, table, loading, hasUpdatedFileds } = toRefs(state);
const { tableDataHeight, execRes, loading, hasUpdatedFileds } = toRefs(state);
watch(
() => props.editorHeight,
@@ -529,27 +527,20 @@ const onDataSelectionChange = (datas: []) => {
state.selectionDatas = datas;
};
const changeUpdatedField = (updatedFields: []) => {
const changeUpdatedField = (updatedFields: any) => {
// 如果存在要更新字段,则显示提交和取消按钮
state.hasUpdatedFileds = updatedFields && updatedFields.length > 0;
state.hasUpdatedFileds = updatedFields && updatedFields.size > 0;
};
/**
* 执行删除数据事件
* 数据删除事件
*/
const onDeleteData = async () => {
const deleteDatas = state.selectionDatas;
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
const onDeleteData = async (deleteDatas: any) => {
const db = props.dbName;
const dbInst = getNowDbInst();
const primaryKey = await dbInst.loadTableColumn(db, state.table);
const primaryKeyColumnName = primaryKey.columnName;
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
state.execRes.data = state.execRes.data.filter(
(d: any) => !(deleteDatas.findIndex((x: any) => x[primaryKeyColumnName] == d[primaryKeyColumnName]) != -1)
);
state.selectionDatas = [];
});
state.execRes.data = state.execRes.data.filter((d: any) => !(deleteDatas.findIndex((x: any) => x[primaryKeyColumnName] == d[primaryKeyColumnName]) != -1));
};
const submitUpdateFields = () => {
@@ -572,10 +563,6 @@ const cancelUpdateFields = () => {
text-decoration: none;
}
.update_field_active {
background-color: var(--el-color-success);
}
.editor-move-resize {
cursor: n-resize;
height: 3px;

View File

@@ -1,51 +1,122 @@
<template>
<div>
<el-table
@cell-dblclick="(row: any, column: any, cell: any, event: any) => cellClick(row, column, cell)"
@sort-change="(sort: any) => onTableSortChange(sort)"
@selection-change="onDataSelectionChange"
:data="datas"
size="small"
:max-height="tableHeight"
v-loading="loading"
element-loading-text="查询中..."
:empty-text="emptyText"
highlight-current-row
stripe
border
class="mt5"
>
<el-table-column v-if="datas.length > 0 && table" type="selection" width="35" />
<template v-for="(item, index) in columns">
<el-table-column
min-width="100"
:width="DbInst.flexColumnWidth(item.columnName, datas)"
align="center"
v-if="item.show"
:key="index"
:prop="item.columnName"
:label="item.columnName"
show-overflow-tooltip
:sortable="sortable"
<div class="db-table-data mt5" :style="{ height: `${tableHeight}px` }">
<el-auto-resizer>
<template #default="{ height, width }">
<el-table-v2
ref="tableRef"
:header-height="32"
:row-height="32"
:row-class="rowClass"
:columns="state.columns"
:data="datas"
:width="width"
:height="height"
fixed
:row-event-handlers="rowEventHandlers"
>
<template #header v-if="showColumnTip">
<el-tooltip :show-after="500" raw-content placement="top">
<template #content> {{ getColumnTip(item) }} </template>
{{ item.columnName }}
</el-tooltip>
<template #header="{ columns }">
<div v-for="(column, i) in columns" :key="i">
<div
:style="{
width: `${column.width}px`,
height: '100%',
lineHeight: '32px',
textAlign: 'center',
borderRight: 'var(--el-table-border)',
}"
>
<!-- 行号列表头 -->
<div v-if="column.key == rowNoColumn.key || !showColumnTip">
<el-text tag="b"> {{ column.title }} </el-text>
</div>
<div v-else @contextmenu="headerContextmenuClick($event, column)">
<div v-if="showColumnTip" @mouseover="column.showSetting = true" @mouseleave="column.showSetting = false">
<el-tooltip :show-after="500" raw-content placement="top">
<template #content> {{ getColumnTip(column) }} </template>
<el-text tag="b" style="cursor: pointer"> {{ column.title }} </el-text>
</el-tooltip>
<span>
<SvgIcon
color="var(--el-color-primary)"
v-if="column.title == nowSortColumn?.columnName"
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
></SvgIcon>
</span>
</div>
<div v-else>
<el-text tag="b" style="cursor: pointer"> {{ column.title }} </el-text>
</div>
</div>
</div>
</div>
</template>
</el-table-column>
<template #cell="{ rowData, column, rowIndex, columnIndex }">
<div style="width: 100%; height: 100%; line-height: 32px">
<!-- 行号列 -->
<div v-if="column.key == 'tableDataRowNo'">
<el-text tag="b" size="small">
{{ rowIndex + 1 }}
</el-text>
</div>
<!-- 数据列 -->
<div v-else @dblclick="onEnterEditMode($event.target, rowData, column, rowIndex, columnIndex)">
<div v-if="canEdit(rowIndex, columnIndex)">
<el-input
:ref="(el: any) => el?.focus()"
@blur="onExitEditMode(rowData, column, rowIndex)"
class="w100"
input-style="text-align: center"
size="small"
v-model="rowData[column.dataKey!]"
></el-input>
</div>
<div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active' : ''">
<el-text :title="rowData[column.dataKey!]" size="small" truncated>
{{ rowData[column.dataKey!] }}
</el-text>
</div>
</div>
</div>
</template>
<template v-if="loading" #overlay>
<div class="el-loading-mask" style="display: flex; align-items: center; justify-content: center">
<SvgIcon name="loading" color="var(--el-color-primary)" :size="26" />
</div>
</template>
<template #empty>
<div style="text-align: center">
<el-empty :description="state.emptyText" :image-size="100" />
</div>
</template>
</el-table-v2>
</template>
</el-table>
</el-auto-resizer>
<el-dialog @close="state.genSqlDialog.visible = false" v-model="state.genSqlDialog.visible" :title="state.genSqlDialog.title" width="1000px">
<el-input v-model="state.genSqlDialog.sql" type="textarea" rows="20" />
</el-dialog>
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="headerContextmenuRef" />
</div>
</template>
<script lang="ts" setup>
import { onMounted, watch, reactive, toRefs } from 'vue';
import { DbInst, UpdateFieldsMeta, FieldsMeta } from '@/views/ops/db/db';
import { ref, onMounted, watch, reactive, toRefs } from 'vue';
import { ElInput } from 'element-plus';
import { DbInst } from '@/views/ops/db/db';
import Contextmenu from '@/components/contextmenu/index.vue';
import { ContextmenuItem } from '@/components/contextmenu/index';
import SvgIcon from '@/components/svgIcon/index.vue';
const emits = defineEmits(['sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
const props = defineProps({
dbId: {
@@ -87,50 +158,348 @@ const props = defineProps({
default: false,
},
height: {
type: String,
default: '600',
type: Number,
default: 600,
},
});
const headerContextmenuRef = ref();
const tableRef = ref();
const cmHeaderAsc = new ContextmenuItem('asc', '升序').withIcon('top').withOnClick((data: any) => {
onTableSortChange({ columnName: data.dataKey, order: 'asc' });
});
const cmHeaderDesc = new ContextmenuItem('desc', '降序').withIcon('bottom').withOnClick((data: any) => {
onTableSortChange({ columnName: data.dataKey, order: 'desc' });
});
const cmDataDel = new ContextmenuItem('desc', '删除')
.withIcon('delete')
.withOnClick(() => onDeleteData())
.withHideFunc(() => {
return state.table == '';
});
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
.withIcon('document')
.withOnClick(() => onGenerateInsertSql())
.withHideFunc(() => {
return state.table == '';
});
const cmDataGenJson = new ContextmenuItem('genJson', '生成JSON').withIcon('document').withOnClick(() => onGenerateJson());
class NowUpdateCell {
rowIndex: number;
colIndex: number;
oldValue: any;
}
class UpdatedRow {
/**
* 主键值
*/
primaryValue: any;
/**
* 行数据
*/
rowData: any;
/**
* 修改到的列信息, columnName -> tablecelldata
*/
columnsMap: Map<string, TableCellData> = new Map();
}
class TableCellData {
/**
* 旧值
*/
oldValue: any;
}
let nowSortColumn = null as any;
// 当前正在更新的单元格
let nowUpdateCell: NowUpdateCell = null as any;
// 选中的数据, key->rowIndex value->primaryKeyValue
const selectionRowsMap: Map<number, any> = new Map();
// 更新单元格 key-> rowIndex value -> 更新行
const cellUpdateMap: Map<number, UpdatedRow> = new Map();
const state = reactive({
dbId: 0, // 当前选中操作的数据库实例
dbType: '',
db: '', // 数据库名
table: '', // 当前的表名
datas: [],
columns: [],
columns: [] as any,
sortable: false,
loading: false,
selectionDatas: [] as any,
showColumnTip: false,
tableHeight: '600',
tableHeight: 600,
emptyText: '',
updatedFields: [] as UpdateFieldsMeta[], // 各个tab表被修改的字段信息
contextmenu: {
dropdown: {
x: 0,
y: 0,
},
items: [] as ContextmenuItem[],
},
genSqlDialog: {
title: 'SQL',
visible: false,
sql: '',
},
});
const { tableHeight, datas } = toRefs(state);
watch(props, (newValue: any) => {
setState(newValue);
});
/**
* 行号字段列
*/
const rowNoColumn = {
title: 'No.',
key: 'tableDataRowNo',
dataKey: 'tableDataRowNo',
width: 45,
fixed: true,
align: 'center',
headerClass: 'table-data-cell',
class: 'table-data-cell',
};
watch(
() => props.data,
(newValue: any) => {
setTableData(newValue);
}
);
watch(
() => props.columns,
(newValue: any) => {
setTableColumns(newValue);
},
{
deep: true,
}
);
watch(
() => props.table,
(newValue: any) => {
state.table = newValue;
}
);
watch(
() => props.height,
(newValue: any) => {
state.tableHeight = newValue;
}
);
watch(
() => props.loading,
(newValue: any) => {
state.loading = newValue;
}
);
onMounted(async () => {
console.log('in DbTable mounted');
setState(props);
});
state.tableHeight = props.height;
state.sortable = props.sortable as any;
state.loading = props.loading;
state.showColumnTip = props.showColumnTip;
state.emptyText = props.emptyText;
const setState = (props: any) => {
state.dbId = props.dbId;
state.dbType = props.dbType;
state.db = props.db;
state.table = props.table;
state.datas = props.data;
state.tableHeight = props.height;
state.sortable = props.sortable;
state.loading = props.loading;
state.columns = props.columns;
state.showColumnTip = props.showColumnTip;
state.emptyText = props.emptyText;
});
const setTableData = (datas: any) => {
console.log('set table datas', props);
tableRef.value.scrollTo({ scrollLeft: 0, scrollTop: 0 });
selectionRowsMap.clear();
cellUpdateMap.clear();
state.datas = datas;
setTableColumns(props.columns);
};
const setTableColumns = (columns: any) => {
state.columns = columns.map((x: any) => {
const columnName = x.columnName;
return {
...x,
key: columnName,
dataKey: columnName,
width: DbInst.flexColumnWidth(columnName, state.datas),
title: columnName,
align: 'center',
headerClass: 'table-data-cell',
class: 'table-data-cell',
sortable: true,
hidden: !x.show,
};
});
state.columns.unshift(rowNoColumn);
};
/**
* 当前单元格是否允许编辑
* @param rowIndex ri
* @param colIndex ci
*/
const canEdit = (rowIndex: number, colIndex: number) => {
return state.table && nowUpdateCell && nowUpdateCell.rowIndex == rowIndex && nowUpdateCell.colIndex == colIndex;
};
const isUpdated = (rowIndex: number, columnName: string) => {
return cellUpdateMap.get(rowIndex)?.columnsMap.get(columnName);
};
/**
* 判断当前行是否被选中
* @param rowIndex
*/
const isSelection = (rowIndex: number): boolean => {
return selectionRowsMap.get(rowIndex);
};
/**
* 选中指定行
* @param rowIndex
* @param rowData
* @param isMultiple 是否允许多选
*/
const selectionRow = (rowIndex: number, rowData: any, isMultiple = false) => {
if (isMultiple) {
// 如果重复点击,则取消改选中数据
if (selectionRowsMap.get(rowIndex)) {
selectionRowsMap.delete(rowIndex);
triggerRefresh();
return;
}
} else {
selectionRowsMap.clear();
}
selectionRowsMap.set(rowIndex, rowData);
triggerRefresh();
};
/**
* 行事件处理
*/
const rowEventHandlers = {
onClick: (e: any) => {
const event = e.event;
const rowIndex = e.rowIndex;
const rowData = e.rowData;
// 按住ctrl点击则新建标签页打开, metaKey对应mac command键
if (event.ctrlKey || event.metaKey) {
selectionRow(rowIndex, rowData, true);
return;
}
selectionRow(rowIndex, rowData);
},
onContextmenu: (e: any) => {
dataContextmenuClick(e.event, e.rowIndex, e.rowData);
},
};
const headerContextmenuClick = (event: any, data: any) => {
event.preventDefault(); // 阻止默认的右击菜单行为
const { clientX, clientY } = event;
state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY;
state.contextmenu.items = [cmHeaderAsc, cmHeaderDesc];
headerContextmenuRef.value.openContextmenu(data);
};
const dataContextmenuClick = (event: any, rowIndex: number, data: any) => {
event.preventDefault(); // 阻止默认的右击菜单行为
// 当前行未选中,则单行选中该行
if (!isSelection(rowIndex)) {
selectionRow(rowIndex, data);
}
const { clientX, clientY } = event;
state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY;
state.contextmenu.items = [cmDataDel, cmDataGenInsertSql, cmDataGenJson];
headerContextmenuRef.value.openContextmenu(data);
};
const onEnterEditMode = (el: any, rowData: any, column: any, rowIndex = 0, columnIndex = 0) => {
if (!state.table) {
return;
}
triggerRefresh();
const oldVal = rowData[column.dataKey];
nowUpdateCell = {
rowIndex: rowIndex,
colIndex: columnIndex,
oldValue: oldVal,
};
};
const onExitEditMode = (rowData: any, column: any, rowIndex = 0) => {
const oldValue = nowUpdateCell.oldValue;
const newValue = rowData[column.dataKey];
// 未改变单元格值
if (oldValue == newValue) {
nowUpdateCell = null as any;
triggerRefresh();
return;
}
let updatedRow = cellUpdateMap.get(rowIndex);
if (!updatedRow) {
updatedRow = new UpdatedRow();
updatedRow.rowData = rowData;
cellUpdateMap.set(rowIndex, updatedRow);
}
const columnName = column.dataKey;
let cellData = updatedRow.columnsMap.get(columnName);
if (cellData) {
// 多次修改情况,可能又修改回原值,则移除该修改单元格
if (cellData.oldValue == newValue) {
cellUpdateMap.delete(rowIndex);
}
} else {
cellData = new TableCellData();
cellData.oldValue = oldValue;
updatedRow.columnsMap.set(columnName, cellData);
}
nowUpdateCell = null as any;
triggerRefresh();
changeUpdatedField();
};
const rowClass = (row: any) => {
if (isSelection(row.rowIndex)) {
return 'data-selection';
}
if (row.rowIndex % 2 != 0) {
return 'data-spacing';
}
return '';
};
const getColumnTip = (column: any) => {
@@ -138,184 +507,127 @@ const getColumnTip = (column: any) => {
return `${column.columnType} ${comment ? ' | ' + comment : ''}`;
};
/**
* 触发响应式实时刷新,否则需要滑动或移动才能使样式实时生效
*/
const triggerRefresh = () => {
// 改变columns等属性值才能触发slot中的if条件等, 暂不知为啥
if (state.columns[0].opTimes) {
state.columns[0].opTimes = state.columns[0].opTimes + 1;
} else {
state.columns[0].opTimes = 1;
}
};
/**
* 表排序字段变更
*/
const onTableSortChange = async (sort: any) => {
if (!sort.prop) {
return;
}
nowSortColumn = sort;
cancelUpdateFields();
emits('sortChange', sort);
};
const onDataSelectionChange = (datas: []) => {
state.selectionDatas = datas;
emits('selectionChange', datas);
/**
* 执行删除数据事件
*/
const onDeleteData = async () => {
const deleteDatas = Array.from(selectionRowsMap.values());
const db = state.db;
const dbInst = getNowDbInst();
dbInst.promptExeSql(db, await dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas as any), null, () => {
emits('dataDelete', deleteDatas);
});
};
// 监听单元格点击事件
const cellClick = (row: any, column: any, cell: any) => {
const property = column.property;
// 如果当前操作的表名不存在 或者 当前列的property不存在(如多选框),则不允许修改当前单元格内容
if (!state.table || !property) {
return;
}
let div: HTMLElement = cell.children[0];
if (div && div.tagName === 'DIV') {
// 转为字符串比较,可能存在数字等
let text = (row[property] || row[property] == 0 ? row[property] : '') + '';
let input = document.createElement('input');
input.setAttribute('value', text);
// 将表格width也赋值于输入框避免输入框长度超过表格长度
input.setAttribute('style', 'height:23px;text-align:center;border:none;' + div.getAttribute('style'));
cell.replaceChildren(input);
input.focus();
input.addEventListener('blur', async () => {
row[property] = input.value;
cell.replaceChildren(div);
if (input.value !== text) {
let currentUpdatedFields = state.updatedFields;
const dbInst = getNowDbInst();
// 主键
const primaryKey = await dbInst.loadTableColumn(state.db, state.table);
const primaryKeyValue = row[primaryKey.columnName];
// 更新字段列信息
const updateColumn = await dbInst.loadTableColumn(state.db, state.table, property);
const newField = {
div,
row,
fieldName: property,
fieldType: updateColumn.columnType,
oldValue: text,
newValue: input.value,
} as FieldsMeta;
// 被修改的字段
const primaryKeyFields = currentUpdatedFields.filter((meta) => meta.primaryKey === primaryKeyValue);
let hasKey = false;
if (primaryKeyFields.length <= 0) {
primaryKeyFields[0] = {
primaryKey: primaryKeyValue,
primaryKeyName: primaryKey.columnName,
primaryKeyType: primaryKey.columnType,
fields: [newField],
};
} else {
hasKey = true;
let hasField = primaryKeyFields[0].fields.some((a) => {
if (a.fieldName === newField.fieldName) {
a.newValue = newField.newValue;
}
return a.fieldName === newField.fieldName;
});
if (!hasField) {
primaryKeyFields[0].fields.push(newField);
}
}
let fields = primaryKeyFields[0].fields;
const fieldsParam = fields.filter((a) => {
if (a.fieldName === column.property) {
a.newValue = input.value;
}
return a.fieldName === column.property;
});
const field = (fieldsParam.length > 0 && fieldsParam[0]) || ({} as FieldsMeta);
if (field.oldValue === input.value) {
// 新值=旧值
// 删除数据
div.classList.remove('update_field_active');
let delIndex: number[] = [];
currentUpdatedFields.forEach((a, i) => {
if (a.primaryKey === primaryKeyValue) {
a.fields = a.fields && a.fields.length > 0 ? a.fields.filter((f) => f.fieldName !== column.property) : [];
a.fields.length <= 0 && delIndex.push(i);
}
});
delIndex.forEach((i) => delete currentUpdatedFields[i]);
currentUpdatedFields = currentUpdatedFields.filter((a) => a);
} else {
// 新增数据
div.classList.add('update_field_active');
if (hasKey) {
currentUpdatedFields.forEach((value, index, array) => {
if (value.primaryKey === primaryKeyValue) {
array[index].fields = fields;
}
});
} else {
currentUpdatedFields.push({
primaryKey: primaryKeyValue,
primaryKeyName: primaryKey.columnName,
primaryKeyType: primaryKey.columnType,
fields,
});
}
}
state.updatedFields = currentUpdatedFields;
changeUpdatedField();
}
});
}
const onGenerateInsertSql = async () => {
const selectionDatas = Array.from(selectionRowsMap.values());
state.genSqlDialog.sql = await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas);
state.genSqlDialog.title = 'SQL';
state.genSqlDialog.visible = true;
};
const submitUpdateFields = () => {
const dbInst = DbInst.getInst(state.dbId);
let currentUpdatedFields = state.updatedFields;
if (currentUpdatedFields.length <= 0) {
const onGenerateJson = async () => {
const selectionDatas = Array.from(selectionRowsMap.values());
// 按列字段重新排序对象key
const jsonObj = [];
for (let selectionData of selectionDatas) {
let obj = {};
for (let column of state.columns) {
obj[column.title] = selectionData[column.dataKey];
}
jsonObj.push(obj);
}
state.genSqlDialog.sql = JSON.stringify(jsonObj, null, 4);
state.genSqlDialog.title = 'JSON';
state.genSqlDialog.visible = true;
};
const submitUpdateFields = async () => {
const dbInst = getNowDbInst();
if (cellUpdateMap.size == 0) {
return;
}
const db = state.db;
let res = '';
let divs: HTMLElement[] = [];
currentUpdatedFields.forEach((a) => {
for (let updateRow of cellUpdateMap.values()) {
let sql = `UPDATE ${dbInst.wrapName(state.table)} SET `;
let primaryKey = a.primaryKey;
let primaryKeyType = a.primaryKeyType;
let primaryKeyName = a.primaryKeyName;
a.fields.forEach((f) => {
sql += ` ${dbInst.wrapName(f.fieldName)} = ${DbInst.wrapColumnValue(f.fieldType, f.newValue)},`;
// 如果修改的字段是主键
if (f.fieldName === primaryKeyName) {
primaryKey = f.oldValue;
const rowData = updateRow.rowData;
// 主键列信息
const primaryKey = await dbInst.loadTableColumn(db, state.table);
let primaryKeyType = primaryKey.columnType;
let primaryKeyName = primaryKey.columnName;
let primaryKeyValue = rowData[primaryKeyName];
for (let k of updateRow.columnsMap.keys()) {
const v = updateRow.columnsMap.get(k);
if (!v) {
continue;
}
divs.push(f.div);
});
// 更新字段列信息
const updateColumn = await dbInst.loadTableColumn(db, state.table, k);
sql += ` ${dbInst.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, rowData[k])},`;
// 如果修改的字段是主键
if (k === primaryKeyName) {
primaryKeyValue = v.oldValue;
}
}
sql = sql.substring(0, sql.length - 1);
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKey)} ;`;
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`;
res += sql;
});
}
dbInst.promptExeSql(
db,
res,
() => {},
() => {
currentUpdatedFields = [];
divs.forEach((a) => {
a.classList.remove('update_field_active');
});
state.updatedFields = [];
triggerRefresh();
cellUpdateMap.clear();
changeUpdatedField();
}
);
};
const cancelUpdateFields = () => {
state.updatedFields.forEach((a: any) => {
a.fields.forEach((b: any) => {
b.div.classList.remove('update_field_active');
b.row[b.fieldName] = b.oldValue;
const updateRows = cellUpdateMap.values();
// 恢复原值
for (let updateRow of updateRows) {
const rowData = updateRow.rowData;
updateRow.columnsMap.forEach((v: TableCellData, k: string) => {
rowData[k] = v.oldValue;
});
});
state.updatedFields = [];
}
cellUpdateMap.clear();
changeUpdatedField();
};
const changeUpdatedField = () => {
emits('changeUpdatedField', state.updatedFields);
emits('changeUpdatedField', cellUpdateMap);
};
const getNowDbInst = () => {
@@ -328,8 +640,22 @@ defineExpose({
});
</script>
<style lang="scss">
.update_field_active {
background-color: var(--el-color-success);
<style>
.db-table-data {
.table-data-cell {
padding: 0 2px;
font-size: 12px;
border-right: var(--el-table-border);
}
.data-selection {
background-color: var(--el-color-success-light-8);
}
.data-spacing {
background-color: var(--el-fill-color-lighter);
}
.update_field_active {
background-color: var(--el-color-success);
}
}
</style>

View File

@@ -30,19 +30,11 @@
<el-link @click="onShowAddDataDialog()" type="primary" icon="plus" :underline="false"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link @click="onDeleteData()" type="danger" icon="delete" :underline="false"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip :show-after="500" class="box-item" effect="dark" content="commit" placement="top">
<el-link @click="onCommit()" type="success" icon="CircleCheck" :underline="false"> </el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip :show-after="500" class="box-item" effect="dark" content="生成insert sql" placement="top">
<el-link @click="onGenerateInsertSql()" type="success" :underline="false">gi</el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip :show-after="500" class="box-item" effect="dark" content="导出当前页的csv文件" placement="top">
<el-link type="success" :underline="false" @click="exportData"><span class="f12">导出</span></el-link>
</el-tooltip>
@@ -118,6 +110,7 @@
@sort-change="(sort: any) => onTableSortChange(sort)"
@selection-change="onDataSelectionChange"
@change-updated-field="changeUpdatedField"
@data-delete="onRefresh"
></db-table-data>
<el-row type="flex" class="mt5" justify="center">
@@ -187,10 +180,6 @@
</span>
</template>
</el-dialog>
<el-dialog @close="state.genSqlDialog.visible = false" v-model="state.genSqlDialog.visible" title="SQL" width="1000px">
<el-input v-model="state.genSqlDialog.sql" type="textarea" rows="20" />
</el-dialog>
</div>
</template>
@@ -221,8 +210,8 @@ const props = defineProps({
required: true,
},
tableHeight: {
type: [String],
default: '600',
type: [Number],
default: 600,
},
});
@@ -258,11 +247,7 @@ const state = reactive({
placeholder: '',
visible: false,
},
genSqlDialog: {
visible: false,
sql: '',
},
tableHeight: '600',
tableHeight: 600,
hasUpdatedFileds: false,
});
@@ -446,11 +431,8 @@ const onSelectByCondition = async () => {
* 表排序字段变更
*/
const onTableSortChange = async (sort: any) => {
if (!sort.prop) {
return;
}
const sortType = sort.order == 'descending' ? 'DESC' : 'ASC';
state.orderBy = `ORDER BY ${sort.prop} ${sortType}`;
const sortType = sort.order == 'desc' ? 'DESC' : 'ASC';
state.orderBy = `ORDER BY ${sort.columnName} ${sortType}`;
await onRefresh();
};
@@ -458,28 +440,9 @@ const onDataSelectionChange = (datas: []) => {
state.selectionDatas = datas;
};
const changeUpdatedField = (updatedFields: []) => {
const changeUpdatedField = (updatedFields: any) => {
// 如果存在要更新字段,则显示提交和取消按钮
state.hasUpdatedFileds = updatedFields && updatedFields.length > 0;
};
/**
* 执行删除数据事件
*/
const onDeleteData = async () => {
const deleteDatas = state.selectionDatas;
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
const db = props.dbName;
const dbInst = getNowDbInst();
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, props.tableName, deleteDatas), null, () => {
onRefresh();
});
};
const onGenerateInsertSql = async () => {
isTrue(state.selectionDatas && state.selectionDatas.length > 0, '请先选择数据');
state.genSqlDialog.sql = getNowDbInst().genInsertSql(props.dbName, props.tableName, state.selectionDatas);
state.genSqlDialog.visible = true;
state.hasUpdatedFileds = updatedFields && updatedFields.size > 0;
};
const submitUpdateFields = () => {
@@ -530,8 +493,4 @@ const addRow = async () => {
};
</script>
<style lang="scss">
.update_field_active {
background-color: var(--el-color-success);
}
</style>
<style lang="scss"></style>

View File

@@ -232,11 +232,12 @@ export class DbInst {
* @param table 表名
* @param datas 要生成的数据
*/
genInsertSql(dbName: string, table: string, datas: any[]): string {
async genInsertSql(dbName: string, table: string, datas: any[]) {
if (!datas) {
return '';
}
const columns = this.getDb(dbName).getColumns(table);
const columns = await this.loadColumns(dbName, table);
const sqls = [];
for (let data of datas) {
let colNames = [];
@@ -256,8 +257,8 @@ export class DbInst {
* @param table 表名
* @param datas 要删除的记录
*/
genDeleteByPrimaryKeysSql(db: string, table: string, datas: any[]) {
const primaryKey = this.getDb(db).getColumn(table);
async genDeleteByPrimaryKeysSql(db: string, table: string, datas: any[]) {
const primaryKey = await this.loadTableColumn(db, table);
const primaryKeyColumnName = primaryKey.columnName;
const ids = datas.map((d: any) => `${DbInst.wrapColumnValue(primaryKey.columnType, d[primaryKeyColumnName])}`).join(',');
return `DELETE FROM ${this.wrapName(table)} WHERE ${this.wrapName(primaryKeyColumnName)} IN (${ids})`;
@@ -389,7 +390,7 @@ export class DbInst {
}
// 获取列名称的长度 加上排序图标长度
const columnWidth: number = getTextWidth(prop) + 40;
const columnWidth: number = getTextWidth(prop) + 23;
// prop为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
return columnWidth;
@@ -531,33 +532,6 @@ export class TabInfo {
}
}
/** 修改表字段所需数据 */
export type UpdateFieldsMeta = {
// 主键值
primaryKey: string;
// 主键名
primaryKeyName: string;
// 主键类型
primaryKeyType: string;
// 新值
fields: FieldsMeta[];
};
export type FieldsMeta = {
// 字段所在div
div: HTMLElement;
// 字段名
fieldName: string;
// 字段所在的表格行数据
row: any;
// 字段类型
fieldType: string;
// 原值
oldValue: string;
// 新值
newValue: string;
};
/**
* 注册数据库表、字段等信息提示
*