refactor: 虚拟表格与contextmenu菜单优化

This commit is contained in:
meilin.huang
2023-11-18 15:22:25 +08:00
parent a40ec21a05
commit f79760943e
14 changed files with 255 additions and 181 deletions

View File

@@ -101,7 +101,7 @@ const refreshWatermarkTime = () => {
} else { } else {
clearInterval(refreshWatermarkTimeInterval); clearInterval(refreshWatermarkTimeInterval);
} }
}, 10000); }, 60000);
}; };
// 页面销毁时,关闭监听布局配置 // 页面销毁时,关闭监听布局配置

View 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 };
}

View File

@@ -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();
} }

View File

@@ -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 };

View File

@@ -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) => {

View File

@@ -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';

View File

@@ -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);
}); });

View File

@@ -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'));

View File

@@ -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文件执行进度通知缓存
*/ */

View File

@@ -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);
} }

View File

@@ -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);
};
/** /**
* 选择条件列 * 选择条件列
*/ */

View File

@@ -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'));

View File

@@ -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;

View File

@@ -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;