feat(safety): 安全生产五期 - 安全教育培训前端
配套后端培训与证书接口。
新增:
- api/safety/training、api/safety/certificate
- views/safety/training:培训列表 + 培训表单 + 参训人员管理弹窗
* 列表用「参训/实到」形式展示,实到少于应到用橙色警示,到齐用绿色
* 参训弹窗支持多选批量添加人员、逐行设置签到状态与考试成绩、
顶部实时显示总人数与已签到人数
- views/safety/certificate:资格证书列表 + 表单
* 有效期三种状态展示:长期有效(灰)、30天内到期(橙+剩余天数)、
已过期(红+标签),便于提前安排复审换证
- dict-enum.ts 补 5 个字典常量
注:本期只提交代码,未部署。
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '#/packages/effects/request/src'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesSafetyCertificateApi { |
| | | /** èµæ ¼è¯ä¹¦ */ |
| | | export interface Certificate { |
| | | id?: number; |
| | | userId?: number; |
| | | userName?: string; |
| | | deptId?: number; |
| | | deptName?: string; |
| | | certType?: number; |
| | | certNo?: string; |
| | | issueOrg?: string; |
| | | issueDate?: string; |
| | | expireDate?: string; |
| | | daysToExpire?: number; |
| | | expired?: boolean; |
| | | expiringSoon?: boolean; |
| | | status?: number; |
| | | attachmentUrl?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** å页忰 */ |
| | | export interface CertificatePageParam extends PageParam { |
| | | userId?: number; |
| | | certType?: number; |
| | | certNo?: string; |
| | | status?: number; |
| | | expiringSoon?: boolean; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢èµæ ¼è¯ä¹¦å页 */ |
| | | export function getCertificatePage( |
| | | params: MesSafetyCertificateApi.CertificatePageParam, |
| | | ) { |
| | | return requestClient.get<PageResult<MesSafetyCertificateApi.Certificate>>( |
| | | '/mes/safety/certificate/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢èµæ ¼è¯ä¹¦è¯¦æ
*/ |
| | | export function getCertificate(id: number) { |
| | | return requestClient.get<MesSafetyCertificateApi.Certificate>( |
| | | `/mes/safety/certificate/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** åå»ºèµæ ¼è¯ä¹¦ */ |
| | | export function createCertificate(data: MesSafetyCertificateApi.Certificate) { |
| | | return requestClient.post<number>('/mes/safety/certificate/create', data); |
| | | } |
| | | |
| | | /** æ´æ°èµæ ¼è¯ä¹¦ */ |
| | | export function updateCertificate(data: MesSafetyCertificateApi.Certificate) { |
| | | return requestClient.put<boolean>('/mes/safety/certificate/update', data); |
| | | } |
| | | |
| | | /** å é¤èµæ ¼è¯ä¹¦ */ |
| | | export function deleteCertificate(id: number) { |
| | | return requestClient.delete<boolean>( |
| | | `/mes/safety/certificate/delete?id=${id}`, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '#/packages/effects/request/src'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesSafetyTrainingApi { |
| | | /** å¹è®æ¹æ¬¡ */ |
| | | export interface Training { |
| | | id?: number; |
| | | code?: string; |
| | | title?: string; |
| | | trainingType?: number; |
| | | trainingLevel?: number; |
| | | trainer?: string; |
| | | trainerOrg?: string; |
| | | location?: string; |
| | | startTime?: string; |
| | | endTime?: string; |
| | | durationHours?: number; |
| | | content?: string; |
| | | material?: string; |
| | | status?: number; |
| | | attendeeCount?: number; |
| | | signedCount?: number; |
| | | avgScore?: number; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** åè®äººå */ |
| | | export interface Attendee { |
| | | id?: number; |
| | | trainingId?: number; |
| | | userId?: number; |
| | | userName?: string; |
| | | deptId?: number; |
| | | deptName?: string; |
| | | signStatus?: number; |
| | | signTime?: string; |
| | | score?: number; |
| | | passed?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** å页忰 */ |
| | | export interface TrainingPageParam extends PageParam { |
| | | code?: string; |
| | | title?: string; |
| | | trainingType?: number; |
| | | trainingLevel?: number; |
| | | status?: number; |
| | | startTimeStart?: string; |
| | | startTimeEnd?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢å¹è®å页 */ |
| | | export function getTrainingPage(params: MesSafetyTrainingApi.TrainingPageParam) { |
| | | return requestClient.get<PageResult<MesSafetyTrainingApi.Training>>( |
| | | '/mes/safety/training/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢å¹è®è¯¦æ
*/ |
| | | export function getTraining(id: number) { |
| | | return requestClient.get<MesSafetyTrainingApi.Training>( |
| | | `/mes/safety/training/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** å建å¹è® */ |
| | | export function createTraining(data: MesSafetyTrainingApi.Training) { |
| | | return requestClient.post<number>('/mes/safety/training/create', data); |
| | | } |
| | | |
| | | /** æ´æ°å¹è® */ |
| | | export function updateTraining(data: MesSafetyTrainingApi.Training) { |
| | | return requestClient.put<boolean>('/mes/safety/training/update', data); |
| | | } |
| | | |
| | | /** å é¤å¹è® */ |
| | | export function deleteTraining(id: number) { |
| | | return requestClient.delete<boolean>(`/mes/safety/training/delete?id=${id}`); |
| | | } |
| | | |
| | | /** æ¥è¯¢åè®äººåå表 */ |
| | | export function getAttendeeList(trainingId: number) { |
| | | return requestClient.get<MesSafetyTrainingApi.Attendee[]>( |
| | | `/mes/safety/training/attendee-list?trainingId=${trainingId}`, |
| | | ); |
| | | } |
| | | |
| | | /** å
¨éä¿ååè®äººå */ |
| | | export function saveAttendees( |
| | | trainingId: number, |
| | | attendees: MesSafetyTrainingApi.Attendee[], |
| | | ) { |
| | | return requestClient.post<boolean>( |
| | | `/mes/safety/training/save-attendees?trainingId=${trainingId}`, |
| | | attendees, |
| | | ); |
| | | } |
| | | |
| | | /** æ´æ°åè®äººåç¾å°/æç»© */ |
| | | export function updateAttendee(data: MesSafetyTrainingApi.Attendee) { |
| | | return requestClient.put<boolean>( |
| | | '/mes/safety/training/update-attendee', |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** å é¤åè®äººå */ |
| | | export function deleteAttendee(id: number) { |
| | | return requestClient.delete<boolean>( |
| | | `/mes/safety/training/delete-attendee?id=${id}`, |
| | | ); |
| | | } |
| | |
| | | MES_SAFETY_TICKET_TYPE: 'mes_safety_ticket_type', // å®å
¨ç产-ä½ä¸ç±»å |
| | | MES_SAFETY_TICKET_LEVEL: 'mes_safety_ticket_level', // å®å
¨ç产-ä½ä¸çº§å« |
| | | MES_SAFETY_TICKET_STATUS: 'mes_safety_ticket_status', // å®å
¨ç产-ä½ä¸ç¥¨ç¶æ |
| | | MES_SAFETY_TRAINING_TYPE: 'mes_safety_training_type', // å®å
¨ç产-å¹è®ç±»å |
| | | MES_SAFETY_TRAINING_LEVEL: 'mes_safety_training_level', // å®å
¨ç产-å¹è®å±çº§ |
| | | MES_SAFETY_TRAINING_STATUS: 'mes_safety_training_status', // å®å
¨ç产-å¹è®ç¶æ |
| | | MES_SAFETY_ATTENDEE_STATUS: 'mes_safety_attendee_status', // å®å
¨ç产-ç¾å°ç¶æ |
| | | MES_SAFETY_CERT_TYPE: 'mes_safety_cert_type', // å®å
¨ç产-è¯ä¹¦ç±»å |
| | | } as const; |
| | | |
| | | /** ========== WMS - ä»å¨ç®¡ç模å ========== */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesSafetyCertificateApi } from '#/api/safety/certificate'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictOptions } from '#/packages/effects/hooks/src'; |
| | | |
| | | import { getSimpleUserList } from '../risk/shared'; |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'certNo', |
| | | label: 'è¯ä¹¦ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥è¯ä¹¦ç¼å·' }, |
| | | }, |
| | | { |
| | | fieldName: 'certType', |
| | | label: 'è¯ä¹¦ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_SAFETY_CERT_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©è¯ä¹¦ç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'expiringSoon', |
| | | label: 'å³å°å°æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: [ |
| | | { label: 'ä»
ç 30 天å
å°æï¼å«å·²è¿æï¼', value: true }, |
| | | { label: 'å
¨é¨', value: false }, |
| | | ], |
| | | placeholder: 'å°æçé', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'userId', |
| | | label: 'æè¯äºº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | valueField: 'id', |
| | | placeholder: 'è¯·éæ©æè¯äºº', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'certType', |
| | | label: 'è¯ä¹¦ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.MES_SAFETY_CERT_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©è¯ä¹¦ç±»å', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'certNo', |
| | | label: 'è¯ä¹¦ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥è¯ä¹¦ç¼å·' }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'issueOrg', |
| | | label: 'åè¯æºå
³', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'å¦ æ°çåºæ¥ç®¡çå
' }, |
| | | }, |
| | | { |
| | | fieldName: 'issueDate', |
| | | label: 'åè¯æ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | placeholder: 'è¯·éæ©åè¯æ¥æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'expireDate', |
| | | label: 'æææè³', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | format: 'YYYY-MM-DD', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | placeholder: 'çç©ºè¡¨ç¤ºé¿æææ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'attachmentUrl', |
| | | label: 'è¯ä¹¦æ«æä»¶', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'æ«æä»¶å°åï¼éå¡«ï¼' }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'RadioGroup', |
| | | componentProps: { |
| | | buttonStyle: 'solid', |
| | | optionType: 'button', |
| | | options: [ |
| | | { label: 'ææ', value: 1 }, |
| | | { label: 'å·²ä½åº', value: 0 }, |
| | | ], |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { placeholder: '请è¾å
¥å¤æ³¨', rows: 2 }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<MesSafetyCertificateApi.Certificate>['columns'] { |
| | | return [ |
| | | { field: 'userName', title: 'æè¯äºº', width: 120 }, |
| | | { field: 'deptName', title: 'é¨é¨', width: 130 }, |
| | | { |
| | | field: 'certType', |
| | | title: 'è¯ä¹¦ç±»å', |
| | | width: 150, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_SAFETY_CERT_TYPE }, |
| | | }, |
| | | }, |
| | | { field: 'certNo', title: 'è¯ä¹¦ç¼å·', minWidth: 160 }, |
| | | { field: 'issueOrg', title: 'åè¯æºå
³', minWidth: 150 }, |
| | | { field: 'issueDate', title: 'åè¯æ¥æ', width: 120 }, |
| | | { |
| | | field: 'expireDate', |
| | | title: 'æææè³', |
| | | width: 160, |
| | | slots: { default: 'expire' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 90, |
| | | slots: { default: 'status' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 160, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesSafetyCertificateApi } from '#/api/safety/certificate'; |
| | | |
| | | import { Page } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteCertificate, |
| | | getCertificatePage, |
| | | } from '#/api/safety/certificate'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import FormModalComp from './modules/form.vue'; |
| | | |
| | | defineOptions({ name: 'SafetyCertificate' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: FormModalComp, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => |
| | | await getCertificatePage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }), |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<MesSafetyCertificateApi.Certificate>, |
| | | }); |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: MesSafetyCertificateApi.Certificate) { |
| | | formModalApi.setData({ formType: 'update', id: row.id }).open(); |
| | | } |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | async function handleDelete(row: MesSafetyCertificateApi.Certificate) { |
| | | await deleteCertificate(row.id!); |
| | | message.success('å 餿å'); |
| | | gridApi.query(); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <Grid> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å¢è¯ä¹¦', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:safety-certificate:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | |
| | | <template #expire="{ row }"> |
| | | <span v-if="!row.expireDate" class="text-gray-500">é¿æææ</span> |
| | | <span v-else-if="row.expired" class="font-semibold text-red-600"> |
| | | {{ row.expireDate }} |
| | | <Tag color="error" class="ml-1">å·²è¿æ</Tag> |
| | | </span> |
| | | <span v-else-if="row.expiringSoon" class="font-semibold text-orange-600"> |
| | | {{ row.expireDate }} |
| | | <Tag color="warning" class="ml-1">å© {{ row.daysToExpire }} 天</Tag> |
| | | </span> |
| | | <span v-else> |
| | | {{ row.expireDate }} |
| | | <span class="ml-1 text-gray-400">({{ row.daysToExpire }} 天)</span> |
| | | </span> |
| | | </template> |
| | | |
| | | <template #status="{ row }"> |
| | | <Tag :color="row.status === 1 ? 'success' : 'default'"> |
| | | {{ row.status === 1 ? 'ææ' : 'å·²ä½åº' }} |
| | | </Tag> |
| | | </template> |
| | | |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ç¼è¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['mes:safety-certificate:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mes:safety-certificate:delete'], |
| | | popConfirm: { |
| | | title: 'ç¡®å®å é¤è¯¥è¯ä¹¦åï¼', |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesSafetyCertificateApi } from '#/api/safety/certificate'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createCertificate, |
| | | getCertificate, |
| | | updateCertificate, |
| | | } from '#/api/safety/certificate'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const formType = ref<'create' | 'update'>('create'); |
| | | const isUpdate = computed(() => formType.value === 'update'); |
| | | const getTitle = computed(() => |
| | | isUpdate.value ? 'ä¿®æ¹èµæ ¼è¯ä¹¦' : 'æ°å¢èµæ ¼è¯ä¹¦', |
| | | ); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 100, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'horizontal', |
| | | schema: useFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = |
| | | (await formApi.getValues()) as MesSafetyCertificateApi.Certificate; |
| | | try { |
| | | await (isUpdate.value |
| | | ? updateCertificate(data) |
| | | : createCertificate(data)); |
| | | emit('success'); |
| | | message.success('æä½æå'); |
| | | await modalApi.close(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | formType: 'create' | 'update'; |
| | | id?: number; |
| | | }>(); |
| | | formType.value = data.formType; |
| | | await formApi.resetForm(); |
| | | if (data?.id) { |
| | | modalApi.lock(); |
| | | try { |
| | | const cert = await getCertificate(data.id); |
| | | await formApi.setValues(cert); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } else { |
| | | formApi.setFieldValue('status', 1); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-1/2"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesSafetyTrainingApi } from '#/api/safety/training'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictOptions } from '#/packages/effects/hooks/src'; |
| | | |
| | | export const DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; |
| | | |
| | | /** å¹è®ç¶æ */ |
| | | export const TRAINING_STATUS = { |
| | | PLANNED: 0, |
| | | ONGOING: 10, |
| | | FINISHED: 20, |
| | | CANCELED: 30, |
| | | }; |
| | | |
| | | export function trainingStatusColor(status?: number): string { |
| | | switch (status) { |
| | | case 0: return 'processing'; |
| | | case 10: return 'warning'; |
| | | case 20: return 'success'; |
| | | case 30: return 'default'; |
| | | default: return 'default'; |
| | | } |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'code', |
| | | label: 'å¹è®ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥å¹è®ç¼å·' }, |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'å¹è®ä¸»é¢', |
| | | component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥å¹è®ä¸»é¢' }, |
| | | }, |
| | | { |
| | | fieldName: 'trainingType', |
| | | label: 'å¹è®ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_SAFETY_TRAINING_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©å¹è®ç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_SAFETY_TRAINING_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'code', |
| | | label: 'å¹è®ç¼å·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'å¦ PX-2026-001' }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'title', |
| | | label: 'å¹è®ä¸»é¢', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'å¦ ç¹ç§ä½ä¸å®å
¨æä½å¹è®' }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'trainingType', |
| | | label: 'å¹è®ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_SAFETY_TRAINING_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©å¹è®ç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'trainingLevel', |
| | | label: 'å¹è®å±çº§', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_SAFETY_TRAINING_LEVEL, 'number'), |
| | | placeholder: 'ä¸çº§å®å
¨æè²æ¶éæ©å±çº§', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'trainer', |
| | | label: '讲å¸', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥è®²å¸å§å' }, |
| | | }, |
| | | { |
| | | fieldName: 'trainerOrg', |
| | | label: '讲å¸åä½', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'å¤é¨è®²å¸æ¶å¡«å' }, |
| | | }, |
| | | { |
| | | fieldName: 'location', |
| | | label: 'å¹è®å°ç¹', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'å¦ ä¸æ¥¼å¹è®å®¤' }, |
| | | }, |
| | | { |
| | | fieldName: 'startTime', |
| | | label: 'å¼å§æ¶é´', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | showTime: true, |
| | | format: DATETIME_FORMAT, |
| | | valueFormat: DATETIME_FORMAT, |
| | | placeholder: 'è¯·éæ©å¹è®å¼å§æ¶é´', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'endTime', |
| | | label: 'ç»ææ¶é´', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | showTime: true, |
| | | format: DATETIME_FORMAT, |
| | | valueFormat: DATETIME_FORMAT, |
| | | placeholder: 'è¯·éæ©å¹è®ç»ææ¶é´', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'durationHours', |
| | | label: '妿¶', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 1, |
| | | placeholder: 'å¦ 4', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'RadioGroup', |
| | | componentProps: { |
| | | buttonStyle: 'solid', |
| | | optionType: 'button', |
| | | options: getDictOptions(DICT_TYPE.MES_SAFETY_TRAINING_STATUS, 'number'), |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'content', |
| | | label: 'å¹è®å
容', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { placeholder: '请è¾å
¥å¹è®å
容', rows: 3 }, |
| | | }, |
| | | { |
| | | fieldName: 'material', |
| | | label: 'å¹è®èµæ', |
| | | component: 'Input', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { placeholder: 'ææ/èµæåç§°æé¾æ¥' }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { placeholder: '请è¾å
¥å¤æ³¨', rows: 2 }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<MesSafetyTrainingApi.Training>['columns'] { |
| | | return [ |
| | | { field: 'code', title: 'å¹è®ç¼å·', width: 140 }, |
| | | { field: 'title', title: 'å¹è®ä¸»é¢', minWidth: 200 }, |
| | | { |
| | | field: 'trainingType', |
| | | title: 'ç±»å', |
| | | width: 130, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_SAFETY_TRAINING_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'trainingLevel', |
| | | title: 'å±çº§', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_SAFETY_TRAINING_LEVEL }, |
| | | }, |
| | | }, |
| | | { field: 'trainer', title: '讲å¸', width: 100 }, |
| | | { field: 'startTime', title: 'å¼å§æ¶é´', width: 170 }, |
| | | { field: 'durationHours', title: '妿¶', width: 80, align: 'right' }, |
| | | { |
| | | field: 'attendeeCount', |
| | | title: 'åè®/å®å°', |
| | | width: 120, |
| | | align: 'center', |
| | | slots: { default: 'attendee' }, |
| | | }, |
| | | { |
| | | field: 'avgScore', |
| | | title: 'å¹³åå', |
| | | width: 90, |
| | | align: 'right', |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_SAFETY_TRAINING_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 220, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesSafetyTrainingApi } from '#/api/safety/training'; |
| | | |
| | | import { Page } from '#/packages/effects/common-ui/src'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { deleteTraining, getTrainingPage } from '#/api/safety/training'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import AttendeeModal from './modules/attendee.vue'; |
| | | import FormModalComp from './modules/form.vue'; |
| | | |
| | | defineOptions({ name: 'SafetyTraining' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: FormModalComp, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [AttendeeModalComp, attendeeModalApi] = useVbenModal({ |
| | | connectedComponent: AttendeeModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => |
| | | await getTrainingPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }), |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<MesSafetyTrainingApi.Training>, |
| | | }); |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: MesSafetyTrainingApi.Training) { |
| | | formModalApi.setData({ formType: 'update', id: row.id }).open(); |
| | | } |
| | | |
| | | function handleAttendee(row: MesSafetyTrainingApi.Training) { |
| | | attendeeModalApi.setData({ id: row.id, title: row.title }).open(); |
| | | } |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | async function handleDelete(row: MesSafetyTrainingApi.Training) { |
| | | await deleteTraining(row.id!); |
| | | message.success('å 餿å'); |
| | | gridApi.query(); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <AttendeeModalComp @success="handleRefresh" /> |
| | | <Grid> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æ°å»ºå¹è®', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:safety-training:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | |
| | | <template #attendee="{ row }"> |
| | | <span> |
| | | {{ row.attendeeCount ?? 0 }} / |
| | | <span |
| | | :class=" |
| | | (row.signedCount ?? 0) < (row.attendeeCount ?? 0) |
| | | ? 'text-orange-600' |
| | | : 'text-green-600' |
| | | " |
| | | > |
| | | {{ row.signedCount ?? 0 }} |
| | | </span> |
| | | </span> |
| | | </template> |
| | | |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'åè®äººå', |
| | | type: 'link', |
| | | auth: ['mes:safety-training:attendee'], |
| | | onClick: handleAttendee.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'ç¼è¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['mes:safety-training:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mes:safety-training:delete'], |
| | | popConfirm: { |
| | | title: 'å é¤å¹è®ä¼åæ¶å é¤å
¶åè®è®°å½ï¼ç¡®å®å é¤åï¼', |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesSafetyTrainingApi } from '#/api/safety/training'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { |
| | | Button, |
| | | InputNumber, |
| | | message, |
| | | Select, |
| | | Table, |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | getAttendeeList, |
| | | saveAttendees, |
| | | } from '#/api/safety/training'; |
| | | import { getSimpleUserList } from '../../risk/shared'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const trainingId = ref<number>(); |
| | | const trainingTitle = ref<string>(); |
| | | const attendees = ref<MesSafetyTrainingApi.Attendee[]>([]); |
| | | const loading = ref(false); |
| | | const saving = ref(false); |
| | | const userOptions = ref<Array<{ id: number; nickname: string }>>([]); |
| | | const pickingUsers = ref<number[]>([]); |
| | | const showPicker = ref(false); |
| | | |
| | | /** ç¾å°ç¶æé项 */ |
| | | const SIGN_OPTIONS = [ |
| | | { label: 'æªç¾å°', value: 0 }, |
| | | { label: 'å·²ç¾å°', value: 1 }, |
| | | { label: 'è¿å°', value: 2 }, |
| | | { label: '缺å¸', value: 3 }, |
| | | ]; |
| | | |
| | | /** éè¿ç¶æé项 */ |
| | | const PASS_OPTIONS = [ |
| | | { label: 'æªè', value: 0 }, |
| | | { label: 'éè¿', value: 1 }, |
| | | { label: 'ä¸éè¿', value: 2 }, |
| | | ]; |
| | | |
| | | const columns = [ |
| | | { title: 'å§å', dataIndex: 'userName', width: 120 }, |
| | | { title: 'ç¾å°ç¶æ', dataIndex: 'signStatus', width: 140, key: 'sign' }, |
| | | { title: 'èè¯æç»©', dataIndex: 'score', width: 120, key: 'score' }, |
| | | { title: 'æ¯å¦éè¿', dataIndex: 'passed', width: 130, key: 'passed' }, |
| | | { title: 'æä½', dataIndex: 'action', width: 80, key: 'action' }, |
| | | ]; |
| | | |
| | | const totalCount = computed(() => attendees.value.length); |
| | | const signedCount = computed( |
| | | () => |
| | | attendees.value.filter((a) => a.signStatus === 1 || a.signStatus === 2) |
| | | .length, |
| | | ); |
| | | |
| | | /** å·²éä¸ä½ä¸å¨å表éçç¨æ·ï¼ç¨äºéæ©å¨ç¦ç¨å·²æ·»å é¡¹ï¼ */ |
| | | const existedUserIds = computed(() => |
| | | attendees.value.map((a) => a.userId).filter(Boolean), |
| | | ); |
| | | |
| | | async function loadUsers() { |
| | | if (userOptions.value.length > 0) return; |
| | | const list = await getSimpleUserList(); |
| | | userOptions.value = list.map((u) => ({ id: u.id, nickname: u.nickname })); |
| | | } |
| | | |
| | | function handleOpenPicker() { |
| | | showPicker.value = true; |
| | | pickingUsers.value = []; |
| | | loadUsers(); |
| | | } |
| | | |
| | | function handleAddUsers() { |
| | | if (pickingUsers.value.length === 0) { |
| | | message.warning('è¯·éæ©è¦æ·»å ç人å'); |
| | | return; |
| | | } |
| | | for (const uid of pickingUsers.value) { |
| | | if (existedUserIds.value.includes(uid)) continue; |
| | | const user = userOptions.value.find((u) => u.id === uid); |
| | | attendees.value.push({ |
| | | userId: uid, |
| | | userName: user?.nickname, |
| | | signStatus: 0, |
| | | passed: 0, |
| | | }); |
| | | } |
| | | showPicker.value = false; |
| | | pickingUsers.value = []; |
| | | } |
| | | |
| | | function handleRemove(index: number) { |
| | | attendees.value.splice(index, 1); |
| | | } |
| | | |
| | | async function handleSave() { |
| | | saving.value = true; |
| | | try { |
| | | await saveAttendees(trainingId.value!, attendees.value); |
| | | emit('success'); |
| | | message.success('åè®äººåå·²ä¿å'); |
| | | await modalApi.close(); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onConfirm: handleSave, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | attendees.value = []; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ id: number; title?: string }>(); |
| | | trainingId.value = data.id; |
| | | trainingTitle.value = data.title; |
| | | loading.value = true; |
| | | try { |
| | | attendees.value = (await getAttendeeList(data.id)) ?? []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="`åè®äººå - ${trainingTitle ?? ''}`" class="w-3/4"> |
| | | <div class="mx-4 flex flex-col gap-3"> |
| | | <div class="flex items-center justify-between"> |
| | | <div class="text-gray-600"> |
| | | å
± <b>{{ totalCount }}</b> 人ï¼å·²ç¾å° |
| | | <b>{{ signedCount }}</b> 人 |
| | | </div> |
| | | <Button type="primary" @click="handleOpenPicker">æ·»å 人å</Button> |
| | | </div> |
| | | |
| | | <div v-if="showPicker" class="flex items-center gap-2 rounded bg-gray-50 p-3"> |
| | | <Select |
| | | v-model:value="pickingUsers" |
| | | mode="multiple" |
| | | class="flex-1" |
| | | placeholder="éæ©è¦åè®ç人åï¼å¯å¤éï¼" |
| | | :options=" |
| | | userOptions |
| | | .filter((u) => !existedUserIds.includes(u.id)) |
| | | .map((u) => ({ label: u.nickname, value: u.id })) |
| | | " |
| | | option-filter-prop="label" |
| | | /> |
| | | <Button type="primary" @click="handleAddUsers">ç¡®å®æ·»å </Button> |
| | | <Button @click="showPicker = false">åæ¶</Button> |
| | | </div> |
| | | |
| | | <Table |
| | | :data-source="attendees" |
| | | :columns="columns" |
| | | :loading="loading" |
| | | :pagination="false" |
| | | size="small" |
| | | row-key="userId" |
| | | :scroll="{ y: 380 }" |
| | | > |
| | | <template #bodyCell="{ column, record, index }"> |
| | | <template v-if="column.key === 'sign'"> |
| | | <Select |
| | | v-model:value="record.signStatus" |
| | | :options="SIGN_OPTIONS" |
| | | class="w-full" |
| | | /> |
| | | </template> |
| | | <template v-else-if="column.key === 'score'"> |
| | | <InputNumber |
| | | v-model:value="record.score" |
| | | :min="0" |
| | | :max="100" |
| | | :precision="1" |
| | | class="w-full" |
| | | placeholder="æç»©" |
| | | /> |
| | | </template> |
| | | <template v-else-if="column.key === 'passed'"> |
| | | <Select |
| | | v-model:value="record.passed" |
| | | :options="PASS_OPTIONS" |
| | | class="w-full" |
| | | /> |
| | | </template> |
| | | <template v-else-if="column.key === 'action'"> |
| | | <Tag |
| | | color="error" |
| | | class="cursor-pointer" |
| | | @click="handleRemove(index)" |
| | | > |
| | | ç§»é¤ |
| | | </Tag> |
| | | </template> |
| | | </template> |
| | | </Table> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesSafetyTrainingApi } from '#/api/safety/training'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createTraining, |
| | | getTraining, |
| | | updateTraining, |
| | | } from '#/api/safety/training'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const formType = ref<'create' | 'update'>('create'); |
| | | const isUpdate = computed(() => formType.value === 'update'); |
| | | const getTitle = computed(() => |
| | | isUpdate.value ? 'ä¿®æ¹å¹è®æ¹æ¬¡' : 'æ°å»ºå¹è®æ¹æ¬¡', |
| | | ); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 100, |
| | | }, |
| | | wrapperClass: 'grid-cols-2', |
| | | layout: 'horizontal', |
| | | schema: useFormSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as MesSafetyTrainingApi.Training; |
| | | try { |
| | | await (isUpdate.value ? updateTraining(data) : createTraining(data)); |
| | | emit('success'); |
| | | message.success('æä½æå'); |
| | | await modalApi.close(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | formType: 'create' | 'update'; |
| | | id?: number; |
| | | }>(); |
| | | formType.value = data.formType; |
| | | await formApi.resetForm(); |
| | | if (data?.id) { |
| | | modalApi.lock(); |
| | | try { |
| | | const training = await getTraining(data.id); |
| | | await formApi.setValues(training); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } else { |
| | | formApi.setFieldValue('status', 0); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-2/3"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |