!78 feat: 表格支持编辑json、xml数据

Merge pull request !78 from zongyangleo/dev_1221
This commit is contained in:
Coder慌
2023-12-21 10:55:30 +00:00
committed by Gitee
5 changed files with 244 additions and 25 deletions

View File

@@ -263,10 +263,20 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
// pg类数据库会多一层schema
if (params.type == 'postgres' || params.type === 'dm') {
return [new TagTreeNode(`${params.id}.${params.db}.schema-menu`, 'schema', NodeTypePostgresScheamMenu).withParams(params).withIcon(SchemaIcon)];
const params = parentNode.params;
const { id, db } = params;
const schemaNames = await dbApi.pgSchemas.request({ id, db });
return schemaNames.map((sn: any) => {
// 将db变更为 db/schema;
const nParams = { ...params };
nParams.schema = sn;
nParams.db = nParams.db + '/' + sn;
nParams.dbs = schemaNames;
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresScheam).withParams(nParams).withIcon(SchemaIcon);
});
}
return [
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
@@ -274,23 +284,6 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
})
.withNodeClickFunc(nodeClickChangeDb);
// postgres schema模式菜单
const NodeTypePostgresScheamMenu = new NodeType(SqlExecNodeType.PgSchemaMenu)
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
const { id, db } = params;
const schemaNames = await dbApi.pgSchemas.request({ id, db });
return schemaNames.map((sn: any) => {
// 将db变更为 db/schema;
const nParams = { ...params };
nParams.schema = sn;
nParams.db = nParams.db + '/' + sn;
nParams.dbs = schemaNames;
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresScheam).withParams(nParams).withIcon(SchemaIcon);
});
})
.withNodeClickFunc(nodeClickChangeDb);
// postgres schema模式
const NodeTypePostgresScheam = new NodeType(SqlExecNodeType.PgSchema)
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {

View File

@@ -11,7 +11,7 @@
/>
<el-input
v-if="dataType == DataType.Number"
v-else-if="dataType == DataType.Number"
:ref="(el: any) => focus && el?.focus()"
@blur="emit('blur')"
class="w100 mb4"
@@ -23,7 +23,7 @@
/>
<el-date-picker
v-if="dataType == DataType.Date"
v-else-if="dataType == DataType.Date"
:ref="(el: any) => focus && el?.focus()"
@change="emit('blur')"
@blur="emit('blur')"
@@ -38,7 +38,7 @@
/>
<el-date-picker
v-if="dataType == DataType.DateTime"
v-else-if="dataType == DataType.DateTime"
:ref="(el: any) => focus && el?.focus()"
@change="emit('blur')"
@blur="emit('blur')"
@@ -53,7 +53,7 @@
/>
<el-time-picker
v-if="dataType == DataType.Time"
v-else-if="dataType == DataType.Time"
:ref="(el: any) => focus && el?.focus()"
@change="emit('blur')"
@blur="emit('blur')"

View File

@@ -161,6 +161,7 @@ import { dateStrFormat } from '@/common/utils/date';
import { useIntervalFn } from '@vueuse/core';
import { ColumnTypeSubscript, DataType, DbDialect, DbType, getDbDialect } from '../../dialect/index';
import ColumnFormItem from './ColumnFormItem.vue';
import TableDataEditorBox from '@/views/ops/db/component/table/DbTableDataEditorDialog';
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
@@ -334,6 +335,7 @@ const state = reactive({
loading: false,
tableHeight: '600px',
emptyText: '',
editorLang: 'string',
execTime: 0,
contextmenu: {
@@ -501,7 +503,7 @@ const cancelLoading = async () => {
* @param colIndex ci
*/
const canEdit = (rowIndex: number, colIndex: number) => {
return state.table && nowUpdateCell && nowUpdateCell.rowIndex == rowIndex && nowUpdateCell.colIndex == colIndex;
return state.table && nowUpdateCell?.rowIndex == rowIndex && nowUpdateCell?.colIndex == colIndex && state.editorLang === 'string';
};
/**
@@ -646,6 +648,29 @@ const onExportSql = async () => {
);
};
const getEditorLangByValue = (value: any) => {
if (typeof value === 'string') {
// 判断是否是json
try {
if (typeof JSON.parse(value) === 'object') {
return 'json';
}
} catch (e) {
/* empty */
}
// 判断是否是html
try {
const doc = new DOMParser().parseFromString(value, 'text/html');
if (Array.from(doc.body.childNodes).some((node) => node.nodeType === 1)) {
return 'html';
}
} catch (e) {
/* empty */
}
}
return 'string';
};
const onEnterEditMode = (rowData: any, column: any, rowIndex = 0, columnIndex = 0) => {
if (!state.table) {
return;
@@ -658,10 +683,24 @@ const onEnterEditMode = (rowData: any, column: any, rowIndex = 0, columnIndex =
oldValue: rowData[column.dataKey],
dataType: dbDialect.getDataType(column.columnType),
};
// 编辑器语言json、html、string目前支持json、html使用MonacoEditor编辑器
let editorLang = getEditorLangByValue(rowData[column.dataKey]);
state.editorLang = editorLang;
if (editorLang === 'html' || editorLang === 'json') {
TableDataEditorBox({
content: rowData[column.dataKey],
title: '编辑字段' + column.dataKey,
language: editorLang,
runSuccessCallback: (newVal: any) => {
rowData[column.dataKey] = newVal;
onExitEditMode(rowData, column, rowIndex);
},
});
}
};
const onExitEditMode = (rowData: any, column: any, rowIndex = 0) => {
console.trace('exit');
const oldValue = nowUpdateCell.oldValue;
const newValue = rowData[column.dataKey];

View File

@@ -0,0 +1,45 @@
import { h, render, VNode } from 'vue';
import SqlExecDialog from './DbTableDataEditorDialog.vue';
export type TableDataEditorProps = {
content: string;
title: string;
language: string;
runSuccessCallback?: Function;
cancelCallback?: Function;
};
const boxId = 'table-data-editor-id';
const renderBox = (): VNode => {
const props: TableDataEditorProps = {
content: '',
title: '',
language: '',
};
const container = document.createElement('div');
container.id = boxId;
// 创建 虚拟dom
const boxVNode = h(SqlExecDialog, props);
// 将虚拟dom渲染到 container dom 上
render(boxVNode, container);
// 最后将 container 追加到 body 上
document.body.appendChild(container);
return boxVNode;
};
let boxInstance: any;
const TableDataEditorBox = (props: TableDataEditorProps): void => {
if (boxInstance) {
const boxVue = boxInstance.component;
// 调用open方法显示弹框注意不能使用boxVue.ctx来调用组件函数build打包后ctx会获取不到
boxVue.exposed.open(props);
} else {
boxInstance = renderBox();
TableDataEditorBox(props);
}
};
export default TableDataEditorBox;

View File

@@ -0,0 +1,142 @@
<template>
<div>
<el-dialog :destroy-on-close="true" :title="state.title" v-model="dialogVisible" :show-close="false" width="800px" @close="cancel">
<monaco-editor height="600px" class="codesql" :language="state.language" v-model="contentValue" />
<template #footer>
<span class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button @click="submit" type="primary">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { toRefs, ref, nextTick, reactive } from 'vue';
import { ElDialog, ElButton, InputInstance, ElMessage } from 'element-plus';
// import base style
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { TableDataEditorProps } from './DbTableDataEditorDialog';
const remarkInputRef = ref<InputInstance>();
const state = reactive({
dialogVisible: false,
contentValue: '',
title: '',
language: '',
});
const { dialogVisible, contentValue } = toRefs(state);
let cancelCallback: any;
let runSuccessCallback: any;
let runSuccess: boolean = false;
function compressHTML(html: string) {
return (
html
.replace(/[\r\n\t]+/g, ' ') // 移除换行符和制表符
// .replace(/<!--[\s\S]*?-->/g, '') // 移除注释
.replace(/\s{2,}/g, ' ') // 合并多个空格为一个空格
.replace(/>\s+</g, '><')
); // 移除标签之间的空格
}
/**
* 执行sql
*/
const submit = async () => {
runSuccess = true;
if (runSuccessCallback) {
if (state.language === 'json') {
let val;
try {
val = JSON.parse(contentValue.value);
if (typeof val !== 'object') {
ElMessage.error('请输入正确的json');
return;
}
} catch (e) {
ElMessage.error('请输入正确的json');
return;
}
// 压缩json字符串
runSuccessCallback(JSON.stringify(val));
} else if (state.language === 'html') {
// 压缩html字符串
runSuccessCallback(compressHTML(contentValue.value));
} else {
runSuccessCallback(contentValue.value);
}
}
state.dialogVisible = false;
setTimeout(() => {
state.contentValue = '';
state.title = '';
runSuccessCallback = null;
runSuccess = false;
}, 200);
};
const cancel = () => {
state.dialogVisible = false;
// 没有执行成功,并且取消回调函数存在,则执行
if (!runSuccess && cancelCallback) {
cancelCallback();
}
setTimeout(() => {
state.contentValue = '';
state.title = '';
cancelCallback = null;
runSuccess = false;
}, 200);
};
const formatXML = function (xml: string, tab?: string) {
let formatted = '',
indent = '';
tab = tab || ' ';
xml.split(/>\s*</).forEach(function (node) {
if (node.match(/^\/\w/)) indent = indent.substring(tab!.length);
formatted += indent + '<' + node + '>\r\n';
if (node.match(/^<?\w[^>]*[^\/]$/)) indent += tab;
});
return formatted.substring(1, formatted.length - 3);
};
const open = (props: TableDataEditorProps) => {
cancelCallback = props.cancelCallback;
runSuccessCallback = props.runSuccessCallback;
// 格式化输出json
if (props.language === 'json') {
try {
state.contentValue = JSON.stringify(JSON.parse(props.content), null, '\t');
} catch (e) {
state.contentValue = 'json格式字符串错误: ' + props.content;
}
}
// 格式化输出html
if (props.language === 'html') {
state.contentValue = formatXML(props.content);
}
state.title = props.title;
state.language = props.language;
state.dialogVisible = true;
nextTick(() => {
setTimeout(() => {
remarkInputRef.value?.focus();
});
});
};
defineExpose({ open });
</script>
<style lang="scss">
.codesql {
font-size: 9pt;
font-weight: 600;
}
</style>