tuqin
6 天以前 8c72a08077e0ac31f56f1f2601dcde15e00ba4a0
feat(safety): 安全生产四期 - 特种作业票证管理前端

配套后端作业票接口。

新增:
- api/safety/ticket
- views/safety/ticket:作业票列表 + 申请表单 + 审批弹窗 + 完工/归档弹窗
* 列表按状态动态显示操作:待审批→审批/编辑/删除,已批准→完工,
已完工→归档,已驳回→编辑/删除,已归档只读
* 计划时间超期用红色 + 「超期」标签提示
* 审批弹窗顶部回显作业内容与安全措施,便于审批判断
* 完工弹窗支持勾选「完工后直接归档」,减少一次操作
- dict-enum.ts 补 3 个作业票字典常量

注:本期只提交代码,未部署。
已添加6个文件
已修改1个文件
891 ■■■■■ 文件已修改
src/api/safety/ticket/index.ts 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safety/ticket/data.ts 275 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safety/ticket/index.vue 193 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safety/ticket/modules/approve.vue 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safety/ticket/modules/close.vue 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safety/ticket/modules/form.vue 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/safety/ticket/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,115 @@
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);
}
src/packages/constants/src/dict-enum.ts
@@ -312,6 +312,9 @@
  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 - ä»“储管理模块 ========== */
src/views/safety/ticket/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,275 @@
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' },
    },
  ];
}
src/views/safety/ticket/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,193 @@
<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>
src/views/safety/ticket/modules/approve.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
<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>
src/views/safety/ticket/modules/close.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,119 @@
<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>
src/views/safety/ticket/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
<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>