selectObjectNoDedup.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. <template>
  2. <view class="page_select_object">
  3. <CommonHeader title="选择通知对象" @back="goBack"></CommonHeader>
  4. <view class="tabs_container" :style="{ top: tabsTop + 'rpx' }">
  5. <view v-for="tab in tabs" :key="tab.key" class="tab_item" :class="{ active: activeTab === tab.key, disabled: editMode }"
  6. @click="!editMode && handleTabChange(tab.key)">
  7. <text class="tab_text">{{ tab.label }}</text>
  8. </view>
  9. </view>
  10. <SearchBar v-model="searchKeyword" @search="searchTree" :border="true" :isFixed="true" :top="searchBarTop + 'rpx'" :zIndex="100"></SearchBar>
  11. <scroll-view scroll-y class="content_list" :style="{ paddingTop: contentPaddingTop + 'rpx' }">
  12. <TreeNodeNoDedup :tree-data="treeData" :expanded-ids="expandedIds" @toggle-expand="toggleExpand"
  13. @toggle-select="toggleSelect" />
  14. </scroll-view>
  15. <view class="select_footer">
  16. <view class="confirm_btn" @click="handleConfirm">
  17. <text class="btn_text">确定(已选{{ selectedCount }}人)</text>
  18. </view>
  19. </view>
  20. </view>
  21. </template>
  22. <script setup>
  23. import { ref, computed, onMounted } from 'vue';
  24. import TreeNodeNoDedup from '@/components/treeNodeNoDedup.vue';
  25. import SearchBar from '@/components/searchBar.vue';
  26. import CommonHeader from '@/components/commonHeader.vue';
  27. import overview from '@/reqApi/overview.js';
  28. import { useSafeArea } from '@/common/safeArea';
  29. const { statusBarHeight, initSafeArea } = useSafeArea();
  30. const HEADER_CONTENT_HEIGHT = 130;
  31. const TABS_HEIGHT = 120;
  32. const SEARCHBAR_HEIGHT = 84;
  33. const SPACING = 16;
  34. const tabsTop = computed(() => statusBarHeight.value + HEADER_CONTENT_HEIGHT);
  35. const searchBarTop = computed(() => statusBarHeight.value + HEADER_CONTENT_HEIGHT + TABS_HEIGHT);
  36. const contentPaddingTop = computed(() => statusBarHeight.value + HEADER_CONTENT_HEIGHT + TABS_HEIGHT + SEARCHBAR_HEIGHT + SPACING);
  37. const activeTab = ref('department');
  38. const searchKeyword = ref('');
  39. const schoolYearId = ref('');
  40. const editMode = ref(false);
  41. const noticeType = ref(0);
  42. const selectedUserIds = ref([]);
  43. const tabs = [
  44. { key: 'department', label: '按部门' },
  45. { key: 'subject', label: '按学科' },
  46. { key: 'grade', label: '按年级' },
  47. { key: 'permission', label: '按权限' },
  48. { key: 'student', label: '按学生' }
  49. ];
  50. const treeData = ref([]);
  51. const fullTreeData = ref([]);
  52. const expandedIds = ref([]);
  53. // 转换按部门接口数据为树形结构(不去重)
  54. const transformDepartmentData = (data) => {
  55. if (!data || !Array.isArray(data)) return [];
  56. return data.map(dept => {
  57. const groups = (dept.groupVOS || []).map(group => ({
  58. id: `${dept.id}_${group.groupId}`,
  59. name: group.groupName,
  60. checked: false,
  61. halfChecked: false,
  62. children: (group.groupUserVOS || []).map(user => ({
  63. id: user.userId,
  64. name: user.teacherName,
  65. userId: user.userId,
  66. userAccount: user.userAccount,
  67. checked: false,
  68. halfChecked: false
  69. }))
  70. }));
  71. return {
  72. id: dept.id,
  73. name: dept.name,
  74. checked: false,
  75. halfChecked: false,
  76. children: groups
  77. };
  78. });
  79. };
  80. // 转换按学科的数据为树形结构(不去重)
  81. const transformSubjectData = (data) => {
  82. if (!data || !Array.isArray(data)) return [];
  83. return data.map(subject => {
  84. const grades = (subject.gradePersonVos || []).map(grade => ({
  85. id: `${subject.subjectCode}_${grade.schoolYearGradeId || grade.gradeCode}`,
  86. name: grade.gradeName,
  87. checked: false,
  88. halfChecked: false,
  89. children: (grade.personVoList || []).map(user => ({
  90. id: user.userId,
  91. name: user.teacherName,
  92. userId: user.userId,
  93. userAccount: user.userAccount,
  94. checked: false,
  95. halfChecked: false
  96. }))
  97. }));
  98. return {
  99. id: `subject_${subject.subjectCode}`,
  100. name: subject.subjectName,
  101. checked: false,
  102. halfChecked: false,
  103. children: grades
  104. };
  105. });
  106. };
  107. // 转换按年级的数据为树形结构(不去重)
  108. const transformGradeData = (data) => {
  109. if (!data || !Array.isArray(data)) return [];
  110. return data.map(grade => {
  111. const classTypes = (grade.classTypePersonVoList || []).map(classType => {
  112. const classes = (classType.classInfoPersonVoList || []).map(cls => ({
  113. id: `${classType.classType}_${cls.classCode}`,
  114. name: cls.className,
  115. checked: false,
  116. halfChecked: false,
  117. children: (cls.personVoList || []).map(user => ({
  118. id: user.userId,
  119. name: user.teacherName,
  120. userId: user.userId,
  121. userAccount: user.userAccount,
  122. checked: false,
  123. halfChecked: false
  124. }))
  125. }));
  126. return {
  127. id: `${grade.schoolYearGradeId || grade.gradeCode}_${classType.classType}`,
  128. name: classType.classTypeName,
  129. checked: false,
  130. halfChecked: false,
  131. children: classes
  132. };
  133. });
  134. return {
  135. id: grade.schoolYearGradeId || grade.gradeCode,
  136. name: grade.gradeName,
  137. checked: false,
  138. halfChecked: false,
  139. children: classTypes
  140. };
  141. });
  142. };
  143. // 转换按权限的数据为树形结构(不去重)
  144. const transformPersonalData = (data) => {
  145. if (!data || !Array.isArray(data)) return [];
  146. return data.map(item => {
  147. return {
  148. id: item.roleId,
  149. name: item.roleName,
  150. checked: false,
  151. halfChecked: false,
  152. children: (item.personVoList || []).map(user => ({
  153. id: user.userId,
  154. name: user.teacherName,
  155. userId: user.userId,
  156. userAccount: user.userAccount,
  157. checked: false,
  158. halfChecked: false
  159. }))
  160. };
  161. });
  162. };
  163. // 转换按学生的数据为树形结构(不去重)
  164. const transformStudentData = (data) => {
  165. if (!data || !Array.isArray(data)) return [];
  166. return data.map(grade => {
  167. const classes = (grade.clsVOS || []).map(cls => ({
  168. id: `${grade.schoolYearGradeId || grade.gradeCode}_${cls.classId}`,
  169. name: cls.className,
  170. checked: false,
  171. halfChecked: false,
  172. children: (cls.studentTblVOS || []).map(student => ({
  173. id: student.userId,
  174. name: student.studentName,
  175. userId: student.userId,
  176. userAccount: student.studentCode,
  177. checked: false,
  178. halfChecked: false
  179. }))
  180. }));
  181. return {
  182. id: `student_${grade.schoolYearGradeId || grade.gradeCode}`,
  183. name: grade.gradeName,
  184. checked: false,
  185. halfChecked: false,
  186. children: classes
  187. };
  188. });
  189. };
  190. // 获取学年
  191. const getSchoolYear = async () => {
  192. try {
  193. const res = await overview.findSchoolYear();
  194. if (res.data && res.data.length > 0) {
  195. schoolYearId.value = res.data[0].id;
  196. }
  197. } catch (error) {
  198. console.error('获取学年失败:', error);
  199. }
  200. };
  201. // 获取按部门数据
  202. const loadDepartmentData = async () => {
  203. if (!schoolYearId.value) return;
  204. try {
  205. const res = await overview.find_depart_contain_user({ schoolYearId: schoolYearId.value });
  206. fullTreeData.value = transformDepartmentData(res.data);
  207. treeData.value = fullTreeData.value;
  208. expandedIds.value = [];
  209. } catch (error) {
  210. console.error('获取部门数据失败:', error);
  211. }
  212. };
  213. // 获取按学科/年级/权限数据
  214. const loadPersonalData = async (type) => {
  215. if (!schoolYearId.value) return;
  216. try {
  217. const res = await overview.find_collect_personal_info({ schoolYearId: schoolYearId.value });
  218. let data = [];
  219. let transformFn = transformPersonalData;
  220. if (type === 'subject') {
  221. data = res.data.subjectPersonVoList || [];
  222. transformFn = transformSubjectData;
  223. } else if (type === 'grade') {
  224. data = res.data.collectGradePersonVos || [];
  225. transformFn = transformGradeData;
  226. } else if (type === 'permission') {
  227. data = res.data.collectPermissionPersonVos || [];
  228. }
  229. fullTreeData.value = transformFn(data);
  230. treeData.value = fullTreeData.value;
  231. expandedIds.value = [];
  232. } catch (error) {
  233. console.error(`获取${type}数据失败:`, error);
  234. }
  235. };
  236. // 获取按学生数据
  237. const loadStudentData = async () => {
  238. if (!schoolYearId.value) return;
  239. try {
  240. const res = await overview.query_notice_student({ id: 0, schoolYearId: schoolYearId.value });
  241. fullTreeData.value = transformStudentData(res.data || []);
  242. treeData.value = fullTreeData.value;
  243. expandedIds.value = [];
  244. } catch (error) {
  245. console.error('获取学生数据失败:', error);
  246. }
  247. };
  248. // 切换标签
  249. const handleTabChange = async (key) => {
  250. activeTab.value = key;
  251. if (key === 'department') {
  252. await loadDepartmentData();
  253. } else if (key === 'subject' || key === 'grade' || key === 'permission') {
  254. await loadPersonalData(key);
  255. } else if (key === 'student') {
  256. await loadStudentData();
  257. }
  258. };
  259. const searchTree = (keyword) => {
  260. if (!keyword.trim()) {
  261. treeData.value = fullTreeData.value;
  262. return;
  263. }
  264. const filterNodes = (nodes) => {
  265. const filtered = [];
  266. nodes.forEach(node => {
  267. const hasMatch = node.name && node.name.includes(keyword);
  268. let childrenFiltered = [];
  269. if (node.children && node.children.length > 0) {
  270. childrenFiltered = filterNodes(node.children);
  271. }
  272. if (hasMatch || childrenFiltered.length > 0) {
  273. filtered.push({
  274. ...node,
  275. children: childrenFiltered
  276. });
  277. }
  278. });
  279. return filtered;
  280. };
  281. treeData.value = filterNodes(fullTreeData.value);
  282. };
  283. const toggleExpand = ({ itemId, siblingIds, isExpanded }) => {
  284. if (isExpanded) {
  285. expandedIds.value = expandedIds.value.filter(id => id !== itemId);
  286. } else {
  287. expandedIds.value = expandedIds.value.filter(id => {
  288. if (siblingIds.includes(id)) {
  289. return false;
  290. }
  291. return true;
  292. });
  293. expandedIds.value = [...expandedIds.value, itemId];
  294. }
  295. };
  296. // 向下级联:更新所有子节点的选中状态
  297. const toggleChildren = (children, checked) => {
  298. children.forEach(child => {
  299. child.checked = checked;
  300. child.halfChecked = false;
  301. if (child.children && child.children.length > 0) {
  302. toggleChildren(child.children, checked);
  303. }
  304. });
  305. };
  306. // 向上级联:更新父节点的选中状态
  307. const updateParentStatus = () => {
  308. const updateNode = (nodes) => {
  309. nodes.forEach(node => {
  310. if (node.children && node.children.length > 0) {
  311. updateNode(node.children);
  312. const childrenChecked = node.children.filter(c => c.checked);
  313. const childrenHalfChecked = node.children.filter(c => c.halfChecked);
  314. if (childrenChecked.length === node.children.length) {
  315. node.checked = true;
  316. node.halfChecked = false;
  317. } else if (childrenChecked.length > 0 || childrenHalfChecked.length > 0) {
  318. node.checked = false;
  319. node.halfChecked = true;
  320. } else {
  321. node.checked = false;
  322. node.halfChecked = false;
  323. }
  324. }
  325. });
  326. };
  327. updateNode(treeData.value);
  328. };
  329. const toggleSelect = (item) => {
  330. item.checked = !item.checked;
  331. item.halfChecked = false;
  332. if (item.children && item.children.length > 0) {
  333. // 向下级联
  334. toggleChildren(item.children, item.checked);
  335. }
  336. // 向上级联
  337. updateParentStatus();
  338. };
  339. const selectedCount = computed(() => {
  340. let count = 0;
  341. const collectChecked = (items) => {
  342. items.forEach(item => {
  343. if (item.children && item.children.length > 0) {
  344. collectChecked(item.children);
  345. } else if (item.checked && item.userId) {
  346. count++;
  347. }
  348. });
  349. };
  350. collectChecked(treeData.value);
  351. return count;
  352. });
  353. const goBack = () => {
  354. uni.navigateBack();
  355. };
  356. const collectSelectedUsers = () => {
  357. const selectedUsers = [];
  358. const currentTab = activeTab.value;
  359. const collect = (nodes, parentInfo = {}) => {
  360. nodes.forEach(node => {
  361. if (node.children && node.children.length > 0) {
  362. const newParentInfo = { ...parentInfo };
  363. if (node.name && !node.userId) {
  364. if (currentTab === 'department') {
  365. if (!newParentInfo.departmentName) {
  366. newParentInfo.departmentName = node.name;
  367. } else if (!newParentInfo.groupName) {
  368. newParentInfo.groupName = node.name;
  369. }
  370. } else if (currentTab === 'subject') {
  371. if (!newParentInfo.subjectName) {
  372. newParentInfo.subjectName = node.name;
  373. } else if (!newParentInfo.gradeName) {
  374. newParentInfo.gradeName = node.name;
  375. }
  376. } else if (currentTab === 'grade') {
  377. if (!newParentInfo.gradeName) {
  378. newParentInfo.gradeName = node.name;
  379. } else if (!newParentInfo.classTypeName) {
  380. newParentInfo.classTypeName = node.name;
  381. } else if (!newParentInfo.className) {
  382. newParentInfo.className = node.name;
  383. }
  384. } else if (currentTab === 'permission') {
  385. if (!newParentInfo.roleName) {
  386. newParentInfo.roleName = node.name;
  387. }
  388. } else if (currentTab === 'student') {
  389. if (!newParentInfo.gradeName) {
  390. newParentInfo.gradeName = node.name;
  391. } else if (!newParentInfo.classTypeName) {
  392. newParentInfo.classTypeName = node.name;
  393. } else if (!newParentInfo.className) {
  394. newParentInfo.className = node.name;
  395. }
  396. }
  397. }
  398. collect(node.children, newParentInfo);
  399. } else if (node.checked && node.userId) {
  400. selectedUsers.push({
  401. id: node.id,
  402. name: node.name,
  403. userId: node.userId,
  404. thirdCode: node.userAccount || '',
  405. departmentName: parentInfo.departmentName || '',
  406. groupName: parentInfo.groupName || '',
  407. subjectName: parentInfo.subjectName || '',
  408. gradeName: parentInfo.gradeName || '',
  409. classTypeName: parentInfo.classTypeName || '',
  410. className: parentInfo.className || '',
  411. roleName: parentInfo.roleName || ''
  412. });
  413. }
  414. });
  415. };
  416. collect(treeData.value);
  417. return selectedUsers;
  418. };
  419. const handleConfirm = () => {
  420. const data = {
  421. tab: tabs.find(t => t.key === activeTab.value).label,
  422. count: selectedCount.value,
  423. users: collectSelectedUsers()
  424. };
  425. uni.setStorageSync('selectObjectResult', JSON.stringify(data));
  426. uni.navigateBack();
  427. };
  428. const setSelectedUsers = (users) => {
  429. if (!users || !Array.isArray(users)) return;
  430. const selectedTeacherIds = new Set(users.map(u => u.teacherId));
  431. const setNode = (nodes) => {
  432. nodes.forEach(node => {
  433. if (node.children && node.children.length > 0) {
  434. setNode(node.children);
  435. } else if (node.teacherId && selectedTeacherIds.has(node.teacherId)) {
  436. node.checked = true;
  437. }
  438. });
  439. };
  440. setNode(treeData.value);
  441. updateParentStatus();
  442. };
  443. const setSelectedUsersById = (userIds) => {
  444. if (!userIds || !Array.isArray(userIds)) return;
  445. const selectedIds = new Set(userIds);
  446. const setNode = (nodes) => {
  447. nodes.forEach(node => {
  448. if (node.children && node.children.length > 0) {
  449. setNode(node.children);
  450. } else if (node.id && selectedIds.has(node.id)) {
  451. node.checked = true;
  452. }
  453. });
  454. };
  455. setNode(treeData.value);
  456. updateParentStatus();
  457. };
  458. onMounted(async () => {
  459. initSafeArea();
  460. const pages = getCurrentPages();
  461. if (pages.length === 1) {
  462. uni.navigateBack();
  463. return;
  464. }
  465. const lastSelection = uni.getStorageSync('selectObjectLastSelection');
  466. if (lastSelection) {
  467. try {
  468. const data = JSON.parse(lastSelection);
  469. if (data.isEdit) {
  470. editMode.value = true;
  471. }
  472. if (data.noticeType) {
  473. noticeType.value = data.noticeType;
  474. const typeMap = {
  475. 1: 'department',
  476. 2: 'subject',
  477. 3: 'grade',
  478. 4: 'permission',
  479. 5: 'student'
  480. };
  481. const tabKey = typeMap[data.noticeType];
  482. if (tabKey) {
  483. activeTab.value = tabKey;
  484. }
  485. } else if (data.tab) {
  486. const tab = tabs.find(t => t.label === data.tab);
  487. if (tab) {
  488. activeTab.value = tab.key;
  489. }
  490. }
  491. if (data.users) {
  492. selectedUserIds.value = data.users.map(u => u.userId || u.id);
  493. }
  494. } catch (e) {
  495. console.error('解析上次选择失败:', e);
  496. }
  497. }
  498. await getSchoolYear();
  499. if (activeTab.value === 'department') {
  500. await loadDepartmentData();
  501. } else if (activeTab.value === 'subject' || activeTab.value === 'grade' || activeTab.value === 'permission') {
  502. await loadPersonalData(activeTab.value);
  503. } else if (activeTab.value === 'student') {
  504. await loadStudentData();
  505. }
  506. if (selectedUserIds.value.length > 0) {
  507. setSelectedUsersById(selectedUserIds.value);
  508. } else if (lastSelection) {
  509. try {
  510. const data = JSON.parse(lastSelection);
  511. if (data.users) {
  512. setSelectedUsers(data.users);
  513. }
  514. } catch (e) {
  515. console.error('设置选中状态失败:', e);
  516. }
  517. }
  518. });
  519. </script>
  520. <style lang="scss" scoped>
  521. .page_select_object {
  522. display: flex;
  523. flex-direction: column;
  524. min-height: 100vh;
  525. }
  526. .tabs_container {
  527. position: fixed;
  528. left: 0;
  529. right: 0;
  530. z-index: 99;
  531. display: flex;
  532. padding: 24rpx ;
  533. background-color: #FFFFFF;
  534. justify-content: space-between;
  535. .tab_item {
  536. flex-shrink: 0;
  537. width: 122rpx;
  538. height: 72rpx;
  539. background: #FFFFFF;
  540. border-radius: 8rpx;
  541. border: 2rpx solid #DCDFE6;
  542. display: flex;
  543. align-items: center;
  544. justify-content: center;
  545. &.active {
  546. background: #2E64FA;
  547. border-color: #2E64FA;
  548. .tab_text {
  549. color: #FFFFFF;
  550. }
  551. }
  552. .tab_text {
  553. font-weight: 400;
  554. font-size: 28rpx;
  555. color: #333333;
  556. }
  557. &.disabled {
  558. pointer-events: none;
  559. opacity: 0.5;
  560. }
  561. }
  562. }
  563. .content_list {
  564. flex: 1;
  565. padding-bottom: 150rpx;
  566. }
  567. .select_footer {
  568. position: fixed;
  569. bottom: 0;
  570. left: 0;
  571. right: 0;
  572. padding: 24rpx;
  573. background-color: #FFFFFF;
  574. border-top: 2rpx solid #F3F3F3;
  575. .confirm_btn {
  576. width: 100%;
  577. height: 90rpx;
  578. background: #2E64FA;
  579. border-radius: 8rpx;
  580. text-align: center;
  581. line-height: 90rpx;
  582. .btn_text {
  583. font-size: 32rpx;
  584. font-weight: 500;
  585. color: #FFFFFF;
  586. }
  587. }
  588. }
  589. </style>