Explorar o código

Merge branch 'dev-wyf' into 'dev'

演练记录&签到

See merge request product-group-fe/sfy-safety-group/sfy-safety!143
yunfeng wu hai 9 meses
pai
achega
fa0339c555
Modificáronse 23 ficheiros con 1226 adicións e 304 borrados
  1. 38 0
      src/api/emergency-drill/emergency-drill.ts
  2. 5 0
      src/components/Qrcode/index.ts
  3. 114 0
      src/components/Qrcode/src/Qrcode.vue
  4. 37 0
      src/components/Qrcode/src/drawCanvas.ts
  5. 88 0
      src/components/Qrcode/src/drawLogo.ts
  6. 4 0
      src/components/Qrcode/src/qrcodePlus.ts
  7. 10 0
      src/components/Qrcode/src/toCanvas.ts
  8. 39 0
      src/components/Qrcode/src/typing.ts
  9. 16 3
      src/views/emergency/emergency-drill/PageDrillPlanItem.vue
  10. 72 48
      src/views/emergency/emergency-drill/PageDrillPlanList.vue
  11. 135 0
      src/views/emergency/emergency-drill/components/DrillApprovalDialog.vue
  12. 22 4
      src/views/emergency/emergency-drill/components/DrillPlanCreateItem.vue
  13. 6 6
      src/views/emergency/emergency-drill/components/DrillPlanExecuteForm.vue
  14. 103 51
      src/views/emergency/emergency-drill/components/DrillPlanExecuteItem.vue
  15. 143 0
      src/views/emergency/emergency-drill/components/DrillPlanRecordForm.vue
  16. 56 2
      src/views/emergency/emergency-drill/components/DrillPlanRecordItem.vue
  17. 161 64
      src/views/emergency/emergency-drill/components/DrillPlanViewActivities.vue
  18. 45 0
      src/views/emergency/emergency-drill/components/ShowImages.vue
  19. 58 15
      src/views/emergency/emergency-drill/configs/plan/form.ts
  20. 2 16
      src/views/emergency/emergency-drill/configs/plan/search.ts
  21. 4 2
      src/views/emergency/emergency-drill/configs/plan/table.ts
  22. 1 73
      src/views/emergency/emergency-drill/constants.ts
  23. 67 20
      src/views/emergency/emergency-drill/types.ts

+ 38 - 0
src/api/emergency-drill/emergency-drill.ts

@@ -7,6 +7,7 @@ import {
   DrillPlanItemDetail,
   DrillPlanRecord,
   ExecuteDrillPlanRuleForm,
+  DrillRecordRuleForm,
 } from '@/views/emergency/emergency-drill/types';
 import type { DrillSignlistQuery, DrillSignListResponse } from '@/types/emergency-drill';
 
@@ -67,6 +68,28 @@ export const submitEmergencyDrillExecute = (params: ExecuteDrillPlanRuleForm) =>
   });
 };
 
+export const saveEmergencyDrillRecord = (params: DrillRecordRuleForm) => {
+  return http.request({
+    url: '/emergencyDrill/saveEmergencyDrillRecord',
+    method: 'POST',
+    data: {
+      ...params,
+      saveOrSubmit: 0,
+    },
+  });
+};
+
+export const submitEmergencyDrillRecord = (params: DrillRecordRuleForm) => {
+  return http.request({
+    url: '/emergencyDrill/saveEmergencyDrillRecord',
+    method: 'POST',
+    data: {
+      ...params,
+      saveOrSubmit: 1,
+    },
+  });
+};
+
 export const deleteEmergencyDrillPlan = (id) => {
   return http.request({
     url: `/emergencyDrill/deleteEmergencyDrillPlan?drillPlanId=${id}`,
@@ -123,3 +146,18 @@ export const signDrillScript = (data: { drillPlanId: number; planToParticipateCo
     data,
   });
 };
+
+/**
+ * 查询审批模板
+ */
+export const queryDrillApproval = (approvalTemplateId: number, drillId: number) => {
+  return http.request<any>({
+    url: `/approvalManagement/queryApprovalProcess`,
+    method: 'post',
+    data: {
+      approvalTemplateId,
+      sourceType: 2,
+      sourceId: drillId,
+    },
+  });
+};

+ 5 - 0
src/components/Qrcode/index.ts

@@ -0,0 +1,5 @@
+import { withInstall } from '@/utils';
+import qrCode from './src/Qrcode.vue';
+
+export const QrCode = withInstall(qrCode);
+export * from './src/typing';

+ 114 - 0
src/components/Qrcode/src/Qrcode.vue

@@ -0,0 +1,114 @@
+<template>
+  <div>
+    <component :is="tag" ref="wrapRef" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, watch, PropType, ref, unref, onMounted } from 'vue';
+  import { toCanvas, QRCodeRenderersOptions, LogoType } from './qrcodePlus';
+  import { toDataURL } from 'qrcode';
+  import { downloadByUrl } from '@/utils/file/download';
+  import { QrcodeDoneEventParams } from './typing';
+
+  export default defineComponent({
+    name: 'QrCode',
+    props: {
+      value: {
+        type: [String, Array] as PropType<string | any[]>,
+        default: null,
+      },
+      // 参数
+      options: {
+        type: Object as PropType<QRCodeRenderersOptions>,
+        default: null,
+      },
+      // 宽度
+      width: {
+        type: Number as PropType<number>,
+        default: 200,
+      },
+      // 中间logo图标
+      logo: {
+        type: [String, Object] as PropType<Partial<LogoType> | string>,
+        default: '',
+      },
+      // img 不支持内嵌logo
+      tag: {
+        type: String as PropType<'canvas' | 'img'>,
+        default: 'canvas',
+        validator: (v: string) => ['canvas', 'img'].includes(v),
+      },
+    },
+    emits: { done: (data: QrcodeDoneEventParams) => !!data, error: (error: any) => !!error },
+    setup(props, { emit }) {
+      const wrapRef = ref<HTMLCanvasElement | HTMLImageElement | null>(null);
+
+      async function createQrcode() {
+        try {
+          const { tag, value, options = {}, width, logo } = props;
+          const renderValue = String(value);
+          const wrapEl = unref(wrapRef);
+
+          if (!wrapEl) return;
+
+          if (tag === 'canvas') {
+            const url: string = await toCanvas({
+              canvas: wrapEl,
+              width,
+              logo: logo as any,
+              content: renderValue,
+              options: options || {},
+            });
+            emit('done', { url, ctx: (wrapEl as HTMLCanvasElement).getContext('2d') });
+            return;
+          }
+
+          if (tag === 'img') {
+            const url = await toDataURL(renderValue, {
+              errorCorrectionLevel: 'H',
+              width,
+              ...options,
+            });
+            (unref(wrapRef) as HTMLImageElement).src = url;
+            emit('done', { url });
+          }
+        } catch (error) {
+          emit('error', error);
+        }
+      }
+
+      /**
+       * file download
+       */
+      function download(fileName?: string) {
+        let url = '';
+        const wrapEl = unref(wrapRef);
+        if (wrapEl instanceof HTMLCanvasElement) {
+          url = wrapEl.toDataURL();
+        } else if (wrapEl instanceof HTMLImageElement) {
+          url = wrapEl.src;
+        }
+        if (!url) return;
+        downloadByUrl({
+          url,
+          fileName,
+        });
+      }
+
+      onMounted(createQrcode);
+
+      // 监听参数变化重新生成二维码
+      watch(
+        props,
+        () => {
+          createQrcode();
+        },
+        {
+          deep: true,
+        },
+      );
+
+      return { wrapRef, download };
+    },
+  });
+</script>

+ 37 - 0
src/components/Qrcode/src/drawCanvas.ts

@@ -0,0 +1,37 @@
+import { toCanvas } from 'qrcode';
+import type { QRCodeRenderersOptions } from 'qrcode';
+import { RenderQrCodeParams, ContentType } from './typing';
+import { cloneDeep } from 'lodash-es';
+
+export const renderQrCode = ({
+  canvas,
+  content,
+  width = 0,
+  options: params = {},
+}: RenderQrCodeParams) => {
+  const options = cloneDeep(params);
+  // 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
+  options.errorCorrectionLevel = options.errorCorrectionLevel || getErrorCorrectionLevel(content);
+
+  return getOriginWidth(content, options).then((_width: number) => {
+    options.scale = width === 0 ? undefined : (width / _width) * 4;
+    return toCanvas(canvas, content, options);
+  });
+};
+
+// 得到原QrCode的大小,以便缩放得到正确的QrCode大小
+function getOriginWidth(content: ContentType, options: QRCodeRenderersOptions) {
+  const _canvas = document.createElement('canvas');
+  return toCanvas(_canvas, content, options).then(() => _canvas.width);
+}
+
+// 对于内容少的QrCode,增大容错率
+function getErrorCorrectionLevel(content: ContentType) {
+  if (content.length > 36) {
+    return 'M';
+  } else if (content.length > 16) {
+    return 'Q';
+  } else {
+    return 'H';
+  }
+}

+ 88 - 0
src/components/Qrcode/src/drawLogo.ts

@@ -0,0 +1,88 @@
+import { isString } from '@/utils/is';
+import { RenderQrCodeParams, LogoType } from './typing';
+export const drawLogo = ({ canvas, logo }: RenderQrCodeParams) => {
+  if (!logo) {
+    return new Promise((resolve) => {
+      resolve((canvas as HTMLCanvasElement).toDataURL());
+    });
+  }
+  const canvasWidth = (canvas as HTMLCanvasElement).width;
+  const {
+    logoSize = 0.15,
+    bgColor = '#ffffff',
+    borderSize = 0.05,
+    crossOrigin,
+    borderRadius = 8,
+    logoRadius = 0,
+  } = logo as LogoType;
+
+  const logoSrc: string = isString(logo) ? logo : logo.src;
+  const logoWidth = canvasWidth * logoSize;
+  const logoXY = (canvasWidth * (1 - logoSize)) / 2;
+  const logoBgWidth = canvasWidth * (logoSize + borderSize);
+  const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2;
+
+  const ctx = canvas.getContext('2d');
+  if (!ctx) return;
+
+  // logo 底色
+  canvasRoundRect(ctx)(logoBgXY, logoBgXY, logoBgWidth, logoBgWidth, borderRadius);
+  ctx.fillStyle = bgColor;
+  ctx.fill();
+
+  // logo
+  const image = new Image();
+  if (crossOrigin || logoRadius) {
+    image.setAttribute('crossOrigin', crossOrigin || 'anonymous');
+  }
+  image.src = logoSrc;
+
+  // 使用image绘制可以避免某些跨域情况
+  const drawLogoWithImage = (image: CanvasImageSource) => {
+    ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
+  };
+
+  // 使用canvas绘制以获得更多的功能
+  const drawLogoWithCanvas = (image: HTMLImageElement) => {
+    const canvasImage = document.createElement('canvas');
+    canvasImage.width = logoXY + logoWidth;
+    canvasImage.height = logoXY + logoWidth;
+    const imageCanvas = canvasImage.getContext('2d');
+    if (!imageCanvas || !ctx) return;
+    imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
+
+    canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius);
+    if (!ctx) return;
+    const fillStyle = ctx.createPattern(canvasImage, 'no-repeat');
+    if (fillStyle) {
+      ctx.fillStyle = fillStyle;
+      ctx.fill();
+    }
+  };
+
+  // 将 logo绘制到 canvas上
+  return new Promise((resolve) => {
+    image.onload = () => {
+      logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image);
+      resolve((canvas as HTMLCanvasElement).toDataURL());
+    };
+  });
+};
+
+// copy来的方法,用于绘制圆角
+function canvasRoundRect(ctx: CanvasRenderingContext2D) {
+  return (x: number, y: number, w: number, h: number, r: number) => {
+    const minSize = Math.min(w, h);
+    if (r > minSize / 2) {
+      r = minSize / 2;
+    }
+    ctx.beginPath();
+    ctx.moveTo(x + r, y);
+    ctx.arcTo(x + w, y, x + w, y + h, r);
+    ctx.arcTo(x + w, y + h, x, y + h, r);
+    ctx.arcTo(x, y + h, x, y, r);
+    ctx.arcTo(x, y, x + w, y, r);
+    ctx.closePath();
+    return ctx;
+  };
+}

+ 4 - 0
src/components/Qrcode/src/qrcodePlus.ts

@@ -0,0 +1,4 @@
+// 参考 qr-code-with-logo 进行ts版本修改
+import { toCanvas } from './toCanvas';
+export * from './typing';
+export { toCanvas };

+ 10 - 0
src/components/Qrcode/src/toCanvas.ts

@@ -0,0 +1,10 @@
+import { renderQrCode } from './drawCanvas';
+import { drawLogo } from './drawLogo';
+import { RenderQrCodeParams } from './typing';
+export const toCanvas = (options: RenderQrCodeParams) => {
+  return renderQrCode(options)
+    .then(() => {
+      return options;
+    })
+    .then(drawLogo) as Promise<string>;
+};

+ 39 - 0
src/components/Qrcode/src/typing.ts

@@ -0,0 +1,39 @@
+import type { QRCodeSegment, QRCodeRenderersOptions } from 'qrcode';
+import { Fn } from '/#/index';
+
+export type ContentType = string | QRCodeSegment[];
+
+export type { QRCodeRenderersOptions };
+
+export type LogoType = {
+  src: string;
+  logoSize: number;
+  borderColor: string;
+  bgColor: string;
+  borderSize: number;
+  crossOrigin: string;
+  borderRadius: number;
+  logoRadius: number;
+};
+
+export interface RenderQrCodeParams {
+  canvas: any;
+  content: ContentType;
+  width?: number;
+  options?: QRCodeRenderersOptions;
+  logo?: LogoType | string;
+  image?: HTMLImageElement;
+  downloadName?: string;
+  download?: boolean | Fn;
+}
+
+export type ToCanvasFn = (options: RenderQrCodeParams) => Promise<unknown>;
+
+export interface QrCodeActionType {
+  download: (fileName?: string) => void;
+}
+
+export interface QrcodeDoneEventParams {
+  url: string;
+  ctx?: CanvasRenderingContext2D | null;
+}

+ 16 - 3
src/views/emergency/emergency-drill/PageDrillPlanItem.vue

@@ -66,9 +66,10 @@
   });
 
   const dynamicComponentRef = ref();
+  // 创建演练计划页 提交按钮
   const createSubmit = async () => {
     if (!dynamicComponentRef.value) return;
-    const res = await dynamicComponentRef.value.handleValidate();
+    const res = await dynamicComponentRef.value.formValidate();
     if (!res) return;
     const formData = dynamicComponentRef.value.getFormData();
     let message;
@@ -85,19 +86,31 @@
     }
   };
 
+  // 演练执行页 保存按钮
   const executeSave = async () => {
     formLoading.value = true;
     await dynamicComponentRef.value.executeSaveOrSubmit('save');
     formLoading.value = false;
   };
+
+  // 演练执行页 提交按钮
   const executeSubmit = async () => {
     formLoading.value = true;
     await dynamicComponentRef.value.executeSaveOrSubmit('submit');
     formLoading.value = false;
   };
 
-  const recordSave = async () => {};
-  const recordSubmit = async () => {};
+  // 演练记录页 保存按钮
+  const recordSave = async () => {
+    formLoading.value = true;
+    await dynamicComponentRef.value.saveDrillRecord();
+    formLoading.value = false;
+  };
+
+  // 演练记录页 提交按钮
+  const recordSubmit = async () => {
+    await dynamicComponentRef.value.startSubmitDrillRecord();
+  };
 </script>
 
 <style scoped lang="scss">

+ 72 - 48
src/views/emergency/emergency-drill/PageDrillPlanList.vue

@@ -15,7 +15,18 @@
             :searchConfig="DRILL_PLAN_LIST_SEARCH_CONFIG"
             :searchData="searchData"
             @update:searchData="handleSearch"
-          ></BasicSearch>
+          >
+            <template #drillScope>
+              <el-select v-model="searchData.drillScope" placeholder="全部" filterable>
+                <el-option
+                  v-for="item in [{ itemCode: undefined, itemValue: '全部' }, ...drillScopeDice]"
+                  :key="item.itemCode"
+                  :label="item.itemValue"
+                  :value="item.itemCode"
+                />
+              </el-select>
+            </template>
+          </BasicSearch>
         </header>
         <!-- 表格 -->
         <BasicTable
@@ -24,6 +35,12 @@
           @update:pageSize="handleSizeChange"
           @update:pageNumber="handleCurrentChange"
         >
+          <template #drillScope="scope">
+            <div>{{ decodeScope(scope.row.drillScope) }}</div>
+          </template>
+          <template #status="scope">
+            <div>{{ decodeStatus(scope.row.status) }}</div>
+          </template>
           <template #action="scope">
             <div class="action-container--div">
               <ActionButton text="查看" @click="handleViewDrillPlan(scope.row.id)"></ActionButton>
@@ -61,54 +78,14 @@
   import BasicTable from '@/components/BasicTable.vue';
   import ActionButton from '@/components/ActionButton.vue';
   import useTableConfig from '@/hooks/useTableConfigHook';
-
+  import { queryEnergencyDrillPlanList, deleteEmergencyDrillPlan } from '@/api/emergency-drill/emergency-drill';
   import { DRILL_PLAN_LIST_SEARCH_CONFIG } from './configs/plan/search';
   import { TABLE_OPTIONS, DRILL_PLAN_LIST_TABLE_COLUMNS } from './configs/plan/table';
-
   import { DrillPlanListSearch, DrillPlanItem } from './types';
-
-  import { queryEnergencyDrillPlanList, deleteEmergencyDrillPlan } from '@/api/emergency-drill/emergency-drill';
-  import { EMERGENCY_DRILL_STATUS_DICT, EMERGENCY_DRILL_SCOPE_LABEL } from './constants';
+  import { EMERGENCY_DRILL_STATUS_DICT } from './constants';
+  import { useEmergencyDrillHook } from './hook';
 
   const router = useRouter();
-  // 按钮操作
-  function handleCreateDrillPlan() {
-    router.push({
-      name: 'emergency-drill-plan-item',
-      query: {
-        operate: 'create',
-      },
-    });
-  }
-
-  function handleViewDrillPlan(id: number) {
-    router.push({
-      name: 'emergency-drill-plan-view',
-      query: {
-        id: id,
-      },
-    });
-  }
-
-  function handleToExecute(id: number) {
-    router.push({
-      name: 'emergency-drill-plan-item',
-      query: {
-        id: id,
-        operate: 'execute',
-      },
-    });
-  }
-
-  function handleToRecord(id: number) {
-    router.push({
-      name: 'emergency-drill-plan-item',
-      query: {
-        id: id,
-        operate: 'record',
-      },
-    });
-  }
 
   function handleDeleteDrillPlan(id: number) {
     deleteEmergencyDrillPlan(id).then(() => {
@@ -145,15 +122,22 @@
     getTabelData();
   };
 
+  // 解析演练规模字典
+  const { drillScopeDice, getDrillScopeDict } = useEmergencyDrillHook();
+  function decodeScope(code: string) {
+    return drillScopeDice.value.find((x) => x.itemCode === code)?.itemValue;
+  }
+  // 解析演练状态
+  function decodeStatus(status: number) {
+    return EMERGENCY_DRILL_STATUS_DICT[status];
+  }
+
   async function getTabelData() {
     tableConfig.loading = true;
-
     const res = await queryEnergencyDrillPlanList(tabelQuery.value);
     res.records.forEach((item) => {
-      item.drillScope = EMERGENCY_DRILL_SCOPE_LABEL[item.drillScope];
       item.responsibleDeptNameList = item.responsibleDeptNameList.replace(/^\[|\]$/g, '');
       item.coordinateDeptNameList = item.coordinateDeptNameList?.replace(/^\[|\]$/g, '');
-      item.statusLabel = EMERGENCY_DRILL_STATUS_DICT[item.status];
     });
     tableData.value = res.records;
     pagination.total = res.totalRow;
@@ -161,9 +145,49 @@
   }
 
   // 初始化
-  onMounted(() => {
+  onMounted(async () => {
+    await getDrillScopeDict();
     getTabelData();
   });
+
+  // 按钮操作
+  function handleCreateDrillPlan() {
+    router.push({
+      name: 'emergency-drill-plan-item',
+      query: {
+        operate: 'create',
+      },
+    });
+  }
+
+  function handleViewDrillPlan(id: number) {
+    router.push({
+      name: 'emergency-drill-plan-view',
+      query: {
+        id: id,
+      },
+    });
+  }
+
+  function handleToExecute(id: number) {
+    router.push({
+      name: 'emergency-drill-plan-item',
+      query: {
+        id: id,
+        operate: 'execute',
+      },
+    });
+  }
+
+  function handleToRecord(id: number) {
+    router.push({
+      name: 'emergency-drill-plan-item',
+      query: {
+        id: id,
+        operate: 'record',
+      },
+    });
+  }
 </script>
 
 <style scoped lang="scss">

+ 135 - 0
src/views/emergency/emergency-drill/components/DrillApprovalDialog.vue

@@ -0,0 +1,135 @@
+<template>
+  <BasicDialog ref="basicDialogRef" title="提交审批" @refresh="refreshFromData">
+    <template #form>
+      <el-form ref="formRef" :model="formData" label-width="auto">
+        <el-form-item prop="approvalDescription" label="审批描述">
+          <el-input v-model="formData.approvalDescription" placeholder="请输入审批描述" />
+        </el-form-item>
+        <el-form-item
+          v-for="(item, index) in formData.approverListInfo"
+          :key="item.approvalOrder"
+          :label="`第${item.approvalOrder + 1}步,${item.deptName}`"
+          :prop="'approverListInfo.' + index + '.approverName'"
+          :rules="{ required: true, message: '请选择审批人', trigger: 'change' }"
+        >
+          <el-select
+            v-model="item.approverName"
+            placeholder="请输入审批人"
+            value-key="id"
+            filterable
+            remote
+            :disabled="formDisabled"
+            :remote-method="remoteMethod"
+            :loading="loading"
+            @change="(value) => selectKeeper(value, index)"
+          >
+            <el-option
+              v-for="item in userOptions"
+              :key="item.id"
+              :label="`${item.realname}(${item.username})${item.deptName}`"
+              :value="item"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+    </template>
+    <template #footer>
+      <el-button type="primary" @click="handleSumbit">提交</el-button>
+      <el-button @click="handleCancel">取消</el-button>
+    </template>
+  </BasicDialog>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, onMounted } from 'vue';
+  import BasicDialog from '@/components/BasicDialog.vue';
+  import { DrillApprovalItem, DrillApprovalRuleForm } from '../types';
+  import type { QueryUserInfoByUserNameRes } from '@/types/person-group/type';
+  import { queryUserInfoByUserName } from '@/api/system/person-group';
+  import { FormInstance } from 'element-plus';
+  import { rules } from '@/types/camera/constant';
+
+  const props = defineProps<{
+    approvalData: DrillApprovalItem;
+  }>();
+  const emits = defineEmits<{
+    (e: 'success'): void;
+  }>();
+
+  const basicDialogRef = ref<InstanceType<typeof BasicDialog>>();
+  const formRef = ref<FormInstance>();
+
+  const openDialog = () => {
+    initForm();
+    basicDialogRef.value?.openDialog();
+  };
+
+  const formDisabled = ref(props.approvalData.approverType === 0);
+
+  const formData = reactive<DrillApprovalRuleForm>({
+    approvalDescription: '',
+    approverListInfo: [],
+  });
+
+  function initForm() {
+    props.approvalData.processInfoList.forEach((x, index) => {
+      formData.approverListInfo.push({
+        approvalOrder: index,
+        deptName: x.approvalDeptName,
+        approverId: x.approverId,
+        approverName: x.approverName,
+      });
+    });
+  }
+
+  function clearForm() {
+    formData.approvalDescription = '';
+    formData.approverListInfo = [];
+  }
+
+  const handleSumbit = async () => {
+    const validate = await new Promise((resolve) => {
+      formRef.value?.validate((valid: boolean) => {
+        resolve(valid);
+      });
+    });
+    if (!validate) return;
+    console.log(formData);
+    // 提交表单接口
+    // emits('success');
+  };
+
+  const handleCancel = () => {
+    if (!basicDialogRef.value) return;
+    clearForm();
+    basicDialogRef.value.closeDialog();
+  };
+
+  // 人员选择
+  const userOptions = ref<QueryUserInfoByUserNameRes[]>([]);
+  const loading = ref<boolean>(false);
+  const remoteMethod = async (query: string) => {
+    if (!query) {
+      userOptions.value = [];
+      return;
+    }
+    loading.value = true;
+    userOptions.value = await queryUserInfoByUserName(query);
+    loading.value = false;
+  };
+  function selectKeeper(value, index) {
+    if (!value) return;
+    formData.approverListInfo[index].approverId = value.id;
+    formData.approverListInfo[index].approverName = value.realname;
+  }
+
+  const refreshFromData = () => {
+    formRef.value?.clearValidate();
+  };
+
+  defineExpose({
+    openDialog,
+  });
+</script>
+
+<style scoped></style>

+ 22 - 4
src/views/emergency/emergency-drill/components/DrillPlanCreateItem.vue

@@ -1,6 +1,16 @@
 <template>
   <div>
     <BasicForm ref="basicFormRef" :formData="ruleFormData" :formRules="formRules" :formConfig="ruleFormConfig">
+      <template #drillScope>
+        <el-select v-model="ruleFormData.drillScope" placeholder="请选择演练规模" filterable>
+          <el-option
+            v-for="item in drillScopeDice"
+            :key="item.itemCode"
+            :label="item.itemValue"
+            :value="item.itemCode"
+          />
+        </el-select>
+      </template>
       <template #responsibleDeptIdList>
         <el-cascader
           v-model="ruleFormData.responsibleDeptIdList"
@@ -66,48 +76,56 @@
   import { getAllDepartments } from '@/api/auth/dept';
   import { getAllApproval } from '@/api/approval/approval';
   import { ApprovalInstanceType } from '@/views/system/approval/types';
+  import { useEmergencyDrillHook } from '../hook';
 
+  // 表单配置初始化
   const basicFormRef = ref<InstanceType<typeof BasicForm>>();
-
   const { ruleFormConfig, ruleFormData, formRules, cloneRuleFormData, beforeRouteLeave } =
     useFormConfigHook<CreateEmergencyDrillRuleForm>(
       DRILL_CREATE_FORM_CONFIG,
       DRILL_CREATE_FORM_DATA,
       DRILL_CREATE_FORM_RULES,
     );
-
   const cascaderProp = { multiple: true, expandTrigger: 'hover', emitPath: false, value: 'id', label: 'deptName' };
 
+  // 获取级联部门数据
   const deptTree = ref<DeptTree[]>();
   const loadDeptTreeData = async () => {
     const result = await getAllDepartments();
     deptTree.value = result;
   };
 
+  // 获取所有审批流程
   const allApprovals = ref<ApprovalInstanceType[]>();
   const loadApprovalData = async () => {
     const result = await getAllApproval();
     allApprovals.value = result;
   };
 
+  // 获取演练规模字典
+  const { drillScopeDice, getDrillScopeDict } = useEmergencyDrillHook();
+
   onMounted(() => {
+    getDrillScopeDict();
     loadDeptTreeData();
     loadApprovalData();
   });
 
-  const handleValidate = async () => {
+  // 表单校验
+  const formValidate = async () => {
     if (!basicFormRef.value) return;
     const parentValidateResult = await basicFormRef.value.validateForm();
     return parentValidateResult;
   };
 
+  // 表单数据获取
   const getFormData = () => {
     cloneRuleFormData();
     return ruleFormData;
   };
 
   defineExpose({
-    handleValidate,
+    formValidate,
     getFormData,
   });
 </script>

+ 6 - 6
src/views/emergency/emergency-drill/components/DrillPlanExecuteForm.vue

@@ -68,7 +68,7 @@
   import UploadFiles from '@/views/disaster/components/UploadFiles.vue';
 
   const props = defineProps<{
-    insCode: string;
+    insName: string;
     drillData: DrillPlanItemDetail;
   }>();
 
@@ -88,7 +88,7 @@
             drillScript: props.drillData.drillScript,
           }
         : DRILL_EXECUTE_FORM_DATA,
-      props.drillData.drillScope === props.insCode ? INS_DRILL_EXECUTE_FORM_RULES : DEP_DRILL_EXECUTE_FORM_RULES,
+      props.drillData.drillScope === props.insName ? INS_DRILL_EXECUTE_FORM_RULES : DEP_DRILL_EXECUTE_FORM_RULES,
     );
 
   const cascaderProp = { multiple: true, expandTrigger: 'hover', emitPath: false, value: 'id', label: 'deptName' };
@@ -119,13 +119,13 @@
     loadDeptTreeData();
   });
 
-  const handleValidate = async () => {
+  const formValidate = async () => {
     if (!basicFormRef.value) return;
     const validateResult = await basicFormRef.value.validateForm();
     return validateResult;
   };
 
-  const handleClearValidate = () => {
+  const formClearValidate = () => {
     if (!basicFormRef.value) return;
     basicFormRef.value.clearValidate();
   };
@@ -135,8 +135,8 @@
   }
 
   defineExpose({
-    handleValidate,
-    handleClearValidate,
+    formValidate,
+    formClearValidate,
     getFormData,
   });
 

+ 103 - 51
src/views/emergency/emergency-drill/components/DrillPlanExecuteItem.vue

@@ -1,21 +1,82 @@
 <template>
   <div v-if="drillData">
-    <div>
-      <div class="name">演练活动</div>
-      <div class="data">
-        <div v-for="item in DRILL_VIEW_CONTENT" :key="item.value" class="item">
-          <span class="label">{{ item.label }}</span>
-          <a v-if="item.link" class="link" :href="item.link">{{ drillData![item.value] }}</a>
-          <span v-else class="value"> {{ drillData![item.value] }}</span>
-        </div>
+    <div class="drill-activity-container">
+      <div class="drill-container__title">
+        <div class="drill-container--line"></div>
+        <span>演练活动</span>
+      </div>
+      <div class="drill-container__content">
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">演练规模:</span>
+              <span class="value">{{ getDrillScope(drillData.drillScope) }}</span>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">演练内容:</span>
+              <span class="value">{{ drillData.drillContent }}</span>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">计划完成时间:</span>
+              <span class="value">{{ drillData.dueCompleteTime }}</span>
+            </div>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">责任部门:</span>
+              <template v-for="(dept, index) in safatyJsonParse(drillData.responsibleDeptNameList)" :key="index">
+                <span class="value">
+                  {{ dept }}
+                  <span v-if="index !== safatyJsonParse(drillData.responsibleDeptNameList).length - 1">、</span>
+                </span>
+              </template>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="drill-container__content--item" v-if="drillData.coordinateDeptNameList">
+              <span class="label">配合部门:</span>
+              <template v-for="(dept, index) in safatyJsonParse(drillData.coordinateDeptNameList)" :key="index">
+                <span class="value">
+                  {{ dept }}
+                  <span v-if="index !== safatyJsonParse(drillData.coordinateDeptNameList).length - 1">、</span>
+                </span>
+              </template>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">关联应急预案:</span>
+              <a v-if="emergencyPlanDetail" class="value font-primary" :href="emergencyPlanDetail.appendix">{{
+                emergencyPlanDetail.planName
+              }}</a>
+            </div>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col>
+            <div class="drill-container__content--item">
+              <span class="label">审批流程:</span>
+              <span class="value">{{ getApprovalName(drillData.approvalTemplateId) }}</span>
+            </div>
+          </el-col>
+        </el-row>
       </div>
     </div>
-    <div>
-      <div class="name">演练实施</div>
+    <div class="drill-execute-form">
+      <div class="drill-container__title">
+        <div class="drill-container--line"></div>
+        <span>演练实施</span>
+      </div>
       <DrillPlanExecuteForm
         v-if="drillData"
         ref="drillPlanExecuteFormRef"
-        :ins-code="drillScopeDice[0].itemCode"
+        :ins-name="drillScopeDice[0].itemValue"
         :drill-data="drillData"
         style="margin-top: 20px"
       />
@@ -28,7 +89,6 @@
   import { ElMessage } from 'element-plus';
   import { useRoute } from 'vue-router';
   import { DrillPlanItemDetail } from '../types';
-  import { DRILL_VIEW_CONTENT } from '../constants';
   import DrillPlanExecuteForm from './DrillPlanExecuteForm.vue';
   import {
     queryEmergencyDrillPlanDetail,
@@ -41,40 +101,43 @@
   import { uploadFileApi, UPLOAD_BIZ_TYPE } from '@/api/minio';
   import { useEmergencyDrillHook } from '../hook';
 
-  const { drillScopeDice, getDrillScopeDict } = useEmergencyDrillHook();
-
   const route = useRoute();
   const id = route.query.id;
-
+  const approvalList = ref();
+  const emergencyPlanDetail = ref();
   const drillData = ref<DrillPlanItemDetail>();
+
+  const { drillScopeDice, getDrillScopeDict, getDrillScope } = useEmergencyDrillHook();
+
+  const getApprovalList = async () => {
+    approvalList.value = await getAllApproval();
+  };
   async function getDrillData() {
     try {
       drillData.value = await queryEmergencyDrillPlanDetail(id);
 
-      // 解析详情数据
-      drillData.value.drillScope = drillScopeDice.value.find(
-        (x) => x.itemCode === drillData.value!.drillScope,
-      )?.itemValue;
-
-      drillData.value.responsibleDeptNameList = drillData.value.responsibleDeptNameList!.replace(/^\[|\]$/g, '');
-      drillData.value.coordinateDeptNameList = drillData.value.coordinateDeptNameList
-        ? drillData.value.responsibleDeptNameList!.replace(/^\[|\]$/g, '')
-        : undefined;
-
+      // 获取应急预案名
       if (drillData.value.emergencyPlanId) {
-        const emergencyPlan = await queryEmergencyPlanDetail(drillData.value.emergencyPlanId);
-        drillData.value.emergencyPlanFile = emergencyPlan.planName + emergencyPlan.appendix;
+        emergencyPlanDetail.value = await queryEmergencyPlanDetail(id);
       }
-
-      const allApprovals = await getAllApproval();
-      drillData.value.approvalTemplateName = allApprovals.find(
-        (x) => x.id === drillData.value!.approvalTemplateId,
-      )?.templateName;
     } catch (e) {
       console.log(e);
     }
   }
 
+  onMounted(async () => {
+    await getApprovalList();
+    await getDrillScopeDict();
+    getDrillData();
+  });
+
+  const getApprovalName = (id: number) => {
+    return approvalList.value.find((item) => item.id === id)?.templateName;
+  };
+  const safatyJsonParse = (str: string) => {
+    return str.slice(1, -1).split(',');
+  };
+
   const drillPlanExecuteFormRef = ref();
 
   const formatAttachmentList = async (data: FileItem[]) => {
@@ -100,7 +163,7 @@
     if (!drillPlanExecuteFormRef.value) return;
     // drillPlanExecuteFormRef.value.handleClearValidate();
     if (mode === 'submit') {
-      const res = await drillPlanExecuteFormRef.value.handleValidate();
+      const res = await drillPlanExecuteFormRef.value.formValidate();
       if (!res) return;
     }
 
@@ -127,6 +190,9 @@
       try {
         await saveEmergencyDrillExecute(executeParams);
         ElMessage.success('保存成功');
+        drillPlanExecuteFormRef.value.formClearValidate();
+        drillData.value = undefined;
+        getDrillData();
       } catch (e) {
         console.log(e);
       }
@@ -134,33 +200,19 @@
       try {
         await submitEmergencyDrillExecute(executeParams);
         ElMessage.success('提交成功');
+        drillPlanExecuteFormRef.value.formClearValidate();
+        getDrillData();
       } catch (e) {
         console.log(e);
       }
     }
   }
 
-  onMounted(() => {
-    getDrillScopeDict();
-    getDrillData();
-  });
-
   defineExpose({
     executeSaveOrSubmit,
   });
 </script>
 
-<style scoped>
-  .name {
-    margin-top: 10px;
-    padding-left: 12px;
-    border-left: 3px #1890ff solid;
-  }
-  .data {
-    display: grid;
-    grid-template-columns: repeat(auto-fill, 33%);
-  }
-  .item {
-    padding: 25px 16px;
-  }
+<style scoped lang="scss">
+  @use '../style/common.scss' as *;
 </style>

+ 143 - 0
src/views/emergency/emergency-drill/components/DrillPlanRecordForm.vue

@@ -0,0 +1,143 @@
+<template>
+  <div>
+    <BasicForm ref="basicFormRef" :formData="ruleFormData" :formRules="formRules" :formConfig="ruleFormConfig">
+      <template #drillImage>
+        <UploadImages
+          v-if="!uploadImgDisabled"
+          :maxCount="9"
+          ref="uploadImagesRef"
+          @uploadChange="handleUploadChange"
+        />
+        <ShowImages v-else :image-list="ruleFormData.drillImage" />
+      </template>
+    </BasicForm>
+  </div>
+  <DrillApprovalDialog v-if="approvalInfo" ref="drillApprovalDialogRef" :approval-data="approvalInfo" />
+</template>
+
+<script setup lang="ts">
+  import BasicForm from '@/components/BasicForm.vue';
+  import UploadImages from '@/views/disaster/disaster-control/src/components/UploadImages.vue';
+  import ShowImages from './ShowImages.vue';
+  import DrillApprovalDialog from './DrillApprovalDialog.vue';
+  import { useFormConfigHook } from '@/hooks/useFormConfigHook';
+  import { DRILL_RECORD_FORM_CONFIG, DRILL_RECORD_FORM_RULES, DRILL_RECORD_FORM_DATA } from '../configs/plan/form';
+  import { DrillApprovalItem, DrillPlanRecord, DrillRecordRuleForm } from '../types';
+  import { onMounted, ref } from 'vue';
+  import { queryDrillApproval } from '@/api/emergency-drill/emergency-drill';
+
+  const props = defineProps<{
+    drillRecord?: DrillPlanRecord;
+  }>();
+
+  const handleUploadChange = (files: File[]) => {};
+
+  const approvalInfo = ref();
+  const approvalDataExample: DrillApprovalItem = {
+    // 审批顺序为第1步
+    approvalOrder: 1,
+    // 审批人选择方式为固定(0-固定)
+    approverType: 0,
+    // 审批流程列表,包含两个审批节点
+    processInfoList: [
+      {
+        approvalDeptId: 101,
+        approvalDeptName: '技术部',
+        approverId: 1001,
+        approverName: '张明',
+        approvalType: 0, // 会签
+        approvalStatus: 1, // 已审批
+        approvalContent: '同意该方案,建议补充安全预案',
+      },
+      {
+        approvalDeptId: 102,
+        approvalDeptName: '安全管理部',
+        approverId: 1002,
+        approverName: '李华',
+        approvalType: 1, // 或签
+        approvalStatus: 2, // 待审批
+        approvalContent: '', // 待审批时无内容
+      },
+    ],
+  };
+
+  async function getApprovalInfo() {
+    // if (!props.drillRecord) return;
+    try {
+      // approvalInfo.value = await queryDrillApproval(props.drillRecord.approvalTemplateId, props.drillRecord.drillPlanId);
+      approvalInfo.value = approvalDataExample;
+      console.log(approvalInfo.value);
+    } catch (e) {
+      console.log(e);
+    }
+  }
+
+  onMounted(async () => {
+    getApprovalInfo();
+  });
+
+  // 表单
+  const basicFormRef = ref<typeof BasicForm>();
+
+  const { ruleFormConfig, ruleFormData, formRules, cloneRuleFormData, beforeRouteLeave } =
+    useFormConfigHook<DrillRecordRuleForm>(
+      DRILL_RECORD_FORM_CONFIG,
+      props.drillRecord
+        ? {
+            drillPlanId: props.drillRecord.drillPlanId,
+            participants: props.drillRecord.participants,
+            drillDescription: props.drillRecord.drillDescription,
+            drillImage: props.drillRecord.drillImage,
+            drillEffectAssess: props.drillRecord.drillEffectAssess,
+          }
+        : DRILL_RECORD_FORM_DATA,
+      DRILL_RECORD_FORM_RULES,
+    );
+
+  const formValidate = async () => {
+    if (!basicFormRef.value) return;
+    const validateResult = await basicFormRef.value.validateForm();
+    return validateResult;
+  };
+
+  const formClearValidate = () => {
+    if (!basicFormRef.value) return;
+    basicFormRef.value.clearValidate();
+  };
+
+  function getFormData() {
+    return ruleFormData;
+  }
+
+  const uploadImgDisabled = ref(false);
+  function disableFormInput() {
+    uploadImgDisabled.value = true;
+    ruleFormConfig.value.forEach((x) => {
+      if (x.componentProps) x.componentProps.disabled = true;
+    });
+  }
+  function enableFormInput() {
+    uploadImgDisabled.value = false;
+    ruleFormConfig.value.forEach((x) => {
+      if (x.componentProps) x.componentProps.disabled = false;
+    });
+  }
+
+  // 弹窗
+  const drillApprovalDialogRef = ref();
+
+  function openApprovalDialog() {
+    drillApprovalDialogRef.value.openDialog();
+  }
+
+  defineExpose({
+    formValidate,
+    formClearValidate,
+    getFormData,
+    disableFormInput,
+    enableFormInput,
+    openApprovalDialog,
+  });
+</script>
+
+<style scoped></style>

+ 56 - 2
src/views/emergency/emergency-drill/components/DrillPlanRecordItem.vue

@@ -1,7 +1,61 @@
 <template>
-  <div> 记录 </div>
+  <div>
+    <DrillPlanRecordForm ref="drillPlanRecordFormRef" :drill-record="recordData" />
+  </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import DrillPlanRecordForm from './DrillPlanRecordForm.vue';
+  import { onMounted, ref } from 'vue';
+  import { useRoute } from 'vue-router';
+  import { DrillPlanRecord } from '../types';
+  import { queryEmergencyDrillRecord, saveEmergencyDrillRecord } from '@/api/emergency-drill/emergency-drill';
+  import { ElMessage } from 'element-plus';
+
+  const route = useRoute();
+  const id = route.query.id;
+  const recordData = ref<DrillPlanRecord>();
+
+  async function getDrillData() {
+    try {
+      recordData.value = await queryEmergencyDrillRecord(id);
+    } catch (e) {
+      console.log(e);
+    }
+  }
+
+  onMounted(() => {
+    getDrillData();
+  });
+
+  const drillPlanRecordFormRef = ref();
+
+  async function saveDrillRecord() {
+    if (!drillPlanRecordFormRef) return;
+    const formData = drillPlanRecordFormRef.value.getFormData();
+    if (!formData.drillPlanId) formData.drillPlanId = Number(id);
+    try {
+      await saveEmergencyDrillRecord(formData);
+      ElMessage.success('保存成功');
+      drillPlanRecordFormRef.value.formClearValidate();
+      recordData.value = undefined;
+      getDrillData();
+    } catch (e) {
+      console.log(e);
+    }
+  }
+
+  async function startSubmitDrillRecord() {
+    if (!drillPlanRecordFormRef) return;
+    const res = await drillPlanRecordFormRef.value.formValidate();
+    if (!res) return;
+    drillPlanRecordFormRef.value.openApprovalDialog();
+  }
+
+  defineExpose({
+    saveDrillRecord,
+    startSubmitDrillRecord,
+  });
+</script>
 
 <style scoped></style>

+ 161 - 64
src/views/emergency/emergency-drill/components/DrillPlanViewActivities.vue

@@ -1,84 +1,179 @@
 <template>
   <div v-if="drillData" class="drill-plan-detail">
-    <div>
-      <div class="name">演练活动</div>
-      <div class="data">
-        <div v-for="item in DRILL_VIEW_CONTENT" :key="item.value" class="item">
-          <span class="label">{{ item.label }}</span>
-          <a v-if="item.link" class="link" :href="item.link">{{ drillData![item.value] }}</a>
-          <span v-else class="value"> {{ drillData![item.value] }}</span>
-        </div>
+    <div class="drill-activity-container">
+      <div class="drill-container__title">
+        <div class="drill-container--line"></div>
+        <span>演练活动</span>
+      </div>
+      <div class="drill-container__content">
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">演练规模:</span>
+              <span class="value">{{ getDrillScope(drillData.drillScope) }}</span>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">演练内容:</span>
+              <span class="value">{{ drillData.drillContent }}</span>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">计划完成时间:</span>
+              <span class="value">{{ drillData.dueCompleteTime }}</span>
+            </div>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">责任部门:</span>
+              <template v-for="(dept, index) in safatyJsonParse(drillData.responsibleDeptNameList)" :key="index">
+                <span class="value">
+                  {{ dept }}
+                  <span v-if="index !== safatyJsonParse(drillData.responsibleDeptNameList).length - 1">、</span>
+                </span>
+              </template>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="drill-container__content--item" v-if="drillData.coordinateDeptNameList">
+              <span class="label">配合部门:</span>
+              <template v-for="(dept, index) in safatyJsonParse(drillData.coordinateDeptNameList)" :key="index">
+                <span class="value">
+                  {{ dept }}
+                  <span v-if="index !== safatyJsonParse(drillData.coordinateDeptNameList).length - 1">、</span>
+                </span>
+              </template>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">关联应急预案:</span>
+              <a v-if="emergencyPlanDetail" class="value font-primary" :href="emergencyPlanDetail.appendix">{{
+                emergencyPlanDetail.planName
+              }}</a>
+            </div>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col>
+            <div class="drill-container__content--item">
+              <span class="label">审批流程:</span>
+              <span class="value">{{ getApprovalName(drillData.approvalTemplateId) }}</span>
+            </div>
+          </el-col>
+        </el-row>
       </div>
     </div>
-    <div>
-      <div class="name">演练实施</div>
-      <div class="data">
-        <div v-for="item in DRILL_VIEW_EXECUTE" :key="item.value" class="item">
-          <span class="label">{{ item.label }}</span>
-          <a v-if="item.link" class="link" :href="drillData![item.link]">{{ drillData![item.value] }}</a>
-          <span v-else class="value"> {{ drillData![item.value] }}</span>
-        </div>
+    <div class="drill-activity-container">
+      <div class="drill-container__title">
+        <div class="drill-container--line"></div>
+        <span>演练实施</span>
+      </div>
+      <div class="drill-container__content">
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">演练时间:</span>
+              <span class="value">{{ drillData.drillTime }}</span>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">演练地点:</span>
+              <span class="value">{{ drillData.drillLocation }}</span>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">演练负责人:</span>
+              <span class="value">{{ drillData.personInChargeName }}</span>
+            </div>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <div class="drill-container__content--item" v-if="drillData.drillScript">
+              <span class="label">演练脚本:</span>
+              <span class="value font-primary link" @click="handlePreviewScript(drillData.drillScript)">{{
+                JSON.parse(drillData.drillScript).fileName
+              }}</span>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="drill-container__content--item">
+              <span class="label">签到码:</span>
+              <el-popover placement="bottom" trigger="hover" width="224">
+                <template #reference>
+                  <span style="cursor: pointer">查看签到码</span>
+                </template>
+                <QrCode :value="qrCode" :width="200" />
+              </el-popover>
+            </div>
+          </el-col>
+        </el-row>
       </div>
     </div>
-    <div>
-      <div class="name">演练参与部门</div>
-      <el-button style="margin-top: 20px" type="primary" :icon="Download" @click=""> 下载签到名单 </el-button>
-      <BasicTable style="margin-top: 20px" :tableConfig="tableConfig" :tableData="drillData.planDetailList!">
-      </BasicTable>
+
+    <div class="drill-container__title">
+      <div class="drill-container--line"></div>
+      <span>演练参与部门</span>
     </div>
+    <el-button style="margin-top: 20px" type="primary" :icon="Download" @click=""> 下载签到名单 </el-button>
+    <BasicTable style="margin-top: 20px" :tableConfig="tableConfig" :tableData="drillData.planDetailList!">
+      <template #drillScriptStatus="scope">
+        <span>{{ scope.row.drillScriptStatus === 1 ? '未会签' : '已会签' }}</span>
+      </template>
+    </BasicTable>
   </div>
 </template>
 
 <script setup lang="ts">
   import { onMounted, ref } from 'vue';
+  import { ElPopover } from 'element-plus';
   import BasicTable from '@/components/BasicTable.vue';
   import { Download } from '@element-plus/icons-vue';
   import { useRoute } from 'vue-router';
   import { DrillPlanItemDetail } from '../types';
-  import { DRILL_VIEW_CONTENT, DRILL_VIEW_EXECUTE } from '../constants';
   import useTableConfig from '@/hooks/useTableConfigHook';
   import { getAllApproval } from '@/api/approval/approval';
   import { queryEmergencyDrillPlanDetail, queryEmergencyPlanDetail } from '@/api/emergency-drill/emergency-drill';
   import { DRILL_PLAN_ACTIVITIES_TABLE_OPTIONS, DRILL_PLAN_ACTIVITIES_TABLE_COLUMNS } from '../configs/plan/table';
   import { useEmergencyDrillHook } from '../hook';
+  import QrCode from '@/components/Qrcode/src/Qrcode.vue';
+  import PreviewOnline from '@/views/disaster/components/PreviewOnline.vue';
+  import { FILE_TYPE_ICON } from '@/views/disaster/constant';
 
   const route = useRoute();
   const id = route.query.id;
+  const approvalList = ref();
+  const emergencyPlanDetail = ref();
   const drillData = ref<DrillPlanItemDetail>();
+  const qrCode = ref();
+  const previewOnlineRef = ref<InstanceType<typeof PreviewOnline>>();
 
-  const { drillScopeDice, getDrillScopeDict } = useEmergencyDrillHook();
+  const { getDrillScopeDict, getDrillScope } = useEmergencyDrillHook();
 
-  async function getData() {
+  const getApprovalList = async () => {
+    approvalList.value = await getAllApproval();
+  };
+
+  async function getDrillData() {
     try {
       tableConfig.loading = true;
       drillData.value = await queryEmergencyDrillPlanDetail(id);
 
-      // 解析详情数据
-      drillData.value.drillScope = drillScopeDice.value.find(
-        (x) => x.itemCode === drillData.value!.drillScope,
-      )?.itemValue;
-
-      drillData.value.responsibleDeptNameList = drillData.value.responsibleDeptNameList!.replace(/^\[|\]$/g, '');
-      drillData.value.coordinateDeptNameList = drillData.value.coordinateDeptNameList
-        ? drillData.value.responsibleDeptNameList!.replace(/^\[|\]$/g, '')
-        : undefined;
-
+      // 获取应急预案名
       if (drillData.value.emergencyPlanId) {
-        const emergencyPlan = await queryEmergencyPlanDetail(drillData.value.emergencyPlanId);
-        drillData.value.emergencyPlanFile = emergencyPlan.planName + emergencyPlan.appendix;
+        emergencyPlanDetail.value = await queryEmergencyPlanDetail(id);
       }
 
-      const allApprovals = await getAllApproval();
-      drillData.value.approvalTemplateName = allApprovals.find(
-        (x) => x.id === drillData.value!.approvalTemplateId,
-      )?.templateName;
+      // 获取二维码
+      qrCode.value = 'https://cn.bing.com/?id=' + id + '&type=test';
 
-      if (drillData.value.drillScript && drillData.value.drillScript.length > 0) {
-        const scriptFile = unformatAttachment(drillData.value.drillScript);
-        console.log(scriptFile);
-        drillData.value.drillScriptName = scriptFile.fileName;
-        drillData.value.drillScriptUrl = scriptFile.fileUrl;
-      }
       tableConfig.loading = false;
     } catch (e) {
       console.log(e);
@@ -92,28 +187,30 @@
   );
 
   onMounted(async () => {
+    await getApprovalList();
     await getDrillScopeDict();
-    getData();
+    getDrillData();
   });
 
-  function unformatAttachment(file?: string) {
-    if (!file) return undefined;
-    const fileData = JSON.parse(file);
-    return fileData;
-  }
+  const getApprovalName = (id: number) => {
+    return approvalList.value.find((item) => item.id === id)?.templateName;
+  };
+  const safatyJsonParse = (str: string) => {
+    return str.slice(1, -1).split(',');
+  };
+
+  const handlePreviewScript = (str: string) => {
+    const file = JSON.parse(str);
+    const url = file.fileUrl;
+    const type = file.fileType as keyof typeof FILE_TYPE_ICON;
+    if (!url) return;
+    previewOnlineRef.value?.open(url, type);
+  };
 </script>
 
-<style scoped>
-  .name {
-    margin-top: 10px;
-    padding-left: 12px;
-    border-left: 3px #1890ff solid;
-  }
-  .data {
-    display: grid;
-    grid-template-columns: repeat(auto-fill, 33%);
-  }
-  .item {
-    padding: 25px 16px;
+<style scoped lang="scss">
+  @use '../style/common.scss' as *;
+  .link {
+    cursor: pointer;
   }
 </style>

+ 45 - 0
src/views/emergency/emergency-drill/components/ShowImages.vue

@@ -0,0 +1,45 @@
+<template>
+  <div class="show-box-container">
+    <div v-for="(image, index) in imageList" :key="index" class="image-preview">
+      <el-image
+        :src="image.url"
+        :zoom-rate="1.2"
+        :max-scale="7"
+        :min-scale="0.2"
+        :preview-src-list="imageList.map((item) => item.url)"
+        show-progress
+        :initial-index="index"
+        fit="contain"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  // import type { ImageItem } from '@/types/disaster-control';
+  import { computed } from 'vue';
+
+  const props = defineProps<{
+    imageList?: string;
+  }>();
+
+  const imageList = computed(() => {
+    if (!props.imageList) return [];
+    return JSON.parse(props.imageList);
+  });
+</script>
+
+<style scoped>
+  .show-box-container {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 5px;
+    width: 100%;
+  }
+
+  .image-preview {
+    width: 100px;
+    height: 100px;
+    border-radius: 4px;
+  }
+</style>

+ 58 - 15
src/views/emergency/emergency-drill/configs/plan/form.ts

@@ -1,24 +1,10 @@
 import { FormConfig } from '@/types/basic-form';
-import { EMERGENCY_DRILL_SCOPE, EMERGENCY_DRILL_SCOPE_DICT } from '../../constants';
 
 export const DRILL_CREATE_FORM_CONFIG: FormConfig[] = [
   {
     prop: 'drillScope',
     label: '演练规模:',
-    component: 'el-select',
-    componentProps: {
-      placeholder: '请选择演练规模',
-    },
-    selectOptions: [
-      {
-        label: EMERGENCY_DRILL_SCOPE_DICT[EMERGENCY_DRILL_SCOPE.INSTITUTE].label,
-        value: EMERGENCY_DRILL_SCOPE_DICT[EMERGENCY_DRILL_SCOPE.INSTITUTE].dict,
-      },
-      {
-        label: EMERGENCY_DRILL_SCOPE_DICT[EMERGENCY_DRILL_SCOPE.DEPT].label,
-        value: EMERGENCY_DRILL_SCOPE_DICT[EMERGENCY_DRILL_SCOPE.DEPT].dict,
-      },
-    ],
+    slot: 'drillScope',
   },
   {
     prop: 'drillContent',
@@ -163,3 +149,60 @@ export const DRILL_EXECUTE_FORM_DATA = {
   drillDeptIdList: undefined,
   drillScript: undefined,
 };
+
+export const DRILL_RECORD_FORM_CONFIG: FormConfig[] = [
+  {
+    prop: 'participants',
+    label: '参与人员:',
+    component: 'el-input',
+    componentProps: {
+      type: 'textarea',
+      maxlength: 1000,
+      autosize: { minRows: 3 },
+      showWordLimit: true,
+      placeholder: '请描述演练人员,不超过1000字',
+    },
+  },
+  {
+    prop: 'drillDescription',
+    label: '演练描述:',
+    component: 'el-input',
+    componentProps: {
+      type: 'textarea',
+      maxlength: 2000,
+      showWordLimit: true,
+      autosize: { minRows: 8 },
+      placeholder: '请描述演练过程,不超过2000字',
+    },
+  },
+  {
+    prop: 'drillImage',
+    label: '演练照片:',
+    slot: 'drillImage',
+  },
+  {
+    prop: 'drillEffectAssess',
+    label: '演练效果评估:',
+    component: 'el-input',
+    componentProps: {
+      type: 'textarea',
+      maxlength: 2000,
+      showWordLimit: true,
+      autosize: { minRows: 5 },
+      placeholder: '请描述演练效果评估,不超过2000字',
+    },
+  },
+];
+
+export const DRILL_RECORD_FORM_RULES = {
+  participants: [{ required: true, message: '请填写参与人员', trigger: 'blur' }],
+  drillDescription: [{ required: true, message: '请填写演练描述', trigger: 'blur' }],
+  drillEffectAssess: [{ required: true, message: '请填写演练效果评估', trigger: 'blur' }],
+};
+
+export const DRILL_RECORD_FORM_DATA = {
+  participants: '',
+  drillDescription: '',
+  drillImage: '',
+  drillEffectAssess: '',
+};

+ 2 - 16
src/views/emergency/emergency-drill/configs/plan/search.ts

@@ -1,26 +1,12 @@
 import type { SearchConfig } from '@/types/basic-search';
-import {
-  EMERGENCY_DRILL_SCOPE,
-  EMERGENCY_DRILL_SCOPE_DICT,
-  EMERGENCY_DRILL_STATUS,
-  EMERGENCY_DRILL_STATUS_DICT,
-} from '../../constants';
+import { EMERGENCY_DRILL_STATUS, EMERGENCY_DRILL_STATUS_DICT } from '../../constants';
 
 // drillplan表格搜索 演练规模 演练内容 责任部门 计划完成日期 状态
 export const DRILL_PLAN_LIST_SEARCH_CONFIG: SearchConfig[] = [
   {
     label: '演练规模:',
     prop: 'drillScope',
-    component: 'ElSelect',
-    selectOptions: [
-      { label: '全部', value: undefined },
-      {
-        label: EMERGENCY_DRILL_SCOPE_DICT[EMERGENCY_DRILL_SCOPE.INSTITUTE].label,
-        value: EMERGENCY_DRILL_SCOPE.INSTITUTE,
-      },
-      { label: EMERGENCY_DRILL_SCOPE_DICT[EMERGENCY_DRILL_SCOPE.DEPT].label, value: EMERGENCY_DRILL_SCOPE.DEPT },
-    ],
-    componentProps: { placeholder: '全部' },
+    slot: 'drillScope',
   },
   {
     label: '演练内容:',

+ 4 - 2
src/views/emergency/emergency-drill/configs/plan/table.ts

@@ -12,11 +12,12 @@ export const DRILL_PLAN_LIST_TABLE_COLUMNS: TableColumnProps[] = [
     label: '序号',
     align: 'center',
     width: '80px',
-    type: 'index'
+    type: 'index',
   },
   {
     prop: 'drillScope',
     label: '演练规模',
+    slot: 'drillScope',
     align: 'center',
     minWidth: '120px',
   },
@@ -46,8 +47,9 @@ export const DRILL_PLAN_LIST_TABLE_COLUMNS: TableColumnProps[] = [
     minWidth: '120px',
   },
   {
-    prop: 'statusLabel',
+    prop: 'status',
     label: '状态',
+    slot: 'status',
     align: 'center',
     minWidth: '120px',
   },

+ 1 - 73
src/views/emergency/emergency-drill/constants.ts

@@ -1,18 +1,3 @@
-export const EMERGENCY_DRILL_SCOPE = {
-  TOTAL: 0,
-  INSTITUTE: 1,
-  DEPT: 2,
-};
-
-export const EMERGENCY_DRILL_SCOPE_DICT = {
-  1: { dict: 'institute_level', label: '院级演练' },
-  2: { dict: 'dept_level', label: '部门级演练' },
-};
-export const EMERGENCY_DRILL_SCOPE_LABEL = {
-  institute_level: '院级演练',
-  dept_level: '部门级演练',
-};
-
 export const EMERGENCY_DRILL_STATUS = {
   TOTAL: undefined,
   WAIT_SCRIPT: 1, // 待传脚本
@@ -43,63 +28,6 @@ export const EMERGENCY_DRILL_DETAIL_SUBPAGE = [
   },
 ];
 
-export const DRILL_VIEW_CONTENT = [
-  {
-    label: '演练规模:',
-    value: 'drillScope',
-  },
-  {
-    label: '演练内容:',
-    value: 'drillContent',
-  },
-  {
-    label: '计划完成日期:',
-    value: 'dueCompleteTime',
-  },
-  {
-    label: '责任部门:',
-    value: 'responsibleDeptNameList',
-  },
-  {
-    label: '配合部门:',
-    value: 'coordinateDeptNameList',
-  },
-  {
-    label: '关联应急预案:',
-    value: 'emergencyPlanFile',
-    link: 'emergencyPlanFile',
-  },
-  {
-    label: '审批流程:',
-    value: 'approvalTemplateName',
-  },
-];
-
-export const DRILL_VIEW_EXECUTE = [
-  {
-    label: '演练时间:',
-    value: 'drillTime',
-  },
-  {
-    label: '演练地点:',
-    value: 'drillLocation',
-  },
-  {
-    label: '演练负责人:',
-    value: 'personInChargeName',
-  },
-  {
-    label: '演练脚本:',
-    value: 'drillScriptName',
-    link: 'drillScriptUrl',
-  },
-  {
-    label: '签到码:',
-    value: 'qr',
-    link: 'qr',
-  },
-];
-
 export const DRILL_VIEW_RECORD = [
   {
     label: '演练内容:',
@@ -156,4 +84,4 @@ export const CONFIRM_STATUS_MAP = {
   [CONFIRM_STATUS_TYPE.WAIT_CONFIRM]: '待确认',
   [CONFIRM_STATUS_TYPE.CONFIRMED]: '已确认',
   [CONFIRM_STATUS_TYPE.REJECT]: '已退回',
-};
+};

+ 67 - 20
src/views/emergency/emergency-drill/types.ts

@@ -21,7 +21,6 @@ export interface DrillPlanItem {
   approvalTemplateId: number;
   /*状态: 1-待传脚本,2-脚本会签,3-待执行,4-待记录,5-记录待审批,6-已完成 */
   status: number;
-  statusLabel: string;
   /*演练时间 */
   drillTime: string;
   /*演练地点 */
@@ -82,10 +81,8 @@ export interface DrillPlanItemDetail {
   coordinateDeptNameList?: string;
   /*应急预案id */
   emergencyPlanId?: number;
-  emergencyPlanFile?: string;
   /*审批模板id */
   approvalTemplateId: number;
-  approvalTemplateName?: string;
   /*状态?: 1-待传脚本,2-脚本会签,3-待执行,4-待记录,5-记录待审批,6-已完成 */
   status?: number;
   /*演练时间 */
@@ -98,8 +95,6 @@ export interface DrillPlanItemDetail {
   drillDeptIdList?: string;
   /*演练脚本 */
   drillScript?: string;
-  drillScriptName?: string;
-  drillScriptUrl?: string;
   /*提交人 */
   createdBy?: number;
   /*创建时间 */
@@ -135,11 +130,26 @@ export interface DrillPlanItemDetail {
   }[];
 }
 
+export interface ExecuteDrillPlanRuleForm {
+  drillPlanId?: number;
+  /*演练时间 */
+  drillTime?: string;
+  /*演练地点 */
+  drillLocation?: string;
+  /*演练负责人id */
+  personInChargeId?: number;
+  personInChargeName?: string;
+  /*演练部门id列表 */
+  drillDeptIdList?: number[];
+  /*演练脚本 */
+  drillScript?: any;
+}
+
 export interface DrillPlanRecord {
   /*自增主键 */
-  id?: number;
+  id: number;
   /*演练计划id */
-  drillPlanId?: number;
+  drillPlanId: number;
   /*参与人员 */
   participants?: string;
   /*演练描述 */
@@ -175,20 +185,57 @@ export interface DrillPlanRecord {
   /*审批通过时间 */
   approvalTime?: string;
   /*审批模板id */
-  approvalTemplateId?: number;
+  approvalTemplateId: number;
 }
 
-export interface ExecuteDrillPlanRuleForm {
+export interface DrillRecordRuleForm {
   drillPlanId?: number;
-  /*演练时间 */
-  drillTime?: string;
-  /*演练地点 */
-  drillLocation?: string;
-  /*演练负责人id */
-  personInChargeId?: number;
-  personInChargeName?: string;
-  /*演练部门id列表 */
-  drillDeptIdList?: number[];
-  /*演练脚本 */
-  drillScript?: any;
+  /*参与人员 */
+  participants?: string;
+  /*演练描述 */
+  drillDescription?: string;
+  /*演练照片 */
+  drillImage?: string;
+  /*演练效果评估 */
+  drillEffectAssess?: string;
+  /*演练记录审批内容 */
+  approvalDescription?: string;
+  /*审批人 */
+  approvalInfoList?: { approvalOrder?: number; approverIdList?: string }[];
+}
+
+export interface DrillApprovalItem {
+  /*审批顺序 */
+  approvalOrder: number;
+  /*审批人选择方式: 0-固定,1-自选 */
+  approverType: number;
+  /*审批流程列表 */
+  processInfoList: {
+    /*审批部门id */
+    approvalDeptId: number;
+    /*审批部门名称 */
+    approvalDeptName: string;
+    /*审批人id */
+    approverId: number;
+    /*审批人名称 */
+    approverName: string;
+    /*审批方式: 0-会签,1-或签 */
+    approvalType: number;
+    /*审批状态: 1-已审批,2-待审批,3-退回 */
+    approvalStatus: number;
+    /*审批时间 */
+    approvalTime?: string;
+    /*审批内容 */
+    approvalContent: string;
+  }[];
+}
+
+export interface DrillApprovalRuleForm {
+  approvalDescription?: string;
+  approverListInfo: {
+    approvalOrder: number;
+    deptName: string;
+    approverId: number;
+    approverName: string;
+  }[];
 }