效果图
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>
<!-- 所有属性 -->
<Draggable
class="item-container item-container1"
group="drag"
v-model:list="allProps"
item-key="label"
>
<template #item="{ element }">
<div class="item">{{ element.label }}</div>
</template>
</Draggable>
<!-- 多级表头子属性:基础表头① -->
<Draggable
class="item-container item-container2"
group="drag"
v-model:list="baseProps"
item-key="label"
>
<template #item="{ element }">
<div class="item">{{ element.label }}</div>
</template>
</Draggable>
<!-- 多级表头:③ -->
<Draggable
class="item-container item-container3"
group="drag"
v-model:list="multiLevelProps"
item-key="label"
>
<template #item="{ element }">
<div class="item">{{ element.label }}</div>
</template>
</Draggable>
<!-- 分组属性:② -->
<Draggable
class="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-column
v-for="item in groupProps"
:prop="item.prop"
:label="item.label"/>
<MultiHeaders
:multiHeaders="multiHeaderValues"
:baseProps="baseProps"
v-if="multiLevelProps && multiLevelProps.length"/>
<el-table-column
v-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和arr
getBrr()
//排序
// 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.value
brr.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 = true
arr.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.length
if(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.length
let 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 + mainLength
const 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.data
allProps.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 = false
getTemplate()
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'] = item
obj['prop'] = item
obj['child'] = []
arr.push(obj)
})
}
return arr
}
function transListDataToTreeData(list,i) {
//共3条数据
const arr = list[i] // 第一层数组 1
if(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*size
let end = start + size
res.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>