crud.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  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. import { ref, onMounted } from 'vue';
  7. import { ElMessage } from 'element-plus';
  8. /**
  9. *
  10. * @param crudExpose:index传递过来的示例
  11. * @param context:index传递过来的自定义参数
  12. * @returns
  13. */
  14. export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
  15. const pageRequest = async (query: any) => {
  16. return await api.GetList(query);
  17. };
  18. const editRequest = async ({ form, row }: EditReq) => {
  19. form.id = row.id;
  20. return await api.UpdateObj(form);
  21. };
  22. const delRequest = async ({ row }: DelReq) => {
  23. return await api.DelObj(row.id);
  24. };
  25. const addRequest = async ({ form }: AddReq) => {
  26. return await api.AddObj(form);
  27. };
  28. /* // 修改分类和标签数据获取方式
  29. const categoryOptions = ref<Array<{value: number, label: string}>>([]);
  30. const tagOptions = ref<Array<{value: number, label: string}>>([]);
  31. // 获取分类数据
  32. const fetchCategories = async () => {
  33. try {
  34. const res = await api.GetcategoryList({page:1, limit:1000, tenant_id:1});
  35. if (res.code === 0 && res.data && res.data.items) {
  36. categoryOptions.value = res.data.items;
  37. }
  38. } catch (error) {
  39. console.error('获取分类数据失败', error);
  40. }
  41. };
  42. // 获取标签数据
  43. const fetchTags = async (categoryId?: number) => {
  44. try {
  45. const params = {
  46. page: 1,
  47. limit: 1000,
  48. tenant_id: 1
  49. };
  50. if (categoryId) {
  51. params.category_id = categoryId;
  52. }
  53. const res = await api.GetTagList(params);
  54. if (res.code === 0 && res.data && res.data.items) {
  55. tagOptions.value = res.data.items;
  56. }
  57. } catch (error) {
  58. console.error('获取标签数据失败', error);
  59. }
  60. }; */
  61. // 初始化时获取分类数据
  62. /* onMounted(() => {
  63. fetchCategories();
  64. fetchTags();
  65. }); */
  66. // 添加批量更新标签的方法
  67. const batchUpdateTags = async (questionIds: number[], tagIds: number[]) => {
  68. try {
  69. const res = await api.BatchUpdateTags({
  70. question_ids: questionIds,
  71. tag_ids: tagIds,
  72. tenant_id: 1
  73. });
  74. if (res.code === 0) {
  75. successMessage('批量更新标签成功');
  76. // 刷新列表
  77. crudExpose.doRefresh();
  78. }
  79. } catch (error) {
  80. console.error('批量更新标签失败', error);
  81. }
  82. };
  83. return {
  84. crudOptions: {
  85. request: {
  86. pageRequest,
  87. addRequest,
  88. editRequest,
  89. delRequest,
  90. },
  91. pagination: {
  92. show: true,
  93. },
  94. actionbar: {
  95. buttons: {
  96. add: {
  97. show: auth('role:Create'),
  98. },
  99. // 添加批量绑定标签按钮
  100. /* batchBindTags: {
  101. text: '批量绑定标签',
  102. type: 'primary',
  103. show: true,
  104. order: 2,
  105. click: () => {
  106. // 尝试不同的方式获取选中行
  107. const selection = crudExpose.getSelection?.() || crudExpose.selectedRows || [];
  108. console.log('选中的行:', selection);
  109. if (!selection || selection.length === 0) {
  110. warningMessage('请先选择要操作的题目');
  111. return;
  112. }
  113. // 打开批量绑定标签对话框
  114. context.openBatchTagsDialog(selection);
  115. },
  116. }, */
  117. },
  118. },
  119. rowHandle: {
  120. //固定右侧
  121. fixed: 'right',
  122. width: 320,
  123. buttons: {
  124. view: {
  125. show: true,
  126. },
  127. edit: {
  128. show: auth('role:Update'),
  129. },
  130. remove: {
  131. show: auth('role:Delete'),
  132. },
  133. /* permission: {
  134. type: 'primary',
  135. text: '权限配置',
  136. show: auth('role:Permission'),
  137. click: (clickContext: any): void => {
  138. const { row } = clickContext;
  139. context.RoleDrawer.handleDrawerOpen(row);
  140. context.RoleMenuBtn.setState([]);
  141. context.RoleMenuField.setState([]);
  142. },
  143. }, */
  144. },
  145. },
  146. form: {
  147. col: { span: 24 },
  148. labelWidth: '100px',
  149. wrapper: {
  150. is: 'el-dialog',
  151. width: '600px',
  152. },
  153. },
  154. columns: {
  155. _selection: {
  156. title: '选择',
  157. form: { show: false },
  158. column: {
  159. type: 'selection',
  160. align: 'center',
  161. width: 50,
  162. fixed: 'left',
  163. columnSetDisabled: true,
  164. },
  165. },
  166. id: {
  167. title: 'ID',
  168. column: { show: true ,width:80,},
  169. search: { show: false },
  170. form: { show: false },
  171. },
  172. question: {
  173. title: '题目内容',
  174. search: { show: true },
  175. column: {
  176. showOverflowTooltip: true, // 超出显示提示
  177. width: 300 ,// 固定列宽
  178. sortable: 'custom',
  179. },
  180. form: {
  181. rules: [{ required: true, message: '题目内容必填' }],
  182. component: {
  183. placeholder: '请输入题目内容',
  184. },
  185. },
  186. },
  187. question_type: {
  188. title: '问题类型',
  189. search: { show: true },
  190. type: 'dict-select',
  191. column: {
  192. minWidth: 100,
  193. },
  194. dict: dict({
  195. data: [
  196. { value: 1, label: '专业技能' },
  197. { value: 2, label: '通用能力' },
  198. { value: 3, label: '个人特质' }
  199. ]
  200. }),
  201. form: {
  202. rules: [{ required: true, message: '问题类型必填' }],
  203. component: {
  204. placeholder: '请选择问题类型',
  205. },
  206. helper: '选择问题的类型分类',
  207. },
  208. },
  209. question_form: {
  210. title: '题目形式',
  211. search: { show: true },
  212. type: 'dict-select',
  213. column: {
  214. minWidth: 100,
  215. },
  216. dict: dict({
  217. data: [
  218. { value: 0, label: '开放问题' },
  219. { value: 1, label: '单选题' },
  220. { value: 2, label: '多选题' }
  221. ]
  222. }),
  223. form: {
  224. rules: [{ required: true, message: '题目形式必填' }],
  225. component: {
  226. placeholder: '请选择题目形式',
  227. onChange: ({ form }: { form: any }) => {
  228. // 重置相关字段
  229. if (form.question_form === 1) {
  230. // 单选题
  231. form.options = [{ option_text: '', is_correct: false, sort: 1 }, { option_text: '', is_correct: false, sort: 2 }];
  232. } else if (form.question_form === 2) {
  233. // 多选题
  234. form.options = [{ option_text: '', is_correct: false, sort: 1 }, { option_text: '', is_correct: false, sort: 2 }];
  235. } else {
  236. // 开放问题
  237. form.options = [];
  238. }
  239. }
  240. },
  241. helper: '选择题目的形式:开放问题、单选题或多选题',
  242. },
  243. },
  244. position_types: {
  245. title: '适用职位',
  246. search: { show: true },
  247. type: 'dict-select',
  248. column: {
  249. minWidth: 120,
  250. },
  251. dict: dict({
  252. data: [
  253. { value: '0', label: '技术' },
  254. { value: '1', label: '管理' },
  255. ]
  256. }),
  257. form: {
  258. component: {
  259. /* name: 'el-select', */
  260. props: {
  261. multiple: true,
  262. filterable: true,
  263. placeholder: '请选择适用职位类型',
  264. /* options: [
  265. { value: '0', label: '技术' },
  266. { value: '1', label: '管理' },
  267. ] */
  268. }
  269. },
  270. helper: '选择题目适用的职位类型,可多选'
  271. }
  272. },
  273. recommended_duration: {
  274. title: '建议时长(秒)',
  275. search: { show: false },
  276. column: {
  277. minWidth: 100,
  278. },
  279. form: {
  280. component: {
  281. name: 'el-input-number',
  282. props: {
  283. min: 10,
  284. max: 600,
  285. step: 10
  286. }
  287. },
  288. helper: '建议回答此题目的时长,单位为秒'
  289. }
  290. },
  291. difficulty: {
  292. title: '难度等级',
  293. search: { show: true },
  294. type: 'dict-select',
  295. column: {
  296. minWidth: 80,
  297. },
  298. dict: dict({
  299. data: [
  300. { value: 1, label: '初级' },
  301. { value: 2, label: '中级' },
  302. { value: 3, label: '高级' }
  303. ]
  304. }),
  305. form: {
  306. rules: [{ required: true, message: '难度等级必填' }],
  307. helper: '选择题目的难度级别'
  308. },
  309. },
  310. is_system: {
  311. title: '系统题目',
  312. search: { show: true },
  313. column: {
  314. width: 80,
  315. component: {
  316. name: 'el-switch'
  317. }
  318. },
  319. form: {
  320. component: {
  321. name: 'el-switch',
  322. props: {
  323. activeText: '是',
  324. inactiveText: '否'
  325. }
  326. },
  327. helper: '是否为系统预设题目'
  328. }
  329. },
  330. status: {
  331. title: '状态',
  332. search: { show: true },
  333. type: 'dict-select',
  334. column: {
  335. width: 80,
  336. /* component: {
  337. name: 'fs-dict-label',
  338. props: {
  339. dict: dict({
  340. data: [
  341. { value: 0, label: '停用' },
  342. { value: 1, label: '启用' }
  343. ]
  344. })
  345. }
  346. } */
  347. },
  348. dict: dict({
  349. data: [
  350. { value: 0, label: '停用' },
  351. { value: 1, label: '启用' }
  352. ]
  353. }),
  354. form: {
  355. rules: [{ required: true, message: '状态必填' }],
  356. component: {
  357. placeholder: '请选择状态',
  358. },
  359. helper: '题目的启用状态'
  360. },
  361. },
  362. sort: {
  363. title: '排序',
  364. search: { show: false },
  365. column: {
  366. width: 80,
  367. },
  368. form: {
  369. component: {
  370. name: 'el-input-number',
  371. props: {
  372. min: 1,
  373. max: 999
  374. }
  375. },
  376. helper: '题目的排序值,值越小排序越靠前'
  377. }
  378. },
  379. option_items: {
  380. title: '选项列表',
  381. search: { show: false },
  382. column: { show: false },
  383. form: {
  384. show: compute(({ form }) => {
  385. return form && (form.question_form === 1 || form.question_form === 2);
  386. }),
  387. component: {
  388. name: 'el-card',
  389. children: {
  390. default: ({ form }: { form: any }) => {
  391. // 确保options数组已初始化
  392. if (!form.options) {
  393. form.options = [];
  394. }
  395. return (
  396. <div>
  397. <div class="option-header" style="display: flex; margin-bottom: 10px; font-weight: bold;">
  398. <span style="flex: 1;">选项内容</span>
  399. <span style="width: 80px; text-align: center;">是否正确</span>
  400. </div>
  401. {form.options.map((option: any, index: number) => (
  402. <div class="option-item" key={index} style="display: flex; align-items: center; margin-bottom: 10px;">
  403. <el-input
  404. v-model={option.option_text}
  405. placeholder="请输入选项内容"
  406. style="flex: 1; margin-right: 10px;"
  407. />
  408. <el-tooltip
  409. content="设置为正确答案"
  410. placement="top"
  411. effect="light"
  412. >
  413. <div
  414. onClick={() => {
  415. if (form.question_form === 1) {
  416. // 单选题:只能有一个正确答案
  417. form.options.forEach((item: any, idx: number) => {
  418. item.is_correct = (idx === index);
  419. });
  420. } else {
  421. // 多选题:可以有多个正确答案
  422. option.is_correct = !option.is_correct;
  423. }
  424. }}
  425. style="cursor: pointer; width: 80px; text-align: center;"
  426. >
  427. {form.question_form === 1 ? (
  428. // 单选题使用单选按钮
  429. <el-radio
  430. modelValue={option.is_correct}
  431. label={true}
  432. />
  433. ) : (
  434. // 多选题使用复选框
  435. <el-checkbox
  436. modelValue={option.is_correct}
  437. />
  438. )}
  439. </div>
  440. </el-tooltip>
  441. </div>
  442. ))}
  443. <div style="display: flex; justify-content: space-between; margin-top: 10px;">
  444. <el-button
  445. type="primary"
  446. onClick={() => {
  447. // 确保options数组已初始化
  448. if (!form.options) {
  449. form.options = [];
  450. }
  451. // 添加新选项时自动设置排序值
  452. const sort = form.options.length > 0 ?
  453. Math.max(...form.options.map((o: any) => o.sort || 0)) + 1 : 1;
  454. form.options.push({ option_text: '', is_correct: false, sort });
  455. }}
  456. >
  457. 添加选项
  458. </el-button>
  459. {form.options.length > 2 && (
  460. <el-button
  461. type="danger"
  462. onClick={() => {
  463. form.options.pop();
  464. }}
  465. >
  466. 删除最后一项
  467. </el-button>
  468. )}
  469. </div>
  470. </div>
  471. )
  472. }
  473. }
  474. },
  475. helper: compute(({ form }) => {
  476. if (form.question_form === 1) {
  477. return '添加单选题的选项,并标记正确答案(只能有一个正确答案)';
  478. } else if (form.question_form === 2) {
  479. return '添加多选题的选项,并标记正确答案(可以有多个正确答案)';
  480. }
  481. return '';
  482. })
  483. }
  484. },
  485. scoring_reference : {
  486. title: '备注',
  487. search: { show: false },
  488. column: { show: false },
  489. form: {
  490. component: {
  491. name: 'el-input',
  492. type: 'textarea',
  493. rows: 4,
  494. placeholder: '请输入答案解析'
  495. },
  496. },
  497. },
  498. category_id: {
  499. title: '题目分类',
  500. search: { show: true },
  501. type: 'dict-select',
  502. column: {
  503. minWidth: 120,
  504. },
  505. dict: dict({
  506. // 使用API方式获取数据
  507. getData: async () => {
  508. const res = await api.GetcategoryList({page:1, limit:1000, tenant_id:1});
  509. return res.data.items;
  510. },
  511. label: 'name',
  512. value: 'id'
  513. }),
  514. form: {
  515. rules: [{ required: true, message: '题目分类必填' }],
  516. component: {
  517. placeholder: '请选择题目分类',
  518. /* onChange: ({ value }: { value: number }) => {
  519. // 当分类变化时,重新获取对应的标签
  520. fetchTags(value);
  521. } */
  522. },
  523. helper: '选择题目所属的分类'
  524. },
  525. },
  526. tag_ids: {
  527. title: '题目标签',
  528. search: { show: true },
  529. type: 'dict-select',
  530. column: {
  531. minWidth: 150,
  532. component: {
  533. name: 'fs-dict-label',
  534. props: {
  535. multiple: true
  536. }
  537. }
  538. },
  539. dict: dict({
  540. // 使用API方式获取数据
  541. getData: async () => {
  542. const res = await api.GetTagList({page:1, limit:1000, tenant_id:1});
  543. return res.data.items;
  544. },
  545. label: 'name',
  546. value: 'id'
  547. }),
  548. form: {
  549. component: {
  550. props: {
  551. multiple: true,
  552. filterable: true,
  553. placeholder: '请选择题目标签'
  554. }
  555. },
  556. helper: '选择题目关联的标签,可多选'
  557. }
  558. },
  559. },
  560. // 确保表格配置正确
  561. table: {
  562. selection: true,
  563. },
  564. },
  565. };
  566. };
  567. // 导出批量更新标签方法,供组件使用
  568. export const useBatchUpdateTags = (crudExpose: any) => {
  569. const batchUpdateTags = async (questionIds: number[], tagIds: number[]) => {
  570. try {
  571. const res = await api.BatchUpdateTags({
  572. question_ids: questionIds,
  573. tag_ids: tagIds,
  574. tenant_id: 1
  575. });
  576. if (res.code === 0) {
  577. successMessage('批量更新标签成功');
  578. // 刷新列表
  579. crudExpose.doRefresh();
  580. }
  581. } catch (error) {
  582. console.error('批量更新标签失败', error);
  583. }
  584. };
  585. return { batchUpdateTags };
  586. };