selectObject.vue 18 KB

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