mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-03 16:00:25 +08:00
refactor: 虚拟表格与contextmenu菜单优化
This commit is contained in:
@@ -101,7 +101,7 @@ const refreshWatermarkTime = () => {
|
|||||||
} else {
|
} else {
|
||||||
clearInterval(refreshWatermarkTimeInterval);
|
clearInterval(refreshWatermarkTimeInterval);
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 60000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 页面销毁时,关闭监听布局配置
|
// 页面销毁时,关闭监听布局配置
|
||||||
|
|||||||
17
mayfly_go_web/src/common/use.ts
Normal file
17
mayfly_go_web/src/common/use.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const vw = ref(document.documentElement.clientWidth);
|
||||||
|
const vh = ref(document.documentElement.clientHeight);
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
vw.value = document.documentElement.clientWidth;
|
||||||
|
vh.value = document.documentElement.clientHeight;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取视图宽高
|
||||||
|
* @returns 视图宽高
|
||||||
|
*/
|
||||||
|
export function useViewport() {
|
||||||
|
return { vw, vh };
|
||||||
|
}
|
||||||
@@ -29,15 +29,19 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
|
|||||||
cvsData.push(dataValueArr);
|
cvsData.push(dataValueArr);
|
||||||
}
|
}
|
||||||
const csvString = cvsData.map((e) => e.join(',')).join('\n');
|
const csvString = cvsData.map((e) => e.join(',')).join('\n');
|
||||||
|
exportFile(`${filename}.csv`, csvString);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportFile(filename: string, content: string) {
|
||||||
// 导出
|
// 导出
|
||||||
let link = document.createElement('a');
|
let link = document.createElement('a');
|
||||||
let exportContent = '\uFEFF';
|
let exportContent = '\uFEFF';
|
||||||
let blob = new Blob([exportContent + csvString], {
|
let blob = new Blob([exportContent + content], {
|
||||||
type: 'text/plain;charset=utf-8',
|
type: 'text/plain;charset=utf-8',
|
||||||
});
|
});
|
||||||
link.id = 'download-csv';
|
link.id = 'download-file';
|
||||||
link.setAttribute('href', URL.createObjectURL(blob));
|
link.setAttribute('href', URL.createObjectURL(blob));
|
||||||
link.setAttribute('download', `${filename}.csv`);
|
link.setAttribute('download', `${filename}`);
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export class ContextmenuItem {
|
import Contextmenu from './index.vue';
|
||||||
|
|
||||||
|
class ContextmenuItem {
|
||||||
clickId: any;
|
clickId: any;
|
||||||
|
|
||||||
txt: string;
|
txt: string;
|
||||||
@@ -53,3 +55,5 @@ export class ContextmenuItem {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { Contextmenu, ContextmenuItem };
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="el-zoom-in-center">
|
<transition @enter="onEnter" name="el-zoom-in-center">
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
data-popper-placement="bottom"
|
data-popper-placement="bottom"
|
||||||
:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
|
:style="`top: ${state.dropdown.y + 5}px;left: ${state.dropdown.x}px;`"
|
||||||
:key="Math.random()"
|
:key="Math.random()"
|
||||||
v-show="state.isShow && !allHide"
|
v-show="state.isShow && !allHide"
|
||||||
>
|
>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
|
<div v-if="state.arrowLeft > 0" class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
@@ -33,6 +33,8 @@
|
|||||||
<script setup lang="ts" name="layoutTagsViewContextmenu">
|
<script setup lang="ts" name="layoutTagsViewContextmenu">
|
||||||
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
|
import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import { ContextmenuItem } from './index';
|
import { ContextmenuItem } from './index';
|
||||||
|
import { useViewport } from '@/common/use';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
|
||||||
// 定义父组件传过来的值
|
// 定义父组件传过来的值
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -54,14 +56,56 @@ const props = defineProps({
|
|||||||
// 定义子组件向父组件传值/事件
|
// 定义子组件向父组件传值/事件
|
||||||
const emit = defineEmits(['currentContextmenuClick']);
|
const emit = defineEmits(['currentContextmenuClick']);
|
||||||
|
|
||||||
|
const { vw, vh } = useViewport();
|
||||||
|
|
||||||
// 定义变量内容
|
// 定义变量内容
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
isShow: false,
|
isShow: false,
|
||||||
dropdownList: [] as ContextmenuItem[],
|
dropdownList: [] as ContextmenuItem[],
|
||||||
item: {} as any,
|
item: {} as any,
|
||||||
arrowLeft: 10,
|
arrowLeft: 10,
|
||||||
|
dropdown: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 下拉菜单宽高
|
||||||
|
let contextmenuWidth = 117;
|
||||||
|
let contextmenuHeight = 117;
|
||||||
|
// 下拉菜单元素
|
||||||
|
let ele = null as any;
|
||||||
|
|
||||||
|
const onEnter = (el: any) => {
|
||||||
|
if (ele || el.offsetHeight == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ele = el;
|
||||||
|
contextmenuHeight = el.offsetHeight;
|
||||||
|
contextmenuWidth = el.offsetWidth;
|
||||||
|
setDropdowns(props.dropdown);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDropdowns = (dropdown: any) => {
|
||||||
|
let { x, y } = dropdown;
|
||||||
|
|
||||||
|
state.arrowLeft = 10;
|
||||||
|
|
||||||
|
// `Dropdown 下拉菜单` 的宽度
|
||||||
|
if (x + contextmenuWidth > vw.value) {
|
||||||
|
state.arrowLeft = contextmenuWidth - (vw.value - x);
|
||||||
|
x = vw.value - contextmenuWidth - 5;
|
||||||
|
}
|
||||||
|
if (y + contextmenuHeight > vh.value) {
|
||||||
|
y = vh.value - contextmenuHeight - 5;
|
||||||
|
state.arrowLeft = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.dropdown.x = x;
|
||||||
|
state.dropdown.y = y;
|
||||||
|
};
|
||||||
|
|
||||||
const allHide = computed(() => {
|
const allHide = computed(() => {
|
||||||
for (let item of state.dropdownList) {
|
for (let item of state.dropdownList) {
|
||||||
if (!item.isHide(state.item)) {
|
if (!item.isHide(state.item)) {
|
||||||
@@ -71,18 +115,6 @@ const allHide = computed(() => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 父级传过来的坐标 x,y 值
|
|
||||||
const dropdowns = computed(() => {
|
|
||||||
// 117 为 `Dropdown 下拉菜单` 的宽度
|
|
||||||
if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
|
|
||||||
return {
|
|
||||||
x: document.documentElement.clientWidth - 117 - 5,
|
|
||||||
y: props.dropdown.y,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return props.dropdown;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 当前项菜单点击
|
// 当前项菜单点击
|
||||||
const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
|
const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
|
||||||
// 存在点击事件,则触发该事件函数
|
// 存在点击事件,则触发该事件函数
|
||||||
@@ -91,6 +123,7 @@ const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
|
|||||||
}
|
}
|
||||||
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
|
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
|
// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
|
||||||
const openContextmenu = (item: any) => {
|
const openContextmenu = (item: any) => {
|
||||||
state.item = item;
|
state.item = item;
|
||||||
@@ -99,30 +132,34 @@ const openContextmenu = (item: any) => {
|
|||||||
state.isShow = true;
|
state.isShow = true;
|
||||||
}, 10);
|
}, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 关闭右键菜单
|
// 关闭右键菜单
|
||||||
const closeContextmenu = () => {
|
const closeContextmenu = () => {
|
||||||
state.isShow = false;
|
state.isShow = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听页面监听进行右键菜单的关闭
|
// 监听页面监听进行右键菜单的关闭
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.body.addEventListener('click', closeContextmenu);
|
document.body.addEventListener('click', closeContextmenu);
|
||||||
state.dropdownList = props.items;
|
state.dropdownList = props.items;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 页面卸载时,移除右键菜单监听事件
|
// 页面卸载时,移除右键菜单监听事件
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.body.removeEventListener('click', closeContextmenu);
|
document.body.removeEventListener('click', closeContextmenu);
|
||||||
});
|
});
|
||||||
// 监听下拉菜单位置
|
|
||||||
watch(
|
watch(
|
||||||
() => props.dropdown,
|
() => props.dropdown,
|
||||||
({ x }) => {
|
() => {
|
||||||
if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
|
// 元素置为空,重新在onEnter赋值元素,否则会造成堆栈溢出
|
||||||
else state.arrowLeft = 10;
|
ele = null;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
deep: true,
|
deep: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.items,
|
() => props.items,
|
||||||
(x: any) => {
|
(x: any) => {
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ import { storeToRefs } from 'pinia';
|
|||||||
import { useThemeConfig } from '@/store/themeConfig';
|
import { useThemeConfig } from '@/store/themeConfig';
|
||||||
import mittBus from '@/common/utils/mitt';
|
import mittBus from '@/common/utils/mitt';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
import Contextmenu from '@/components/contextmenu/index.vue';
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||||
import { ContextmenuItem } from '@/components/contextmenu/index';
|
|
||||||
import { getTagViews, setTagViews, removeTagViews } from '@/common/utils/storage';
|
import { getTagViews, setTagViews, removeTagViews } from '@/common/utils/storage';
|
||||||
import { useTagsViews } from '@/store/tagsViews';
|
import { useTagsViews } from '@/store/tagsViews';
|
||||||
import { useKeepALiveNames } from '@/store/keepAliveNames';
|
import { useKeepALiveNames } from '@/store/keepAliveNames';
|
||||||
|
|||||||
@@ -42,16 +42,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
|
import { onMounted, reactive, ref, watch, toRefs, onUnmounted } from 'vue';
|
||||||
import { TagTreeNode } from './tag';
|
import { TagTreeNode } from './tag';
|
||||||
import TagInfo from './TagInfo.vue';
|
import TagInfo from './TagInfo.vue';
|
||||||
import Contextmenu from '@/components/contextmenu/index.vue';
|
import { Contextmenu } from '@/components/contextmenu';
|
||||||
|
import { useViewport } from '@/common/use';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
height: {
|
|
||||||
type: [Number, String],
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
load: {
|
load: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -76,6 +73,8 @@ const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
|
|||||||
const treeRef: any = ref(null);
|
const treeRef: any = ref(null);
|
||||||
const contextmenuRef = ref();
|
const contextmenuRef = ref();
|
||||||
|
|
||||||
|
const { vh } = useViewport();
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
height: 600 as any,
|
height: 600 as any,
|
||||||
filterText: '',
|
filterText: '',
|
||||||
@@ -89,18 +88,18 @@ const state = reactive({
|
|||||||
const { filterText } = toRefs(state);
|
const { filterText } = toRefs(state);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!props.height) {
|
setHeight();
|
||||||
setHeight();
|
window.addEventListener('resize', setHeight);
|
||||||
window.onresize = () => setHeight();
|
|
||||||
} else {
|
|
||||||
state.height = props.height;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const setHeight = () => {
|
const setHeight = () => {
|
||||||
state.height = window.innerHeight - 157 + 'px';
|
state.height = vh.value - 148 + 'px';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', setHeight);
|
||||||
|
});
|
||||||
|
|
||||||
watch(filterText, (val) => {
|
watch(filterText, (val) => {
|
||||||
treeRef.value?.filter(val);
|
treeRef.value?.filter(val);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ import TagTree from '../component/TagTree.vue';
|
|||||||
import { dbApi } from './api';
|
import { dbApi } from './api';
|
||||||
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { ContextmenuItem } from '@/components/contextmenu/index';
|
import { ContextmenuItem } from '@/components/contextmenu';
|
||||||
|
|
||||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
||||||
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
|
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
|
||||||
|
|||||||
@@ -50,9 +50,6 @@
|
|||||||
|
|
||||||
<div class="mt5">
|
<div class="mt5">
|
||||||
<el-row>
|
<el-row>
|
||||||
<span v-if="execRes.data.length > 0">
|
|
||||||
<el-link type="success" :underline="false" @click="exportData"><span style="font-size: 12px">导出</span></el-link>
|
|
||||||
</span>
|
|
||||||
<span v-if="hasUpdatedFileds">
|
<span v-if="hasUpdatedFileds">
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
<el-link type="success" :underline="false" @click="submitUpdateFields()"><span style="font-size: 12px">提交</span></el-link>
|
<el-link type="success" :underline="false" @click="submitUpdateFields()"><span style="font-size: 12px">提交</span></el-link>
|
||||||
@@ -83,7 +80,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { h, nextTick, watch, onMounted, reactive, toRefs, ref, Ref } from 'vue';
|
import { h, nextTick, watch, onMounted, reactive, toRefs, ref, Ref } from 'vue';
|
||||||
import { getToken } from '@/common/utils/storage';
|
import { getToken } from '@/common/utils/storage';
|
||||||
import { isTrue, notBlank } from '@/common/assert';
|
import { notBlank } from '@/common/assert';
|
||||||
import { format as sqlFormatter } from 'sql-formatter';
|
import { format as sqlFormatter } from 'sql-formatter';
|
||||||
import config from '@/common/config';
|
import config from '@/common/config';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
@@ -93,8 +90,6 @@ import { editor } from 'monaco-editor';
|
|||||||
|
|
||||||
import DbTableData from '@/views/ops/db/component/table/DbTableData.vue';
|
import DbTableData from '@/views/ops/db/component/table/DbTableData.vue';
|
||||||
import { DbInst } from '../../db';
|
import { DbInst } from '../../db';
|
||||||
import { exportCsv } from '@/common/utils/export';
|
|
||||||
import { dateStrFormat } from '@/common/utils/date';
|
|
||||||
import { dbApi } from '../../api';
|
import { dbApi } from '../../api';
|
||||||
|
|
||||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
@@ -143,7 +138,7 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
selectionDatas: [] as any,
|
selectionDatas: [] as any,
|
||||||
editorHeight: '500',
|
editorHeight: '500',
|
||||||
tableDataHeight: 240 as any,
|
tableDataHeight: 255 as any,
|
||||||
hasUpdatedFileds: false,
|
hasUpdatedFileds: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -458,19 +453,6 @@ const replaceSelection = (str: string, selection: any) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出当前页数据
|
|
||||||
*/
|
|
||||||
const exportData = () => {
|
|
||||||
const dataList = state.execRes.data as any;
|
|
||||||
isTrue(dataList.length > 0, '没有数据可导出');
|
|
||||||
exportCsv(
|
|
||||||
`数据查询导出-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`,
|
|
||||||
state.execRes.tableColumn.map((x: any) => x.columnName),
|
|
||||||
dataList
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* sql文件执行进度通知缓存
|
* sql文件执行进度通知缓存
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
:width="width"
|
:width="width"
|
||||||
:height="height"
|
:height="height"
|
||||||
fixed
|
fixed
|
||||||
|
class="table"
|
||||||
:row-event-handlers="rowEventHandlers"
|
:row-event-handlers="rowEventHandlers"
|
||||||
>
|
>
|
||||||
<template #header="{ columns }">
|
<template #header="{ columns }">
|
||||||
@@ -104,7 +105,7 @@
|
|||||||
<el-input v-model="state.genSqlDialog.sql" type="textarea" rows="20" />
|
<el-input v-model="state.genSqlDialog.sql" type="textarea" rows="20" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="headerContextmenuRef" />
|
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -112,9 +113,10 @@
|
|||||||
import { ref, onMounted, watch, reactive, toRefs } from 'vue';
|
import { ref, onMounted, watch, reactive, toRefs } from 'vue';
|
||||||
import { ElInput } from 'element-plus';
|
import { ElInput } from 'element-plus';
|
||||||
import { DbInst } from '@/views/ops/db/db';
|
import { DbInst } from '@/views/ops/db/db';
|
||||||
import Contextmenu from '@/components/contextmenu/index.vue';
|
import { ContextmenuItem, Contextmenu } from '@/components/contextmenu';
|
||||||
import { ContextmenuItem } from '@/components/contextmenu/index';
|
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
import { exportCsv, exportFile } from '@/common/utils/export';
|
||||||
|
import { dateStrFormat } from '@/common/utils/date';
|
||||||
|
|
||||||
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
|
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
|
||||||
|
|
||||||
@@ -163,9 +165,11 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const headerContextmenuRef = ref();
|
const contextmenuRef = ref();
|
||||||
const tableRef = ref();
|
const tableRef = ref();
|
||||||
|
|
||||||
|
/** 表头 contextmenu items **/
|
||||||
|
|
||||||
const cmHeaderAsc = new ContextmenuItem('asc', '升序').withIcon('top').withOnClick((data: any) => {
|
const cmHeaderAsc = new ContextmenuItem('asc', '升序').withIcon('top').withOnClick((data: any) => {
|
||||||
onTableSortChange({ columnName: data.dataKey, order: 'asc' });
|
onTableSortChange({ columnName: data.dataKey, order: 'asc' });
|
||||||
});
|
});
|
||||||
@@ -174,7 +178,21 @@ const cmHeaderDesc = new ContextmenuItem('desc', '降序').withIcon('bottom').wi
|
|||||||
onTableSortChange({ columnName: data.dataKey, order: 'desc' });
|
onTableSortChange({ columnName: data.dataKey, order: 'desc' });
|
||||||
});
|
});
|
||||||
|
|
||||||
const cmDataDel = new ContextmenuItem('desc', '删除')
|
const cmHeaderFixed = new ContextmenuItem('fixed', '固定')
|
||||||
|
.withIcon('Paperclip')
|
||||||
|
.withOnClick((data: any) => {
|
||||||
|
data.fixed = true;
|
||||||
|
})
|
||||||
|
.withHideFunc((data: any) => data.fixed);
|
||||||
|
|
||||||
|
const cmHeaderCancenFixed = new ContextmenuItem('cancelFixed', '取消固定')
|
||||||
|
.withIcon('Minus')
|
||||||
|
.withOnClick((data: any) => (data.fixed = false))
|
||||||
|
.withHideFunc((data: any) => !data.fixed);
|
||||||
|
|
||||||
|
/** 表数据 contextmenu items **/
|
||||||
|
|
||||||
|
const cmDataDel = new ContextmenuItem('deleteData', '删除')
|
||||||
.withIcon('delete')
|
.withIcon('delete')
|
||||||
.withOnClick(() => onDeleteData())
|
.withOnClick(() => onDeleteData())
|
||||||
.withHideFunc(() => {
|
.withHideFunc(() => {
|
||||||
@@ -182,13 +200,22 @@ const cmDataDel = new ContextmenuItem('desc', '删除')
|
|||||||
});
|
});
|
||||||
|
|
||||||
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
||||||
.withIcon('document')
|
.withIcon('tickets')
|
||||||
.withOnClick(() => onGenerateInsertSql())
|
.withOnClick(() => onGenerateInsertSql())
|
||||||
.withHideFunc(() => {
|
.withHideFunc(() => {
|
||||||
return state.table == '';
|
return state.table == '';
|
||||||
});
|
});
|
||||||
|
|
||||||
const cmDataGenJson = new ContextmenuItem('genJson', '生成JSON').withIcon('document').withOnClick(() => onGenerateJson());
|
const cmDataGenJson = new ContextmenuItem('genJson', '生成JSON').withIcon('tickets').withOnClick(() => onGenerateJson());
|
||||||
|
|
||||||
|
const cmDataExportCsv = new ContextmenuItem('exportCsv', '导出CSV').withIcon('document').withOnClick(() => onExportCsv());
|
||||||
|
|
||||||
|
const cmDataExportSql = new ContextmenuItem('exportSql', '导出SQL')
|
||||||
|
.withIcon('document')
|
||||||
|
.withOnClick(() => onExportSql())
|
||||||
|
.withHideFunc(() => {
|
||||||
|
return state.table == '';
|
||||||
|
});
|
||||||
|
|
||||||
class NowUpdateCell {
|
class NowUpdateCell {
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
@@ -328,7 +355,6 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const setTableData = (datas: any) => {
|
const setTableData = (datas: any) => {
|
||||||
console.log('set table datas', props);
|
|
||||||
tableRef.value.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
tableRef.value.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
||||||
selectionRowsMap.clear();
|
selectionRowsMap.clear();
|
||||||
cellUpdateMap.clear();
|
cellUpdateMap.clear();
|
||||||
@@ -364,6 +390,11 @@ const canEdit = (rowIndex: number, colIndex: number) => {
|
|||||||
return state.table && nowUpdateCell && nowUpdateCell.rowIndex == rowIndex && nowUpdateCell.colIndex == colIndex;
|
return state.table && nowUpdateCell && nowUpdateCell.rowIndex == rowIndex && nowUpdateCell.colIndex == colIndex;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前单元格是否被更新了
|
||||||
|
* @param rowIndex ri
|
||||||
|
* @param columnName cn
|
||||||
|
*/
|
||||||
const isUpdated = (rowIndex: number, columnName: string) => {
|
const isUpdated = (rowIndex: number, columnName: string) => {
|
||||||
return cellUpdateMap.get(rowIndex)?.columnsMap.get(columnName);
|
return cellUpdateMap.get(rowIndex)?.columnsMap.get(columnName);
|
||||||
};
|
};
|
||||||
@@ -423,8 +454,8 @@ const headerContextmenuClick = (event: any, data: any) => {
|
|||||||
const { clientX, clientY } = event;
|
const { clientX, clientY } = event;
|
||||||
state.contextmenu.dropdown.x = clientX;
|
state.contextmenu.dropdown.x = clientX;
|
||||||
state.contextmenu.dropdown.y = clientY;
|
state.contextmenu.dropdown.y = clientY;
|
||||||
state.contextmenu.items = [cmHeaderAsc, cmHeaderDesc];
|
state.contextmenu.items = [cmHeaderAsc, cmHeaderDesc, cmHeaderFixed, cmHeaderCancenFixed];
|
||||||
headerContextmenuRef.value.openContextmenu(data);
|
contextmenuRef.value.openContextmenu(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataContextmenuClick = (event: any, rowIndex: number, data: any) => {
|
const dataContextmenuClick = (event: any, rowIndex: number, data: any) => {
|
||||||
@@ -437,86 +468,8 @@ const dataContextmenuClick = (event: any, rowIndex: number, data: any) => {
|
|||||||
const { clientX, clientY } = event;
|
const { clientX, clientY } = event;
|
||||||
state.contextmenu.dropdown.x = clientX;
|
state.contextmenu.dropdown.x = clientX;
|
||||||
state.contextmenu.dropdown.y = clientY;
|
state.contextmenu.dropdown.y = clientY;
|
||||||
state.contextmenu.items = [cmDataDel, cmDataGenInsertSql, cmDataGenJson];
|
state.contextmenu.items = [cmDataDel, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
||||||
headerContextmenuRef.value.openContextmenu(data);
|
contextmenuRef.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) => {
|
|
||||||
const comment = column.columnComment;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -563,6 +516,77 @@ const onGenerateJson = async () => {
|
|||||||
state.genSqlDialog.visible = true;
|
state.genSqlDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出当前页数据
|
||||||
|
*/
|
||||||
|
const onExportCsv = () => {
|
||||||
|
const dataList = state.datas as any;
|
||||||
|
let columnNames = [];
|
||||||
|
for (let column of state.columns) {
|
||||||
|
if (column.show) {
|
||||||
|
columnNames.push(column.columnName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onExportSql = async () => {
|
||||||
|
const selectionDatas = state.datas;
|
||||||
|
exportFile(
|
||||||
|
`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}.sql`,
|
||||||
|
await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEnterEditMode = (el: any, rowData: any, column: any, rowIndex = 0, columnIndex = 0) => {
|
||||||
|
if (!state.table) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerRefresh();
|
||||||
|
nowUpdateCell = {
|
||||||
|
rowIndex: rowIndex,
|
||||||
|
colIndex: columnIndex,
|
||||||
|
oldValue: rowData[column.dataKey],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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 submitUpdateFields = async () => {
|
const submitUpdateFields = async () => {
|
||||||
const dbInst = getNowDbInst();
|
const dbInst = getNowDbInst();
|
||||||
if (cellUpdateMap.size == 0) {
|
if (cellUpdateMap.size == 0) {
|
||||||
@@ -630,6 +654,33 @@ const changeUpdatedField = () => {
|
|||||||
emits('changeUpdatedField', cellUpdateMap);
|
emits('changeUpdatedField', cellUpdateMap);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rowClass = (row: any) => {
|
||||||
|
if (isSelection(row.rowIndex)) {
|
||||||
|
return 'data-selection';
|
||||||
|
}
|
||||||
|
if (row.rowIndex % 2 != 0) {
|
||||||
|
return 'data-spacing';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColumnTip = (column: any) => {
|
||||||
|
const comment = column.columnComment;
|
||||||
|
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 getNowDbInst = () => {
|
const getNowDbInst = () => {
|
||||||
return DbInst.getInst(state.dbId);
|
return DbInst.getInst(state.dbId);
|
||||||
};
|
};
|
||||||
@@ -640,8 +691,13 @@ defineExpose({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
.db-table-data {
|
.db-table-data {
|
||||||
|
.table {
|
||||||
|
border-left: var(--el-table-border);
|
||||||
|
border-top: var(--el-table-border);
|
||||||
|
}
|
||||||
|
|
||||||
.table-data-cell {
|
.table-data-cell {
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -650,6 +706,7 @@ defineExpose({
|
|||||||
.data-selection {
|
.data-selection {
|
||||||
background-color: var(--el-color-success-light-8);
|
background-color: var(--el-color-success-light-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-spacing {
|
.data-spacing {
|
||||||
background-color: var(--el-fill-color-lighter);
|
background-color: var(--el-fill-color-lighter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,11 +35,6 @@
|
|||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
<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>
|
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
|
||||||
|
|
||||||
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" class="box-item" effect="dark" content="提交修改" placement="top">
|
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" class="box-item" effect="dark" content="提交修改" placement="top">
|
||||||
<el-link @click="submitUpdateFields()" type="success" :underline="false" class="f12">提交</el-link>
|
<el-link @click="submitUpdateFields()" type="success" :underline="false" class="f12">提交</el-link>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@@ -185,12 +180,10 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, computed, watch, reactive, toRefs, ref, Ref, onUnmounted } from 'vue';
|
import { onMounted, computed, watch, reactive, toRefs, ref, Ref, onUnmounted } from 'vue';
|
||||||
import { isTrue, notEmpty } from '@/common/assert';
|
import { notEmpty } from '@/common/assert';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
import { DbInst } from '@/views/ops/db/db';
|
import { DbInst } from '@/views/ops/db/db';
|
||||||
import { exportCsv } from '@/common/utils/export';
|
|
||||||
import { dateStrFormat } from '@/common/utils/date';
|
|
||||||
import DbTableData from './DbTableData.vue';
|
import DbTableData from './DbTableData.vue';
|
||||||
|
|
||||||
const dataForm: any = ref(null);
|
const dataForm: any = ref(null);
|
||||||
@@ -227,7 +220,7 @@ const state = reactive({
|
|||||||
columns: [] as any,
|
columns: [] as any,
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: DbInst.DefaultLimit,
|
pageSize: DbInst.DefaultLimit,
|
||||||
pageSizes: [20, 40, 80, 100, 200, 300, 400],
|
pageSizes: [20, 50, 100, 200, 500, 1000],
|
||||||
count: 0,
|
count: 0,
|
||||||
selectionDatas: [] as any,
|
selectionDatas: [] as any,
|
||||||
condPopVisible: false,
|
condPopVisible: false,
|
||||||
@@ -333,21 +326,6 @@ const handleSizeChange = async (size: any) => {
|
|||||||
await selectData();
|
await selectData();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出当前页数据
|
|
||||||
*/
|
|
||||||
const exportData = () => {
|
|
||||||
const dataList = state.datas as any;
|
|
||||||
isTrue(dataList.length > 0, '没有数据可导出');
|
|
||||||
let columnNames = [];
|
|
||||||
for (let column of state.columns) {
|
|
||||||
if (column.show) {
|
|
||||||
columnNames.push(column.columnName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exportCsv(`数据导出-${props.tableName}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, columnNames, dataList);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选择条件列
|
* 选择条件列
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -179,8 +179,7 @@ import { isTrue, notBlank, notNull } from '@/common/assert';
|
|||||||
import { TagTreeNode, NodeType } from '../component/tag';
|
import { TagTreeNode, NodeType } from '../component/tag';
|
||||||
import TagTree from '../component/TagTree.vue';
|
import TagTree from '../component/TagTree.vue';
|
||||||
import { keysToTree, sortByTreeNodes, keysToList } from './utils';
|
import { keysToTree, sortByTreeNodes, keysToList } from './utils';
|
||||||
import Contextmenu from '@/components/contextmenu/index.vue';
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||||
import { ContextmenuItem } from '@/components/contextmenu/index';
|
|
||||||
|
|
||||||
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
|
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
|
||||||
|
|
||||||
|
|||||||
@@ -88,8 +88,7 @@ import { toRefs, ref, watch, reactive, onMounted } from 'vue';
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { tagApi } from './api';
|
import { tagApi } from './api';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
import { dateFormat } from '@/common/utils/date';
|
||||||
import Contextmenu from '@/components/contextmenu/index.vue';
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu/index';
|
||||||
import { ContextmenuItem } from '@/components/contextmenu/index';
|
|
||||||
|
|
||||||
interface Tree {
|
interface Tree {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -101,8 +101,7 @@ import { ResourceTypeEnum } from '../enums';
|
|||||||
import { resourceApi } from '../api';
|
import { resourceApi } from '../api';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
import { dateFormat } from '@/common/utils/date';
|
||||||
import EnumValue from '@/common/Enum';
|
import EnumValue from '@/common/Enum';
|
||||||
import Contextmenu from '@/components/contextmenu/index.vue';
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||||
import { ContextmenuItem } from '@/components/contextmenu/index';
|
|
||||||
|
|
||||||
const menuTypeValue = ResourceTypeEnum.Menu.value;
|
const menuTypeValue = ResourceTypeEnum.Menu.value;
|
||||||
const permissionTypeValue = ResourceTypeEnum.Permission.value;
|
const permissionTypeValue = ResourceTypeEnum.Permission.value;
|
||||||
|
|||||||
Reference in New Issue
Block a user