feat(safety): 安全生产四期 - 特种作业票证管理前端
配套后端作业票接口。
新增:
- api/safety/ticket
- views/safety/ticket:作业票列表 + 申请表单 + 审批弹窗 + 完工/归档弹窗
* 列表按状态动态显示操作:待审批→审批/编辑/删除,已批准→完工,
已完工→归档,已驳回→编辑/删除,已归档只读
* 计划时间超期用红色 + 「超期」标签提示
* 审批弹窗顶部回显作业内容与安全措施,便于审批判断
* 完工弹窗支持勾选「完工后直接归档」,减少一次操作
- dict-enum.ts 补 3 个作业票字典常量
注:本期只提交代码,未部署。
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '#/packages/effects/request/src'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesSafetyTicketApi { |
| | | /** ä½ä¸ç¥¨ */ |
| | | export interface WorkTicket { |
| | | id?: number; |
| | | code?: string; |
| | | ticketType?: number; |
| | | ticketLevel?: number; |
| | | unitId?: number; |
| | | unitName?: string; |
| | | workshopId?: number; |
| | | workshopName?: string; |
| | | workLocation?: string; |
| | | workContent?: string; |
| | | planStartTime?: string; |
| | | planEndTime?: string; |
| | | actualStartTime?: string; |
| | | actualEndTime?: string; |
| | | applicantUserId?: number; |
| | | applicantUserName?: string; |
| | | applicantDeptId?: number; |
| | | contractor?: string; |
| | | workerNames?: string; |
| | | guardianUserId?: number; |
| | | guardianUserName?: string; |
| | | guardianConfirm?: boolean; |
| | | safetyMeasures?: string; |
| | | safetyCheckResult?: string; |
| | | approverUserId?: number; |
| | | approverUserName?: string; |
| | | approveTime?: string; |
| | | approveComment?: string; |
| | | rejectReason?: string; |
| | | status?: number; |
| | | overdue?: boolean; |
| | | closeTime?: string; |
| | | closeUserId?: number; |
| | | closeRemark?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | } |
| | | |
| | | /** å页忰 */ |
| | | export interface WorkTicketPageParam extends PageParam { |
| | | code?: string; |
| | | ticketType?: number; |
| | | ticketLevel?: number; |
| | | status?: number; |
| | | workshopId?: number; |
| | | applicantUserId?: number; |
| | | workLocation?: string; |
| | | planStartTimeStart?: string; |
| | | planStartTimeEnd?: string; |
| | | } |
| | | |
| | | /** 审æ¹åæ° */ |
| | | export interface TicketApproveParam { |
| | | approveComment?: string; |
| | | id: number; |
| | | pass: boolean; |
| | | rejectReason?: string; |
| | | } |
| | | |
| | | /** å®å·¥/彿¡£åæ° */ |
| | | export interface TicketCloseParam { |
| | | actualEndTime?: string; |
| | | actualStartTime?: string; |
| | | archive?: boolean; |
| | | closeRemark?: string; |
| | | id: number; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢ä½ä¸ç¥¨å页 */ |
| | | export function getTicketPage(params: MesSafetyTicketApi.WorkTicketPageParam) { |
| | | return requestClient.get<PageResult<MesSafetyTicketApi.WorkTicket>>( |
| | | '/mes/safety/work-ticket/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢ä½ä¸ç¥¨è¯¦æ
*/ |
| | | export function getTicket(id: number) { |
| | | return requestClient.get<MesSafetyTicketApi.WorkTicket>( |
| | | `/mes/safety/work-ticket/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** ç³è¯·ä½ä¸ç¥¨ */ |
| | | export function createTicket(data: MesSafetyTicketApi.WorkTicket) { |
| | | return requestClient.post<number>('/mes/safety/work-ticket/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹ä½ä¸ç¥¨ */ |
| | | export function updateTicket(data: MesSafetyTicketApi.WorkTicket) { |
| | | return requestClient.put<boolean>('/mes/safety/work-ticket/update', data); |
| | | } |
| | | |
| | | /** å é¤ä½ä¸ç¥¨ */ |
| | | export function deleteTicket(id: number) { |
| | | return requestClient.delete<boolean>(`/mes/safety/work-ticket/delete?id=${id}`); |
| | | } |
| | | |
| | | /** 审æ¹ä½ä¸ç¥¨ */ |
| | | export function approveTicket(data: MesSafetyTicketApi.TicketApproveParam) { |
| | | return requestClient.put<boolean>('/mes/safety/work-ticket/approve', data); |
| | | } |
| | | |
| | | /** å®å·¥ç»è®° / 彿¡£ */ |
| | | export function closeTicket(data: MesSafetyTicketApi.TicketCloseParam) { |
| | | return requestClient.put<boolean>('/mes/safety/work-ticket/close', data); |
| | | } |
| | |
| | | MES_SAFETY_HAZARD_SOURCE: 'mes_safety_hazard_source', // å®å
¨ç产-éæ£æ¥æº |
| | | MES_SAFETY_HAZARD_LEVEL: 'mes_safety_hazard_level', // å®å
¨ç产-鿣ç级 |
| | | MES_SAFETY_HAZARD_STATUS: 'mes_safety_hazard_status', // å®å
¨ç产-éæ£ç¶æ |
| | | MES_SAFETY_TICKET_TYPE: 'mes_safety_ticket_type', // å®å
¨ç产-ä½ä¸ç±»å |
| | | MES_SAFETY_TICKET_LEVEL: 'mes_safety_ticket_level', // å®å
¨ç产-ä½ä¸çº§å« |
| | | MES_SAFETY_TICKET_STATUS: 'mes_safety_ticket_status', // å®å
¨ç产-ä½ä¸ç¥¨ç¶æ |
| | | } as const; |
| | | |
| | | /** ========== WMS - ä»å¨ç®¡ç模å ========== */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesSafetyTicketApi } from '#/api/safety/ticket'; |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src'; |
| | | import { getDictOptions } from '#/packages/effects/hooks/src'; |
| | | |
| | | import { getRiskUnitSimpleList } from '#/api/safety/risk/unit'; |
| | | import { getSimpleUserList, getWorkshopOptions } from '../risk/shared'; |
| | | |
| | | /** ä½ä¸ç¥¨ç¶æ */ |
| | | export const TICKET_STATUS = { |
| | | PENDING: 0, |
| | | APPROVED: 10, |
| | | FINISHED: 20, |
| | | ARCHIVED: 30, |
| | | REJECTED: 90, |
| | | }; |
| | | |
| | | export function ticketStatusName(status?: number): string { |
| | | switch (status) { |
| | | case 0: return 'å¾
审æ¹'; |
| | | case 10: return 'å·²æ¹å'; |
| | | case 20: return 'å·²å®å·¥'; |
| | | case 30: return '已彿¡£'; |
| | | case 90: return '已驳å'; |
| | | default: return '-'; |
| | | } |
| | | } |
| | | |
| | | export function ticketStatusColor(status?: number): string { |
| | | switch (status) { |
| | | case 0: return 'processing'; |
| | | case 10: return 'success'; |
| | | case 20: return 'default'; |
| | | case 30: return 'default'; |
| | | case 90: return 'error'; |
| | | default: return 'default'; |
| | | } |
| | | } |
| | | |
| | | /** æ¶é´æ ¼å¼ï¼å
¼å®¹å端 LocalDateTime ç "yyyy-MM-dd HH:mm:ss" */ |
| | | export const DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'code', |
| | | label: 'ä½ä¸ç¥¨å·', |
| | | component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥ä½ä¸ç¥¨å·' }, |
| | | }, |
| | | { |
| | | fieldName: 'ticketType', |
| | | label: 'ä½ä¸ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_SAFETY_TICKET_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©ä½ä¸ç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_SAFETY_TICKET_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'workLocation', |
| | | label: 'ä½ä¸å°ç¹', |
| | | component: 'Input', |
| | | componentProps: { allowClear: true, placeholder: '请è¾å
¥ä½ä¸å°ç¹' }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** ç³è¯·/ä¿®æ¹ç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'code', |
| | | label: 'ä½ä¸ç¥¨å·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'å¦ ZY-2026-001' }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'ticketType', |
| | | label: 'ä½ä¸ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.MES_SAFETY_TICKET_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©ä½ä¸ç±»å', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'ticketLevel', |
| | | label: 'ä½ä¸çº§å«', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_SAFETY_TICKET_LEVEL, 'number'), |
| | | placeholder: 'è¯·éæ©ä½ä¸çº§å«', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'unitId', |
| | | label: 'å
³èé£é©åå
', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getRiskUnitSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | placeholder: 'å¯å
³èå°å
·ä½é£é©ç¹', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'workshopId', |
| | | label: 'æå±è½¦é´', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getWorkshopOptions, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | placeholder: 'è¯·éæ©æå±è½¦é´', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'workLocation', |
| | | label: 'ä½ä¸å°ç¹', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'å¦ é 纸车é´ä¸ä¾§ç®¡é' }, |
| | | }, |
| | | { |
| | | fieldName: 'workContent', |
| | | label: 'ä½ä¸å
容', |
| | | component: 'Textarea', |
| | | componentProps: { placeholder: '请æè¿°å
·ä½ä½ä¸å
容', rows: 2 }, |
| | | }, |
| | | { |
| | | fieldName: 'planStartTime', |
| | | label: '计åå¼å§', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | showTime: true, |
| | | format: DATETIME_FORMAT, |
| | | valueFormat: DATETIME_FORMAT, |
| | | placeholder: 'è¯·éæ©è®¡åå¼å§æ¶é´', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'planEndTime', |
| | | label: '计åç»æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | showTime: true, |
| | | format: DATETIME_FORMAT, |
| | | valueFormat: DATETIME_FORMAT, |
| | | placeholder: 'è¯·éæ©è®¡åç»ææ¶é´', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'guardianUserId', |
| | | label: 'çæ¤äºº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | api: getSimpleUserList, |
| | | labelField: 'nickname', |
| | | valueField: 'id', |
| | | placeholder: 'è¯·éæ©çæ¤äºº', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'contractor', |
| | | label: 'æ½å·¥åä½', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'å¤åä½ä¸æ¶å¡«åæ¿å
ååç§°' }, |
| | | }, |
| | | { |
| | | fieldName: 'workerNames', |
| | | label: 'ä½ä¸äººå', |
| | | component: 'Input', |
| | | componentProps: { placeholder: 'å¤ä¸ªç¨éå·åéï¼å¦ å¼ ä¸,æå' }, |
| | | }, |
| | | { |
| | | fieldName: 'safetyMeasures', |
| | | label: 'å®å
¨æªæ½', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { |
| | | placeholder: 'å¦ï¼1.æ¸
çå¨ç«ç¹å¨è¾¹å¯çç©ï¼2.é
å¤çç«å¨ï¼3.æ°ä½æ£æµåæ ¼', |
| | | rows: 3, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'safetyCheckResult', |
| | | label: 'å®å
¨äº¤åº', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { placeholder: 'å®å
¨äº¤åº / æ£æ¥ç»æ', rows: 2 }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { placeholder: '请è¾å
¥å¤æ³¨', rows: 2 }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<MesSafetyTicketApi.WorkTicket>['columns'] { |
| | | return [ |
| | | { field: 'code', title: 'ä½ä¸ç¥¨å·', width: 140 }, |
| | | { |
| | | field: 'ticketType', |
| | | title: 'ä½ä¸ç±»å', |
| | | width: 120, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_SAFETY_TICKET_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'ticketLevel', |
| | | title: '级å«', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_SAFETY_TICKET_LEVEL }, |
| | | }, |
| | | }, |
| | | { field: 'workLocation', title: 'ä½ä¸å°ç¹', minWidth: 150 }, |
| | | { |
| | | field: 'planStartTime', |
| | | title: 'è®¡åæ¶é´', |
| | | width: 200, |
| | | slots: { default: 'planTime' }, |
| | | }, |
| | | { field: 'applicantUserName', title: 'ç³è¯·äºº', width: 100 }, |
| | | { field: 'guardianUserName', title: 'çæ¤äºº', width: 100 }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 100, |
| | | slots: { default: 'status' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 240, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesSafetyTicketApi } from '#/api/safety/ticket'; |
| | | |
| | | 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 { deleteTicket, getTicketPage } from '#/api/safety/ticket'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { |
| | | TICKET_STATUS, |
| | | ticketStatusColor, |
| | | ticketStatusName, |
| | | useGridColumns, |
| | | useGridFormSchema, |
| | | } from './data'; |
| | | import ApproveModal from './modules/approve.vue'; |
| | | import CloseModal from './modules/close.vue'; |
| | | import FormModalComp from './modules/form.vue'; |
| | | |
| | | defineOptions({ name: 'SafetyWorkTicket' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: FormModalComp, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ApproveModalComp, approveModalApi] = useVbenModal({ |
| | | connectedComponent: ApproveModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [CloseModalComp, closeModalApi] = useVbenModal({ |
| | | connectedComponent: CloseModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => |
| | | await getTicketPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }), |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<MesSafetyTicketApi.WorkTicket>, |
| | | }); |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: MesSafetyTicketApi.WorkTicket) { |
| | | formModalApi.setData({ formType: 'update', id: row.id }).open(); |
| | | } |
| | | |
| | | function handleApprove(row: MesSafetyTicketApi.WorkTicket) { |
| | | approveModalApi |
| | | .setData({ |
| | | id: row.id, |
| | | code: row.code, |
| | | workContent: row.workContent, |
| | | safetyMeasures: row.safetyMeasures, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | function handleClose(row: MesSafetyTicketApi.WorkTicket) { |
| | | closeModalApi |
| | | .setData({ id: row.id, code: row.code, status: row.status }) |
| | | .open(); |
| | | } |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | async function handleDelete(row: MesSafetyTicketApi.WorkTicket) { |
| | | await deleteTicket(row.id!); |
| | | message.success('å 餿å'); |
| | | gridApi.query(); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ApproveModalComp @success="handleRefresh" /> |
| | | <CloseModalComp @success="handleRefresh" /> |
| | | <Grid> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ç³è¯·ä½ä¸ç¥¨', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:safety-ticket:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | |
| | | <template #planTime="{ row }"> |
| | | <span v-if="row.planStartTime"> |
| | | {{ row.planStartTime }} |
| | | <Tag v-if="row.overdue" color="error" class="ml-1">è¶
æ</Tag> |
| | | </span> |
| | | <span v-else class="text-gray-400">-</span> |
| | | </template> |
| | | |
| | | <template #status="{ row }"> |
| | | <Tag :color="ticketStatusColor(row.status)"> |
| | | {{ ticketStatusName(row.status) }} |
| | | </Tag> |
| | | </template> |
| | | |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: '审æ¹', |
| | | type: 'link', |
| | | auth: ['mes:safety-ticket:approve'], |
| | | ifShow: row.status === TICKET_STATUS.PENDING, |
| | | onClick: handleApprove.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å®å·¥', |
| | | type: 'link', |
| | | auth: ['mes:safety-ticket:close'], |
| | | ifShow: row.status === TICKET_STATUS.APPROVED, |
| | | onClick: handleClose.bind(null, row), |
| | | }, |
| | | { |
| | | label: '彿¡£', |
| | | type: 'link', |
| | | auth: ['mes:safety-ticket:close'], |
| | | ifShow: row.status === TICKET_STATUS.FINISHED, |
| | | onClick: handleClose.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'ç¼è¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['mes:safety-ticket:update'], |
| | | ifShow: |
| | | row.status === TICKET_STATUS.PENDING || |
| | | row.status === TICKET_STATUS.REJECTED, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mes:safety-ticket:delete'], |
| | | ifShow: |
| | | row.status === TICKET_STATUS.PENDING || |
| | | row.status === TICKET_STATUS.REJECTED, |
| | | popConfirm: { |
| | | title: 'ç¡®å®å é¤è¯¥ä½ä¸ç¥¨åï¼', |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Input, message, Radio } from 'ant-design-vue'; |
| | | |
| | | import { approveTicket } from '#/api/safety/ticket'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const ticketId = ref<number>(); |
| | | const ticketCode = ref<string>(); |
| | | const workContent = ref<string>(); |
| | | const safetyMeasures = ref<string>(); |
| | | const passed = ref<boolean>(true); |
| | | const approveComment = ref<string>(''); |
| | | const rejectReason = ref<string>(''); |
| | | const loading = ref(false); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | if (!passed.value && !rejectReason.value.trim()) { |
| | | message.warning('驳忶å¿
须填ååå '); |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | await approveTicket({ |
| | | id: ticketId.value!, |
| | | pass: passed.value, |
| | | approveComment: approveComment.value, |
| | | rejectReason: rejectReason.value, |
| | | }); |
| | | emit('success'); |
| | | message.success(passed.value ? 'å·²æ¹åï¼å¯å¼å§ä½ä¸' : '已驳å'); |
| | | await modalApi.close(); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }, |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | code?: string; |
| | | id: number; |
| | | safetyMeasures?: string; |
| | | workContent?: string; |
| | | }>(); |
| | | ticketId.value = data.id; |
| | | ticketCode.value = data.code; |
| | | workContent.value = data.workContent; |
| | | safetyMeasures.value = data.safetyMeasures; |
| | | passed.value = true; |
| | | approveComment.value = ''; |
| | | rejectReason.value = ''; |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="ä½ä¸ç¥¨å®¡æ¹" class="w-1/2"> |
| | | <div class="mx-4 flex flex-col gap-4"> |
| | | <div> |
| | | <div class="mb-1 text-gray-600">ä½ä¸ç¥¨å·</div> |
| | | <div class="font-medium">{{ ticketCode ?? '-' }}</div> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 text-gray-600">ä½ä¸å
容</div> |
| | | <div class="rounded bg-gray-50 p-2">{{ workContent || '-' }}</div> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 text-gray-600">å®å
¨æªæ½</div> |
| | | <div class="rounded bg-gray-50 p-2 whitespace-pre-wrap"> |
| | | {{ safetyMeasures || '-' }} |
| | | </div> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 text-gray-600">审æ¹ç»è®º</div> |
| | | <Radio.Group v-model:value="passed" button-style="solid"> |
| | | <Radio.Button :value="true">æ¹å</Radio.Button> |
| | | <Radio.Button :value="false">驳å</Radio.Button> |
| | | </Radio.Group> |
| | | </div> |
| | | <div v-if="passed"> |
| | | <div class="mb-1 text-gray-600">å®¡æ¹æè§</div> |
| | | <Input.TextArea |
| | | v-model:value="approveComment" |
| | | :rows="2" |
| | | placeholder="å¦ï¼å®å
¨æªæ½å°ä½ï¼åæä½ä¸" |
| | | /> |
| | | </div> |
| | | <div v-else> |
| | | <div class="mb-1 text-gray-600"> |
| | | 驳ååå <span class="text-red-500">*</span> |
| | | </div> |
| | | <Input.TextArea |
| | | v-model:value="rejectReason" |
| | | :rows="2" |
| | | placeholder="å¦ï¼ç¼ºå°æ°ä½æ£æµè®°å½ï¼å®å
¨æªæ½ä¸å®å" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Checkbox, DatePicker, Input, message } from 'ant-design-vue'; |
| | | |
| | | import { closeTicket } from '#/api/safety/ticket'; |
| | | |
| | | import { DATETIME_FORMAT } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const ticketId = ref<number>(); |
| | | const ticketCode = ref<string>(); |
| | | const isArchiveStage = ref(false); |
| | | const actualStartTime = ref<string>(); |
| | | const actualEndTime = ref<string>(); |
| | | const archiveNow = ref(false); |
| | | const closeRemark = ref<string>(''); |
| | | const loading = ref(false); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | loading.value = true; |
| | | try { |
| | | await closeTicket({ |
| | | id: ticketId.value!, |
| | | actualStartTime: actualStartTime.value, |
| | | actualEndTime: actualEndTime.value, |
| | | archive: archiveNow.value || isArchiveStage.value, |
| | | closeRemark: closeRemark.value, |
| | | }); |
| | | emit('success'); |
| | | message.success( |
| | | archiveNow.value || isArchiveStage.value ? '已彿¡£' : 'å·²ç»è®°å®å·¥', |
| | | ); |
| | | await modalApi.close(); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }, |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ |
| | | code?: string; |
| | | id: number; |
| | | status?: number; |
| | | }>(); |
| | | ticketId.value = data.id; |
| | | ticketCode.value = data.code; |
| | | // status=20 表示已å®å·¥ï¼æ¤æ¶åªå©å½æ¡£æä½ |
| | | isArchiveStage.value = data.status === 20; |
| | | actualStartTime.value = undefined; |
| | | actualEndTime.value = undefined; |
| | | archiveNow.value = false; |
| | | closeRemark.value = ''; |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title="isArchiveStage ? 'ä½ä¸ç¥¨å½æ¡£' : 'å®å·¥ç»è®°'" |
| | | class="w-1/2" |
| | | > |
| | | <div class="mx-4 flex flex-col gap-4"> |
| | | <div> |
| | | <div class="mb-1 text-gray-600">ä½ä¸ç¥¨å·</div> |
| | | <div class="font-medium">{{ ticketCode ?? '-' }}</div> |
| | | </div> |
| | | |
| | | <template v-if="!isArchiveStage"> |
| | | <div> |
| | | <div class="mb-1 text-gray-600">å®é
å¼å§æ¶é´</div> |
| | | <DatePicker |
| | | v-model:value="actualStartTime" |
| | | class="w-full" |
| | | show-time |
| | | :format="DATETIME_FORMAT" |
| | | :value-format="DATETIME_FORMAT" |
| | | placeholder="è¯·éæ©å®é
å¼å§æ¶é´" |
| | | /> |
| | | </div> |
| | | <div> |
| | | <div class="mb-1 text-gray-600">å®é
ç»ææ¶é´</div> |
| | | <DatePicker |
| | | v-model:value="actualEndTime" |
| | | class="w-full" |
| | | show-time |
| | | :format="DATETIME_FORMAT" |
| | | :value-format="DATETIME_FORMAT" |
| | | placeholder="è¯·éæ©å®é
ç»ææ¶é´" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <div> |
| | | <div class="mb-1 text-gray-600">å®å·¥ / 彿¡£è¯´æ</div> |
| | | <Input.TextArea |
| | | v-model:value="closeRemark" |
| | | :rows="2" |
| | | placeholder="å¦ï¼ä½ä¸å®æï¼ç°åºå·²æ¸
çï¼æ éç鿣" |
| | | /> |
| | | </div> |
| | | |
| | | <div v-if="!isArchiveStage"> |
| | | <Checkbox v-model:checked="archiveNow"> |
| | | å®å·¥åç´æ¥å½æ¡£ï¼ä¸ååç¬æ§è¡å½æ¡£æä½ï¼ |
| | | </Checkbox> |
| | | </div> |
| | | <div v-else class="text-gray-500"> |
| | | 该ä½ä¸ç¥¨å·²å®å·¥ï¼æ¬æ¬¡æä½å°ç´æ¥å½æ¡£ã |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesSafetyTicketApi } from '#/api/safety/ticket'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createTicket, getTicket, updateTicket } from '#/api/safety/ticket'; |
| | | |
| | | 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: 110, |
| | | }, |
| | | 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 MesSafetyTicketApi.WorkTicket; |
| | | try { |
| | | await (isUpdate.value ? updateTicket(data) : createTicket(data)); |
| | | emit('success'); |
| | | message.success(isUpdate.value ? 'ä¿®æ¹æå' : 'å·²æäº¤ï¼çå¾
审æ¹'); |
| | | 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 ticket = await getTicket(data.id); |
| | | await formApi.setValues(ticket); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-2/3"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |