tuqin
7 天以前 e0eadec8ab4a1da79517f923f595ae19328a3703
feat(mes): 新增安全生产模块 - 应急处置预案(一期)

背景:蓝羽此前无任何安全生产功能。经全仓库排查(22 个后端分支 + 21 个前端分支),
完整安全生产模块不存在,仅新高电力分支有应急处置预案,作为安全生产第一个能力点先落地。

新增:
- views/mes/pro/emergencyplan:列表 + 新增/编辑表单(预案编码、名称、类型、处置内容、责任单位、状态)
- api/mes/pro/emergencyplan:对接 /mes/pro/emergency-plan 接口
- components/import-modal:预案导入依赖的通用导入弹窗组件(新高电力独有,蓝羽基线缺失)
- dict-enum.ts 补 MES_PRO_EMERGENCY_PLAN_TYPE 字典常量,否则预案类型下拉框全空

配套后端同批提交,建表/菜单/字典 SQL 见 mes_safety_emergency_plan_init.sql。
后续能力点(风险分级管控、隐患排查治理、特种作业票证、安全教育培训、
可视化看板、统计分析)分期补充。
已添加6个文件
已修改1个文件
881 ■■■■■ 文件已修改
src/api/mes/pro/emergencyplan/index.ts 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/import-modal/import-modal.vue 317 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/import-modal/index.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/emergencyplan/data.ts 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/emergencyplan/index.vue 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/emergencyplan/modules/form.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/emergencyplan/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProEmergencyPlanApi {
  /** åº”急处置预案 */
  export interface EmergencyPlan {
    id?: number;
    code?: string; // é¢„案编码
    name?: string; // é¢„案名称
    planType?: number; // é¢„案类型:1停电 2设备故障 3火灾 4自然灾害 5其他
    content?: string; // å¤„置内容
    orgUnit?: string; // è´£ä»»å•位
    status?: number; // çŠ¶æ€ï¼š0草稿 10已发布
    remark?: string; // å¤‡æ³¨
    createTime?: string;
  }
}
/** åˆ›å»ºåº”急处置预案 */
export function createPlan(data: MesProEmergencyPlanApi.EmergencyPlan) {
  return requestClient.post('/mes/pro/emergency-plan/create', data);
}
/** æ›´æ–°åº”急处置预案 */
export function updatePlan(data: MesProEmergencyPlanApi.EmergencyPlan) {
  return requestClient.put('/mes/pro/emergency-plan/update', data);
}
/** åˆ é™¤åº”急处置预案 */
export function deletePlan(ids: number[]) {
  return requestClient.delete('/mes/pro/emergency-plan/delete', { params: { ids: ids.join(',') } });
}
/** èŽ·å–åº”æ€¥å¤„ç½®é¢„æ¡ˆ */
export function getPlan(id: number) {
  return requestClient.get<MesProEmergencyPlanApi.EmergencyPlan>(
    `/mes/pro/emergency-plan/get?id=${id}`,
  );
}
/** èŽ·å–åº”æ€¥å¤„ç½®é¢„æ¡ˆåˆ†é¡µ */
export function getPlanPage(params: PageParam) {
  return requestClient.get<PageResult<MesProEmergencyPlanApi.EmergencyPlan>>(
    '/mes/pro/emergency-plan/page',
    { params },
  );
}
/** å‘布应急处置预案 */
export function publishPlan(id: number) {
  return requestClient.put(`/mes/pro/emergency-plan/publish?id=${id}`);
}
/** ä¸‹è½½åº”急处置预案导入模板 */
export function importPlanTemplate() {
  return requestClient.download('/mes/pro/emergency-plan/get-import-template');
}
/** å¯¼å…¥åº”急处置预案 */
export function importPlan(file: File, updateSupport: boolean) {
  return requestClient.upload(
    `/mes/pro/emergency-plan/import?updateSupport=${updateSupport}`,
    { file },
  );
}
src/components/import-modal/import-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,317 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import { h, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Modal as AntModal, Upload } from 'ant-design-vue';
import { useVbenForm, z } from '#/adapter/form';
import { $t } from '#/locales';
/**
 * å¯¼å…¥æŽ¥å£è¿”回结构。
 *
 * åŽç«¯å„模块的导入返回值不统一,这里描述可展示结果中约定明确的字段;
 * create* / update* / failure* ç­‰ä¸šåŠ¡ä¸“ç”¨å­—æ®µä¼šåœ¨è¿è¡Œæ—¶ç»Ÿä¸€è¯†åˆ«ã€‚
 */
export interface ImportResultLike {
  /** è¢«è·³è¿‡çš„行:key ä¸º Excel è¡Œå·ï¼Œvalue ä¸ºåŽŸå›  */
  skippedRows?: Record<string, string>;
  /** è¢«è·³è¿‡çš„行数 */
  skippedCount?: number;
  /** Excel ä¸­å®žé™…有数据的行数 */
  totalRows?: number;
  /** æˆåŠŸå¯¼å…¥çš„è¡Œæ•° */
  successRows?: number;
  /** æˆåŠŸå¯¼å…¥çš„æ•°é‡ */
  successCount?: number;
  /** å¯¼å…¥å¤±è´¥çš„æ•°é‡ */
  failureCount?: number;
  /** å…¶å®ƒæ¨¡å—的通用字段:失败列表 */
  failureList?: Record<string, string> | string[];
  /** å…¶å®ƒæ¨¡å—的通用字段:失败原因 */
  failureMessages?: Record<string, string> | string[];
  [key: string]: unknown;
}
export interface ImportModalProps {
  /** å¼¹çª—标题,如「导入用户」 */
  title: string;
  /** ä¸‹è½½çš„æ¨¡æ¿æ–‡ä»¶åï¼Œå¦‚「用户导入模板.xls」 */
  fileName: string;
  /** æ‰§è¡Œå¯¼å…¥çš„æŽ¥å£å‡½æ•° (file, updateSupport) => Promise */
  importFn: (file: File, updateSupport: boolean) => Promise<unknown>;
  /** ä¸‹è½½æ¨¡æ¿çš„æŽ¥å£å‡½æ•° () => Promise(blob) */
  templateFn: () => Promise<BlobPart>;
}
const emit = defineEmits(['success']);
const config = ref<ImportModalProps>({
  title: '',
  fileName: '',
  importFn: async () => {},
  templateFn: () => Promise.resolve(new Blob()),
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  layout: 'horizontal',
  schema: [
    {
      fieldName: 'file',
      label: '数据文件',
      component: 'Upload',
      rules: 'required',
      help: '仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶',
    },
    {
      fieldName: 'updateSupport',
      label: '是否覆盖',
      component: 'Switch',
      componentProps: {
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(false),
      help: '是否更新已经存在的数据',
    },
  ],
  showDefaultActions: false,
});
/**
 * ä»Žå¯¼å…¥æŽ¥å£çš„返回值里提取可展示的结果。
 *
 * å…³é”®ç‚¹ï¼šä¸èƒ½åªçœ‹æŽ¥å£æœ‰æ²¡æœ‰æŠ¥é”™å°±æç¤ºã€Œæ“ä½œæˆåŠŸã€â€”â€”åŽç«¯å¯èƒ½è·³è¿‡äº†éƒ¨åˆ†è¡Œï¼Œ
 * è¿™äº›è¡Œä¸ä¼šæŠ›å¼‚常,必须把跳过/失败的数量和行号明确告诉用户,否则数据无声丢失。
 */
function summarizeImportResult(raw: unknown) {
  const result = unwrapImportResult(raw);
  // å„模块分别返回 createCodes、createContractNames、updateNames ç­‰å­—段,
  // ä¸èƒ½æŠŠé€šç”¨ç»„件绑定到某一个业务的字段名。
  const successRows = firstNumber(result.successRows, result.successCount)
    ?? countArrayFields(result, /^(?:create|update|success)(?!Count$)/i);
  const failureEntries = collectEntries(result, /^failure(?!Count$)/i);
  const skippedEntries = toEntries(result.skippedRows);
  const failureRows = firstNumber(result.failureCount) ?? failureEntries.length;
  const skippedRows = firstNumber(result.skippedCount) ?? skippedEntries.length;
  const totalRows = firstNumber(result.totalRows);
  const recognized = hasOutcomeFields(result);
  if (failureRows > 0 && failureEntries.length === 0) {
    failureEntries.push([`失败 ${failureRows} æ¡`, '后端未返回失败明细']);
  }
  if (skippedRows > 0 && skippedEntries.length === 0) {
    skippedEntries.push([`跳过 ${skippedRows} æ¡`, '后端未返回跳过明细']);
  }
  return {
    successRows,
    failureRows,
    skippedRows,
    failureEntries,
    skippedEntries,
    totalRows,
    recognized,
    hasIssue: failureRows > 0 || skippedRows > 0,
  };
}
const OUTCOME_KEY_PATTERN = /^(?:create|update|success|failure|skipped|totalRows)/i;
function hasOutcomeFields(result: ImportResultLike) {
  return Object.keys(result).some((key) => OUTCOME_KEY_PATTERN.test(key));
}
/** requestClient é»˜è®¤ä¼šè§£åŒ… data,同时兼容 Axios / CommonResult çš„嵌套 data。 */
function unwrapImportResult(raw: unknown): ImportResultLike {
  let current = raw;
  for (let depth = 0; depth < 3; depth++) {
    if (!current || typeof current !== 'object' || Array.isArray(current)) {
      return {};
    }
    const result = current as ImportResultLike;
    if (hasOutcomeFields(result)) {
      return result;
    }
    current = result.data;
  }
  return {};
}
function firstNumber(...values: unknown[]): number | undefined {
  return values.find((value): value is number => typeof value === 'number');
}
/** æ±‡æ€» create* / update* / success* æ•°ç»„的长度。 */
function countArrayFields(result: ImportResultLike, keyPattern: RegExp) {
  return Object.entries(result).reduce(
    (total, [key, value]) =>
      total + (keyPattern.test(key) && Array.isArray(value) ? value.length : 0),
    0,
  );
}
/** åˆå¹¶ failure* ç­‰ç»“果字段,保留业务单号及失败原因。 */
function collectEntries(result: ImportResultLike, keyPattern: RegExp) {
  return Object.entries(result).flatMap(([key, value]) =>
    keyPattern.test(key) ? toEntries(value) : [],
  );
}
/** æŠŠ Record / string[] / undefined ç»Ÿä¸€æˆ [label, reason][] */
function toEntries(input: unknown): Array<[string, string]> {
  if (!input) {
    return [];
  }
  if (Array.isArray(input)) {
    return input.map((item) => [String(item), '']);
  }
  if (typeof input === 'object') {
    return Object.entries(input as Record<string, string>).map(([k, v]) => [
      k,
      String(v ?? ''),
    ]);
  }
  return [];
}
/** æŠŠç»“果拼成用户能看懂的一段话 */
function buildResultMessage(summary: ReturnType<typeof summarizeImportResult>) {
  const parts: string[] = [];
  if (summary.successRows > 0) {
    parts.push(`成功 ${summary.successRows} æ¡`);
  }
  if (summary.skippedRows > 0) {
    parts.push(`跳过 ${summary.skippedRows} æ¡`);
  }
  if (summary.failureRows > 0) {
    parts.push(`失败 ${summary.failureRows} æ¡`);
  }
  if (parts.length === 0) {
    return summary.recognized
      ? '导入完成,但没有可处理的数据'
      : '导入请求已完成,请刷新列表确认结果';
  }
  const total = summary.totalRows === undefined ? '' : `(表格共 ${summary.totalRows} è¡Œï¼‰`;
  return `导入完成:${parts.join(',')}${total}`;
}
const [Modal, modalApi] = useVbenModal({
  async onOpenChange(isOpen: boolean) {
    if (isOpen) {
      config.value = modalApi.getData<ImportModalProps>() ?? config.value;
      await formApi.resetForm();
    }
  },
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    // æäº¤è¡¨å•
    const data = await formApi.getValues();
    try {
      // ç»Ÿä¸€å…œåº•:后端各模块历史实现对 updateSupport çš„æ¥æºä¸ä¸€è‡´ï¼ˆæœ‰çš„从 query è¯»ã€
      // æœ‰çš„从表单读),漏传会被 @NotNull æ‹¦ä¸‹å¹¶å¼¹å‡ºç”¨æˆ·æ ¹æœ¬çœ‹ä¸åˆ°å¯¹åº”字段的
      // "是否支持更新不能为空"。这里强制显式传布尔值,避免任何页面因未初始化踩坑。
      const updateSupport = data.updateSupport === true;
      const raw = await config.value.importFn(data.file, updateSupport);
      const summary = summarizeImportResult(raw);
      const text = buildResultMessage(summary);
      if (summary.hasIssue) {
        // æœ‰è·³è¿‡æˆ–失败的行:保持弹窗打开,把明细展示给用户,避免数据无声丢失
        message.warning({
          content: text,
          duration: 8,
        });
        showDetail(summary);
        emit('success');
        return;
      }
      await modalApi.close();
      emit('success');
      message.success(text);
    } finally {
      modalApi.unlock();
    }
  },
});
/** ç”¨ Modal.info å±•示跳过/失败明细,比 toast æ›´è€è¯» */
function showDetail(summary: ReturnType<typeof summarizeImportResult>) {
  const sections: Array<{ title: string; rows: Array<[string, string]> }> = [];
  if (summary.skippedEntries.length > 0) {
    sections.push({ title: '被跳过的行(未导入)', rows: summary.skippedEntries });
  }
  if (summary.failureEntries.length > 0) {
    sections.push({ title: '导入失败', rows: summary.failureEntries });
  }
  AntModal.info({
    title: '导入结果明细',
    width: 560,
    content: h(
      'div',
      { style: 'max-height: 360px; overflow-y: auto;' },
      sections.map((section) =>
        h('div', { style: 'margin-bottom: 12px;' }, [
          h('div', { style: 'font-weight: 600; margin-bottom: 4px;' }, section.title),
          ...section.rows.map(([label, reason]) =>
            h('div', { style: 'font-size: 13px; line-height: 20px;' }, [
              h('span', { style: 'color: #cf1322;' }, `${label}`),
              h('span', {}, `:${reason || '未说明原因'}`),
            ]),
          ),
        ]),
      ),
    ),
  });
}
/** ä¸Šä¼ å‰ */
function beforeUpload(file: FileType) {
  formApi.setFieldValue('file', file);
  return false;
}
/** ä¸‹è½½æ¨¡ç‰ˆ */
async function handleDownload() {
  const data = await config.value.templateFn();
  downloadFileFromBlobPart({ fileName: config.value.fileName, source: data });
}
</script>
<template>
  <Modal :title="config.title" class="w-1/3">
    <Form class="mx-4">
      <template #file>
        <div class="w-full">
          <Upload
            :max-count="1"
            accept=".xls,.xlsx"
            :before-upload="beforeUpload"
          >
            <Button type="primary"> é€‰æ‹© Excel æ–‡ä»¶ </Button>
          </Upload>
        </div>
      </template>
    </Form>
    <template #prepend-footer>
      <div class="flex flex-auto items-center">
        <Button @click="handleDownload"> ä¸‹è½½å¯¼å…¥æ¨¡æ¿ </Button>
      </div>
    </template>
  </Modal>
</template>
src/components/import-modal/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
export { default } from './import-modal.vue';
src/packages/constants/src/dict-enum.ts
@@ -304,6 +304,7 @@
  MES_EM_ALERT_TYPE: 'mes_em_alert_type', // MES èƒ½è€—预警类型
  MES_EM_ALERT_LEVEL: 'mes_em_alert_level', // MES èƒ½è€—预警级别
  MES_EM_ALERT_STATUS: 'mes_em_alert_status', // MES èƒ½è€—预警处理状态
  MES_PRO_EMERGENCY_PLAN_TYPE: 'mes_pro_emergency_plan_type', // MES åº”急处置预案类型
} as const;
/** ========== WMS - ä»“储管理模块 ========== */
src/views/mes/pro/emergencyplan/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,187 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'edit';
/** çŠ¶æ€å¸¸é‡ */
export const PLAN_STATUS = {
  DRAFT: 0,
  PUBLISHED: 10,
};
/** è¡¨å•的配置项 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  const isReadonly = formType === 'detail';
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'code',
      label: '预案编码',
      component: 'Input',
      componentProps: {
        placeholder: '系统自动生成',
        disabled: true,
      },
    },
    {
      fieldName: 'name',
      label: '预案名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入预案名称',
        disabled: isReadonly,
      },
      rules: 'required',
    },
    {
      fieldName: 'planType',
      label: '预案类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE, 'number'),
        placeholder: '请选择预案类型',
        disabled: isReadonly,
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'orgUnit',
      label: '责任单位',
      component: 'Input',
      componentProps: {
        placeholder: '请输入责任单位/部门',
        disabled: isReadonly,
      },
    },
    {
      fieldName: 'content',
      label: '处置内容',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入应急处置内容',
        rows: 4,
        disabled: isReadonly,
      },
      formItemClass: 'col-span-3',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        autoSize: { minRows: 1, maxRows: 1 },
        disabled: isReadonly,
      },
      formItemClass: 'col-span-3',
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '预案编码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入预案编码',
      },
    },
    {
      fieldName: 'name',
      label: '预案名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入预案名称',
      },
    },
    {
      fieldName: 'planType',
      label: '预案类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE, 'number'),
        placeholder: '请选择预案类型',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: [
          { label: '草稿', value: PLAN_STATUS.DRAFT },
          { label: '已发布', value: PLAN_STATUS.PUBLISHED },
        ],
        placeholder: '请选择状态',
      },
    },
  ];
}
/** åˆ—表的表格列 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'checkbox', width: 50 },
    { type: 'seq', title: '序号', width: 60 },
    {
      field: 'code',
      title: '预案编码',
      minWidth: 150,
      slots: { default: 'code' },
    },
    {
      field: 'name',
      title: '预案名称',
      minWidth: 200,
    },
    {
      field: 'planType',
      title: '预案类型',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE },
      },
    },
    {
      field: 'orgUnit',
      title: '责任单位',
      minWidth: 140,
    },
    {
      field: 'status',
      title: '状态',
      width: 100,
      slots: { default: 'status' },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      field: 'action',
      title: '操作',
      width: 260,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/mes/pro/emergencyplan/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,218 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProEmergencyPlanApi } from '#/api/mes/pro/emergencyplan';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { isEmpty } from '@vben/utils';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deletePlan,
  getPlanPage,
  importPlan,
  importPlanTemplate,
  publishPlan,
} from '#/api/mes/pro/emergencyplan';
import ImportModalComponent from '#/components/import-modal';
import { $t } from '#/locales';
import { PLAN_STATUS, useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
/** MES åº”急处置预案列表 */
defineOptions({ name: 'MesProEmergencyPlan' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ImportModal, importModalApi] = useVbenModal({
  connectedComponent: ImportModalComponent,
  destroyOnClose: true,
});
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
function handleEdit(row: MesProEmergencyPlanApi.EmergencyPlan) {
  formModalApi.setData({ formType: 'edit', id: row.id }).open();
}
function handleDetail(row: MesProEmergencyPlanApi.EmergencyPlan) {
  formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
async function handleDelete(ids: number[]) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting'),
    duration: 0,
  });
  try {
    await deletePlan(ids);
    message.success($t('ui.actionMessage.deleteSuccess'));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
async function handlePublish(row: MesProEmergencyPlanApi.EmergencyPlan) {
  await publishPlan(row.id!);
  message.success('已发布');
  handleRefresh();
}
/** å¯¼å…¥åº”急处置预案 */
function handleImport() {
  importModalApi
    .setData({
      title: $t('ui.actionTitle.import', ['应急处置预案']),
      fileName: '应急处置预案导入模板.xls',
      importFn: importPlan,
      templateFn: importPlanTemplate,
    })
    .open();
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
  records,
}: {
  records: MesProEmergencyPlanApi.EmergencyPlan[];
}) {
  checkedIds.value = records.map((item) => item.id!);
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getPlanPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesProEmergencyPlanApi.EmergencyPlan>,
  gridEvents: {
    checkboxAll: handleRowCheckboxChange,
    checkboxChange: handleRowCheckboxChange,
  },
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ImportModal @success="handleRefresh" />
    <Grid table-title="应急处置预案列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['应急处置预案']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:pro-emergency-plan:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.import', ['应急处置预案']),
              icon: ACTION_ICON.UPLOAD,
              auth: ['mes:pro-emergency-plan:import'],
              onClick: handleImport,
            },
            {
              label: '批量删除',
              type: 'primary',
              danger: true,
              disabled: isEmpty(checkedIds),
              icon: ACTION_ICON.DELETE,
              auth: ['mes:pro-emergency-plan:delete'],
              popConfirm: {
                title: `是否删除所选中数据?`,
                confirm: handleDelete.bind(null, checkedIds),
              },
            },
          ]"
        />
      </template>
      <template #code="{ row }">
        <a class="link" @click="handleDetail(row)">{{ row.code }}</a>
      </template>
      <template #status="{ row }">
        <Tag v-if="row.status === PLAN_STATUS.DRAFT" color="default">草稿</Tag>
        <Tag v-else-if="row.status === PLAN_STATUS.PUBLISHED" color="success">已发布</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              onClick: handleDetail.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:pro-emergency-plan:update'],
              ifShow: row.status === PLAN_STATUS.DRAFT,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '发布',
              type: 'link',
              auth: ['mes:pro-emergency-plan:update'],
              ifShow: row.status === PLAN_STATUS.DRAFT,
              popConfirm: {
                title: '确认发布该预案?',
                confirm: handlePublish.bind(null, row),
              },
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:pro-emergency-plan:delete'],
              ifShow: row.status === PLAN_STATUS.DRAFT,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
                confirm: handleDelete.bind(null, [row.id!]),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/mes/pro/emergencyplan/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,91 @@
<script lang="ts" setup>
import type { MesProEmergencyPlanApi } from '#/api/mes/pro/emergencyplan';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createPlan, getPlan, updatePlan } from '#/api/mes/pro/emergencyplan';
import { $t } from '#/locales';
import type { FormType } from '../data';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesProEmergencyPlanApi.EmergencyPlan>();
const formType = ref<FormType>('create');
const getTitle = computed(() => {
  if (formType.value === 'create') {
    return $t('ui.actionTitle.create', ['应急处置预案']);
  } else if (formType.value === 'edit') {
    return $t('ui.actionTitle.edit', ['应急处置预案']);
  } else {
    return '应急处置预案详情';
  }
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'vertical',
  schema: useFormSchema(formType.value),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data =
      (await formApi.getValues()) as MesProEmergencyPlanApi.EmergencyPlan;
    try {
      await (formType.value === 'create'
        ? createPlan(data)
        : updatePlan(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formType.value));
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getPlan(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-2/3" :show-confirm-button="formType !== 'detail'">
    <Form class="mx-3" />
  </Modal>
</template>