办学质量监测教学评价系统
shenrongliang
2025-06-13 11d86cc6c26bb4f709e407acadf4805c2024e79f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
<script setup lang="ts">
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
import type { DataNode } from 'ant-design-vue/es/tree';
import type { CheckInfo } from 'ant-design-vue/es/vc-tree/props';
 
import type { PropType, SetupContext } from 'vue';
 
import { computed, nextTick, onMounted, ref, useSlots, watch } from 'vue';
 
import { findGroupParentIds, treeToList } from '@vben/utils';
 
import { Checkbox, Tree } from 'ant-design-vue';
import { uniq } from 'lodash-es';
 
/** 需要禁止透传 */
defineOptions({ inheritAttrs: false });
 
const props = defineProps({
  checkStrictly: {
    default: true,
    type: Boolean,
  },
  expandAllOnInit: {
    default: false,
    type: Boolean,
  },
  fieldNames: {
    default: () => ({ key: 'id', title: 'label' }),
    type: Object as PropType<{ key: string; title: string }>,
  },
  /** 点击节点关联/独立时 清空已勾选的节点 */
  resetOnStrictlyChange: {
    default: true,
    type: Boolean,
  },
  treeData: {
    default: () => [],
    type: Array as PropType<DataNode[]>,
  },
});
const emit = defineEmits<{ checkStrictlyChange: [boolean] }>();
 
const expandStatus = ref(false);
const selectAllStatus = ref(false);
 
/**
 * 后台的这个字段跟antd/ele是反的
 * 组件库这个字段代表不关联
 * 后台这个代表关联
 */
const innerCheckedStrictly = computed(() => {
  return !props.checkStrictly;
});
 
const associationText = computed(() => {
  return props.checkStrictly ? '父子节点关联' : '父子节点独立';
});
 
/**
 * 这个只用于界面显示
 * 关联情况下 只会有最末尾的节点被选中
 */
const checkedKeys = defineModel('value', {
  default: () => [],
  type: Array as PropType<(number | string)[]>,
});
// 所有节点的ID
const allKeys = computed(() => {
  const idField = props.fieldNames.key;
  return treeToList(props.treeData).map((item: any) => item[idField]);
});
 
/** 已经选择的所有节点  包括子/父节点 用于提交 */
const checkedRealKeys = ref<(number | string)[]>([]);
 
/**
 * 取第一次的menuTree id 设置到checkedMenuKeys
 * 主要为了解决没有任何修改 直接点击保存的情况
 *
 * length为0情况(即新增时候没有勾选节点) 勾选这里会延迟触发 节点会拼接上父节点 导致ID重复
 */
const stop = watch([checkedKeys, () => props.treeData], () => {
  if (
    props.checkStrictly &&
    checkedKeys.value.length > 0 &&
    props.treeData.length > 0
  ) {
    /** 找到父节点 添加上 */
    const parentIds = findGroupParentIds(
      props.treeData,
      checkedKeys.value as any,
      { id: props.fieldNames.key },
    );
    /**
     * uniq 解决上面的id重复问题
     */
    checkedRealKeys.value = uniq([...parentIds, ...checkedKeys.value]);
    stop();
  }
  if (!props.checkStrictly && checkedKeys.value.length > 0) {
    /** 节点独立 这里是全部的节点 */
    checkedRealKeys.value = checkedKeys.value;
    stop();
  }
});
 
/**
 *
 * @param checkedStateKeys 已经选中的子节点的ID
 * @param info info.halfCheckedKeys为父节点的ID
 */
type CheckedState<T = number | string> =
  | T[]
  | { checked: T[]; halfChecked: T[] };
function handleChecked(checkedStateKeys: CheckedState, info: CheckInfo) {
  // 数组的话为节点关联
  if (Array.isArray(checkedStateKeys)) {
    const halfCheckedKeys: number[] = (info.halfCheckedKeys || []) as number[];
    checkedRealKeys.value = [...halfCheckedKeys, ...checkedStateKeys];
  } else {
    checkedRealKeys.value = [...checkedStateKeys.checked];
    // fix: Invalid prop: type check failed for prop "value". Expected Array, got Object
    checkedKeys.value = [...checkedStateKeys.checked];
  }
}
 
function handleExpandChange(e: CheckboxChangeEvent) {
  // 这个用于展示
  checkedKeys.value = e.target.checked ? allKeys.value : [];
  // 这个用于提交
  checkedRealKeys.value = e.target.checked ? allKeys.value : [];
}
 
const expandedKeys = ref<string[]>([]);
function handleExpandOrCollapseAll(e: CheckboxChangeEvent) {
  const expand = e.target.checked;
  expandedKeys.value = expand ? allKeys.value : [];
}
 
function handleCheckStrictlyChange(e: CheckboxChangeEvent) {
  emit('checkStrictlyChange', e.target.checked);
  if (props.resetOnStrictlyChange) {
    checkedKeys.value = [];
    checkedRealKeys.value = [];
  }
}
 
/**
 * 暴露方法来获取用于提交的全部节点
 * uniq去重(保险方案)
 */
defineExpose({
  getCheckedKeys: () => uniq(checkedRealKeys.value),
});
 
onMounted(async () => {
  if (props.expandAllOnInit) {
    await nextTick();
    expandedKeys.value = allKeys.value;
  }
});
 
const slots = useSlots() as SetupContext['slots'];
</script>
 
<template>
  <div class="bg-background w-full rounded-lg border-[1px] p-[12px]">
    <div class="flex items-center justify-between gap-2 border-b-[1px] pb-2">
      <div>
        <span>节点状态: </span>
        <span :class="[props.checkStrictly ? 'text-primary' : 'text-red-500']">
          {{ associationText }}
        </span>
      </div>
      <div>
        已选中
        <span class="text-primary mx-1 font-semibold">
          {{ checkedRealKeys.length }}
        </span>
        个节点
      </div>
    </div>
    <div
      class="flex flex-wrap items-center justify-between border-b-[1px] py-2"
    >
      <Checkbox
        v-model:checked="expandStatus"
        @change="handleExpandOrCollapseAll"
      >
        展开/折叠全部
      </Checkbox>
      <Checkbox v-model:checked="selectAllStatus" @change="handleExpandChange">
        全选/取消全选
      </Checkbox>
      <Checkbox :checked="checkStrictly" @change="handleCheckStrictlyChange">
        父子节点关联
      </Checkbox>
    </div>
    <div class="py-2">
      <Tree
        v-if="treeData.length > 0"
        v-model:check-strictly="innerCheckedStrictly"
        v-model:checked-keys="checkedKeys"
        v-model:expanded-keys="expandedKeys"
        :checkable="true"
        :field-names="fieldNames"
        :selectable="false"
        :tree-data="treeData"
        @check="handleChecked"
      >
        <template
          v-for="slotName in Object.keys(slots)"
          :key="slotName"
          #[slotName]="data"
        >
          <slot :name="slotName" v-bind="data ?? {}"></slot>
        </template>
      </Tree>
    </div>
  </div>
</template>