2026-06-25 10:21:53 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
:open="visible"
|
|
|
|
|
|
title="自定义数据列"
|
|
|
|
|
|
:width="1280"
|
|
|
|
|
|
:style="{ maxWidth: '80vw' }"
|
|
|
|
|
|
:footer="null"
|
|
|
|
|
|
@cancel="handleClose"
|
|
|
|
|
|
:destroyOnClose="true"
|
|
|
|
|
|
class="custom-column-modal"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="custom-column-content">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(group, idx) in groupedColumns"
|
|
|
|
|
|
:key="idx"
|
|
|
|
|
|
class="column-group"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 分组标题行 -->
|
|
|
|
|
|
<div class="group-header">
|
|
|
|
|
|
<span class="group-title">{{ group.name }}</span>
|
|
|
|
|
|
<div class="group-actions">
|
|
|
|
|
|
<a-checkbox
|
|
|
|
|
|
:checked="isGroupAllSelected(group)"
|
|
|
|
|
|
:indeterminate="isGroupIndeterminate(group)"
|
|
|
|
|
|
:disabled="isGroupAllDisabled(group)"
|
|
|
|
|
|
@change="e => handleGroupSelectAll(group, e.target.checked)"
|
|
|
|
|
|
>
|
|
|
|
|
|
全选
|
|
|
|
|
|
</a-checkbox>
|
|
|
|
|
|
<a-checkbox
|
|
|
|
|
|
:disabled="isGroupAllDisabled(group)"
|
|
|
|
|
|
@change="() => handleGroupSelectInverse(group)"
|
|
|
|
|
|
>
|
|
|
|
|
|
反选
|
|
|
|
|
|
</a-checkbox>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 子项列表 -->
|
|
|
|
|
|
<div class="group-items">
|
|
|
|
|
|
<a-checkbox
|
|
|
|
|
|
v-for="item in group.children"
|
|
|
|
|
|
:key="item.filed"
|
|
|
|
|
|
:checked="tempSelectedKeys.includes(item.filed)"
|
|
|
|
|
|
:disabled="item.disabled"
|
|
|
|
|
|
@change="e => handleItemChange(item, e.target.checked)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item.name }}
|
|
|
|
|
|
</a-checkbox>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<a-button @click="handleClose">取消</a-button>
|
|
|
|
|
|
<a-button type="primary" @click="handleConfirm">确定</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
|
|
|
import { ref, watch } from 'vue';
|
|
|
|
|
|
|
|
|
|
|
|
interface ColumnItem {
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
filed: string;
|
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ColumnGroup {
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
children: ColumnItem[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-22 17:43:09 +08:00
|
|
|
|
export interface UserColumnItem {
|
|
|
|
|
|
columnEn: string;
|
|
|
|
|
|
columnName: string;
|
|
|
|
|
|
checked: number;
|
|
|
|
|
|
groupType: string;
|
|
|
|
|
|
orderIndex: number;
|
|
|
|
|
|
defaultConfig: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 10:21:53 +08:00
|
|
|
|
interface Props {
|
|
|
|
|
|
visible: boolean;
|
2026-07-22 17:43:09 +08:00
|
|
|
|
userColumnList: UserColumnItem[];
|
2026-06-25 10:21:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<Props>();
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
(e: 'update:visible', value: boolean): void;
|
|
|
|
|
|
(e: 'confirm', keys: string[]): void;
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const groupedColumns = ref<ColumnGroup[]>([]);
|
|
|
|
|
|
const tempSelectedKeys = ref<string[]>([]);
|
|
|
|
|
|
|
2026-07-22 17:43:09 +08:00
|
|
|
|
// 始终勾选的 filed(电站名称)
|
|
|
|
|
|
const STATION_NAME_FILED = 'stnm';
|
|
|
|
|
|
|
|
|
|
|
|
// 根据 API 返回的 userColumnList 构建分组结构
|
2026-06-25 10:21:53 +08:00
|
|
|
|
const buildGroupedColumns = () => {
|
|
|
|
|
|
const groups: ColumnGroup[] = [];
|
2026-07-22 17:43:09 +08:00
|
|
|
|
const groupMap = new Map<string, ColumnItem[]>();
|
|
|
|
|
|
|
|
|
|
|
|
// 过滤掉 defaultConfig: true 的项(兼容字符串类型)
|
2026-07-31 11:13:49 +08:00
|
|
|
|
const filteredList = props.userColumnList.filter(item => item.enable == 1);
|
2026-07-22 17:43:09 +08:00
|
|
|
|
|
|
|
|
|
|
// 按 groupType 分组
|
|
|
|
|
|
for (const item of filteredList) {
|
|
|
|
|
|
if (!item.groupType) continue;
|
|
|
|
|
|
if (!groupMap.has(item.groupType)) {
|
|
|
|
|
|
groupMap.set(item.groupType, []);
|
2026-06-25 10:21:53 +08:00
|
|
|
|
}
|
2026-07-22 17:43:09 +08:00
|
|
|
|
const isStationName = item.columnEn === STATION_NAME_FILED;
|
|
|
|
|
|
groupMap.get(item.groupType)!.push({
|
|
|
|
|
|
name: item.columnName,
|
|
|
|
|
|
filed: item.columnEn,
|
|
|
|
|
|
disabled: isStationName
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为分组数组
|
|
|
|
|
|
for (const [groupName, children] of groupMap.entries()) {
|
|
|
|
|
|
groups.push({ name: groupName, children });
|
2026-06-25 10:21:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return groups;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-22 17:43:09 +08:00
|
|
|
|
// 初始化选中状态(根据 API 的 checked 字段)
|
2026-06-25 10:21:53 +08:00
|
|
|
|
const initSelectedKeys = () => {
|
|
|
|
|
|
const allKeys: string[] = [STATION_NAME_FILED];
|
|
|
|
|
|
for (const group of groupedColumns.value) {
|
|
|
|
|
|
for (const child of group.children) {
|
2026-07-22 17:43:09 +08:00
|
|
|
|
if (child.disabled) continue;
|
|
|
|
|
|
// 从 userColumnList 中查找对应的 checked 状态
|
|
|
|
|
|
const apiItem = props.userColumnList.find(
|
|
|
|
|
|
item => item.columnEn === child.filed
|
|
|
|
|
|
);
|
|
|
|
|
|
if (apiItem && apiItem.checked === 1) {
|
2026-06-25 10:21:53 +08:00
|
|
|
|
allKeys.push(child.filed);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
tempSelectedKeys.value = [...allKeys];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 判断分组是否全选
|
|
|
|
|
|
const isGroupAllSelected = (group: ColumnGroup) => {
|
|
|
|
|
|
const selectable = group.children.filter(child => !child.disabled);
|
|
|
|
|
|
if (selectable.length === 0) return false;
|
|
|
|
|
|
return selectable.every(child =>
|
|
|
|
|
|
tempSelectedKeys.value.includes(child.filed)
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 判断分组是否半选
|
|
|
|
|
|
const isGroupIndeterminate = (group: ColumnGroup) => {
|
|
|
|
|
|
const selectable = group.children.filter(child => !child.disabled);
|
|
|
|
|
|
if (selectable.length === 0) return false;
|
|
|
|
|
|
const selectedCount = selectable.filter(child =>
|
|
|
|
|
|
tempSelectedKeys.value.includes(child.filed)
|
|
|
|
|
|
).length;
|
|
|
|
|
|
return selectedCount > 0 && selectedCount < selectable.length;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 判断分组是否所有项都被禁用
|
|
|
|
|
|
const isGroupAllDisabled = (group: ColumnGroup) => {
|
|
|
|
|
|
return group.children.every(child => child.disabled);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 全选 / 取消全选
|
|
|
|
|
|
const handleGroupSelectAll = (group: ColumnGroup, checked: boolean) => {
|
|
|
|
|
|
const selectableFiled = group.children
|
|
|
|
|
|
.filter(child => !child.disabled)
|
|
|
|
|
|
.map(child => child.filed);
|
|
|
|
|
|
|
|
|
|
|
|
if (checked) {
|
|
|
|
|
|
for (const filed of selectableFiled) {
|
|
|
|
|
|
if (!tempSelectedKeys.value.includes(filed)) {
|
|
|
|
|
|
tempSelectedKeys.value.push(filed);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const filedSet = new Set(selectableFiled);
|
|
|
|
|
|
tempSelectedKeys.value = tempSelectedKeys.value.filter(
|
|
|
|
|
|
filed => !filedSet.has(filed)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 反选
|
|
|
|
|
|
const handleGroupSelectInverse = (group: ColumnGroup) => {
|
|
|
|
|
|
for (const child of group.children) {
|
|
|
|
|
|
if (child.disabled) continue;
|
|
|
|
|
|
const index = tempSelectedKeys.value.indexOf(child.filed);
|
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
|
tempSelectedKeys.value.splice(index, 1);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tempSelectedKeys.value.push(child.filed);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 单个项变化
|
|
|
|
|
|
const handleItemChange = (item: ColumnItem, checked: boolean) => {
|
|
|
|
|
|
if (checked) {
|
|
|
|
|
|
if (!tempSelectedKeys.value.includes(item.filed)) {
|
|
|
|
|
|
tempSelectedKeys.value.push(item.filed);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tempSelectedKeys.value = tempSelectedKeys.value.filter(
|
|
|
|
|
|
filed => filed !== item.filed
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleClose = () => {
|
|
|
|
|
|
emit('update:visible', false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleConfirm = () => {
|
|
|
|
|
|
// 确保电站名称始终在结果中
|
|
|
|
|
|
const keys = [...new Set([STATION_NAME_FILED, ...tempSelectedKeys.value])];
|
|
|
|
|
|
emit('confirm', keys);
|
|
|
|
|
|
emit('update:visible', false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.visible,
|
|
|
|
|
|
newVal => {
|
|
|
|
|
|
if (newVal) {
|
|
|
|
|
|
groupedColumns.value = buildGroupedColumns();
|
|
|
|
|
|
initSelectedKeys();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
2026-07-22 17:43:09 +08:00
|
|
|
|
|
|
|
|
|
|
// 当弹窗打开时,如果 userColumnList 更新则同步刷新
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.userColumnList,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
if (props.visible) {
|
|
|
|
|
|
groupedColumns.value = buildGroupedColumns();
|
|
|
|
|
|
initSelectedKeys();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
2026-06-25 10:21:53 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.custom-column-modal {
|
|
|
|
|
|
:deep(.ant-modal-header) {
|
|
|
|
|
|
background-color: #005294;
|
|
|
|
|
|
.ant-modal-title {
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.custom-column-content {
|
|
|
|
|
|
max-height: 500px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
|
|
|
|
|
|
|
.column-group {
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
border: 1px solid #e8e8e8;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
.group-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
.group-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.group-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-checkbox-wrapper) {
|
|
|
|
|
|
margin-right: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.group-items {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-checkbox-wrapper) {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
padding-top: 16px;
|
|
|
|
|
|
border-top: 1px solid #f0f0f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|