crud.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. import { CreateCrudOptionsProps, CreateCrudOptionsRet, AddReq, DelReq, EditReq, dict, compute } from '@fast-crud/fast-crud';
  2. import * as api from './api';
  3. import { dictionary } from '/@/utils/dictionary';
  4. import { successMessage, warningMessage } from '../../../utils/message';
  5. import { auth } from '/@/utils/authFunction';
  6. /* interface PositionContext {
  7. router: any;
  8. selectedRows: any[];
  9. openBatchTagsDialog: (selection: any[]) => void;
  10. } */
  11. /**
  12. *
  13. * @param crudExpose:index传递过来的示例
  14. * @param context:index传递过来的自定义参数
  15. * @returns
  16. */
  17. export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
  18. const pageRequest = async (query: any) => {
  19. return await api.GetList(query);
  20. };
  21. const editRequest = async ({ form, row }: EditReq) => {
  22. form.id = row.id;
  23. return await api.UpdateObj(form);
  24. };
  25. const delRequest = async ({ row }: DelReq) => {
  26. return await api.DelObj(row.id);
  27. };
  28. const addRequest = async ({ form }: AddReq) => {
  29. return await api.AddObj(form);
  30. };
  31. return {
  32. crudOptions: {
  33. toolbar:{
  34. buttons:{
  35. search:{show:false},
  36. // 刷新按钮
  37. refresh:{show:false},
  38. // 紧凑模式
  39. compact:{show:false},
  40. // 导出按钮
  41. export:{
  42. text: '导出',
  43. type: 'primary',
  44. size: 'small',
  45. icon: 'upload',
  46. circle: false,
  47. display: true
  48. },
  49. // 列设置按钮
  50. columns:{
  51. show:false
  52. },
  53. }
  54. },
  55. request: {
  56. pageRequest,
  57. addRequest,
  58. editRequest,
  59. delRequest,
  60. },
  61. pagination: {
  62. show: true,
  63. },
  64. search: {
  65. show: true,
  66. buttons: {
  67. search: {
  68. size: 'small',
  69. col:{ span:3},
  70. },
  71. reset: {
  72. size: 'small',
  73. col:{ span:3},
  74. },
  75. },
  76. },
  77. actionbar: {
  78. buttons: {
  79. add: {
  80. size: 'small',
  81. show: auth('role:Create'),
  82. click: () => {
  83. context.router.push('/position/create');
  84. }
  85. },
  86. // 添加批量绑定标签按钮
  87. batchBindTags: {
  88. text: '批量绑定标签',
  89. type: 'primary',
  90. size: 'small',
  91. show: false,
  92. order: 2,
  93. click: () => {
  94. // 使用存储的选中行
  95. const selection = context.selectedRows || [];
  96. console.log('选中的行:', selection);
  97. if (!selection || selection.length === 0) {
  98. warningMessage('请先选择要操作的职位');
  99. return;
  100. }
  101. console.log('context', context);
  102. // 打开批量绑定标签对话框
  103. context.openBatchTagsDialog(selection);
  104. },
  105. },
  106. //批量修改状态
  107. batchPublish: {
  108. text: '批量修改状态',
  109. type: 'primary',
  110. size: 'small',
  111. show: true,
  112. order: 3,
  113. click: () => {
  114. // 使用存储的选中行
  115. const selection = context.selectedRows || [];
  116. console.log('选中的行:', selection);
  117. if (!selection || selection.length === 0) {
  118. warningMessage('请先选择要操作的职位');
  119. return;
  120. }
  121. // 打开批量修改状态对话框
  122. context.openBatchStatusDialog(selection);
  123. },
  124. },
  125. // addDialog: {
  126. // text: '快速添加',
  127. // icon: 'Plus',
  128. // show: auth('role:Create'),
  129. // order: 2
  130. // },
  131. },
  132. },
  133. rowHandle: {
  134. //固定右侧
  135. fixed: 'right',
  136. width: 320,
  137. buttons: {
  138. view: {
  139. show: true,
  140. iconRight:"View",
  141. type:"text"
  142. },
  143. edit: {
  144. show: auth('role:Update'),
  145. iconRight:"Edit",
  146. type:"text",
  147. click: (ctx) => {
  148. context.router.push(`/position/detail?id=${ctx.row.id}`);
  149. }
  150. },
  151. remove: {
  152. show: auth('role:Delete'),
  153. iconRight:"Delete",
  154. type:"text"
  155. },
  156. qrcode: {
  157. text: '小程序码',
  158. type: "text",
  159. icon: '',
  160. click: (ctx) => {
  161. context.generateQRCode(ctx.row);
  162. },
  163. order: 4
  164. },
  165. publish: {
  166. text: '发布',
  167. type: "text",
  168. icon: '',
  169. click: (ctx) => {
  170. context.publishPosition(ctx.row);
  171. },
  172. },
  173. /* permission: {
  174. type: 'primary',
  175. text: '权限配置',
  176. show: auth('role:Permission'),
  177. click: (clickContext: any): void => {
  178. const { row } = clickContext;
  179. context.RoleDrawer.handleDrawerOpen(row);
  180. context.RoleMenuBtn.setState([]);
  181. context.RoleMenuField.setState([]);
  182. },
  183. }, */
  184. },
  185. },
  186. form: {
  187. col: { span: 24 },
  188. labelWidth: '100px',
  189. wrapper: {
  190. is: 'el-dialog',
  191. width: '600px',
  192. },
  193. },
  194. table: {
  195. onSelectionChange: (selection: any[]) => {
  196. context.selectedRows = selection;
  197. console.log('选择变化:', selection);
  198. }
  199. },
  200. columns: {
  201. _selection: {
  202. title: '选择',
  203. form: { show: false },
  204. column: {
  205. type: 'selection',
  206. align: 'center',
  207. width: 50,
  208. fixed: 'left',
  209. columnSetDisabled: true,
  210. },
  211. },
  212. /* _index: {
  213. title: '序号',
  214. form: { show: false },
  215. column: {
  216. type: 'index',
  217. align: 'center',
  218. width: '70px',
  219. columnSetDisabled: true, //禁止在列设置中选择
  220. },
  221. }, */
  222. id: {
  223. title: 'ID',
  224. column: { show: false },
  225. search: { show: false },
  226. form: { show: false },
  227. },
  228. title: {
  229. title: '职位名称',
  230. search: { show: true,
  231. size: 'small',
  232. col:{ span:3},
  233. },
  234. column: {
  235. minWidth: 120,
  236. sortable: 'custom',
  237. },
  238. form: {
  239. rules: [{ required: true, message: '角色名称必填' }],
  240. component: {
  241. placeholder: '请输入角色名称',
  242. },
  243. },
  244. },
  245. /* key: {
  246. title: '权限标识',
  247. search: { show: false },
  248. column: {
  249. minWidth: 120,
  250. sortable: 'custom',
  251. columnSetDisabled: true,
  252. },
  253. form: {
  254. rules: [{ required: true, message: '权限标识必填' }],
  255. component: {
  256. placeholder: '输入权限标识',
  257. },
  258. },
  259. valueBuilder(context) {
  260. const { row, key } = context;
  261. return row[key];
  262. },
  263. },
  264. sort: {
  265. title: '排序',
  266. search: { show: false },
  267. type: 'number',
  268. column: {
  269. minWidth: 90,
  270. sortable: 'custom',
  271. },
  272. form: {
  273. rules: [{ required: true, message: '排序必填' }],
  274. value: 1,
  275. },
  276. },*/
  277. job_type: {
  278. title: '职位类型',
  279. search: { show: true,
  280. size: 'small',
  281. col:{ span:3},
  282. },
  283. type: 'dict-select',
  284. column: {
  285. width: 100,
  286. },
  287. dict: dict({
  288. data: [
  289. { value:0, label: '全职' },
  290. { value:1, label: '兼职' },
  291. { value: 2, label: '实习' },
  292. { value: 3, label: "其他" }
  293. ]
  294. }),
  295. form: {
  296. rules: [{ required: true, message: '职位类型必填' }],
  297. component: {
  298. placeholder: '职位类型',
  299. },
  300. },
  301. },
  302. competency_tags: {
  303. title: '胜任力标签',
  304. search: {
  305. show: true,
  306. size: 'small',
  307. col: { span: 3 },
  308. component: {
  309. props: {
  310. multiple: false, // 在搜索表单中使用单选模式
  311. filterable: true,
  312. }
  313. },
  314. },
  315. type: 'dict-select',
  316. column: {
  317. show: false,
  318. minWidth: 180,
  319. component: {
  320. // 自定义渲染组件,显示彩色标签
  321. name: 'fs-component',
  322. render: ({ row }: { row: any }) => {
  323. if (!row.competency_tags || row.competency_tags.length === 0) return <span>-</span>;
  324. const displayTags = row.competency_tags.slice(0, 2); // 只取前两个标签
  325. const remainingCount = row.competency_tags.length - 2; // 计算剩余标签数量
  326. return (
  327. <div style="display: flex; gap: 4px; align-items: center;">
  328. {displayTags.map((tag: any) => (
  329. <el-tag
  330. key={tag.id}
  331. type="warning"
  332. effect="plain"
  333. size="mini"
  334. style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
  335. title={tag.name}
  336. >
  337. {tag.name.length > 4 ? tag.name.slice(0, 4) + '...' : tag.name}
  338. </el-tag>
  339. ))}
  340. {remainingCount > 0 && (
  341. <el-tooltip
  342. placement="top"
  343. effect="light"
  344. popper-class="tag-tooltip"
  345. >
  346. {{
  347. default: () => (
  348. <el-tag
  349. type="info"
  350. effect="plain"
  351. size="mini"
  352. >
  353. +{remainingCount}
  354. </el-tag>
  355. ),
  356. content: () => (
  357. <div>
  358. <div style="font-weight: bold; margin-bottom: 5px">剩余{remainingCount}个标签:</div>
  359. {row.competency_tags.slice(2).map((tag: any) => (
  360. <div key={tag.id} style="margin: 3px 0">{tag.name}</div>
  361. ))}
  362. </div>
  363. )
  364. }}
  365. </el-tooltip>
  366. )}
  367. </div>
  368. );
  369. }
  370. }
  371. },
  372. dict: dict({
  373. getData: async () => {
  374. const res = await api.GetCompetencyList({page:1, limit:1000, tenant_id:1});
  375. return res.data.items;
  376. },
  377. label: 'name',
  378. value: 'id'
  379. }),
  380. form: {
  381. rules: [{ required: true, message: '胜任力标签必填' }],
  382. },
  383. },
  384. salary_range: {
  385. title: '薪资范围',
  386. search: { show: false,
  387. size: 'small',
  388. col:{ span:3},
  389. },
  390. column: {
  391. minWidth: 100,
  392. },
  393. form: {
  394. rules: [{ required: true, message: '薪资范围必填' }],
  395. component: {
  396. placeholder: '请输入薪资范围',
  397. },
  398. },
  399. },
  400. location: {
  401. title: '工作地点',
  402. search: { show: true,
  403. size: 'small',
  404. col:{ span:3},
  405. },
  406. column: {
  407. minWidth: 120,
  408. formatter: ({ row }) => {
  409. if (typeof row.location === 'string' && row.location.startsWith('[')) {
  410. try {
  411. // 解析字符串形式的数组
  412. const locationArray = JSON.parse(row.location.replace(/'/g, '"'));
  413. return locationArray.join(' ');
  414. } catch (e) {
  415. console.error('解析location失败:', e);
  416. return row.location;
  417. }
  418. }
  419. if (Array.isArray(row.location)) {
  420. return row.location.join(' ');
  421. }
  422. if (row.province && row.city) {
  423. return row.province + ' ' + row.city + (row.district ? ' ' + row.district : '');
  424. }
  425. return row.location || '';
  426. }
  427. },
  428. form: {
  429. component: {
  430. name: 'el-cascader',
  431. props: {
  432. options: [
  433. {
  434. value: '上海市',
  435. label: '上海市',
  436. children: [
  437. {
  438. value: '浦东新区',
  439. label: '浦东新区',
  440. children: [
  441. { value: '张江', label: '张江' },
  442. { value: '金桥', label: '金桥' },
  443. { value: '陆家嘴', label: '陆家嘴' }
  444. ]
  445. },
  446. {
  447. value: '徐汇区',
  448. label: '徐汇区',
  449. children: [
  450. { value: '漕河泾', label: '漕河泾' },
  451. { value: '徐家汇', label: '徐家汇' }
  452. ]
  453. },
  454. // 其他区域...
  455. ]
  456. },
  457. {
  458. value: '北京市',
  459. label: '北京市',
  460. children: [
  461. {
  462. value: '海淀区',
  463. label: '海淀区',
  464. children: [
  465. { value: '中关村', label: '中关村' },
  466. { value: '上地', label: '上地' }
  467. ]
  468. },
  469. {
  470. value: '朝阳区',
  471. label: '朝阳区',
  472. children: [
  473. { value: 'CBD', label: 'CBD' },
  474. { value: '望京', label: '望京' }
  475. ]
  476. },
  477. // 其他区域...
  478. ]
  479. },
  480. // 其他省市...
  481. ],
  482. props: {
  483. expandTrigger: 'hover',
  484. checkStrictly: false
  485. },
  486. placeholder: '请选择工作地点',
  487. clearable: true,
  488. showAllLevels: true
  489. },
  490. on: {
  491. change: (value) => {
  492. console.log('地址选择变化:', value);
  493. }
  494. }
  495. },
  496. valueResolve: (form: any) => {
  497. if (form.location && Array.isArray(form.location) && form.location.length > 0) {
  498. form.province = form.location[0];
  499. form.city = form.location[1] || '';
  500. form.district = form.location[2] || '';
  501. form.location = form.location.join(' ');
  502. }
  503. },
  504. valueBuilder: (form: any) => {
  505. if (form.province && form.city) {
  506. form.location = [form.province, form.city];
  507. if (form.district) {
  508. form.location.push(form.district);
  509. }
  510. } else if (typeof form.location === 'string' && form.location) {
  511. const parts = form.location.split(' ');
  512. if (parts.length > 0) {
  513. form.location = parts;
  514. }
  515. }
  516. }
  517. },
  518. },
  519. department: {
  520. title: '所属部门',
  521. search: { show: true,
  522. size: 'small',
  523. col:{ span:3},
  524. },
  525. column: {
  526. minWidth: 100,
  527. },
  528. form: {
  529. rules: [{ required: true, message: '所属部门必填' }],
  530. component: {
  531. placeholder: '请输入所属部门',
  532. },
  533. },
  534. },
  535. status: {
  536. title: '状态',
  537. search: { show: true,
  538. size: 'small',
  539. col:{ span:3},
  540. },
  541. type: 'dict-select',
  542. column: {
  543. minWidth: 100,
  544. },
  545. dict: dict({
  546. data: [
  547. { value: 0, label: '未发布' },
  548. { value: 1, label: '已发布' },
  549. { value: 2, label: '已过期' }
  550. ]
  551. }),
  552. form: {
  553. rules: [{ required: true, message: '职位类型必填' }],
  554. },
  555. },
  556. end_date: {
  557. title: '截止日期',
  558. type: 'datetime',
  559. search: { show: true,
  560. size: 'small',
  561. col:{ span:3},
  562. },
  563. column: {
  564. show: false,
  565. minWidth: 150,
  566. sortable: 'custom',
  567. },
  568. form: {
  569. rules: [{ required: true, message: '截止日期必填' }],
  570. component: {
  571. type: 'datetime',
  572. valueFormat: 'YYYY-MM-DD HH:mm:ss',
  573. placeholder: '请选择截止日期',
  574. },
  575. },
  576. },
  577. requirements: {
  578. title: '职位要求',
  579. search: { show: false,
  580. size: 'small',
  581. col:{ span:3},
  582. },
  583. column: {
  584. show: false,
  585. showOverflowTooltip: true,
  586. width: 120,
  587. sortable: 'custom',
  588. /* minWidth: 120, */
  589. formatter: ({ row }) => {
  590. // 创建一个临时元素来解析HTML
  591. const div = document.createElement('div');
  592. div.innerHTML = row.requirements || '';
  593. // 返回纯文本内容
  594. return div.textContent || div.innerText || '';
  595. }
  596. },
  597. form: {
  598. component: {
  599. type: 'textarea',
  600. rows: 4,
  601. },
  602. },
  603. },
  604. description: {
  605. title: '职位描述',
  606. search: { show: false },
  607. column: {
  608. show: false,
  609. showOverflowTooltip: true,
  610. width: 120,
  611. sortable: 'custom',
  612. /* minWidth: 120, */
  613. formatter: ({ row }) => {
  614. const div = document.createElement('div');
  615. div.innerHTML = row.description || '';
  616. return div.textContent || div.innerText || '';
  617. }
  618. },
  619. form: {
  620. component: {
  621. type: 'textarea',
  622. rows: 4,
  623. },
  624. },
  625. },
  626. province: {
  627. title: '省份',
  628. column: { show: false },
  629. form: { show: false }
  630. },
  631. city: {
  632. title: '城市',
  633. column: { show: false },
  634. form: { show: false }
  635. },
  636. district: {
  637. title: '区域',
  638. column: { show: false },
  639. form: { show: false }
  640. },
  641. },
  642. },
  643. };
  644. };
  645. export const useBatchUpdateTags = (crudExpose: any) => {
  646. const batchUpdateTags = async (questionIds: number[], tagIds: number[]) => {
  647. try {
  648. const res = await api.BatchUpdateTags({
  649. position_ids: questionIds,
  650. tag_ids: tagIds,
  651. tenant_id: 1
  652. });
  653. if (res.code === 2000) {
  654. successMessage('批量更新标签成功');
  655. // 刷新列表
  656. crudExpose.doRefresh();
  657. }
  658. } catch (error) {
  659. console.error('批量更新标签失败', error);
  660. }
  661. };
  662. return { batchUpdateTags };
  663. };
  664. export const useBatchUpdateStatus = (crudExpose: any) => {
  665. const batchUpdateStatus = async (jobIds: number[], status: number) => {
  666. try {
  667. const res = await api.batch_publish({
  668. job_ids: jobIds,
  669. status: status,
  670. tenant_id: 1
  671. });
  672. if (res.code === 2000) {
  673. successMessage('批量修改状态成功');
  674. // 刷新列表
  675. crudExpose.doRefresh();
  676. }
  677. } catch (error) {
  678. console.error('批量修改状态失败', error);
  679. }
  680. };
  681. return { batchUpdateStatus };
  682. };