crud.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, dict, UserPageQuery, compute, InfoReq, EditReq, DelReq } from '@fast-crud/fast-crud';
  2. import * as api from './api';
  3. import { auth } from '/@/utils/authFunction';
  4. import { ElMessage } from 'element-plus';
  5. import { ref } from 'vue';
  6. // 全局变量存储组织树数据,供 valueBuilder 使用
  7. let organizationTreeData: any[] = [];
  8. // 初始化时获取组织树数据
  9. const initOrganizationTree = async () => {
  10. try {
  11. const res = await api.GetTree();
  12. if (res && (res.code === 200 || res.code === 2000 || !res.code)) {
  13. const data = res.data || res.results || res;
  14. if (Array.isArray(data)) {
  15. organizationTreeData = data;
  16. }
  17. }
  18. } catch (error) {
  19. console.error('获取组织架构数据失败:', error);
  20. organizationTreeData = [];
  21. }
  22. };
  23. // 通过名称在组织树中查找节点的辅助函数(在整个树中递归搜索)
  24. function findNodeByName(name: string, tree: any[], depth: number = 0, maxDepth: number = 10): any {
  25. if (!name || !tree || depth > maxDepth) return undefined;
  26. for (const node of tree) {
  27. if (node.name === name) {
  28. return node;
  29. }
  30. if (Array.isArray(node.children)) {
  31. const found = findNodeByName(name, node.children, depth + 1, maxDepth);
  32. if (found) return found;
  33. }
  34. }
  35. return undefined;
  36. }
  37. // 在学院层级中查找节点(跳过学校层)
  38. function findNodeInColleges(name: string, tree: any[]): any {
  39. if (!name || !tree) return undefined;
  40. // 遍历所有学校
  41. for (const school of tree) {
  42. if (Array.isArray(school.children)) {
  43. // 在每个学校的子节点(学院)中查找
  44. for (const college of school.children) {
  45. if (college.name === name) {
  46. return college;
  47. }
  48. }
  49. }
  50. }
  51. return undefined;
  52. }
  53. export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
  54. // 初始化组织树数据(异步执行,不阻塞返回)
  55. initOrganizationTree();
  56. const pageRequest = async (query: UserPageQuery) => {
  57. return await api.GetList(query);
  58. };
  59. const getDetail = async ({ row }: InfoReq) => {
  60. return await api.GetObj(row.id);
  61. };
  62. const editRequest = async ({ form, row }: EditReq) => {
  63. form.id = row.id;
  64. return await api.UpdateObj(form);
  65. };
  66. const delRequest = async ({ row }: DelReq) => {
  67. return await api.DelObj(row.id);
  68. };
  69. const addRequest = async ({ form }: AddReq) => {
  70. return await api.AddObj(form);
  71. };
  72. const selectedIds = ref<any[]>([]);
  73. const onSelectionChange = (changed: any[]) => {
  74. console.log("selection", changed);
  75. selectedIds.value = changed.map((item: any) => item.id);
  76. };
  77. (crudExpose as any).selectedIds = selectedIds;
  78. /**
  79. * 懒加载
  80. * @param row
  81. * @returns {Promise<unknown>}
  82. */
  83. // const loadContentMethod = (tree: any, treeNode: any, resolve: Function) => {
  84. // pageRequest({ pcode: tree.code }).then((res: APIResponseData) => {
  85. // resolve(res.data);
  86. // });
  87. // };
  88. return {
  89. crudOptions: {
  90. request: {
  91. pageRequest,
  92. editRequest,
  93. delRequest,
  94. addRequest,
  95. getDetail,
  96. },
  97. toolbar:{
  98. show:false,
  99. },
  100. actionbar: {
  101. buttons: {
  102. add: {
  103. show:true,// auth('area:Create'),
  104. },
  105. /* batchExport: {
  106. text: '批量导出',
  107. type: 'primary',
  108. show: auth('area:Export'),
  109. click: ({ getSelectedRows }: any) => {
  110. const selectedRows = getSelectedRows();
  111. if (selectedRows.length === 0) {
  112. ElMessage.warning('请先选择要导出的用户');
  113. return;
  114. }
  115. // 触发批量导出 - 通过全局事件总线
  116. window.dispatchEvent(new CustomEvent('batch-export', { detail: selectedRows }));
  117. },
  118. }, */
  119. },
  120. },
  121. form:{
  122. wrapper: {
  123. buttons: {
  124. ok:{
  125. text:'提交',
  126. show:compute((row) => {
  127. return row.mode !=='view'
  128. })
  129. }
  130. }
  131. }
  132. },
  133. pagination: {
  134. show: true,
  135. },
  136. table: {
  137. rowSelection: {
  138. show: true,
  139. multiple: true,
  140. },
  141. onSelectionChange,
  142. },
  143. columns: {
  144. $checked: {
  145. title: "选择",
  146. form: { show: false },
  147. column: {
  148. type: "selection",
  149. align: "center",
  150. width: "55px",
  151. columnSetDisabled: true, //禁止在列设置中选择
  152. selectable(row: any, index: any) {
  153. // return row.id !== 1; //设置第一行不允许选择
  154. return row.id;
  155. }
  156. }
  157. },
  158. _index: {
  159. title: '序号',
  160. form: { show: false },
  161. column: {
  162. type: 'index',
  163. align: 'center',
  164. width: '70px',
  165. columnSetDisabled: true, //禁止在列设置中选择
  166. },
  167. },
  168. search:{
  169. title: '关键字搜索',
  170. search: { show: true,type: 'input', },
  171. type: 'input',
  172. form: {
  173. component: { placeholder: '请输入' },
  174. show:false},
  175. column: {show:false}
  176. },
  177. user_code:{
  178. title:'学号/工号',
  179. type:'input',
  180. column: {
  181. minWidth: 120,
  182. },
  183. form: {
  184. component: { placeholder: '请填写学号/工号' },
  185. rules: [{ required: true, message: '请填写学号/工号' }],
  186. },
  187. viewForm:{
  188. component: { placeholder: '' },
  189. }
  190. },
  191. username:{
  192. title:'用户名',
  193. type:'input',
  194. column: {
  195. show: false,
  196. minWidth: 120,
  197. },
  198. form: {
  199. component: { placeholder: '请填写用户名' },
  200. rules: [{ required: true, message: '请填写用户名' }],
  201. },
  202. viewForm:{
  203. component: { placeholder: '' },
  204. }
  205. },
  206. name: {
  207. title: '姓名',
  208. search: {
  209. show: false,
  210. },
  211. treeNode: true,
  212. type: 'input',
  213. column: {
  214. minWidth: 120,
  215. },
  216. form: {
  217. rules: [
  218. // 表单校验规则
  219. { required: true, message: '姓名必填项' },
  220. ],
  221. component: {
  222. placeholder: '请输入姓名',
  223. },
  224. },
  225. },
  226. password:{
  227. title: '密码',
  228. type: 'input',
  229. column: {
  230. minWidth: 120,
  231. show: false,
  232. },
  233. form: {
  234. component: {
  235. placeholder: '请填写密码',
  236. type: 'password',
  237. showPassword: true
  238. },
  239. },
  240. addForm:{
  241. component:{
  242. placeholder: '请填写密码',
  243. type: 'password',
  244. showPassword: true
  245. } ,
  246. rules: [
  247. { required: true, message: '请填写密码' },
  248. {
  249. validator: (rule: any, value: string, callback: Function) => {
  250. // 新增时密码必填,如果为空则报错
  251. if (!value || value.trim() === '') {
  252. callback(new Error('请填写密码'));
  253. return;
  254. }
  255. // 密码格式校验
  256. const minLength = 8;
  257. const hasLetter = /[a-zA-Z]/.test(value);
  258. const hasNumber = /\d/.test(value);
  259. const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(value);
  260. if (value.length < minLength) {
  261. callback(new Error('密码长度不能少于8位'));
  262. return;
  263. }
  264. if (!hasLetter) {
  265. callback(new Error('密码必须包含字母'));
  266. return;
  267. }
  268. if (!hasNumber) {
  269. callback(new Error('密码必须包含数字'));
  270. return;
  271. }
  272. if (!hasSpecialChar) {
  273. callback(new Error('密码必须包含特殊符号'));
  274. return;
  275. }
  276. callback();
  277. },
  278. trigger: 'blur'
  279. }
  280. ],
  281. },
  282. editForm:{
  283. show:true,
  284. component: {
  285. placeholder: '留空则不修改密码',
  286. type: 'password',
  287. showPassword: true
  288. },
  289. rules: [
  290. {
  291. validator: (rule: any, value: string, callback: Function) => {
  292. // 如果密码为空,则通过验证(非必填)
  293. if (!value || value.trim() === '') {
  294. callback();
  295. return;
  296. }
  297. // 如果填写了密码,则进行格式校验
  298. const minLength = 8;
  299. const hasLetter = /[a-zA-Z]/.test(value);
  300. const hasNumber = /\d/.test(value);
  301. const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(value);
  302. if (value.length < minLength) {
  303. callback(new Error('密码长度不能少于8位'));
  304. return;
  305. }
  306. if (!hasLetter) {
  307. callback(new Error('密码必须包含字母'));
  308. return;
  309. }
  310. if (!hasNumber) {
  311. callback(new Error('密码必须包含数字'));
  312. return;
  313. }
  314. if (!hasSpecialChar) {
  315. callback(new Error('密码必须包含特殊符号'));
  316. return;
  317. }
  318. callback();
  319. },
  320. trigger: 'blur'
  321. }
  322. ]
  323. },
  324. viewForm:{
  325. show:false,
  326. }
  327. },
  328. email:{
  329. title: '邮箱',
  330. type: 'input',
  331. column: {
  332. show: false,
  333. minWidth: 120,
  334. },
  335. form: {
  336. component: { placeholder: '请填写邮箱' },
  337. rules: [{ required: false, message: '请填写邮箱' }],
  338. },
  339. viewForm:{
  340. component: { placeholder: '' },
  341. }
  342. },
  343. mobile:{
  344. title: '手机号',
  345. type: 'input',
  346. column: {
  347. show: false,
  348. minWidth: 120,
  349. },
  350. form: {
  351. component: { placeholder: '请填写手机号' },
  352. rules: [{ required: true, message: '请填写手机号' }],
  353. },
  354. viewForm:{
  355. component: { placeholder: '' },
  356. }
  357. },
  358. gender:{
  359. title: '性别',
  360. type: 'dict-select',
  361. column: {
  362. show: true,
  363. minWidth: 120,
  364. },
  365. dict: dict({
  366. data: [
  367. { label: '男', value: 1 },
  368. { label: '女', value: 2 },
  369. ],
  370. }),
  371. form: {
  372. component: { placeholder: '请选择性别' },
  373. rules: [{ required: true, message: '请选择性别' }],
  374. },
  375. },
  376. user_type:{
  377. title: '用户类型',
  378. type: 'dict-select',
  379. search:{
  380. show:false
  381. },
  382. column: {
  383. minWidth: 120,
  384. },
  385. dict: dict({
  386. data: [
  387. { label: '学生', value: 0 },
  388. { label: '教师', value: 1 },
  389. { label: '外部用户', value: 2 },
  390. { label: '学院领导', value: 3 },
  391. ],
  392. }),
  393. form: {
  394. component: { placeholder: '请选择用户类型' },
  395. rules: [{ required: false, message: '请选择用户类型' }],
  396. },
  397. },
  398. organization_ref:{
  399. title: '学院',//'组织架构',
  400. type: "dict-cascader",
  401. column: {
  402. show: false,
  403. },
  404. // 学生类型表单的级联选择依赖于 index.vue 中的 _college_id/_major_id/_grade_id
  405. // 这里通过 valueBuilder 在打开编辑/查看表单时,将 organization_detail.parent_chain 回显到这三个临时字段
  406. // 同时通过 valueResolve 在提交时将最终选择写回 organization_ref(学生为年级ID,其它类型为学院ID)
  407. valueBuilder({ form, value }) {
  408. // 保留后端原始值
  409. form.organization_ref = value;
  410. // 仅学生类型需要拆分层级进行回显
  411. const od = (form as any).organization_detail;
  412. // 如果组织树数据未加载,尝试加载(异步,不阻塞)
  413. if (organizationTreeData.length === 0) {
  414. initOrganizationTree();
  415. }
  416. if ((form as any).user_type === 0 && od && Array.isArray(od.parent_chain)) {
  417. // 期望 parent_chain: [学校, 学院, 专业, 年级] 或至少包含 [学院, 专业, 年级]
  418. const chain = od.parent_chain;
  419. // 按长度精确映射,避免把学校ID当作学院ID
  420. if (chain.length >= 4) {
  421. // [学校, 学院, 专业, 年级]
  422. (form as any)._college_id = chain[1]?.id;
  423. (form as any)._major_id = chain[2]?.id;
  424. (form as any)._grade_id = od?.id;
  425. } else if (chain.length === 3) {
  426. // 常见后端:含学校,无年级 -> [学校, 学院, 专业]
  427. (form as any)._college_id = chain[1]?.id;
  428. (form as any)._major_id = chain[2]?.id;
  429. (form as any)._grade_id = od?.id;
  430. } else if (chain.length === 2) {
  431. // [学院, 专业]
  432. (form as any)._college_id = chain[0]?.id;
  433. (form as any)._major_id = chain[1]?.id;
  434. (form as any)._grade_id = od?.id;
  435. } else if (chain.length === 1) {
  436. // [学院]
  437. (form as any)._college_id = chain[0]?.id;
  438. (form as any)._major_id = undefined;
  439. (form as any)._grade_id = od?.id;
  440. } else {
  441. (form as any)._college_id = undefined;
  442. (form as any)._major_id = undefined;
  443. (form as any)._grade_id = od?.id;
  444. }
  445. // 学生模式下,不直接把 organization_ref 设为学院,保持由 index.vue 的联动逻辑在变更时再写入
  446. } else if ((form as any).user_type === 1 || (form as any).user_type === 3) {
  447. // 教师/学院领导:直接以当前组织ID为学院选择
  448. if (od && od.id) {
  449. (form as any).organization_ref = od.id;
  450. }
  451. } else if ((form as any).user_type === 0 && !od && organizationTreeData.length > 0) {
  452. // 学生类型且 organization_detail 为 null 时,通过文本名称查找并回显
  453. const orgName = (form as any).organization; // 学院名称
  454. const subOrgName = (form as any).sub_organization; // 专业名称
  455. const gradeName = (form as any).grade_or_level || (form as any).class_or_group; // 班级/年级名称
  456. // 查找学院节点(在学院层级查找)
  457. if (orgName) {
  458. const collegeNode = findNodeInColleges(orgName, organizationTreeData);
  459. if (collegeNode) {
  460. (form as any)._college_id = collegeNode.id;
  461. // 查找专业节点(在学院的直接子节点中查找)
  462. if (subOrgName && Array.isArray(collegeNode.children)) {
  463. const majorNode = collegeNode.children.find((child: any) => child.name === subOrgName);
  464. if (majorNode) {
  465. (form as any)._major_id = majorNode.id;
  466. // 查找班级/年级节点(在专业的直接子节点中查找)
  467. if (gradeName && Array.isArray(majorNode.children)) {
  468. const gradeNode = majorNode.children.find((child: any) => child.name === gradeName);
  469. if (gradeNode) {
  470. (form as any)._grade_id = gradeNode.id;
  471. (form as any).organization_ref = gradeNode.id;
  472. }
  473. }
  474. }
  475. }
  476. }
  477. }
  478. }
  479. },
  480. valueResolve({ form, value }) {
  481. // 非学生:提交学院ID
  482. if ((form as any).user_type !== 0) {
  483. (form as any).organization_ref = value ?? (form as any).organization_ref;
  484. return;
  485. }
  486. // 学生:当选择了年级,则以年级ID为最终 organization_ref
  487. if ((form as any)._grade_id) {
  488. (form as any).organization_ref = (form as any)._grade_id;
  489. return;
  490. }
  491. // 兜底:若仅选择到专业或学院,不改变已有值(让上层联动 onGradeChange 来写入)
  492. }
  493. },
  494. /* 组织架构*/
  495. organization_detail:{
  496. title: '组织架构',
  497. type: 'dict-cascader',
  498. column: {
  499. show: false,
  500. minWidth: 200,
  501. formatter: ({ row, value }: { row: any; value: any }) => {
  502. // 如果有 organization_detail 对象,显示 full_path
  503. if (row.organization_detail && row.organization_detail.full_path) {
  504. return row.organization_detail.full_path;
  505. }
  506. // 否则显示原始值
  507. return value || '';
  508. },
  509. },
  510. form: {
  511. show:false,
  512. component: { placeholder: '请填写组织架构' },
  513. rules: [{ required: false, message: '请填写组织架构' }],
  514. },
  515. viewForm:{
  516. component: { placeholder: '' },
  517. }
  518. },
  519. organization:{
  520. title: '学院',
  521. type: 'input',
  522. search:{
  523. show:false
  524. },
  525. column: {
  526. show: true,
  527. minWidth: 200,
  528. formatter: ({ row, value }: { row: any; value: any }) => {
  529. const isValidValue = (val: any) => {
  530. return val !== null && val !== undefined && val !== '' && val !== 'nan' && !Number.isNaN(val);
  531. };
  532. // 如果有 organization_detail 对象,显示 parent_chain 的第二项(专业)
  533. if(row.user_type !==0) {
  534. if (row.organization_detail && row.organization_detail.name) {
  535. return row.organization_detail.name;
  536. }
  537. // 否则显示原始值
  538. return isValidValue(value) ? value : (isValidValue(row.class_or_group) ? row.class_or_group : '');
  539. }else{
  540. if (row.organization_detail && row.organization_detail.parent_chain && row.organization_detail.parent_chain.length >= 2) {
  541. if(!row.organization_detail.parent_chain[1]) return
  542. const major = row.organization_detail.parent_chain[1]; // 第二项是专业
  543. return major.name || major.code || '';
  544. }
  545. // 否则显示原始值
  546. return isValidValue(value) ? value : (isValidValue(row.sub_organization) ? row.sub_organization : '');
  547. }
  548. },
  549. /* formatter: ({ row, value }: { row: any; value: any }) => {
  550. // 如果有 organization_detail 对象,显示 full_path
  551. if (row.organization_detail && row.organization_detail.full_path) {
  552. return row.organization_detail.full_path;
  553. }
  554. // 否则显示原始值
  555. return value || '';
  556. }, */
  557. },
  558. form: {
  559. show: compute((row:any)=>{
  560. // 学生、教师、学院领导:在表单中显示“学院”
  561. return row.user_type === 0 || row.user_type === 1 || row.user_type === 3
  562. }),
  563. component: { placeholder: '请选择学院' },
  564. rules: [{ required: false, message: '请选择学院' }],
  565. },
  566. viewForm:{
  567. component: { placeholder: '' },
  568. }
  569. },
  570. sub_organization:{
  571. title: '专业',
  572. type: 'input',
  573. search:{
  574. show:false
  575. },
  576. column: {
  577. show: true,
  578. minWidth: 120,
  579. formatter: ({ row, value }: { row: any; value: any }) => {
  580. const isValidValue = (val: any) => {
  581. return val !== null && val !== undefined && val !== '' && val !== 'nan' && !Number.isNaN(val);
  582. };
  583. // 如果有 organization_detail 对象,显示 parent_chain 的第二项(专业)
  584. if (row.organization_detail && row.organization_detail.parent_chain && row.organization_detail.parent_chain.length >= 2) {
  585. if(!row.organization_detail.parent_chain[2]) return
  586. const major = row.organization_detail.parent_chain[2]; // 第二项是专业
  587. return major.name || major.code || '';
  588. }
  589. // 否则显示原始值
  590. return isValidValue(value) ? value : (isValidValue(row.sub_organization) ? row.sub_organization : '');
  591. },
  592. },
  593. form: {
  594. show:compute(({ form }) => {
  595. // 只有当选择了胜任力标签时才显示配置
  596. return form && form.user_type == 0;
  597. }),
  598. component: { placeholder: '请填写专业' },
  599. rules: [{ required: false, message: '请填写专业' }],
  600. },
  601. /* viewForm:{
  602. component: { placeholder: '' },
  603. } */
  604. },
  605. grade_or_level:{
  606. title: '班级',
  607. type: 'input',
  608. search:{
  609. show:false
  610. },
  611. column: {
  612. show: true,
  613. minWidth: 120,
  614. formatter: ({ row, value }: { row: any; value: any }) => {
  615. const isValidValue = (val: any) => {
  616. return val !== null && val !== undefined && val !== '' && val !== 'nan' && !Number.isNaN(val);
  617. };
  618. // 如果有 organization_detail 对象,显示 parent_chain 的第二项(专业)
  619. /* if (row.organization_detail && row.organization_detail.parent_chain && row.organization_detail.parent_chain.length >= 2) {
  620. if(!row.organization_detail.parent_chain[2]) return
  621. const major = row.organization_detail.parent_chain[2]; // 第二项是专业
  622. return major.name || major.code || '';
  623. }
  624. // 否则显示原始值
  625. return value || row.grade_or_level; */
  626. if(row.user_type !==0) return
  627. if (row.organization_detail && row.organization_detail.name) {
  628. return row.organization_detail.name;
  629. }
  630. // 否则显示原始值
  631. return isValidValue(value) ? value : (isValidValue(row.class_or_group) ? row.class_or_group : '');
  632. },
  633. },
  634. form: {
  635. show:compute(({ form }) => {
  636. // 只有当选择了胜任力标签时才显示配置
  637. return form && form.user_type == 0;
  638. }),
  639. /* show: compute((row:any)=> row.user_type === 0), */
  640. component: { placeholder: '请选择年级' },
  641. rules: [{ required: false, message: '请选择年级' }],
  642. },
  643. viewForm:{
  644. component: { placeholder: '' },
  645. }
  646. },
  647. class_or_group:{
  648. title: '班级',
  649. type: 'input',
  650. search:{
  651. show:false
  652. },
  653. column: {
  654. show: false,
  655. minWidth: 120,
  656. formatter: ({ row, value }: { row: any; value: any }) => {
  657. // 如果有 organization_detail 对象,显示 name
  658. if (row.organization_detail && row.organization_detail.name) {
  659. return row.organization_detail.name;
  660. }
  661. // 否则显示原始值
  662. return value || row.class_or_group;
  663. },
  664. },
  665. form: {
  666. show:false,
  667. component: { placeholder: '请填写班级' },
  668. rules: [{ required: false, message: '请填写班级' }],
  669. },
  670. viewForm:{
  671. component: { placeholder: '' },
  672. }
  673. },
  674. },
  675. },
  676. };
  677. };