|
@@ -1,544 +0,0 @@
|
|
|
-<template>
|
|
|
- <div class="table-responsive">
|
|
|
- <table>
|
|
|
- <!-- Table header -->
|
|
|
- <thead>
|
|
|
- <tr v-for="(tr, index) in combineHeads" :key="index">
|
|
|
- <th v-for="cell in tr" :key="cell.__index" :rowspan="cell.rowspan" :colspan="cell.colspan">
|
|
|
- <div :class="{ 'col-corner-bg': cell.isCorner }" :style="{ 'min-height': _getMinHeightByRowCount(cell.rowspan) }">
|
|
|
- {{ cell.isCorner ? (rowPaths.length + ' x ' + colPaths.length) : cell.value }}
|
|
|
- </div>
|
|
|
- </th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <!-- Table body -->
|
|
|
- <tbody>
|
|
|
- <tr v-for="(tr, index) in combineValues" :key="index">
|
|
|
- <!-- Row headers -->
|
|
|
- <th v-for="cell in tr.head" v-if="!cell.isRowspan" :key="cell.__index" :rowspan="cell.rowspan" :colspan="cell.colspan">
|
|
|
- <div :style="{ 'min-height': _getMinHeightByRowCount(cell.rowspan) }">
|
|
|
- {{ cell.value }}
|
|
|
- </div>
|
|
|
- </th>
|
|
|
- <!-- Values -->
|
|
|
- <td v-for="cell in tr.data" :key="cell.__index" :rowspan="cell.rowspan" :colspan="cell.colspan">
|
|
|
- <div :style="{ 'min-height': _getMinHeightByRowCount(cell.rowspan) }">
|
|
|
- {{ cell.value }}
|
|
|
- </div>
|
|
|
- </td>
|
|
|
- </tr>
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script>
|
|
|
-import { mergeBaseInfo, convertPathToMap, getHeightByCount, SEPARATOR } from '@/utils/visual-chart'
|
|
|
-
|
|
|
-export default {
|
|
|
- name: 'PivotTable',
|
|
|
- props: {
|
|
|
- data: {
|
|
|
- type: Array,
|
|
|
- default: () => []
|
|
|
- },
|
|
|
- rows: {
|
|
|
- type: Array,
|
|
|
- default: () => []
|
|
|
- },
|
|
|
- columns: {
|
|
|
- type: Array,
|
|
|
- default: () => []
|
|
|
- },
|
|
|
- values: {
|
|
|
- type: Array,
|
|
|
- default: () => []
|
|
|
- }
|
|
|
- },
|
|
|
- data: () => ({
|
|
|
- localRows: [],
|
|
|
- localColumns: [],
|
|
|
- localValues: [],
|
|
|
- localData: [],
|
|
|
- // 计算列的数据
|
|
|
- calcData: [],
|
|
|
- // Separator
|
|
|
- Separator: SEPARATOR,
|
|
|
- // 合并后的表头
|
|
|
- combineHeads: [],
|
|
|
- // 合并后的单元格
|
|
|
- combineValues: []
|
|
|
- }),
|
|
|
- computed: {
|
|
|
- watchAllProps() {
|
|
|
- const { rows, columns, values, data } = this
|
|
|
- return { rows, columns, values, data }
|
|
|
- },
|
|
|
- rowPaths() {
|
|
|
- const _paths = this._combineRowPaths(
|
|
|
- this.localData,
|
|
|
- ...this.localRows.map(({ key, values }) => { return { key, values } })
|
|
|
- )
|
|
|
- return _paths
|
|
|
- },
|
|
|
- colPaths() {
|
|
|
- const keys = this.localColumns.map(({ values }) => values)
|
|
|
- if (this.localValues.length) {
|
|
|
- keys.push(this.localValues.map(({ key }) => key))
|
|
|
- }
|
|
|
- const _paths = this._combineColPaths(...keys)
|
|
|
- return _paths
|
|
|
- },
|
|
|
- // 列的表头
|
|
|
- colHeads() {
|
|
|
- // 共有多少行
|
|
|
- const _rows = this.localColumns.map(() => [])
|
|
|
- // 有几个值
|
|
|
- const valuesLen = this.localValues.length
|
|
|
- if (valuesLen) {
|
|
|
- _rows.push([])
|
|
|
- }
|
|
|
- // 计算合并单元格
|
|
|
- const colSpans = {}
|
|
|
- this.colPaths.forEach((path, pathIndex) => {
|
|
|
- // 条件值
|
|
|
- const pathValues = path.split(this.Separator)
|
|
|
- // 存储路径
|
|
|
- const currPath = []
|
|
|
- _rows.forEach((row, rowIndex) => {
|
|
|
- const cellData = {}
|
|
|
- const currVal = pathValues[rowIndex] || ''
|
|
|
- // 是否为 values 行
|
|
|
- const isLastRow = rowIndex === _rows.length - 1
|
|
|
- // 存储路径
|
|
|
- currPath.push(currVal)
|
|
|
- const baseX = rowIndex
|
|
|
- const baseY = this.localRows.length + pathIndex
|
|
|
- if (!isLastRow) {
|
|
|
- // 计算合并行数
|
|
|
- let compareVal = valuesLen
|
|
|
- for (let i = rowIndex; i < this.localColumns.length - 1; i++) {
|
|
|
- compareVal *= this.localColumns[rowIndex + 1].values.length
|
|
|
- }
|
|
|
- const currColSpan = colSpans[rowIndex] || {}
|
|
|
- const currColSpanVal = (currColSpan[currPath.join(this.Separator)] || 0) + 1
|
|
|
- currColSpan[currPath.join(this.Separator)] = currColSpanVal
|
|
|
- colSpans[rowIndex] = currColSpan
|
|
|
- // 合并单元格:起始单元格加入
|
|
|
- if (currColSpanVal === 1) {
|
|
|
- row.push(
|
|
|
- Object.assign(
|
|
|
- cellData,
|
|
|
- mergeBaseInfo({
|
|
|
- __index: `${baseX}-${baseY}`,
|
|
|
- x: baseX,
|
|
|
- y: baseY,
|
|
|
- colspan: compareVal,
|
|
|
- path: currPath.filter((item) => !!item),
|
|
|
- value: currVal
|
|
|
- })
|
|
|
- )
|
|
|
- )
|
|
|
- }
|
|
|
- } else {
|
|
|
- row.push(
|
|
|
- Object.assign(
|
|
|
- cellData,
|
|
|
- mergeBaseInfo({
|
|
|
- __index: `${baseX}-${baseY}`,
|
|
|
- x: baseX,
|
|
|
- y: baseY,
|
|
|
- path: currPath.filter((item) => !!item),
|
|
|
- // 最后一行是 values,替换显示文本
|
|
|
- value: this.localValues.find(({ key }) => key === currVal).label
|
|
|
- })
|
|
|
- )
|
|
|
- )
|
|
|
- }
|
|
|
- })
|
|
|
- })
|
|
|
- return _rows
|
|
|
- },
|
|
|
- // 行的表头
|
|
|
- rowHeads() {
|
|
|
- // 共有多少列
|
|
|
- const _columns = []
|
|
|
- // 左上角特殊处理, 有行维、列维时才有
|
|
|
- const columnsLen = this.localColumns.length
|
|
|
- const rowsLen = this.localRows.length
|
|
|
- if (rowsLen && columnsLen) {
|
|
|
- _columns.push(mergeBaseInfo({
|
|
|
- __index: `0-0`,
|
|
|
- colspan: this.localRows.length,
|
|
|
- rowspan: this.localColumns.length,
|
|
|
- // 左上角标记
|
|
|
- isCorner: true
|
|
|
- }))
|
|
|
- }
|
|
|
- this.localRows.forEach(({ label }, index) => {
|
|
|
- _columns.push(mergeBaseInfo({
|
|
|
- __index: `${this.localColumns.length}-${index}`,
|
|
|
- value: label,
|
|
|
- x: this.localColumns.length,
|
|
|
- y: index
|
|
|
- }))
|
|
|
- })
|
|
|
- return _columns
|
|
|
- },
|
|
|
- // 行对应的值
|
|
|
- rowHeadValues() {
|
|
|
- let _values = []
|
|
|
- const rowsLen = this.localRows.length
|
|
|
- if (rowsLen) {
|
|
|
- // 计算合并单元格
|
|
|
- const rowSpans = {}
|
|
|
- _values = this.rowPaths.map((path, pathIndex) => {
|
|
|
- const values = path.split(this.Separator)
|
|
|
- const currPath = []
|
|
|
- return this.localRows.map((item, rowIndex) => {
|
|
|
- const currVal = values[rowIndex] || ''
|
|
|
- const baseX = this.localColumns.length + +Boolean(this.localValues.length) + pathIndex
|
|
|
- const baseY = rowIndex
|
|
|
- currPath.push(currVal)
|
|
|
- // 是否为最后列
|
|
|
- const isLastCol = rowIndex === rowsLen - 1
|
|
|
- if (!isLastCol) {
|
|
|
- // 计算合并列数
|
|
|
- // 过滤条件
|
|
|
- const conditions = {}
|
|
|
- for (let i = 0; i < rowIndex + 1; i++) {
|
|
|
- conditions[i] = values[i] || ''
|
|
|
- }
|
|
|
- const filterData = this.rowPaths.filter((data) => {
|
|
|
- let status = true
|
|
|
- const splitValues = data.split(this.Separator)
|
|
|
- for (const key in conditions) {
|
|
|
- if (conditions[key] !== splitValues[key]) {
|
|
|
- status = false
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
- return status
|
|
|
- }) || []
|
|
|
- const mergeSpans = filterData.length
|
|
|
- const currRowSpan = rowSpans[rowIndex] || {}
|
|
|
- const currRowSpanVal = (currRowSpan[currPath.join(this.Separator)] || 0) + 1
|
|
|
- currRowSpan[currPath.join(this.Separator)] = currRowSpanVal
|
|
|
- rowSpans[rowIndex] = currRowSpan
|
|
|
- if (currRowSpanVal === 1) {
|
|
|
- return mergeBaseInfo({
|
|
|
- __index: `${baseX}-${baseY}`,
|
|
|
- value: currVal,
|
|
|
- x: baseX,
|
|
|
- y: baseY,
|
|
|
- rowspan: mergeSpans,
|
|
|
- path: currPath.filter((item) => !!item)
|
|
|
- })
|
|
|
- } else {
|
|
|
- return mergeBaseInfo({
|
|
|
- __index: `${baseX}-${baseY}`,
|
|
|
- value: currVal,
|
|
|
- x: baseX,
|
|
|
- y: baseY,
|
|
|
- path: currPath.filter((item) => !!item),
|
|
|
- // 是否合并单元格,遍历时判断不显示
|
|
|
- isRowspan: true
|
|
|
- })
|
|
|
- }
|
|
|
- } else {
|
|
|
- return mergeBaseInfo({
|
|
|
- __index: `${baseX}-${baseY}`,
|
|
|
- value: currVal,
|
|
|
- x: baseX,
|
|
|
- y: baseY,
|
|
|
- path: currPath.filter((item) => !!item)
|
|
|
- })
|
|
|
- }
|
|
|
- })
|
|
|
- })
|
|
|
- }
|
|
|
- return _values
|
|
|
- },
|
|
|
- // 计算所有对应条件的值
|
|
|
- dataValues() {
|
|
|
- // 列对应的条件
|
|
|
- const colConditions = convertPathToMap(
|
|
|
- this.colPaths,
|
|
|
- this.localColumns.map(({ key }) => key).concat(this.localValues.length ? ['value'] : [])
|
|
|
- )
|
|
|
- // 行对应的条件
|
|
|
- const rowConditions = convertPathToMap(
|
|
|
- this.rowPaths,
|
|
|
- this.localRows.map(({ key }) => key)
|
|
|
- )
|
|
|
- // console.log('colConditions', colConditions)
|
|
|
- // console.log('rowConditions', rowConditions)
|
|
|
- // 针对没传入行或列的处理
|
|
|
- !colConditions.length && colConditions.push({})
|
|
|
- !rowConditions.length && rowConditions.push({})
|
|
|
- // 过滤数据, 遍历行以及遍历行对应的列
|
|
|
- return rowConditions.map((rowCondition, rowConditionIndex) => {
|
|
|
- const _data = colConditions.map((colCondition, colConditionIndex) => {
|
|
|
- // 存储当前单元对应的数据
|
|
|
- const cellData = {}
|
|
|
- // 当前单元对应的条件
|
|
|
- const conditions = Object.assign({}, rowCondition, colCondition)
|
|
|
- const _filterConditions = Object.fromEntries(
|
|
|
- Object.entries(conditions).filter(
|
|
|
- (item) => item[0] !== 'value'
|
|
|
- )
|
|
|
- )
|
|
|
- // 通过当前单元对应的条件,过滤数据
|
|
|
- const filterData = this._filterData(_filterConditions, this.localData)
|
|
|
- // 对应表格的坐标位置
|
|
|
- const baseX = this.localColumns.length + +Boolean(this.localValues.length) + rowConditionIndex
|
|
|
- const baseY = this.localRows.length + colConditionIndex
|
|
|
- Object.assign(
|
|
|
- cellData,
|
|
|
- mergeBaseInfo({
|
|
|
- conditions,
|
|
|
- x: baseX,
|
|
|
- y: baseY,
|
|
|
- __index: `${baseX}-${baseY}`
|
|
|
- })
|
|
|
- )
|
|
|
- // 针对为指定值 props.values 的空处理(绘制空表格)
|
|
|
- const isEmptyValues = this.localColumns.length && this.localRows.length && !this.localValues.length
|
|
|
- if (isEmptyValues) {
|
|
|
- Object.assign(cellData, { value: '' })
|
|
|
- } else {
|
|
|
- // 从 props.values 中找出对应的值
|
|
|
- const _value = this.values.find(({ key }) => key === conditions.value)
|
|
|
- Object.assign(cellData, { value: _value && _value.key ? this._reduceValue(filterData, _value.key) : '' })
|
|
|
- }
|
|
|
- return cellData
|
|
|
- })
|
|
|
- return {
|
|
|
- __index: _data[0].x,
|
|
|
- data: _data
|
|
|
- }
|
|
|
- })
|
|
|
- }
|
|
|
- },
|
|
|
- watch: {
|
|
|
- watchAllProps() {
|
|
|
- this.init()
|
|
|
- }
|
|
|
- },
|
|
|
- created() {
|
|
|
- this.init()
|
|
|
- },
|
|
|
- methods: {
|
|
|
- init() {
|
|
|
- if (this.rows.length || this.columns.length || this.values.length) {
|
|
|
- this.handleDataClone()
|
|
|
- this.setValuesToColAndRow()
|
|
|
- this.handleCalcData()
|
|
|
- this.handleCombineHeads()
|
|
|
- this.handleCombineValues()
|
|
|
- } else {
|
|
|
- console.warn('[Warn]: props.rows, props.columns, props.values at least one is not empty.')
|
|
|
- }
|
|
|
- },
|
|
|
- // clone data
|
|
|
- handleDataClone() {
|
|
|
- this.localRows = JSON.parse(JSON.stringify(this.rows))
|
|
|
- this.localColumns = JSON.parse(JSON.stringify(this.columns))
|
|
|
- this.localValues = JSON.parse(JSON.stringify(this.values))
|
|
|
- this.localData = Object.freeze(this.data)
|
|
|
- },
|
|
|
- // set the `values` attribute to rows and columns
|
|
|
- setValuesToColAndRow() {
|
|
|
- const rowKeys = this.localRows.map(({ key }) => key)
|
|
|
- const columnKeys = this.localColumns.map(({ key }) => key)
|
|
|
- const rowValues = this._findCategory(rowKeys, this.localData)
|
|
|
- const columnValues = this._findCategory(columnKeys, this.localData)
|
|
|
- this.localRows.forEach((row) => {
|
|
|
- const { key, values } = row
|
|
|
- this.$set(row, 'values', values || rowValues[key] || [])
|
|
|
- })
|
|
|
- this.localColumns.forEach((column) => {
|
|
|
- const { key, values } = column
|
|
|
- this.$set(column, 'values', values || columnValues[key] || [])
|
|
|
- })
|
|
|
- },
|
|
|
- // 合并表头
|
|
|
- handleCombineHeads() {
|
|
|
- let combineColHeads = JSON.parse(JSON.stringify(this.colHeads))
|
|
|
- combineColHeads[0] = combineColHeads[0] || []
|
|
|
- combineColHeads[0].unshift(...this.rowHeads.filter((item) => item.isCorner))
|
|
|
- combineColHeads[combineColHeads.length - 1].unshift(...this.rowHeads.filter((item) => !item.isCorner))
|
|
|
- combineColHeads = combineColHeads.filter((item) => item.length)
|
|
|
- this.combineHeads = combineColHeads
|
|
|
- },
|
|
|
- // 合并值
|
|
|
- handleCombineValues() {
|
|
|
- // values
|
|
|
- const combineValues = []
|
|
|
- const valueRowCount = this.dataValues.length || this.rowHeadValues.length
|
|
|
- for (let i = 0; i < valueRowCount; i++) {
|
|
|
- const _currRowHeadValue = this.rowHeadValues[i] || []
|
|
|
- const _currValue = this.dataValues[i] || {}
|
|
|
- const _row = [...(_currValue.data || [])]
|
|
|
- combineValues.push(
|
|
|
- Object.assign({}, { head: [..._currRowHeadValue] }, { data: _row })
|
|
|
- )
|
|
|
- }
|
|
|
- this.combineValues = combineValues
|
|
|
- },
|
|
|
- // 初始计算值
|
|
|
- handleCalcData() {
|
|
|
- if (!this.localValues.length) return
|
|
|
- const _rowPaths = this._combineRowPaths(
|
|
|
- this.localData,
|
|
|
- ...this.localRows.map(({ key, values }) => { return { key, values } })
|
|
|
- )
|
|
|
- const _rowKeys = this.localRows.map(({ key }) => key)
|
|
|
- const _colPaths = this._combineColPaths(
|
|
|
- ...this.localColumns.map(({ values }) => values)
|
|
|
- )
|
|
|
- const _colKeys = this.localColumns.map(({ key }) => key)
|
|
|
- // conditions of col-head
|
|
|
- const colConditions = convertPathToMap(_colPaths, _colKeys)
|
|
|
- // conditions of row-head
|
|
|
- const rowConditions = convertPathToMap(_rowPaths, _rowKeys)
|
|
|
- // Note: if there are no props.rows or props.columns, push an empty object
|
|
|
- !colConditions.length && colConditions.push({})
|
|
|
- !rowConditions.length && rowConditions.push({})
|
|
|
- // draw data
|
|
|
- this.calcData = Object.freeze(
|
|
|
- rowConditions
|
|
|
- .map((rowCondition, rowConditionIndex) =>
|
|
|
- colConditions
|
|
|
- .map((colCondition, colConditionIndex) => {
|
|
|
- // the condition of current cell
|
|
|
- const conditions = Object.assign({}, rowCondition, colCondition)
|
|
|
- // filter the data
|
|
|
- const filterData = this._filterData(conditions, this.localData)
|
|
|
- // empty cell
|
|
|
- const isEmptyCell = this.localRows.length && this.localColumns.length && !this.localValues.length
|
|
|
- const _values = {}
|
|
|
- // 多个值,多条数据
|
|
|
- this.values.forEach(({ key }) => {
|
|
|
- _values[key] = isEmptyCell ? '' : this._reduceValue(filterData, key)
|
|
|
- })
|
|
|
- return Object.assign({}, conditions, _values)
|
|
|
- })
|
|
|
- .flat()
|
|
|
- )
|
|
|
- .filter((item) => item.length)
|
|
|
- .flat()
|
|
|
- )
|
|
|
- },
|
|
|
- _combineRowPaths(data, ...arrays) {
|
|
|
- const len = arrays.length
|
|
|
- let _result = []
|
|
|
- if (len) {
|
|
|
- const rowPaths = arrays.reduce((prev, curr) => {
|
|
|
- const arr = []
|
|
|
- prev.values.forEach(_prevEl => {
|
|
|
- const prevKey = prev.key.split(SEPARATOR)
|
|
|
- curr.values.forEach(_currEl => {
|
|
|
- const currKey = curr.key
|
|
|
- const conditions = {}
|
|
|
- prevKey.forEach((key, i) => {
|
|
|
- conditions[key] = _prevEl.split(SEPARATOR)[i]
|
|
|
- })
|
|
|
- conditions[currKey] = _currEl
|
|
|
- // 判断数据里是否有该项
|
|
|
- const filter = data.some((data) => {
|
|
|
- let status = true
|
|
|
- for (const key in conditions) {
|
|
|
- if (conditions[key] !== data[key]) {
|
|
|
- status = false
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
- return status
|
|
|
- })
|
|
|
- if (filter) {
|
|
|
- arr.push(_prevEl + SEPARATOR + _currEl)
|
|
|
- }
|
|
|
- })
|
|
|
- })
|
|
|
- return { key: prev.key + SEPARATOR + curr.key, values: arr }
|
|
|
- }) || {}
|
|
|
- _result = rowPaths.values || []
|
|
|
- }
|
|
|
- return _result
|
|
|
- },
|
|
|
- _combineColPaths(...arrays) {
|
|
|
- return arrays.length ? arrays.reduce((prev, curr) => {
|
|
|
- const arr = []
|
|
|
- prev.forEach(_prevEl => {
|
|
|
- curr.forEach(_currEl => {
|
|
|
- arr.push(_prevEl + SEPARATOR + _currEl)
|
|
|
- })
|
|
|
- })
|
|
|
- return arr
|
|
|
- }) : arrays
|
|
|
- },
|
|
|
- _findCategory(keys = [], data = []) {
|
|
|
- const _result = {}
|
|
|
- data.forEach(item => {
|
|
|
- keys.forEach(key => {
|
|
|
- // Remove duplicates
|
|
|
- _result[key] = _result[key] || []
|
|
|
- _result[key].push(item[key])
|
|
|
- _result[key] = [...new Set(_result[key])]
|
|
|
- })
|
|
|
- })
|
|
|
- return _result
|
|
|
- },
|
|
|
- _reduceValue(data, key) {
|
|
|
- if (!data.length) return ''
|
|
|
- return data.reduce((sum, item) => { return sum + Number(item[key]) }, 0)
|
|
|
- },
|
|
|
- _filterData(conditions, data) {
|
|
|
- return data.filter((data) => {
|
|
|
- let status = true
|
|
|
- for (const key in conditions) {
|
|
|
- if (conditions[key] !== data[key]) {
|
|
|
- status = false
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
- return status
|
|
|
- })
|
|
|
- },
|
|
|
- // get min height by rowspan
|
|
|
- _getMinHeightByRowCount(count) {
|
|
|
- return getHeightByCount(count)
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-</script>
|
|
|
-
|
|
|
-<style lang="scss" scoped>
|
|
|
-table {
|
|
|
- border-collapse: collapse;
|
|
|
- border-spacing: 0;
|
|
|
- border: none;
|
|
|
- td, th {
|
|
|
- border: 1px solid #ccc;
|
|
|
- padding: 0;
|
|
|
- vertical-align: middle;
|
|
|
- box-sizing: border-box;
|
|
|
- > div {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- box-sizing: border-box;
|
|
|
- padding: 5px;
|
|
|
- text-align: center;
|
|
|
- white-space: nowrap;
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- min-height: 36px;
|
|
|
- cursor: default;
|
|
|
- &.col-corner-bg {
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-</style>
|