Нет описания
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

OrganizationalWithLeafUserForCourse.vue 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. <template>
  2. <van-popup :close-on-click-overlay="false" v-model:show="deptCascadeVisible" position="bottom">
  3. <div class="content-container">
  4. <div class="action-buttons-container">
  5. <div style="color:#dc3545;" @click="close">取消</div>
  6. <div>部门选择</div>
  7. <div style="color: #007bff" @click="switchPeople">下一步</div>
  8. </div>
  9. <div class="cascade-container">
  10. <van-cascader
  11. v-model="cascadeValue"
  12. title="请选择组织"
  13. :options="options"
  14. :swipeable="true"
  15. :columns-num="3"
  16. @change="SelectedDeptChange"
  17. :field-names="{
  18. text: 'text',
  19. value: 'value',
  20. children: 'children'
  21. }"
  22. />
  23. </div>
  24. </div>
  25. </van-popup>
  26. <van-popup :close-on-click-overlay="false" v-model:show="dialogVisible" position="bottom">
  27. <div class="action-buttons-container">
  28. <div style="color: #ffc107" @click="switchDept">返回</div>
  29. <div>人员选择</div>
  30. <div v-if="multiple" style="color: #28a745" @click="handleSelect">确认</div>
  31. </div>
  32. <div class="content-container">
  33. <div class="search-container">
  34. <div class="search-label">人员查询</div>
  35. <div class="search-row">
  36. <van-field v-model="userName" class="search-field" placeholder="请输入姓名"></van-field>
  37. <div class="action-buttons">
  38. <van-button icon="Search" type="primary" @click="getAllTableData">查询</van-button>
  39. </div>
  40. </div>
  41. </div>
  42. <div class="table-container">
  43. <div class="table-label">人员选择</div>
  44. <div class="table">
  45. <div class="table-header">
  46. <div class="table-column">
  47. <div class="th" style="flex: 0 0 40px;"></div>
  48. <div class="th">工号</div>
  49. <div class="th">姓名</div>
  50. <div v-if="!multiple" class="th">操作</div>
  51. </div>
  52. </div>
  53. <div class="table-body">
  54. <!-- 多选模式 -->
  55. <van-checkbox-group v-if="multiple" v-model="checked" @change="onChange">
  56. <van-checkbox v-for="item in tableData" shape="square" :name="item.user">
  57. <div class="employee-row">
  58. <div class="employee-id">{{ item.user.userCode }}</div>
  59. <div class="employee-name">{{ item.user.userDesc }}</div>
  60. </div>
  61. </van-checkbox>
  62. </van-checkbox-group>
  63. <!-- 单选模式 -->
  64. <div v-else class="employee-list">
  65. <div
  66. v-for="item in tableData"
  67. :key="item.user.id"
  68. class="employee-item"
  69. >
  70. <div class="employee-row">
  71. <div class="employee-id">{{ item.user.userCode }}</div>
  72. <div class="employee-name">{{ item.user.userDesc }}</div>
  73. <div class="employee-name">
  74. <van-button style="margin: 10px"
  75. icon="Select"
  76. type="success"
  77. size="small"
  78. plain
  79. @click="handleSelectSingle(item)"
  80. >
  81. 选择
  82. </van-button>
  83. </div>
  84. </div>
  85. </div>
  86. </div>
  87. </div>
  88. </div>
  89. </div>
  90. </div>
  91. </van-popup>
  92. </template>
  93. <script setup>
  94. import { ref, getCurrentInstance, nextTick } from 'vue';
  95. import { showFailToast } from 'vant';
  96. const {
  97. proxy
  98. } = getCurrentInstance();
  99. const props = defineProps({
  100. multiple: {
  101. type: Boolean,
  102. default: false
  103. },
  104. mainKey: {
  105. type: String,
  106. default: ''
  107. }
  108. });
  109. const getUserCodeList = (mainKey) => {
  110. if (!mainKey) return [];
  111. const json = sessionStorage.getItem(mainKey);
  112. try {
  113. return json ? JSON.parse(json) : [];
  114. } catch (error) {
  115. return [];
  116. }
  117. };
  118. /* 组织树 */
  119. const deptCascadeVisible = ref(false)
  120. const switchPeople = () => {
  121. // 多选模式:初始化缓存数据
  122. if (props.multiple && props.mainKey) {
  123. selectedUsers.value = getUserCodeList(props.mainKey);
  124. }
  125. deptCascadeVisible.value = false
  126. dialogVisible.value = true
  127. }
  128. /* 人员列表 */
  129. const dialogVisible = ref(false);
  130. const switchDept = () => {
  131. if (props.multiple && props.mainKey) {
  132. // 多选模式:删除上次已选数据
  133. for (let item of lastCheckedUsers.value) {
  134. selectedUsers.value = selectedUsers.value.filter(user => user.userCode !== item.userCode);
  135. }
  136. // 切换其他部门前将数据加入缓存
  137. for (let item of checkedUsers.value) {
  138. selectedUsers.value.push(item);
  139. }
  140. sessionStorage.setItem(props.mainKey, JSON.stringify(selectedUsers.value));
  141. }
  142. dialogVisible.value = false
  143. deptCascadeVisible.value = true
  144. }
  145. /* 读取用户所在部门信息 */
  146. const deptJson = localStorage.getItem('dept');
  147. const deptInformation = ref([]);
  148. try {
  149. deptInformation.value = deptJson ? JSON.parse(deptJson) : [];
  150. } catch (error) {
  151. deptInformation.value = [];
  152. }
  153. const deptCode = deptInformation.value[0]?.deptCode || '';
  154. const rawSuperDeptCode = deptCode.length > 2 ? deptCode.slice(0, -2) : '';
  155. /* 读取用户角色信息 */
  156. const roleJson = localStorage.getItem('role');
  157. const roleInformation = ref([]);
  158. try {
  159. roleInformation.value = roleJson ? JSON.parse(roleJson) : [];
  160. } catch (error) {
  161. roleInformation.value = [];
  162. }
  163. const roleCodes = roleInformation.value
  164. .filter(item => item.roleCode && item.roleCode.includes('SX0'))
  165. .map(item => item.roleCode);
  166. const hasRole = (codes) => {
  167. if (!roleCodes.length) return false;
  168. return roleCodes.some(code => codes.some(target => code.includes(target)));
  169. };
  170. const normalizeSuperDept = (code) => {
  171. if (!code) return '';
  172. if (code === 'D11011301') return 'D110113';
  173. if (code === 'D1101') return 'D11';
  174. return code;
  175. };
  176. /* 打开和关闭弹出框 */
  177. const currentDeptCode = ref('');
  178. const open = async () => {
  179. // 多选模式:初始化缓存数据
  180. if (props.multiple && props.mainKey) {
  181. selectedUsers.value = getUserCodeList(props.mainKey);
  182. }
  183. // 始终使用 'D' 查询所有组织,确保显示完整组织树
  184. // currentDeptCode 仅用于默认选择,不影响查询
  185. const userDeptCode = deptInformation.value[0]?.deptCode || 'D';
  186. currentDeptCode.value = userDeptCode;
  187. // 先显示弹窗,再加载数据
  188. deptCascadeVisible.value = true;
  189. // 重置并加载数据
  190. options.value = [];
  191. cascadeValue.value = '';
  192. // getSafeDataTree 内部会强制使用 'D' 查询所有组织
  193. await getSafeDataTree('D');
  194. };
  195. const close = () => {
  196. deptCascadeVisible.value = false
  197. dialogVisible.value = false;
  198. };
  199. defineExpose({
  200. open,
  201. close
  202. });
  203. /* 条件查询该部门下的所有人员信息 */
  204. const currentPage = ref(1);
  205. const pageSize = ref(10);
  206. const userName = ref('');
  207. const tableData = ref([]);
  208. /* 多选模式相关 */
  209. const selectedUsers = ref([]);
  210. const lastCheckedUsers = ref([]);
  211. const checkedUsers = ref([]);
  212. const checked = ref([]);
  213. const currentDeptId = ref('');
  214. /* 获取表格信息 */
  215. const getTableData = async () => {
  216. var url = 'framework/SysUserDept/queryByDeptUserNamePage';
  217. var param = {
  218. page: currentPage.value,
  219. rows: pageSize.value,
  220. deptId: currentDeptId.value,
  221. userName: userName.value
  222. };
  223. await proxy.$axios.get(url, param).then(response => {
  224. if (response.data.code === 0) {
  225. tableData.value = response.data.data.records;
  226. pageSize.value = response.data.data.total;
  227. // 多选模式:处理缓存数据
  228. if (props.multiple && props.mainKey) {
  229. selectedUsers.value = getUserCodeList(props.mainKey);
  230. lastCheckedUsers.value = [];
  231. checked.value = [];
  232. for (let item of tableData.value) {
  233. if (selectedUsers.value.find(content => content.userCode === item.user.userCode)) {
  234. checked.value.push(item.user);
  235. lastCheckedUsers.value.push(item.user);
  236. }
  237. }
  238. }
  239. } else {
  240. showFailToast('操作失败!' + response.data.msg);
  241. }
  242. });
  243. };
  244. const getAllTableData = async () => {
  245. await getTableData();
  246. await getTableData();
  247. };
  248. const SelectedDeptChange = (selectInfo) => {
  249. currentDeptInfo.value = findNodeWithBacktrack(dataTree.value[0], selectInfo.value);
  250. currentDeptId.value = currentDeptInfo.value.id;
  251. getAllTableData();
  252. };
  253. /* 选中数据返回主页面 */
  254. const emit = defineEmits(['receiveFromChild']);
  255. // 单选模式:选择单个人员
  256. const handleSelectSingle = (item) => {
  257. emit('receiveFromChild', item);
  258. close();
  259. };
  260. // 多选模式:确认选择
  261. const handleSelect = () => {
  262. if (!props.multiple) return;
  263. // 确保 selectedUsers 已初始化
  264. if (props.mainKey && selectedUsers.value.length === 0) {
  265. selectedUsers.value = getUserCodeList(props.mainKey);
  266. }
  267. // 删除上次已选数据
  268. for (let item of lastCheckedUsers.value) {
  269. selectedUsers.value = selectedUsers.value.filter(user => user.userCode !== item.userCode);
  270. }
  271. // 将当前选中的数据加入缓存
  272. for (let item of checkedUsers.value) {
  273. // 避免重复添加
  274. if (!selectedUsers.value.find(user => user.userCode === item.userCode)) {
  275. selectedUsers.value.push(item);
  276. }
  277. }
  278. if (props.mainKey) {
  279. sessionStorage.setItem(props.mainKey, JSON.stringify(selectedUsers.value));
  280. }
  281. emit('receiveFromChild', selectedUsers.value);
  282. close();
  283. };
  284. const onChange = (value) => {
  285. if (props.multiple) {
  286. checkedUsers.value = value;
  287. }
  288. };
  289. // 转换部门数据为 cascade 格式
  290. function convertToCascadeFormat(data) {
  291. if (!data || !Array.isArray(data)) return [];
  292. return data.map(item => ({
  293. text: item.deptName,
  294. value: item.deptCode,
  295. children: item.children && item.children.length > 0 ? convertToCascadeFormat(item.children) : []
  296. }));
  297. }
  298. // 叶子节点去掉children
  299. const processTreeNodes = (node) => {
  300. // 如果节点没有子节点,删除children属性并返回
  301. if (!node.children || node.children.length === 0) {
  302. if (node.children) delete node.children;
  303. return;
  304. }
  305. // 递归处理所有子节点
  306. node.children.forEach(child => processTreeNodes(child));
  307. // 检查当前节点是否应该保留children属性
  308. const hasChildrenWithChildren = node.children.some(child =>
  309. child.children && child.children.length > 0
  310. );
  311. // 如果所有子节点都没有子节点(都是叶子节点),删除它们的children属性
  312. if (!hasChildrenWithChildren) {
  313. node.children.forEach(child => {
  314. if (child.children) delete child.children;
  315. });
  316. }
  317. }
  318. const dataTree = ref([]);
  319. const options = ref([]);
  320. const cascadeValue = ref();
  321. const currentDeptInfo = ref({});
  322. const getSafeDataTree = async (deptCode) => {
  323. // 强制使用 'D' 查询所有组织,确保显示完整组织树
  324. const finalDeptCode = 'D';
  325. let url = 'sgsafe/SafeDepartment/queryDeptTreeWithLeafByDeptCode';
  326. let param = {
  327. deptCode: finalDeptCode
  328. };
  329. await proxy.$axios.get(url, param).then(res => {
  330. if (!res || !res.data || !res.data.data) {
  331. showFailToast('获取组织架构数据失败');
  332. return;
  333. }
  334. dataTree.value = res.data.data;
  335. // 不过滤部门树,直接使用原始数据,显示完整组织架构
  336. options.value = convertToCascadeFormat(dataTree.value);
  337. options.value.forEach(child => {
  338. processTreeNodes(child)
  339. })
  340. /* 组件默认选择用户当前部门,如果查询的是所有组织('D'),则使用用户实际部门代码 */
  341. const userDeptCode = deptInformation.value[0]?.deptCode || 'D';
  342. const defaultDeptCode = finalDeptCode === 'D' ? userDeptCode : currentDeptCode.value;
  343. cascadeValue.value = defaultDeptCode;
  344. /* 默认返回当前部门所有信息 */
  345. if (dataTree.value && dataTree.value.length > 0) {
  346. currentDeptInfo.value = findNodeWithBacktrack(dataTree.value[0], defaultDeptCode);
  347. if (currentDeptInfo.value && currentDeptInfo.value.id) {
  348. currentDeptId.value = currentDeptInfo.value.id;
  349. getAllTableData();
  350. } else {
  351. // 如果找不到默认部门,则选择根节点
  352. if (dataTree.value[0]) {
  353. currentDeptInfo.value = dataTree.value[0];
  354. currentDeptId.value = dataTree.value[0].id;
  355. cascadeValue.value = dataTree.value[0].deptCode;
  356. getAllTableData();
  357. }
  358. }
  359. }
  360. }).catch(error => {
  361. showFailToast('获取组织架构数据出错');
  362. });
  363. };
  364. /* 根据deptCode查出部门信息 */
  365. function findNodeWithBacktrack(node, targetDeptCode) {
  366. if (node.deptCode === targetDeptCode) {
  367. return node;
  368. }
  369. if (node.children && node.children.length > 0) {
  370. for (const child of node.children) {
  371. const found = findNodeWithBacktrack(child, targetDeptCode);
  372. if (found && found.deptCode) {
  373. return found;
  374. }
  375. }
  376. }
  377. return {};
  378. }
  379. </script>
  380. <style scoped>
  381. :deep(.van-checkbox__label) {
  382. flex: 1;
  383. }
  384. * {
  385. margin: 0;
  386. padding: 0;
  387. box-sizing: border-box;
  388. font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
  389. }
  390. body {
  391. background: linear-gradient(135deg, #f5f7fa 0%, #e4edf9 100%);
  392. min-height: 100vh;
  393. padding: 20px;
  394. display: flex;
  395. justify-content: center;
  396. align-items: flex-start;
  397. }
  398. .content-container {
  399. height: 100%;
  400. display: flex;
  401. flex-direction: column;
  402. background: #f8fafd;
  403. }
  404. .search-container {
  405. background: #f8fafd;
  406. border-radius: 12px;
  407. padding: 15px;
  408. margin-bottom: 20px;
  409. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
  410. }
  411. .search-label {
  412. font-size: 14px;
  413. color: #5a7eb5;
  414. font-weight: 500;
  415. margin-bottom: 8px;
  416. display: flex;
  417. align-items: center;
  418. }
  419. .search-label::before {
  420. content: "";
  421. display: inline-block;
  422. width: 4px;
  423. height: 16px;
  424. background: #4a90e2;
  425. border-radius: 2px;
  426. margin-right: 8px;
  427. }
  428. .search-row {
  429. display: flex;
  430. align-items: center;
  431. flex-wrap: wrap;
  432. gap: 12px;
  433. padding: 10px;
  434. background: white;
  435. border-radius: 8px;
  436. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  437. }
  438. .search-field {
  439. flex: 1;
  440. min-width: 250px;
  441. }
  442. .action-buttons {
  443. display: flex;
  444. gap: 12px;
  445. }
  446. .table-container {
  447. background: #f8fafd;
  448. border-radius: 12px;
  449. padding: 15px;
  450. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
  451. }
  452. .table-label {
  453. font-size: 14px;
  454. color: #5a7eb5;
  455. font-weight: 500;
  456. margin-bottom: 8px;
  457. display: flex;
  458. align-items: center;
  459. }
  460. .table-label::before {
  461. content: "";
  462. display: inline-block;
  463. width: 4px;
  464. height: 16px;
  465. background: #4a90e2;
  466. border-radius: 2px;
  467. margin-right: 8px;
  468. }
  469. .table {
  470. width: 100%;
  471. border-collapse: separate;
  472. border-spacing: 0;
  473. background: white;
  474. border-radius: 8px;
  475. overflow: hidden;
  476. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  477. }
  478. .table-header {
  479. background: linear-gradient(120deg, #f0f7ff, #e6f1ff);
  480. }
  481. .table-column {
  482. display: flex;
  483. width: 100%;
  484. border-bottom: 1px solid #e0eeff;
  485. }
  486. .th {
  487. padding: 16px 15px;
  488. font-weight: 600;
  489. color: #3a6ab7;
  490. flex: 1;
  491. }
  492. .table-body .table-column {
  493. transition: background-color 0.2s;
  494. border-bottom: 1px solid #f0f7ff;
  495. }
  496. .table-body .tr:last-child {
  497. border-bottom: none;
  498. }
  499. .table-body .tr:hover {
  500. background-color: #f8fbff;
  501. }
  502. .employee-row {
  503. display: flex;
  504. width: 100%;
  505. }
  506. .employee-id, .employee-name {
  507. flex: 1;
  508. padding: 0 15px;
  509. display: flex;
  510. align-items: center;
  511. }
  512. .employee-row {
  513. transition: all 0.3s ease;
  514. }
  515. .van-checkbox {
  516. width: 100%;
  517. padding: 12px 15px;
  518. }
  519. .van-checkbox__label {
  520. width: 100%;
  521. }
  522. .action-buttons-container {
  523. display: flex;
  524. justify-content: space-between;
  525. align-items: center;
  526. padding: 15px 20px;
  527. background: white;
  528. border-bottom: 1px solid #e8e8e8;
  529. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  530. z-index: 10;
  531. }
  532. .cascade-container {
  533. flex: 1;
  534. overflow: hidden;
  535. background: rgb(255, 255, 255);
  536. height: calc(100vh - 200px);
  537. min-height: 400px;
  538. }
  539. .cascade-container :deep(.van-cascader) {
  540. height: 100%;
  541. }
  542. .cascade-container :deep(.van-cascader__content) {
  543. height: calc(100% - 44px);
  544. }
  545. </style>