tuqin
6 天以前 98a9fbaffa47c165fa42313ee8d826628500ed9c
feat(safety): 安全生产二期 - 安全风险分级管控前端

配套后端风险单元 / 风险清单接口。

新增:
- api/safety/risk/unit、api/safety/risk/list
- views/safety/risk/unit:风险单元管理(列表 + 表单)
- views/safety/risk/list:风险清单管理(列表 + 表单)
* L/E/C 选择采用标准 LEC 取值(含每个取值的含义说明),选项变化后实时算出 D 与等级
* 弹窗顶部用 Alert 直接给出「D = xxx,判定为xx风险」
* 列表用四色标签区分重大/较大/一般/低风险
- dict-enum.ts 补 4 个字典常量(不加则下拉框全空)
已添加9个文件
已修改1个文件
1157 ■■■■■ 文件已修改
src/api/safety/risk/list/index.ts 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/safety/risk/unit/index.ts 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safety/risk/list/data.ts 346 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safety/risk/list/index.vue 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safety/risk/list/modules/form.vue 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safety/risk/shared.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safety/risk/unit/data.ts 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safety/risk/unit/index.vue 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safety/risk/unit/modules/form.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/safety/risk/list/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace MesSafetyRiskApi {
  /** é£Žé™©æ¸…单 */
  export interface Risk {
    id?: number;
    unitId?: number;
    unitName?: string;
    workshopName?: string;
    code?: string;
    name?: string;
    accidentType?: number;
    possibleConsequence?: string;
    likelihood?: number;
    exposure?: number;
    consequence?: number;
    riskValue?: number;
    riskLevel?: number;
    riskLevelName?: string;
    controlEngineering?: string;
    controlManagement?: string;
    controlTraining?: string;
    controlPpe?: string;
    controlEmergency?: string;
    evaluateDate?: string;
    nextEvaluateDate?: string;
    chargeUserId?: number;
    chargeUserName?: string;
    status?: number;
    remark?: string;
    createTime?: string;
  }
  /** åˆ†é¡µå‚æ•° */
  export interface RiskPageParam extends PageParam {
    code?: string;
    name?: string;
    unitId?: number;
    accidentType?: number;
    riskLevel?: number;
    status?: number;
  }
}
/** æŸ¥è¯¢é£Žé™©æ¸…单分页 */
export function getRiskPage(params: MesSafetyRiskApi.RiskPageParam) {
  return requestClient.get<PageResult<MesSafetyRiskApi.Risk>>(
    '/mes/safety/risk/page',
    { params },
  );
}
/** æŸ¥è¯¢é£Žé™©æ¸…单详情 */
export function getRisk(id: number) {
  return requestClient.get<MesSafetyRiskApi.Risk>(
    `/mes/safety/risk/get?id=${id}`,
  );
}
/** æŸ¥è¯¢æŒ‡å®šé£Žé™©å•元下的风险清单 */
export function getRiskListByUnit(unitId: number) {
  return requestClient.get<MesSafetyRiskApi.Risk[]>(
    `/mes/safety/risk/list-by-unit?unitId=${unitId}`,
  );
}
/** åˆ›å»ºé£Žé™©æ¸…单 */
export function createRisk(data: MesSafetyRiskApi.Risk) {
  return requestClient.post<number>('/mes/safety/risk/create', data);
}
/** æ›´æ–°é£Žé™©æ¸…单 */
export function updateRisk(data: MesSafetyRiskApi.Risk) {
  return requestClient.put<boolean>('/mes/safety/risk/update', data);
}
/** åˆ é™¤é£Žé™©æ¸…单 */
export function deleteRisk(id: number) {
  return requestClient.delete<boolean>(`/mes/safety/risk/delete?id=${id}`);
}
src/api/safety/risk/unit/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace MesSafetyRiskUnitApi {
  /** é£Žé™©å•å…ƒ */
  export interface RiskUnit {
    id?: number;
    code?: string;
    name?: string;
    workshopId?: number;
    workshopName?: string;
    location?: string;
    riskCategory?: number;
    chargeUserId?: number;
    chargeUserName?: string;
    status?: number;
    riskCount?: number;
    remark?: string;
    createTime?: string;
  }
  /** åˆ†é¡µå‚æ•° */
  export interface RiskUnitPageParam extends PageParam {
    code?: string;
    name?: string;
    workshopId?: number;
    riskCategory?: number;
    status?: number;
  }
}
/** æŸ¥è¯¢é£Žé™©å•元分页 */
export function getRiskUnitPage(params: MesSafetyRiskUnitApi.RiskUnitPageParam) {
  return requestClient.get<PageResult<MesSafetyRiskUnitApi.RiskUnit>>(
    '/mes/safety/risk-unit/page',
    { params },
  );
}
/** æŸ¥è¯¢é£Žé™©å•元详情 */
export function getRiskUnit(id: number) {
  return requestClient.get<MesSafetyRiskUnitApi.RiskUnit>(
    `/mes/safety/risk-unit/get?id=${id}`,
  );
}
/** æŸ¥è¯¢é£Žé™©å•元精简列表(下拉用) */
export function getRiskUnitSimpleList() {
  return requestClient.get<MesSafetyRiskUnitApi.RiskUnit[]>(
    '/mes/safety/risk-unit/simple-list',
  );
}
/** åˆ›å»ºé£Žé™©å•å…ƒ */
export function createRiskUnit(data: MesSafetyRiskUnitApi.RiskUnit) {
  return requestClient.post<number>('/mes/safety/risk-unit/create', data);
}
/** æ›´æ–°é£Žé™©å•å…ƒ */
export function updateRiskUnit(data: MesSafetyRiskUnitApi.RiskUnit) {
  return requestClient.put<boolean>('/mes/safety/risk-unit/update', data);
}
/** åˆ é™¤é£Žé™©å•å…ƒ */
export function deleteRiskUnit(id: number) {
  return requestClient.delete<boolean>(`/mes/safety/risk-unit/delete?id=${id}`);
}
src/packages/constants/src/dict-enum.ts
@@ -305,6 +305,10 @@
  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 åº”急处置预案类型
  MES_SAFETY_RISK_LEVEL: 'mes_safety_risk_level', // å®‰å…¨ç”Ÿäº§-风险等级
  MES_SAFETY_RISK_CATEGORY: 'mes_safety_risk_category', // å®‰å…¨ç”Ÿäº§-风险类别
  MES_SAFETY_ACCIDENT_TYPE: 'mes_safety_accident_type', // å®‰å…¨ç”Ÿäº§-事故类型
  MES_SAFETY_UNIT_STATUS: 'mes_safety_unit_status', // å®‰å…¨ç”Ÿäº§-风险单元状态
} as const;
/** ========== WMS - ä»“储管理模块 ========== */
src/views/safety/risk/list/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,346 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesSafetyRiskApi } from '#/api/safety/risk/list';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { z } from '#/adapter/form';
import { getRiskUnitSimpleList } from '#/api/safety/risk/unit';
import { getSimpleUserList } from '../shared';
/** L / E / C æ ‡å‡†å–值(LEC æ ¼é›·åބ姆-金尼法) */
export const LIKELIHOOD_OPTIONS = [
  { label: '10 - å®Œå…¨å¯ä»¥é¢„æ–™', value: 10 },
  { label: '6 - ç›¸å½“可能', value: 6 },
  { label: '3 - å¯èƒ½ï¼Œä½†ä¸ç»å¸¸', value: 3 },
  { label: '1 - å¯èƒ½æ€§å°ï¼Œå®Œå…¨æ„å¤–', value: 1 },
];
export const EXPOSURE_OPTIONS = [
  { label: '10 - è¿žç»­æš´éœ²', value: 10 },
  { label: '6 - æ¯å¤©å·¥ä½œæ—¶é—´å†…暴露', value: 6 },
  { label: '3 - æ¯å‘¨ä¸€æ¬¡æˆ–偶然暴露', value: 3 },
  { label: '2 - æ¯æœˆä¸€æ¬¡æš´éœ²', value: 2 },
  { label: '1 - æ¯å¹´å‡ æ¬¡æš´éœ²', value: 1 },
  { label: '0.5 - éžå¸¸ç½•见地暴露', value: 0.5 },
];
export const CONSEQUENCE_OPTIONS = [
  { label: '100 - å¤§ç¾éš¾ï¼Œè®¸å¤šäººæ­»äº¡', value: 100 },
  { label: '40 - ç¾éš¾ï¼Œæ•°äººæ­»äº¡', value: 40 },
  { label: '15 - éžå¸¸ä¸¥é‡ï¼Œä¸€äººæ­»äº¡', value: 15 },
  { label: '7 - ä¸¥é‡ï¼Œé‡ä¼¤', value: 7 },
  { label: '5 - é‡å¤§ï¼Œè‡´æ®‹', value: 5 },
  { label: '3 - è¾ƒå¤§ï¼Œè½»ä¼¤', value: 3 },
  { label: '1 - å¼•人注目,需要救护', value: 1 },
];
/** æŒ‰é£Žé™©å€¼ D è®¡ç®—风险等级:≥320 é‡å¤§(1);≥160 è¾ƒå¤§(2);≥70 ä¸€èˆ¬(3);其余低(4) */
export function calcRiskLevel(riskValue?: number): number | undefined {
  if (riskValue === undefined || riskValue === null) {
    return undefined;
  }
  if (riskValue >= 320) return 1;
  if (riskValue >= 160) return 2;
  if (riskValue >= 70) return 3;
  return 4;
}
/** é£Žé™©ç­‰çº§åç§° */
export function riskLevelName(level?: number): string {
  switch (level) {
    case 1: return '重大风险';
    case 2: return '较大风险';
    case 3: return '一般风险';
    case 4: return '低风险';
    default: return '';
  }
}
/** é£Žé™©ç­‰çº§å¯¹åº”的标签颜色 */
export function riskLevelColor(level?: number): string {
  switch (level) {
    case 1: return 'error';
    case 2: return 'warning';
    case 3: return 'processing';
    case 4: return 'success';
    default: return 'default';
  }
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '风险编码',
      component: 'Input',
      componentProps: { allowClear: true, placeholder: '请输入风险编码' },
    },
    {
      fieldName: 'name',
      label: '风险名称',
      component: 'Input',
      componentProps: { allowClear: true, placeholder: '请输入风险名称' },
    },
    {
      fieldName: 'unitId',
      label: '风险单元',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getRiskUnitSimpleList,
        labelField: 'name',
        valueField: 'id',
        placeholder: '请选择风险单元',
      },
    },
    {
      fieldName: 'riskLevel',
      label: '风险等级',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_SAFETY_RISK_LEVEL, 'number'),
        placeholder: '请选择风险等级',
      },
    },
  ];
}
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'unitId',
      label: '风险单元',
      component: 'ApiSelect',
      componentProps: {
        api: getRiskUnitSimpleList,
        labelField: 'name',
        valueField: 'id',
        placeholder: '请选择风险单元',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'code',
      label: '风险编码',
      component: 'Input',
      componentProps: { placeholder: '如 FX-001-01' },
      rules: 'required',
    },
    {
      fieldName: 'name',
      label: '风险名称',
      component: 'Input',
      componentProps: { placeholder: '如 çƒ˜ç¼¸è¶…压爆炸' },
      rules: 'required',
    },
    {
      fieldName: 'accidentType',
      label: '事故类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_SAFETY_ACCIDENT_TYPE, 'number'),
        placeholder: '请选择可能导致的事故类型',
      },
    },
    {
      fieldName: 'possibleConsequence',
      label: '可能后果',
      component: 'Textarea',
      componentProps: { placeholder: '如 äººå‘˜ä¼¤äº¡ã€è®¾å¤‡æŸæ¯', rows: 2 },
    },
    {
      fieldName: 'likelihood',
      label: 'L å¯èƒ½æ€§',
      component: 'Select',
      componentProps: {
        options: LIKELIHOOD_OPTIONS,
        placeholder: '请选择 L',
      },
      help: 'D = L Ã— E Ã— C',
    },
    {
      fieldName: 'exposure',
      label: 'E æš´éœ²é¢‘率',
      component: 'Select',
      componentProps: {
        options: EXPOSURE_OPTIONS,
        placeholder: '请选择 E',
      },
    },
    {
      fieldName: 'consequence',
      label: 'C åŽæžœä¸¥é‡æ€§',
      component: 'Select',
      componentProps: {
        options: CONSEQUENCE_OPTIONS,
        placeholder: '请选择 C',
      },
    },
    {
      fieldName: 'riskValue',
      label: 'D é£Žé™©å€¼',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        disabled: true,
        placeholder: '由 L×E×C è‡ªåŠ¨è®¡ç®—',
      },
    },
    {
      fieldName: 'riskLevel',
      label: '风险等级',
      component: 'Select',
      componentProps: {
        disabled: true,
        options: getDictOptions(DICT_TYPE.MES_SAFETY_RISK_LEVEL, 'number'),
        placeholder: '由风险值自动判定',
      },
    },
    {
      fieldName: 'controlEngineering',
      label: '工程技术措施',
      component: 'Textarea',
      componentProps: { placeholder: '如 å®‰è£…安全阀、联锁保护', rows: 2 },
    },
    {
      fieldName: 'controlManagement',
      label: '管理措施',
      component: 'Textarea',
      componentProps: { placeholder: '如 å®šæœŸå·¡æ£€ã€ä½œä¸šç¥¨å®¡æ‰¹', rows: 2 },
    },
    {
      fieldName: 'controlTraining',
      label: '培训教育措施',
      component: 'Textarea',
      componentProps: { placeholder: '如 å²—前培训、专项培训', rows: 2 },
    },
    {
      fieldName: 'controlPpe',
      label: '个体防护措施',
      component: 'Textarea',
      componentProps: { placeholder: '如 å®‰å…¨å¸½ã€é˜²æŠ¤æ‰‹å¥—', rows: 2 },
    },
    {
      fieldName: 'controlEmergency',
      label: '应急处置措施',
      component: 'Textarea',
      componentProps: { placeholder: '如 ç«‹å³åœæœºã€ç–æ•£äººå‘˜', rows: 2 },
    },
    {
      fieldName: 'chargeUserId',
      label: '管控责任人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        valueField: 'id',
        placeholder: '请选择管控责任人',
      },
    },
    {
      fieldName: 'evaluateDate',
      label: '本次评估日期',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
        placeholder: '请选择评估日期',
      },
    },
    {
      fieldName: 'nextEvaluateDate',
      label: '下次评估日期',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
        placeholder: '请选择下次评估日期',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
        optionType: 'button',
        options: [
          { label: '有效', value: 1 },
          { label: '已失效', value: 0 },
        ],
      },
      rules: z.number().default(1),
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: { placeholder: '请输入备注', rows: 2 },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesSafetyRiskApi.Risk>['columns'] {
  return [
    { field: 'code', title: '风险编码', width: 130 },
    { field: 'name', title: '风险名称', minWidth: 180 },
    { field: 'unitName', title: '风险单元', minWidth: 150 },
    { field: 'workshopName', title: '所属车间', width: 110 },
    {
      field: 'accidentType',
      title: '事故类型',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_SAFETY_ACCIDENT_TYPE },
      },
    },
    {
      field: 'riskValue',
      title: '风险值 D',
      width: 100,
      align: 'right',
      formatter: 'formatAmount2',
    },
    {
      field: 'riskLevel',
      title: '风险等级',
      width: 110,
      slots: { default: 'riskLevel' },
    },
    { field: 'chargeUserName', title: '管控责任人', width: 110 },
    {
      field: 'evaluateDate',
      title: '评估日期',
      width: 120,
    },
    {
      field: 'status',
      title: '状态',
      width: 90,
      slots: { default: 'status' },
    },
    {
      title: '操作',
      width: 200,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/safety/risk/list/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,127 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesSafetyRiskApi } from '#/api/safety/risk/list';
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 { deleteRisk, getRiskPage } from '#/api/safety/risk/list';
import { useVbenModal } from '@vben/common-ui';
import { riskLevelColor, riskLevelName, useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'SafetyRiskList' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getRiskPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesSafetyRiskApi.Risk>,
});
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
function handleEdit(row: MesSafetyRiskApi.Risk) {
  formModalApi.setData({ formType: 'update', id: row.id }).open();
}
function handleRefresh() {
  gridApi.query();
}
async function handleDelete(row: MesSafetyRiskApi.Risk) {
  await deleteRisk(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-risk:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #riskLevel="{ row }">
        <Tag :color="riskLevelColor(row.riskLevel)">
          {{ row.riskLevelName || riskLevelName(row.riskLevel) || '-' }}
        </Tag>
      </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-risk:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '删除',
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:safety-risk:delete'],
              popConfirm: {
                title: '确定删除该风险记录吗?',
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/safety/risk/list/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,121 @@
<script lang="ts" setup>
import type { MesSafetyRiskApi } from '#/api/safety/risk/list';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Alert, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createRisk, getRisk, updateRisk } from '#/api/safety/risk/list';
import { calcRiskLevel, riskLevelName, 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 riskValue = ref<number>();
const riskLevel = ref<number>();
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
  // L / E / C ä»»ä¸€å˜åŒ–时,自动重算 D ä¸Žé£Žé™©ç­‰çº§
  handleValuesChange: (values, changedFields) => {
    const lecFields = ['likelihood', 'exposure', 'consequence'];
    if (!lecFields.some((f) => changedFields.includes(f))) {
      return;
    }
    const { likelihood, exposure, consequence } = values as MesSafetyRiskApi.Risk;
    if (
      likelihood === undefined ||
      likelihood === null ||
      exposure === undefined ||
      exposure === null ||
      consequence === undefined ||
      consequence === null
    ) {
      riskValue.value = undefined;
      riskLevel.value = undefined;
      return;
    }
    const d = Number((likelihood * exposure * consequence).toFixed(2));
    riskValue.value = d;
    riskLevel.value = calcRiskLevel(d);
    formApi.setFieldValue('riskValue', d);
    formApi.setFieldValue('riskLevel', riskLevel.value);
  },
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as MesSafetyRiskApi.Risk;
    try {
      await (isUpdate.value ? updateRisk(data) : createRisk(data));
      emit('success');
      message.success('操作成功');
      await modalApi.close();
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      riskValue.value = undefined;
      riskLevel.value = undefined;
      return;
    }
    const data = modalApi.getData<{
      formType: 'create' | 'update';
      id?: number;
    }>();
    formType.value = data.formType;
    await formApi.resetForm();
    if (data?.id) {
      modalApi.lock();
      try {
        const risk = await getRisk(data.id);
        await formApi.setValues(risk);
        riskValue.value = risk?.riskValue;
        riskLevel.value = risk?.riskLevel;
      } finally {
        modalApi.unlock();
      }
    } else {
      formApi.setFieldValue('status', 1);
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-2/3">
    <Form class="mx-4" />
    <div v-if="riskValue !== undefined" class="mx-4 mt-2">
      <Alert
        :type="riskLevel === 1 ? 'error' : riskLevel === 2 ? 'warning' : 'info'"
        show-icon
        :message="`风险评估结果:D = ${riskValue},判定为「${riskLevelName(riskLevel)}」`"
      />
    </div>
  </Modal>
</template>
src/views/safety/risk/shared.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
import { requestClient } from '#/api/request';
/**
 * è½¦é—´ä¸‹æ‹‰é€‰é¡¹
 *
 * å¤ç”¨èƒ½è€—模块已提供的车间选项接口(返回 MES è½¦é—´è¡¨çš„基础数据)
 */
export function getWorkshopOptions() {
  return requestClient.get<Array<{ code?: string; id: number; name: string }>>(
    '/mes/em/meter/workshop-options',
  );
}
/**
 * ç”¨æˆ·ç²¾ç®€åˆ—表(用于责任人下拉)
 */
export function getSimpleUserList() {
  return requestClient.get<
    Array<{ id: number; nickname: string; username?: string }>
  >('/system/user/simple-list');
}
src/views/safety/risk/unit/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,178 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesSafetyRiskUnitApi } from '#/api/safety/risk/unit';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { z } from '#/adapter/form';
import { getSimpleUserList, getWorkshopOptions } from '../shared';
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '单元编码',
      component: 'Input',
      componentProps: { allowClear: true, placeholder: '请输入风险单元编码' },
    },
    {
      fieldName: 'name',
      label: '单元名称',
      component: 'Input',
      componentProps: { allowClear: true, placeholder: '请输入风险单元名称' },
    },
    {
      fieldName: 'riskCategory',
      label: '风险类别',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_SAFETY_RISK_CATEGORY, 'number'),
        placeholder: '请选择风险类别',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_SAFETY_UNIT_STATUS, 'number'),
        placeholder: '请选择状态',
      },
    },
  ];
}
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'code',
      label: '单元编码',
      component: 'Input',
      componentProps: { placeholder: '如 FX-001' },
      rules: 'required',
    },
    {
      fieldName: 'name',
      label: '单元名称',
      component: 'Input',
      componentProps: { placeholder: '如 é€ çº¸è½¦é—´1#烘缸' },
      rules: 'required',
    },
    {
      fieldName: 'workshopId',
      label: '所属车间',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getWorkshopOptions,
        labelField: 'name',
        valueField: 'id',
        placeholder: '请选择所属车间',
      },
    },
    {
      fieldName: 'location',
      label: '具体位置',
      component: 'Input',
      componentProps: { placeholder: '如 é€ çº¸è½¦é—´ä¸œä¾§' },
    },
    {
      fieldName: 'riskCategory',
      label: '风险类别',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_SAFETY_RISK_CATEGORY, 'number'),
        placeholder: '请选择风险类别',
      },
    },
    {
      fieldName: 'chargeUserId',
      label: '责任人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        valueField: 'id',
        placeholder: '请选择责任人',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: {
        buttonStyle: 'solid',
        optionType: 'button',
        options: getDictOptions(DICT_TYPE.MES_SAFETY_UNIT_STATUS, 'number'),
      },
      rules: z.number().default(1),
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: { placeholder: '请输入备注', rows: 3 },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesSafetyRiskUnitApi.RiskUnit>['columns'] {
  return [
    { field: 'code', title: '单元编码', width: 130 },
    { field: 'name', title: '单元名称', minWidth: 180 },
    { field: 'workshopName', title: '所属车间', width: 120 },
    { field: 'location', title: '具体位置', minWidth: 150 },
    {
      field: 'riskCategory',
      title: '风险类别',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_SAFETY_RISK_CATEGORY },
      },
    },
    { field: 'chargeUserName', title: '责任人', width: 100 },
    {
      field: 'riskCount',
      title: '风险条数',
      width: 100,
      align: 'right',
      slots: { default: 'riskCount' },
    },
    {
      field: 'status',
      title: '状态',
      width: 90,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_SAFETY_UNIT_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 160,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/safety/risk/unit/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,126 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesSafetyRiskUnitApi } from '#/api/safety/risk/unit';
import { Page } from '#/packages/effects/common-ui/src';
import { Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteRiskUnit, getRiskUnitPage } from '#/api/safety/risk/unit';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'SafetyRiskUnit' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getRiskUnitPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesSafetyRiskUnitApi.RiskUnit>,
});
/** æ–°å¢ž */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** ç¼–辑 */
function handleEdit(row: MesSafetyRiskUnitApi.RiskUnit) {
  formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** åˆ·æ–° */
function handleRefresh() {
  gridApi.query();
}
/** åˆ é™¤ */
async function handleDelete(row: MesSafetyRiskUnitApi.RiskUnit) {
  await deleteRiskUnit(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-risk-unit:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #riskCount="{ row }">
        <Tag v-if="row.riskCount > 0" color="processing">{{ row.riskCount }}</Tag>
        <span v-else class="text-gray-400">0</span>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '编辑',
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:safety-risk-unit:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '删除',
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:safety-risk-unit:delete'],
              popConfirm: {
                title: '确定删除该风险单元吗?',
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/safety/risk/unit/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
<script lang="ts" setup>
import type { MesSafetyRiskUnitApi } from '#/api/safety/risk/unit';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createRiskUnit,
  getRiskUnit,
  updateRiskUnit,
} from '#/api/safety/risk/unit';
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 MesSafetyRiskUnitApi.RiskUnit;
    try {
      await (isUpdate.value
        ? updateRiskUnit(data)
        : createRiskUnit(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 unit = await getRiskUnit(data.id);
        await formApi.setValues(unit);
      } finally {
        modalApi.unlock();
      }
    } else {
      formApi.setFieldValue('status', 1);
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-1/2">
    <Form class="mx-4" />
  </Modal>
</template>