初始化
parent
de5d8c6208
commit
bbb43faaa3
@ -0,0 +1,14 @@
|
|||||||
|
import axios from '@/utils/axios';
|
||||||
|
import type { ApiResponse } from '@/api/types';
|
||||||
|
export async function getTableOptionsVOList(options?: { [key: string]: any }) {
|
||||||
|
return axios<ApiResponse<any[]>>('/tableInfo/get/list', {
|
||||||
|
method: 'GET',
|
||||||
|
...(options || {})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function getTableInfoVO(options?: { [key: string]: any }) {
|
||||||
|
return axios<ApiResponse<any[]>>('/tableInfo/get/vo', {
|
||||||
|
method: 'GET',
|
||||||
|
...(options || {})
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
@ -0,0 +1,144 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DataGridSchema } from './type';
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
const props = defineProps({
|
||||||
|
// 表格的列规范
|
||||||
|
columns: {
|
||||||
|
type: Array as PropType<DataGridSchema>,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
// 表格的数据源
|
||||||
|
tableData: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
spanMethod: {
|
||||||
|
type: Function as PropType<
|
||||||
|
(
|
||||||
|
row: any,
|
||||||
|
column: any,
|
||||||
|
rowIndex: number,
|
||||||
|
columnIndex: number
|
||||||
|
) => { rowspan: number; colspan: number } | null
|
||||||
|
>,
|
||||||
|
default: undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['on-action']);
|
||||||
|
const handleActionClick = (actionKey: string, row: any) => {
|
||||||
|
emit('on-action', {
|
||||||
|
actionKey,
|
||||||
|
row
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// 状态字典映射
|
||||||
|
const getStatusConfig = (statusValue: string | number) => {
|
||||||
|
const dict: Record<string, { label: string; elType: string }> = {
|
||||||
|
normal: { label: '正常', elType: 'success' }, // 绿色
|
||||||
|
fault: { label: '故障', elType: 'warning' }, // 黄色
|
||||||
|
alarm: { label: '报警', elType: 'danger' }, // 红色
|
||||||
|
running: { label: '运行', elType: 'primary' }, // 蓝色
|
||||||
|
turnOn: { label: '开启', elType: 'info' }, // 灰色
|
||||||
|
turnOff: { label: '关闭', elType: 'info' }, // 灰色
|
||||||
|
no_action: { label: '未动作', elType: 'info' } // 灰色
|
||||||
|
};
|
||||||
|
return dict[statusValue] || { label: '未知', elType: 'info' };
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="standard-data-grid">
|
||||||
|
<el-table :data="tableData" border :span-method="spanMethod">
|
||||||
|
<template v-for="(item, index) in columns" :key="item.key">
|
||||||
|
<el-table-column
|
||||||
|
v-if="item.children && item.children.length > 0"
|
||||||
|
:label="item.title"
|
||||||
|
:prop="item.key"
|
||||||
|
:align="item.align ?? 'center'"
|
||||||
|
>
|
||||||
|
<el-table-column
|
||||||
|
v-for="subItem in item.children"
|
||||||
|
:key="subItem.key"
|
||||||
|
:prop="subItem.key"
|
||||||
|
:label="subItem.title"
|
||||||
|
:align="subItem.align ?? 'center'"
|
||||||
|
>
|
||||||
|
<template #default="scope">
|
||||||
|
<div v-if="subItem.type === 'action'" class="action-btn-group">
|
||||||
|
<template v-for="btn in subItem.actions" :key="btn.actionKey">
|
||||||
|
<el-button
|
||||||
|
v-if="!btn.showOn || btn.showOn.values.includes(scope.row[btn.showOn.key])"
|
||||||
|
:type="btn.btnType || 'primary'"
|
||||||
|
size="small"
|
||||||
|
@click="handleActionClick(btn.actionKey, scope.row)"
|
||||||
|
>
|
||||||
|
{{ btn.label }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
v-else
|
||||||
|
:label="item.title"
|
||||||
|
:prop="item.key"
|
||||||
|
:align="item.align"
|
||||||
|
:width="item.width"
|
||||||
|
:min-width="item.minWidth"
|
||||||
|
>
|
||||||
|
<template #default="scope">
|
||||||
|
<span v-if="item.type === 'text'">
|
||||||
|
{{ scope.row[item.key] ?? '--' }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="item.type === 'number'">
|
||||||
|
{{ scope.row[item.key] }}<span class="unit">{{ item.unit }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="item.type === 'status'">
|
||||||
|
<el-tag :type="getStatusConfig(scope.row[item.key]).elType">
|
||||||
|
{{ getStatusConfig(scope.row[item.key]).label }}
|
||||||
|
</el-tag>
|
||||||
|
</span>
|
||||||
|
<div v-else-if="item.type === 'action'" class="action-btn-group">
|
||||||
|
<template v-for="btn in item.actions" :key="btn.actionKey">
|
||||||
|
<el-button
|
||||||
|
v-if="!btn.showOn || btn.showOn.values.includes(scope.row[btn.showOn.key])"
|
||||||
|
:type="btn.btnType || 'primary'"
|
||||||
|
:disabled="
|
||||||
|
!btn.disabledOn || btn.disabledOn.values.includes(scope.row[btn.disabledOn.key])
|
||||||
|
"
|
||||||
|
@click="handleActionClick(btn.actionKey, scope.row)"
|
||||||
|
>
|
||||||
|
{{ btn.label }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
item.horizontalMergeRule &&
|
||||||
|
scope.row[item.horizontalMergeRule.matchCondition.key] ===
|
||||||
|
item.horizontalMergeRule.matchCondition.value
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="item?.horizontalMergeRule.renderConfig?.type === 'action'"
|
||||||
|
class="merged-action-cell"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
v-for="btn in item.horizontalMergeRule.renderConfig.actions"
|
||||||
|
:key="btn.actionKey"
|
||||||
|
:type="btn.btnType"
|
||||||
|
@click="handleActionClick(btn.actionKey, scope.row)"
|
||||||
|
>
|
||||||
|
{{ btn.label }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* 表格列的基本规范
|
||||||
|
*/
|
||||||
|
interface BaseColumn {
|
||||||
|
// 表头的标题
|
||||||
|
title: string;
|
||||||
|
// 数据源对应的字段名
|
||||||
|
key: string;
|
||||||
|
// 列的宽度
|
||||||
|
width?: number | string;
|
||||||
|
// 单元格的显示位置
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
// 固定列
|
||||||
|
fixed?: 'left' | 'right';
|
||||||
|
// 最小宽度,适应不同大小的屏幕
|
||||||
|
minWidth?: number | string;
|
||||||
|
// 标记这个列是否需要纵向合并
|
||||||
|
mergeRow?: boolean;
|
||||||
|
// 标记这个列是否需要横向合并
|
||||||
|
horizontalMergeRule?: {
|
||||||
|
matchCondition: { key: string; value: string | boolean };
|
||||||
|
mergeKeys: string[];
|
||||||
|
renderConfig?: {
|
||||||
|
type: string;
|
||||||
|
actions?: ActionButton[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// 多级表头
|
||||||
|
children?: TableColumnSchema[];
|
||||||
|
// 标记合并行时的参考列
|
||||||
|
mergeBy?: string;
|
||||||
|
}
|
||||||
|
// 纯文本列
|
||||||
|
export interface TextColumn extends BaseColumn {
|
||||||
|
type: 'text';
|
||||||
|
}
|
||||||
|
// 状态列
|
||||||
|
export interface StatusColumn extends BaseColumn {
|
||||||
|
type: 'status';
|
||||||
|
dictCode?: string;
|
||||||
|
}
|
||||||
|
// 数值列
|
||||||
|
export interface NumberColumn extends BaseColumn {
|
||||||
|
type: 'number';
|
||||||
|
// 单位后缀
|
||||||
|
unit?: string;
|
||||||
|
}
|
||||||
|
// 操作列
|
||||||
|
export interface ActionColumn extends BaseColumn {
|
||||||
|
type: 'action';
|
||||||
|
// 操作列包含按钮列表
|
||||||
|
actions: ActionButton[];
|
||||||
|
}
|
||||||
|
// 按钮定义的schema
|
||||||
|
export interface ActionButton {
|
||||||
|
// 按钮的文字
|
||||||
|
label: string;
|
||||||
|
//抛出给外层的事件名
|
||||||
|
actionKey: string;
|
||||||
|
// 按钮的视觉类型
|
||||||
|
btnType?: 'primary' | 'default' | 'dashed' | 'danger' | 'link' | 'text';
|
||||||
|
// 根据状态值控制显示
|
||||||
|
showOn?: {
|
||||||
|
key: string; // 依赖哪一列数据
|
||||||
|
values: any[]; // 当值等于数组中的哪些项时,才显示按钮
|
||||||
|
};
|
||||||
|
// 根据状态来控制禁用
|
||||||
|
disabledOn?: {
|
||||||
|
key: string;
|
||||||
|
values: any[];
|
||||||
|
};
|
||||||
|
isRound?: boolean;
|
||||||
|
}
|
||||||
|
export type TableColumnSchema = TextColumn | StatusColumn | NumberColumn | ActionColumn;
|
||||||
|
export type DataGridSchema = TableColumnSchema[];
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
// 提取叶子节点列,解决多级表头索引对齐问题
|
||||||
|
const getLeafColumns = (columns: any[]) => {
|
||||||
|
let leaves: any[] = [];
|
||||||
|
columns.forEach((col) => {
|
||||||
|
if (col.children && col.children.length > 0) {
|
||||||
|
leaves.push(...getLeafColumns(col.children));
|
||||||
|
} else {
|
||||||
|
leaves.push(col);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return leaves;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param data 表格的数据源
|
||||||
|
* @param rowMergeConfigs 纵向合并配置 [{ key: 'xxx', mergeBy: 'yyy' }]
|
||||||
|
* @param rawColumns 表格列配置
|
||||||
|
*/
|
||||||
|
export const createRowSpanMethod = (
|
||||||
|
data: any[],
|
||||||
|
rawColumns: any[],
|
||||||
|
rowMergeConfigs: { key: string; mergeBy: string }[]
|
||||||
|
) => {
|
||||||
|
// 1. 拍平获取物理叶子列
|
||||||
|
const columns = getLeafColumns(rawColumns);
|
||||||
|
const rowSpanInfos: Record<string, number[]> = {};
|
||||||
|
rowMergeConfigs.forEach((config) => {
|
||||||
|
const { key, mergeBy } = config;
|
||||||
|
rowSpanInfos[key] = [];
|
||||||
|
let position = 0;
|
||||||
|
data.forEach((item, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
rowSpanInfos[key].push(1);
|
||||||
|
position = 0;
|
||||||
|
} else {
|
||||||
|
// 如果当前行数据和上一行数据相等,则当前行不显示
|
||||||
|
if (
|
||||||
|
item[mergeBy] === data[index - 1][mergeBy] &&
|
||||||
|
!item.colMergeConfig &&
|
||||||
|
!data[index - 1].colMergeConfig
|
||||||
|
) {
|
||||||
|
rowSpanInfos[key][position] += 1;
|
||||||
|
rowSpanInfos[key].push(0);
|
||||||
|
} else {
|
||||||
|
rowSpanInfos[key].push(1);
|
||||||
|
position = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return ({ row, column, rowIndex, columnIndex }: any) => {
|
||||||
|
// 优先处理横向合并
|
||||||
|
const ruleColumn = columns.find(
|
||||||
|
(col) =>
|
||||||
|
col.horizontalMergeRule &&
|
||||||
|
row[col.horizontalMergeRule.matchCondition.key] ===
|
||||||
|
col.horizontalMergeRule.matchCondition.value
|
||||||
|
);
|
||||||
|
if (ruleColumn) {
|
||||||
|
const rule = ruleColumn.horizontalMergeRule;
|
||||||
|
const targetKeys = rule.mergeKeys;
|
||||||
|
const validIndices = targetKeys
|
||||||
|
.map((key) => columns.findIndex((item) => item.key === key))
|
||||||
|
.filter((index) => index != -1);
|
||||||
|
|
||||||
|
if (validIndices.length > 0) {
|
||||||
|
const minIndex = Math.min(...validIndices);
|
||||||
|
const maxIndex = Math.max(...validIndices);
|
||||||
|
const actualColSpan = maxIndex - minIndex + 1;
|
||||||
|
if (columnIndex == minIndex) {
|
||||||
|
return {
|
||||||
|
rowspan: 1,
|
||||||
|
colspan: actualColSpan
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (columnIndex > minIndex && columnIndex <= maxIndex) {
|
||||||
|
return {
|
||||||
|
rowspan: 0,
|
||||||
|
colspan: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 再处理纵向合并
|
||||||
|
const mergeTarget = rowMergeConfigs.find((c) => c.key === column.property);
|
||||||
|
if (mergeTarget) {
|
||||||
|
const rowspan = rowSpanInfos[column.property][rowIndex];
|
||||||
|
const colspan = rowspan > 0 ? 1 : 0;
|
||||||
|
return { rowspan, colspan };
|
||||||
|
}
|
||||||
|
return { rowspan: 1, colspan: 1 };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用数组聚合标记函数
|
||||||
|
* @param {Array} list - 原始数组
|
||||||
|
* @param {String} groupKey - 按照哪个字段进行分组统计 (例如 'deviceName')
|
||||||
|
* @param {String} flagName - 生成的布尔值标记名称 (例如 'isMultiNode')
|
||||||
|
*/
|
||||||
|
export const injectMultiNodeFlag = (list: any[], groupKey: string, flagName: string) => {
|
||||||
|
const countMap = {} as Record<string, number>;
|
||||||
|
// 统计
|
||||||
|
list.forEach((item) => {
|
||||||
|
const val = item[groupKey];
|
||||||
|
if (val) countMap[val] = (countMap[val] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 映射
|
||||||
|
return list.map((item) => {
|
||||||
|
const val = item[groupKey];
|
||||||
|
if (!val) return item; // 非目标数据,直接返回
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
[flagName]: countMap[val] > 1 // 动态注入标记
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
import { computed, type Ref, unref } from 'vue';
|
||||||
|
import { createRowSpanMethod } from '@/components/custom-components/table-vue/utils/tableUtils';
|
||||||
|
/**
|
||||||
|
* 只能表格合并 hook
|
||||||
|
* @param tableData 表格数据源
|
||||||
|
* @param tableColumns 表格列配置
|
||||||
|
*/
|
||||||
|
export function useTableSpan(tableData: Ref<any[]>, tableColumns: any[] | Ref<any[]>) {
|
||||||
|
const flatColumns = computed(() => {
|
||||||
|
const columns = unref(tableColumns); // 兼容普通对象和ref对象
|
||||||
|
const flatten = (cols: any[]): any[] => {
|
||||||
|
let result: any[] = [];
|
||||||
|
cols.forEach((col) => {
|
||||||
|
result.push(col);
|
||||||
|
if (col.children && col.children.length > 0) {
|
||||||
|
result.push(...flatten(col.children));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
return flatten(columns);
|
||||||
|
});
|
||||||
|
//自动提取需要纵向合并的列配置
|
||||||
|
const rowMergeConfigs = computed(() => {
|
||||||
|
return flatColumns.value
|
||||||
|
.filter((col) => col.mergeRow)
|
||||||
|
.map((col) => {
|
||||||
|
return {
|
||||||
|
key: col.key,
|
||||||
|
mergeBy: col.mergeBy || col.key
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 判断表格需不需要开启合并引擎
|
||||||
|
const needSpan = computed(() => {
|
||||||
|
// 有任何列配置了纵向合并
|
||||||
|
const hasRowMerge = rowMergeConfigs.value.length;
|
||||||
|
// 有任何列配置了横向合并
|
||||||
|
const hasColMerge = flatColumns.value.some((col) => !!col.horizontalMergeRule);
|
||||||
|
return hasRowMerge || hasColMerge;
|
||||||
|
});
|
||||||
|
// 动态返回合并函数
|
||||||
|
const spanMethod = computed(() => {
|
||||||
|
if (!needSpan.value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return createRowSpanMethod(unref(tableData), unref(tableColumns), rowMergeConfigs.value);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
spanMethod
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
const myAxios = axios.create({
|
||||||
|
baseURL: 'http://localhost:8080',
|
||||||
|
timeout: 60000,
|
||||||
|
withCredentials: true
|
||||||
|
});
|
||||||
|
export default myAxios;
|
||||||
Loading…
Reference in New Issue