This commit is contained in:
zhuoda
2022-11-05 11:33:49 +08:00
parent c54716808d
commit 45f0a50344
1850 changed files with 209 additions and 87310 deletions

View File

@@ -0,0 +1,239 @@
<!--
* 数据变动记录 表格 组件
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-12 21:01:52
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="queryForm.keywords" placeholder="变更内容" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="onReload">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="false">
<a-table size="small" :dataSource="tableData" :columns="columns" rowKey="dataTracerId" :pagination="false" bordered>
<template #bodyCell="{ record, index, column }">
<template v-if="column.dataIndex === 'dataTracerId'">
<div>{{ index + 1 }}</div>
</template>
<template v-if="column.dataIndex === 'userName'">
<div>{{record.userName}} ({{ $smartEnumPlugin.getDescByValue('USER_TYPE_ENUM', record.userType) }})</div>
</template>
<template v-if="column.dataIndex === 'userAgent'">
<div>{{ record.browser }} / {{ record.os }} / {{ record.device }}</div>
</template>
<template v-if="column.dataIndex === 'content'">
<div class="operate-content" v-html="record.content"></div>
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-button v-if="record.diffOld || record.diffNew" @click="showDetail(record)" type="link">详情 </a-button>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="onSearch"
@showSizeChange="onSearch"
:show-total="(total) => `${total}`"
/>
</div>
<a-modal v-model:visible="visibleDiff" width="90%" title="数据比对" :footer="null">
<div v-html="prettyHtml"></div>
</a-modal>
</a-card>
</template>
<script setup>
import * as Diff from 'diff';
import * as Diff2Html from 'diff2html';
import 'diff2html/bundles/css/diff2html.min.css';
import uaparser from 'ua-parser-js';
import { nextTick, reactive, ref, watch } from 'vue';
import { dataTracerApi } from '/@/api/support/data-tracer/data-tracer-api';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
let props = defineProps({
// 数据id
dataId: {
type: Number,
},
// 数据 类型
type: {
type: Number,
},
});
const columns = reactive([
{
title: '序号',
dataIndex: 'dataTracerId',
width: 50,
},
{
title: '操作时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '操作人',
dataIndex: 'userName',
width: 100,
ellipsis: true,
},
{
title: 'IP',
dataIndex: 'ip',
ellipsis: true,
width: 100,
},
{
title: '客户端',
dataIndex: 'userAgent',
ellipsis: true,
width: 150,
},
{
title: '操作内容',
dataIndex: 'content',
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 80,
},
]);
// --------------- 查询表单、查询方法 ---------------
const queryFormState = {
pageNum: 1,
pageSize: PAGE_SIZE,
searchCount: true,
keywords: undefined,
};
const queryForm = reactive({ ...queryFormState });
const tableLoading = ref(false);
const tableData = ref([]);
const total = ref(0);
function onReload() {
Object.assign(queryForm, queryFormState);
onSearch();
}
async function onSearch() {
try {
tableLoading.value = true;
let responseModel = await dataTracerApi.queryList(Object.assign({}, queryForm, { dataId: props.dataId, type: props.type }));
for (const e of responseModel.data.list) {
if (!e.userAgent) {
continue;
}
let ua = uaparser(e.userAgent);
e.browser = ua.browser.name;
e.os = ua.os.name;
e.device = ua.device.vendor ? ua.device.vendor + ua.device.model : '';
}
const list = responseModel.data.list;
total.value = responseModel.data.total;
tableData.value = list;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
// ========= 定义 watch 监听 ===============
watch(
() => props.dataId,
(e) => {
if (e) {
queryForm.dataId = e;
onSearch();
}
},
{ immediate: true }
);
// --------------- diff 特效 ---------------
// diff
const visibleDiff = ref(false);
let prettyHtml = ref('');
function showDetail(record) {
visibleDiff.value = true;
let diffOld = record.diffOld.replaceAll('<br/>','\r\n');
let diffNew = record.diffNew.replaceAll('<br/>','\r\n');
console.log(diffOld)
console.log(diffNew)
const args = ['', diffOld, diffNew, '变更前', '变更后'];
let diffPatch = Diff.createPatch(...args);
let html = Diff2Html.html(diffPatch, {
drawFileList: false,
matching: 'words',
diffMaxChanges: 1000,
outputFormat: 'side-by-side',
});
prettyHtml.value = html;
nextTick(() => {
let diffDiv = document.querySelectorAll('.d2h-file-side-diff');
if (diffDiv.length > 0) {
let left = diffDiv[0],
right = diffDiv[1];
left.addEventListener('scroll', function (e) {
if (left.scrollLeft != right.scrollLeft) {
right.scrollLeft = left.scrollLeft;
}
});
right.addEventListener('scroll', function (e) {
if (left.scrollLeft != right.scrollLeft) {
left.scrollLeft = right.scrollLeft;
}
});
}
});
}
</script>
<style scoped lang="less">
.operate-content {
line-height: 20px;
margin: 5px 0px;
}
</style>

View File

@@ -0,0 +1,78 @@
<!---
* 字典key 下拉选择框
*
* @Author: 1024创新实验室罗伊
* @Date: 2022-09-12 22:06:45
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<div>
<a-select
v-model:value="selectValue"
:style="`width: ${width}`"
:placeholder="props.placeholder"
:showSearch="true"
:allowClear="true"
:size="size"
@change="onChange"
>
<a-select-option v-for="item in dictKeyCodeList" :key="item.keyCode" :value="item.keyCode">
{{ item.keyName }}
</a-select-option>
</a-select>
</div>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue';
import { dictApi } from '/@/api/support/dict/dict-api';
const props = defineProps({
value: [Array, String],
placeholder: {
type: String,
default: '请选择字典',
},
width: {
type: String,
default: '100%',
},
size: {
type: String,
default: 'default',
},
// 禁用标识
disabledFlag: {
type: Number,
default: null,
},
});
// -------------------------- 查询 字典数据 --------------------------
const dictKeyCodeList = ref([]);
async function queryDict() {
let responseModel = await dictApi.queryAllKey();
dictKeyCodeList.value = responseModel.data;
}
onMounted(queryDict);
// -------------------------- 选中 相关、事件 --------------------------
const emit = defineEmits(['update:value', 'change']);
const selectValue = ref(props.value);
watch(
() => props.value,
(newValue) => {
selectValue.value = newValue;
}
);
function onChange(value) {
emit('update:value', value);
emit('change', value);
}
</script>

View File

@@ -0,0 +1,116 @@
<!---
* 字段 下拉选择框
*
* @Author: 1024创新实验室罗伊
* @Date: 2022-09-12 22:06:45
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<div>
<a-select
v-model:value="selectValue"
:style="`width: ${width}`"
:placeholder="props.placeholder"
:allowClear="true"
:size="size"
:mode="mode"
@change="onChange"
:disabled="disabled"
>
<a-select-option v-for="item in dictValueList" :key="item.valueCode" :value="item.valueCode">
{{ item.valueName }}
</a-select-option>
</a-select>
</div>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue';
import { dictApi } from '/@/api/support/dict/dict-api';
const props = defineProps({
keyCode: String,
value: [Array, String],
mode: {
type: String,
default: 'combobox',
},
width: {
type: String,
default: '200px',
},
placeholder: {
type: String,
default: '请选择',
},
size: {
type: String,
default: 'default',
},
disabled: {
type: Boolean,
default: false,
},
});
// -------------------------- 查询 字典数据 --------------------------
const dictValueList = ref([]);
async function queryDict() {
let res = await dictApi.valueList(props.keyCode);
dictValueList.value = res.data;
}
const values = computed(() => {
if (!props.value) {
return [];
}
if (!Array.isArray(props.value)) {
console.error('valueList is not array!!!');
return [];
}
let res = [];
if (props.value && props.value.length > 0) {
props.value.forEach((element) => {
res.push(element.valueCode);
});
return res;
}
return res;
});
onMounted(queryDict);
// -------------------------- 选中 相关、事件 --------------------------
const selectValue = ref(props.value);
watch(
() => props.value,
(value) => {
selectValue.value = value;
}
);
const emit = defineEmits(['update:value', 'change']);
function onChange(value) {
let selected = [];
if (!value) {
emit('update:value', selected);
emit('change', selected);
return selected;
}
if (Array.isArray(props.value)) {
let valueList = dictValueList.value.filter((e) => value.includes(e.valueCode));
valueList = valueList.map((e) => e.valueCode);
emit('update:value', valueList);
emit('change', valueList);
} else {
let findValue = dictValueList.value.find((e) => e.valueCode == value);
emit('update:value', findValue.valueCode);
emit('change', findValue.valueCode);
}
}
</script>

View File

@@ -0,0 +1,87 @@
<!--
* 文件预览 弹窗
*
* @Author: 1024创新实验室善逸
* @Date: 2022-09-02 20:19:39
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<a-modal title="文件预览" v-model:visible="visibleFlag" :width="768" @cancel="onClose">
<div class="container">
<img class="img-prev" :src="previewUrl" />
</div>
<template #footer>
<a-button @click="onClose">关闭</a-button>
</template>
</a-modal>
</template>
<script setup>
import { ref } from 'vue';
import { download } from '/@/lib/axios';
import { fileApi } from '/@/api/support/file/file-api';
import { smartSentry } from '/@/lib/smart-sentry';
import { SmartLoading } from '/@/components/framework/smart-loading';
const visibleFlag = ref(false);
const imgFileType = ['jpg', 'jpeg', 'png', 'gif'];
const previewUrl = ref();
function showPreview(fileItem) {
if (!fileItem.fileUrl) {
(async () => {
SmartLoading.show();
try {
let res = await fileApi.getUrl(fileItem.fileKey);
fileItem.fileUrl = res.data;
showFile(fileItem);
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
})();
} else {
showFile(fileItem);
}
}
function showFile(fileItem) {
if (isImg(fileItem.fileType)) {
previewUrl.value = fileItem.fileUrl;
visibleFlag.value = true;
return;
}
download(fileItem.fileName, fileItem.fileUrl);
}
// 判断图片类型
function isImg(fileType) {
return imgFileType.includes(fileType);
}
function onClose() {
visibleFlag.value = false;
}
defineExpose({
showPreview,
});
</script>
<style lang="less" scoped>
.container {
display: flex;
justify-content: center;
align-items: center;
.img-prev {
display: block;
width: 100%;
height: 600px;
object-fit: contain;
}
}
</style>

View File

@@ -0,0 +1,78 @@
<!--
* 文件预览
*
* @Author: 1024创新实验室善逸
* @Date: 2022-07-19 23:19:39
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<div >
<template v-if="type == 'text'">
<a v-for="(item, index) in fileList" :key="index" @click="preview(item, index)">
{{ item.fileName }}
<span v-if="index != fileList.length - 1" v-html="separator"></span>
</a>
</template>
<a-space>
<a-image-preview-group :preview="{ visible, onVisibleChange: setVisible, current: previewCurrent }">
<a-image
v-for="(item, index) in fileList"
:key="index"
:src="item.fileUrl"
:style="{ display: type == 'text' ? 'none' : '' }"
:width="width"
/>
</a-image-preview-group>
</a-space>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { download } from '/@/lib/axios';
let props = defineProps({
fileList: {
type: Array,
default: () => {
return [];
},
},
// 类型 text,picture
type: {
type: String,
default: 'text',
},
// image宽度
width: {
type: Number,
default: 150,
},
// 分隔符 可设置html标签 例如:<br/>
separator: {
type: String,
default: '',
},
});
const imgFileType = ['jpg', 'jpeg', 'png', 'gif'];
// 文件预览
function preview(file, index) {
if (imgFileType.some((e) => e === file.fileType)) {
previewCurrent.value = index;
visible.value = true;
} else {
download(file.fileName, file.fileUrl);
}
}
// 预览
const visible = ref(false);
const previewCurrent = ref(0);
function setVisible(value) {
visible.value = value;
}
</script>

View File

@@ -0,0 +1,199 @@
<!--
* 文件上传
*
* @Author: 1024创新实验室善逸
* @Date: 2022-08-12 20:19:39
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<div class="clearfix">
<a-upload
:accept="props.accept"
:before-upload="beforeUpload"
:customRequest="customRequest"
:file-list="fileList"
:headers="{ 'x-access-token': useUserStore().getToken }"
:list-type="listType"
@change="handleChange"
@preview="handlePreview"
@remove="handleRemove"
>
<div v-if="fileList.length < props.maxUploadSize">
<template v-if="listType == 'picture-card'">
<PlusOutlined />
<div class="ant-upload-text">
{{ buttonText }}
</div>
</template>
<template v-if="listType == 'text'">
<a-button>
<upload-outlined />
{{ buttonText }}
</a-button>
</template>
</div>
</a-upload>
<a-modal :footer="null" :visible="previewVisible" @cancel="handleCancel">
<img :src="previewUrl" alt="example" style="width: 100%" />
</a-modal>
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { fileApi } from '/@/api/support/file/file-api';
import { useUserStore } from '/@/store/modules/system/user';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const';
import { download } from '/@/lib/axios';
import { smartSentry } from '/@/lib/smart-sentry';
const props = defineProps({
value: String,
buttonText: {
type: String,
default: '点击上传附件',
},
showUploadBtn: {
type: Boolean,
default: true,
},
defaultFileList: {
type: Array,
default: () => [],
},
multiple: {
type: Boolean,
default: false,
},
// 最多上传文件数量
maxUploadSize: {
type: Number,
default: 10,
},
maxSize: {
type: Number,
default: 10,
},
// 上传的文件类型
accept: {
type: String,
default: '',
},
// 文件上传类型
folder: {
type: Number,
default: FILE_FOLDER_TYPE_ENUM.COMMON.value,
},
// 上传列表的内建样式,支持三种基本样式 text, picture 和 picture-card
listType: {
type: String,
default: 'picture-card',
},
});
// 图片类型的后缀名
const imgFileType = ['jpg', 'jpeg', 'png', 'gif'];
// 重新修改图片展示字段
const files = computed(() => {
let res = [];
if (props.defaultFileList && props.defaultFileList.length > 0) {
props.defaultFileList.forEach((element) => {
element.url = element.fileUrl;
element.name = element.fileName;
res.push(element);
});
return res;
}
return res;
});
// -------------------- 逻辑 --------------------
const previewVisible = ref(false);
const fileList = ref([]);
const previewUrl = ref('');
watch(
files,
(value) => {
fileList.value = value;
},
{
immediate: true,
}
);
const emit = defineEmits(['update:value', 'change']);
const customRequest = async (options) => {
SmartLoading.show();
try {
console.log(options);
const formData = new FormData();
formData.append('file', options.file);
let res = await fileApi.uploadFile(formData, props.folder);
let file = res.data;
file.url = file.fileUrl;
file.name = file.fileName;
fileList.value.push(file);
emit('change', fileList.value);
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
};
function handleChange(info) {
let fileStatus = info.file.status;
let file = info.file;
if (fileStatus == 'removed') {
let index = fileList.value.findIndex((e) => e.fileId == file.fileId);
if (index != -1) {
fileList.value.splice(index, 1);
emit('change', fileList.value);
}
}
}
function handleRemove(file) {
console.log(fileList.value);
}
function beforeUpload(file) {
const isLimitSize = file.size / 1024 / 1024 < props.maxSize;
if (!isLimitSize) {
return message.error(`上传的文件必须小于${props.maxSize}Mb`);
}
return isLimitSize;
}
function handleCancel() {
previewVisible.value = false;
}
const handlePreview = async (file) => {
if (imgFileType.some((e) => e === file.fileType)) {
previewUrl.value = file.url || file.preview;
previewVisible.value = true;
} else {
download(file.fileName, file.fileUrl);
}
};
// ------------------------ 清空 上传 ------------------------
function clear() {
fileList.value = [];
}
defineExpose({
clear,
});
</script>
<style lang="less" scoped>
:deep(.ant-upload-picture-card-wrapper) {
display: flex;
}
</style>

View File

@@ -0,0 +1,163 @@
<!--
* 表格列设置
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-26 23:45:51
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<span>
<a-tooltip title="全屏" v-if="!fullScreenFlag">
<a-button type="text" @click="fullScreen" size="small">
<template #icon><fullscreen-outlined /></template>
</a-button>
</a-tooltip>
<a-tooltip title="取消全屏" v-if="fullScreenFlag">
<a-button type="text" @click="fullScreen" size="small">
<template #icon><fullscreen-exit-outlined /></template>
</a-button>
</a-tooltip>
<a-tooltip title="刷新">
<a-button type="text" @click="props.refresh" size="small">
<template #icon><redo-outlined /></template>
</a-button>
</a-tooltip>
<a-tooltip title="列设置">
<a-button type="text" @click="showModal" size="small">
<template #icon><setting-outlined /></template>
</a-button>
</a-tooltip>
<SmartTableColumnModal ref="smartTableColumnModal" @change="updateColumn" />
</span>
</template>
<script setup>
import _ from 'lodash';
import { tableColumnApi } from '/@/api/support/table/table-column-api';
import { onMounted, reactive, ref, watch } from 'vue';
import SmartTableColumnModal from './smart-table-column-modal.vue';
import { message } from 'ant-design-vue';
import { mergeColumn } from './smart-table-column-merge';
import { smartSentry } from '/@/lib/smart-sentry';
const props = defineProps({
// 表格列数组
modelValue: {
type: Array,
default: new Array(),
},
// 刷新表格函数
refresh: {
type: Function,
required: true,
},
// 表格id
tableId: {
type: Number,
require: true,
},
});
const emit = defineEmits(['update:modelValue']);
// 原始表格列数据复制一份最原始的columns集合以供后续各个地方使用
let originalColumn = _.cloneDeep(props.modelValue);
onMounted(buildUserTableColumns);
//构建用户的数据列
async function buildUserTableColumns() {
if (!props.tableId) {
return;
}
let userTableColumnArray = [];
try {
let res = await tableColumnApi.getColumns(props.tableId);
if (res.data) {
try {
userTableColumnArray = JSON.parse(res.data);
} catch (e1) {
smartSentry.captureError(e1);
}
}
} catch (e) {
smartSentry.captureError(e);
}
updateColumn(userTableColumnArray);
}
// ----------------- 全屏 -------------------
const fullScreenFlag = ref(false);
function fullScreen() {
if (fullScreenFlag.value) {
//取消全屏
exitFullscreen(document.querySelector('#smartAdminLayoutContent'));
fullScreenFlag.value = false;
} else {
//全屏
launchFullScreen(document.querySelector('#smartAdminLayoutContent'));
fullScreenFlag.value = true;
}
}
//判断各种浏览器 -全屏
function launchFullScreen(element) {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
} else {
message.error('当前浏览器不支持部分全屏!');
}
}
//判断各种浏览器 -退出全屏
function exitFullscreen(element) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
// ----------------- 弹窗 修改表格列 -------------------
const smartTableColumnModal = ref();
function showModal() {
smartTableColumnModal.value.show(originalColumn, props.tableId);
}
// 将弹窗修改的列数据,赋值给原表格 列数组
function updateColumn(changeColumnArray) {
//合并列
const newColumns = mergeColumn(_.cloneDeep(originalColumn), changeColumnArray);
emit(
'update:modelValue',
newColumns.filter((e) => e.showFlag)
);
}
// ========= 定义 watch 监听 ===============
watch(
() => props.tableId,
(e) => {
if (e) {
originalColumn = _.cloneDeep(props.modelValue);
buildUserTableColumns();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,54 @@
/*
* 表格列设置
*
* @Author: 1024创新实验室-主任:卓大
* @Date: 2022-08-26 23:45:51
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*/
import _ from 'lodash';
/**
* 将原视表格列和用户表格列进行合并、排序
* @param {*} originalTableColumnArray
* @param {*} userTableColumnArray
*/
export function mergeColumn(originalTableColumnArray, userTableColumnArray) {
if (!userTableColumnArray) {
return originalTableColumnArray;
}
//第一步将用户的列数据转为Map以后备使用
let userTableColumnMap = new Map();
for (const item of userTableColumnArray) {
userTableColumnMap.set(item.columnKey, item);
}
//第二步以前端的table columns列为基础将用户后端的数据填充到前端表格列里
let fontColumnSort = 1;
let newColumns = [];
for (const fontColumn of originalTableColumnArray) {
//原始表格列默认显示
fontColumn.columnKey = fontColumn.dataIndex;
fontColumn.showFlag = true;
fontColumn.sort = fontColumnSort;
// 如果用户存在此列,则覆盖 sort和width、showFlag字段
let userColumn = userTableColumnMap.get(fontColumn.columnKey);
if (userColumn) {
fontColumn.sort = userColumn.sort;
fontColumn.showFlag = userColumn.showFlag;
if (userColumn.width) {
fontColumn.width = userColumn.width;
}
}
newColumns.push(fontColumn);
fontColumnSort++;
}
//第三步:前端列进行排序
newColumns = _.sortBy(newColumns, (e) => e.sort);
return newColumns;
}

View File

@@ -0,0 +1,280 @@
<!--
* 表格列设置
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-26 23:45:51
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<a-modal :width="700" :visible="visible" title="设置列" :destroyOnClose="true" :closable="false">
<a-alert type="info" show-icon class="smart-margin-bottom10">
<template #icon><smile-outlined /></template>
<template #message> 可以通过拖拽行直接修改顺序哦 <pushpin-outlined />为固定列不可拖拽 </template>
</a-alert>
<a-table
id="smartTableColumnModalTable"
rowKey="columnKey"
row-class-name="column-row"
:columns="tableColumns"
:dataSource="tableData"
:rowSelection="{ checkStrictly: false, selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
:pagination="false"
size="small"
bordered
>
<template #bodyCell="{ text, record, index, column }">
<template v-if="column.dataIndex === 'title'">
<a-button type="text" :class="record.fixed ? '' : 'handle'" size="small" style="width: 100%; text-align: left">
<template #icon v-if="!record.fixed"> <drag-outlined /> </template>
<template #icon v-if="record.fixed"> <pushpin-outlined /> </template>
{{ text }}
</a-button>
</template>
<template v-if="column.dataIndex === 'width'">
<a-input-number v-model:value="record.width" style="width: 90px; margin-left: 10px; margin-right: 3px" size="small" />px
</template>
<template v-if="column.dataIndex === 'operate'">
<div class="smart-table-operate" v-if="!record.fixed">
<a-button @click="up(index)" v-show="index > 0" type="link" class="handle" size="small" style="margin-right: 12px"> 上移 </a-button>
<a-button @click="down(index)" type="link" class="handle" size="small" v-show="index !== tableData.length - 1"> 下移</a-button>
</div>
</template>
</template>
</a-table>
<template #footer>
<a-button key="back" @click="hide">取消</a-button>
<a-button key="submit" type="primary" :loading="submitLoading" @click="save">保存</a-button>
<a-button key="back" :loading="submitLoading" @click="reset" danger style="margin-left: 20px">恢复默认</a-button>
</template>
</a-modal>
</template>
<script setup>
import { SmartLoading } from '/@/components/framework/smart-loading';
import { tableColumnApi } from '/@/api/support/table/table-column-api';
import { ref, reactive, nextTick, computed } from 'vue';
import _ from 'lodash';
import Sortable from 'sortablejs';
import { message, Modal } from 'ant-design-vue';
import { mergeColumn } from './smart-table-column-merge';
import { smartSentry } from '/@/lib/smart-sentry';
const emit = defineEmits(['change']);
defineExpose({ show });
// ---------------- 显示 / 隐藏 --------------------
let tableId = 1;
const visible = ref(false);
//显示
function show(columns, showTableId) {
tableId = showTableId;
visible.value = true;
getUserTableColumns(tableId, _.cloneDeep(columns));
}
//隐藏
function hide() {
visible.value = false;
}
//获取用户的列数据
async function getUserTableColumns(tableId, columns) {
SmartLoading.show();
let userTableColumnArray = [];
try {
let res = await tableColumnApi.getColumns(tableId);
if (res.data) {
try {
userTableColumnArray = JSON.parse(res.data);
} catch (e1) {
smartSentry.captureError(e1);
}
}
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
//根据前端列和后端列构建新的列数据
tableData.value = mergeColumn(columns, userTableColumnArray);
//将已经显示的展示出来
for (const item of tableData.value) {
if (item.showFlag) {
selectedRowKeyList.value.push(item.columnKey);
continue;
}
}
nextTick(() => {
initDrag();
});
}
// --------------------- 表格渲染 --------------------------------
const tableData = ref([]);
const tableColumns = [
{
title: '列',
dataIndex: 'title',
},
{
title: '宽度(像素)',
dataIndex: 'width',
width: 150,
},
{
title: '操作',
dataIndex: 'operate',
width: 150,
rowDrag: true,
},
];
// --------------------- 表格移动【拖拽移动、上移、下移】 --------------------------------
//初始化拖拽
function initDrag() {
let tbody = document.querySelector('#smartTableColumnModalTable tbody');
Sortable.create(tbody, {
animation: 300,
dragClass: 'smart-ghost-class', //设置拖拽样式类名
ghostClass: 'smart-ghost-class', //设置拖拽停靠样式类名
chosenClass: 'smart-ghost-class', //设置选中样式类名
handle: '.handle',
onEnd({ newIndex, oldIndex }) {
if (newIndex == oldIndex) {
return;
}
moveTableData(oldIndex, newIndex);
},
});
}
//上移
function up(oldIndex) {
let newIndex = oldIndex - 1;
if (newIndex < 0) {
return;
}
//如果下一个是固定列,则也不可移动
if (tableData.value[newIndex].fixed) {
return;
}
moveTableData(oldIndex, newIndex);
}
//下移
function down(oldIndex) {
let newIndex = oldIndex + 1;
if (newIndex >= tableData.value.length) {
return;
}
//如果下一个是固定列,则也不可移动
if (tableData.value[newIndex].fixed) {
return;
}
moveTableData(oldIndex, newIndex);
}
//移动表格数据
function moveTableData(oldIndex, newIndex) {
const currRow = tableData.value.splice(oldIndex, 1)[0];
tableData.value.splice(newIndex, 0, currRow);
}
// ----------- table 批量操作 start -----------
const selectedRowKeyList = ref([]);
function onSelectChange(keyArray) {
selectedRowKeyList.value = keyArray;
}
// -------------------------提交表单【恢复默认、保存、取消】 ------------------------
const submitLoading = ref(false);
//重置
function reset() {
Modal.confirm({
title: '确定要恢复默认吗?',
content: '确定恢复默认后,该信息将不可恢复',
okText: '确定恢复',
okType: 'danger',
onOk() {
(async () => {
submitLoading.value = true;
try {
await tableColumnApi.deleteColumns(tableId);
message.success('恢复默认成功');
emit('change', []);
hide();
} catch (e) {
smartSentry.captureError(e);
} finally {
submitLoading.value = false;
}
})();
},
cancelText: '取消',
onCancel() {},
});
}
//保存
async function save() {
submitLoading.value = true;
try {
let columnList = [];
for (let index = 0; index < tableData.value.length; index++) {
let item = tableData.value[index];
let column = {
columnKey: item.columnKey,
sort: index + 1,
};
if (item.width) {
column.width = item.width;
}
column.showFlag = selectedRowKeyList.value.indexOf(item.columnKey) > -1 ? true : false;
columnList.push(column);
}
columnList = _.sortBy(columnList, (e) => e.sort);
await tableColumnApi.updateTableColumn({
tableId,
columnList,
});
message.success('保存成功');
emit('change', columnList);
hide();
} catch (e) {
smartSentry.captureError(e);
} finally {
submitLoading.value = false;
}
}
</script>
<style scoped lang="less">
.column-row:hover {
background-color: red !important;
}
.column-row {
cursor: pointer;
}
.blue-background-class {
background-color: red !important;
}
:deep(.ant-table-tbody) {
.ant-table-row-selected > td {
background-color: #ffffff;
}
}
</style>