效果图
DataAnalysis.vue
<template><div class="app-container"><div class="operate"><el-select class="t_select" v-model="templateName" clearable placeholder="模版" size="default" @clear="clearTemplateData" @change="templateData"><el-option v-for="item in templateList" :key="item.id" :label="item.templateName" :value="item.id"/></el-select><el-button @click="setTemplate" type="primary" size="default">保存模版</el-button><el-button @click="excelExport" type="primary" size="default">Excel导出</el-button></div><main><!-- 所有属性 --><Draggableclass="item-container item-container1"group="drag"v-model:list="allProps"item-key="label"><template #item="{ element }"><div class="item">{{ element.label }}</div></template></Draggable><!-- 多级表头子属性:基础表头① --><Draggableclass="item-container item-container2"group="drag"v-model:list="baseProps"item-key="label"><template #item="{ element }"><div class="item">{{ element.label }}</div></template></Draggable><!-- 多级表头:③ --><Draggableclass="item-container item-container3"group="drag"v-model:list="multiLevelProps"item-key="label"><template #item="{ element }"><div class="item">{{ element.label }}</div></template></Draggable><!-- 分组属性:② --><Draggableclass="item-container item-container4"group="drag"v-model:list="groupProps"item-key="label"><template #item="{ element }"><div class="item">{{ element.label }}</div></template></Draggable><!-- 表格展示数据 --><el-table :data="brr" border :span-method="objectSpanMethod"><el-table-columnv-for="item in groupProps":prop="item.prop":label="item.label"/><MultiHeaders:multiHeaders="multiHeaderValues":baseProps="baseProps"v-if="multiLevelProps && multiLevelProps.length"/><el-table-columnv-for="item in baseProps":prop="item.prop":label="item.label"v-else/></el-table></main><el-dialog v-model="isShowTemplate" title="设置模版" width="30%"><el-form label-width="100px" :model="tForm" style="max-width: 460px"><el-form-item label="模版名称"><el-input v-model="tForm.templateName"/></el-form-item></el-form><template #footer><span class="dialog-footer"><el-button @click="isShowTemplate = false">取消</el-button><el-button type="primary" @click="saveTemplate">保存</el-button></span></template></el-dialog></div>
</template><script setup>
import {ref,onMounted, computed,watch,getCurrentInstance} from "vue"
import Draggable from "vuedraggable"
import MultiHeaders from "./MultiHeaders.vue"
import {useRoute} from "vue-router";
import {useStore} from "vuex";
import {useStorage} from "@vueuse/core";
import {parseTime} from "@/utils/ruoyi";
import {getAnalysisTemplate,saveAnalysisTemplate} from "@/api/common/dataAnalysis";const route = useRoute()
document.title = '数据分析'
const store = useStore()
const {proxy} = getCurrentInstance()
//全部表头属性
const allProps = ref([{label: "项目编码",prop: "businessNo",},{label: "箱号",prop: "caseNum",},{label: "客户",prop: "customerName",},{label: "费用类型",prop: "feeCodeName",},{label: "提单号",prop: "mblNo",},{label: "币种",prop: "currencyName",},{label: "数量",prop: "num",},{label: "金额",prop: "shouldReceipt",},
])
//基础表头属性①
const baseProps = ref([])
//多级表头属性③
const multiLevelProps = ref([])
//分组表头属性②
const groupProps = ref([])
//需要分组的数据
const data = ref([])const multiHeaderValues = computed(() => {//获取去重后的multiLevelProps的值return multiLevelProps.value.map((item) => {return findGroup(item.prop)})
})
//选中的模版
const template = ref([])onMounted(() => {queryData()getTemplate()
})let index = ref(0)watch([()=>groupProps.value,()=>baseProps.value,()=>multiLevelProps.value,()=>template.value],()=>{addGroup()index.value = 0
},{deep:true
})function addGroup(){//获取brr和arrgetBrr()//排序// arr.value.forEach(item=>{// data.value.sort(comprisonFunction(item))// })data.value.sort(comprisonFunction(arr.value[0]))brr.value.sort(comprisonFunction(arr.value[0]))//对brr去重goWeight()//分层级合并列setTabelRowSpan(brr.value, arr.value);//合并baseProps中字段mergeValue()// console.log(brr.value)// console.log(results.value)// console.log(baseProps.value)// console.log(groupProps.value)// console.log(multiHeaderValues.value)// console.log(multiLevelProps.value)// console.table(brr.value)
}const brr = ref([])//分组列去重后的数据(groupProps中值的k:v)
const arr = ref([])//要分组的Table属性(groupProps中值的key)
function getBrr(){brr.value = []arr.value = []data.value.forEach((item) => {let obj={}for (let i = 0; i < groupProps.value.length; i++) {let tempObj = {}tempObj[groupProps.value[i].prop] = item[groupProps.value[i].prop]obj = {...obj,...tempObj}if ( arr.value.length < groupProps.value.length){arr.value.push(groupProps.value[i].prop)}}brr.value.push(obj)})
}function goWeight(){let obj = {};let tempArr=brr.valuebrr.value = tempArr.reduce((curr, next) => {let str=''arr.value.forEach(item=>{str+=next[item]})obj[str] ? '' : obj[str]=curr.push(next);return curr;}, []);
}function setTabelRowSpan(tableData, fieldArr){let lastItem = {};fieldArr.forEach((field, index) => {tableData.forEach(item => {item.mergeCell = fieldArr;const rowSpan = `rowspan_${field}`//判断是否合并到上个单元格。if(fieldArr.slice(0, index + 1).every(e => lastItem[e] === item[e])){//是:合并行item[rowSpan] = 0;lastItem[rowSpan] += 1;}else{//否:完成一次同类合并。lastItem重新赋值,进入下一次合并计算。item[rowSpan] = 1;lastItem = item;}})})
}function objectSpanMethod({ row, column, rowIndex, columnIndex }) {//判断当前单元格是否需要合并if (row.mergeCell.includes(column.property)) {const rowspan = row[`rowspan_${column.property}`]if (rowspan) {return {rowspan: rowspan, colspan: 1};} else {return {rowspan: 0, colspan: 0};}}
}function mergeValue(){if(multiHeaderValues.value.length>0){butes()}brr.value.forEach(item=>{data.value.forEach(temp=>{let isTrue = truearr.value.forEach(arrTmp=>{if (temp[arrTmp]!==item[arrTmp]){isTrue = false}})let m_level = ''multiLevelProps.value.forEach(mpItem=>{m_level = m_level === '' ? temp[mpItem.prop] : m_level+'_'+temp[mpItem.prop]})if (isTrue){//data中每个元素(即对象)的groupProps属性值相等才会合并baseProps.value.forEach(bpItem=>{if ((typeof (temp[bpItem.prop])==='number')){//数据类型累加if(multiLevelProps.value.length!==0){results.value.forEach(rItem=>{if(m_level === rItem){if (temp[bpItem.prop]!==''&&temp[bpItem.prop]!==null){if (item[bpItem.prop+"_"+rItem]===undefined){item[bpItem.prop+"_"+rItem]=temp[bpItem.prop]}else {item[bpItem.prop+"_"+rItem]=item[bpItem.prop+"_"+rItem] + temp[bpItem.prop]}}}})}else{if (temp[bpItem.prop]!==''&&temp[bpItem.prop]!==null){if (item[bpItem.prop]===undefined){item[bpItem.prop]=temp[bpItem.prop]}else {item[bpItem.prop]=item[bpItem.prop] + temp[bpItem.prop]}}}return}//字符串类型拼接if (temp[bpItem.prop]!==''&&temp[bpItem.prop]!==null){if(multiLevelProps.value.length!==0){results.value.forEach(rItem=>{if(m_level === rItem){if (temp[bpItem.prop]!==''&&temp[bpItem.prop]!==null){if (item[bpItem.prop+"_"+rItem]===undefined){item[bpItem.prop+"_"+rItem]=temp[bpItem.prop]}else {if (!item[bpItem.prop+"_"+rItem].includes(temp[bpItem.prop]))item[bpItem.prop+"_"+rItem]=item[bpItem.prop+"_"+rItem] +','+ temp[bpItem.prop]}}}})}else {if (temp[bpItem.prop] !== '' && temp[bpItem.prop] !== null) {if (item[bpItem.prop] === undefined) {item[bpItem.prop] = temp[bpItem.prop]} else {if (!item[bpItem.prop].includes(temp[bpItem.prop]))item[bpItem.prop] = item[bpItem.prop] + ',' + temp[bpItem.prop]}}}}})}})})
}//递归循环数据
let results = ref([])
function butes(){const m_len = multiHeaderValues.value.lengthif(m_len>0){if(m_len >= 2){results.value = recurse(multiHeaderValues.value[index.value],multiHeaderValues.value[++index.value],m_len,multiHeaderValues.value)}else{results.value = multiHeaderValues.value[0]}return results.value}
}function recurse(arr1,arr2,len,arr){let newArr = []arr1.forEach(item=>{arr2.forEach(item2=>{newArr.push(item+'_'+item2)})})index.value++if(index.value < len){return recurse(newArr,arr[index.value],len,arr)}else{return newArr}
}function comprisonFunction (propName) {return function (object1, object2) {let value1 = object1[propName];let value2 = object2[propName];if (value1 > value2 ) {return -1;} else if(value1 < value2) {return 1;} else {return 0;}}}// const obj = ref({})
// const groupArr = ref([])
// function getGroupVal() {
// obj.value = {}
// groupArr.value = []
// for (let i = 0; i < groupProps.value.length; i++) {
// obj.value[groupProps.value[i].prop + 'Arr'] = [];
// groupArr.value[i] = []
// data.value.forEach(item => {
// if (!obj.value[groupProps.value[i].prop + 'Arr'].includes(item[groupProps.value[i].prop])) {
// obj.value[groupProps.value[i].prop + 'Arr'].push(item[groupProps.value[i].prop]);
// let abc = {}
// abc[groupProps.value[i].prop] = item[groupProps.value[i].prop]
// // console.log(abc)
// groupArr.value[i].push(abc)
// }
// })
// }
// }function findGroup(prop) {const set = new Set()data.value.forEach(item => {set.add(item[prop])})//去重后再转换成数组return Array.from(set)
}//导出excel时数据表头
function excelHeader(){const newArr = []results.value.forEach(rItem=>{baseProps.value.forEach(item=>{let arr = []arr.push(item.prop+"_"+rItem)newArr.push(arr)})})return newArr
}function generateArray() {const rows = [] //多重表头例如[[1,2,3][4,5]]const ranges = [] //合并单元格const mainLength = groupProps.value.length//所有多重表头(包括最后一行)const headers = multiHeaderValues.value.concat([baseProps.value])//数据表头const mHeader = excelHeader()//所有多重表头长度乘积const allCols = headers.reduce((res, cur) => {return res * cur.length}, 1)let colspan = allCols // 4 总共需要的单元格for (let i = 0; i < headers.length; i++) {const curRow = headers[i]//需要合并的单元格数量colspan = colspan / curRow.length//需要重复的次数const cycleTime = allCols / colspan / curRow.lengthlet row = new Array(mainLength).fill('')if (i === headers.length - 1) {row = [...groupProps.value]}for (let k = 0; k < cycleTime; k++) {curRow.forEach((val, index) => {const C = index * colspan + k * curRow.length * colspan + mainLengthconst range = {s: {r: i, c: C}, e: {r: i, c: C + colspan - 1}}if (colspan > 1) ranges.push(range)row.push(val)for (let j = 1; j < colspan; j++) {row.push("")}})}rows.push(row)}// for(let m=0;m<mainLength;m++){// ranges.push(// {// s:{// r:headers.length-1,// c:m// },// e:{// r:0,// c:m// }// }// )// }return {ranges,rows,mHeader}
}//报表导出
function excelExport() {import("@/utils/Export2Excel").then((excel) => {// const tHeader = groupProps.value.map((item)=>item.name)// const filterVal = groupProps.value.map((item)=>item.prop)const {ranges, rows,mHeader} = generateArray()const tHeader = rows[rows.length - 1].map((item) => item.label)// const filterVal = rows[rows.length - 1].map((item) => item.prop)const multiHeader = rows.slice(0, -1)const gHeader = groupProps.value.map((item)=>item.prop)const filterVal = gHeader.concat(mHeader.map((item)=>item[0]))const TData = formatJson(filterVal, brr.value)excel.export_json_to_excel({header: tHeader,multiHeader: multiHeader,data: TData,filename: "数据分析",merges: ranges,autoWidth: true,bookType: "xlsx",})})
}function formatJson(filterVal, jsonData) {return jsonData.map((v) =>filterVal.map((j) => {return v[j]}))
}
const myOrigin = window.location.origin
function queryData(){window.addEventListener('message',function (e) {if (e.origin === myOrigin) {document.title = e.data.title+'-数据分析'if (e.data.isPush) {data.value = JSON.parse(e.data.data)allProps.value = JSON.parse(e.data.allProps)data.value.forEach(item=>{//业务类型if(item.businessType === 0){item.businessType = '海运出口'} else if(item.businessType === 1){item.businessType = '空运出口'} else if(item.businessType === 2){item.businessType = '海运进口'}//单据日期if(item.glMarineSpecialOutDTO.id){item.expectSailingStartDate = parseTime(item.glMarineSpecialOutDTO.expectSailingStartDate, "{y}-{m}-{d}")} else if(item.glMarineImportOutDTO.id){item.expectSailingStartDate = parseTime(item.glMarineImportOutDTO.expectSailingStartDate, "{y}-{m}-{d}")} else if(item.glAirExportOutDTO.id){item.expectSailingStartDate = parseTime(item.glAirExportOutDTO.expectSailingStartDate, "{y}-{m}-{d}")}//装运日期if(item.glMarineSpecialOutDTO.shipmentDate){item.shipmentDate = parseTime(item.glMarineSpecialOutDTO.shipmentDate, "{y}-{m}-{d}")}//送达日期if(item.glMarineSpecialOutDTO.deliveryDate){item.deliveryDate = parseTime(item.glMarineSpecialOutDTO.deliveryDate, "{y}-{m}-{d}")}//预计到港日期if(item.glMarineSpecialOutDTO.expectSailingArrivalDate){item.expectSailingArrivalDate = parseTime(item.glMarineSpecialOutDTO.expectSailingArrivalDate, "{y}-{m}-{d}")} else if(item.glMarineImportOutDTO.id){item.expectSailingArrivalDate = parseTime(item.glMarineImportOutDTO.expectSailingArrivalDate, "{y}-{m}-{d}")} else if(item.glAirExportOutDTO.id){item.expectSailingArrivalDate = parseTime(item.glAirExportOutDTO.expectSailingArrivalDate, "{y}-{m}-{d}")}//预计开航日期if(item.glMarineSpecialOutDTO.expectSailingStartDate){item.expectSailingStartDate = parseTime(item.glMarineSpecialOutDTO.expectSailingStartDate, "{y}-{m}-{d}")} else if(item.glMarineImportOutDTO.id){item.expectSailingStartDate = parseTime(item.glMarineImportOutDTO.expectSailingStartDate, "{y}-{m}-{d}")} else if(item.glAirExportOutDTO.id){item.expectSailingStartDate = parseTime(item.glAirExportOutDTO.expectSailingStartDate, "{y}-{m}-{d}")}})let obj = {data:data.value,allProps:allProps.value}useStorage(e.data.tableName,JSON.stringify(obj))} else {let arr = JSON.parse(useStorage(e.data.tableName).value)data.value = arr.dataallProps.value = arr.allProps}}})
}//保存模版
//被选中的模版
const templateName = ref('')
const isShowTemplate = ref(false)
const tForm = ref({templateName:''
})//模版弹窗
function setTemplate(){isShowTemplate.value = true
}//所有的模版
const templateList = ref([])//从数据库中获取当前用户的所有模版数据
function getTemplate(){getAnalysisTemplate({employeeId: store.state.user.info.id,pageNum:1,pageSize:100}).then(res => {templateList.value = res.data.records})
}//将模版数据保存数据库
function saveTemplate(){let params = {"employeeId": store.state.user.info.id,"templateName": tForm.value.templateName,"data":JSON.stringify({"baseProps":JSON.stringify(baseProps.value),"groupProps": JSON.stringify(groupProps.value),"multiLevelProps": JSON.stringify(multiLevelProps.value),"multiHeaderValues": JSON.stringify(multiHeaderValues.value)})}saveAnalysisTemplate(params).then(response => {isShowTemplate.value = falsegetTemplate()proxy.$modal.msgSuccess("保存成功");})
}//根据模版获取数据生成数据报表
function templateData(val){if(val !== undefined){templateList.value.forEach(item=>{if(item.id === val){template.value = JSON.parse(item.data)}})if(template.value.hasOwnProperty("baseProps")){baseProps.value = JSON.parse(template.value.baseProps)groupProps.value = JSON.parse(template.value.groupProps)multiLevelProps.value = JSON.parse(template.value.multiLevelProps)multiHeaderValues.value = JSON.parse(template.value.multiHeaderValues)}}
}function clearTemplateData(){template.value =[]baseProps.value = []groupProps.value = []multiLevelProps.value = []multiHeaderValues.value = []
}</script><style lang="scss" scoped>
.item {background: #333;padding: 5px;
}.item-container {background: #eee;padding: 10px;display: flex;min-height: 51px;align-self: flex-start;flex-wrap: wrap;align-items: flex-start;gap: 10px;cursor: move;color: #ccc;
}.item-container1 {grid-column: 1/3;
}main {display: grid;gap: 10px;// margin-top: 10px;grid-template-columns: 200px 1fr;
}
.operate{margin-bottom: 5px;.t_select{margin-right: 12px;}
}
</style>
MultiHeaders.vue
<template>
<!-- <el-table-column v-for="(item,index) in headers" :label="item" >-->
<!-- <MultiHeaders v-for="item2 in subHeaders" :multiHeaders="subHeaders" v-if="subHeaders&&subHeaders.length" :baseProps="baseProps" :resultsProps="resultsProps" ></MultiHeaders>-->
<!-- <el-table-column v-else v-for="items in baseProps" :label="items.prop" :prop="items.prop"></el-table-column>-->
<!-- </el-table-column>--><el-table-column v-for="item in headers" :label="item.name"><template v-if="item.child.length>0"><multi-header v-if="item.child.length>0" :child="item.child" :baseProps="baseProps" :upProp="item.prop"></multi-header></template><el-table-column v-else v-for="bItem in baseProps" :label="bItem.label" :prop="bItem.prop+'_'+item.prop" ></el-table-column></el-table-column>
</template><script setup>
import {computed, watch} from "vue"
import MultiHeader from "@/views/configTable/MultiHeader.vue";
const props = defineProps({multiHeaders: { //右边款字段的值=table表头typeof: Array,default: [],},baseProps: { // 左上框数据typeof: Array,default: [],},upProp: {typeof: String,default: '',},
})
function childData(list,i){const arr = []//最后一个数组if(i<list.length){list[i].forEach(item=>{const obj = {name:'',prop:'',child:[]}obj['name'] = itemobj['prop'] = itemobj['child'] = []arr.push(obj)})}return arr
}
function transListDataToTreeData(list,i) {//共3条数据const arr = list[i] // 第一层数组 1if(arr){const news = []arr.forEach(item=>{let obj = {name:item,prop:item,child:[]}const child = childData(list,i+1)if(child.length > 0){obj.child = transListDataToTreeData(list,i+1)}else{obj.child = child}news.push(obj)})return news}else{return []}
}function sliceArr(arr,size){const res = []for (let i=0;i<Math.ceil(arr.length/size);i++){let start = i*sizelet end = start + sizeres.push(arr.slice(start,end))}return res
}const headers = computed(() => {// return props.multiHeaders[0]return transListDataToTreeData(props.multiHeaders,0)
})
const subHeaders = computed(() => {return props.multiHeaders.slice(1)
})
watch(() => headers.value,(value) => {console.log(value)}
)
</script><style lang="scss" scoped></style>