ソースを参照

关联预案、上传总结报告

Hwf 7 ヶ月 前
コミット
e06db5bbd7

+ 4 - 0
package.json

@@ -19,11 +19,14 @@
   },
   "dependencies": {
     "@amap/amap-jsapi-loader": "^1.0.1",
+    "@types/file-saver": "^2.0.7",
+    "@types/uuid": "^10.0.0",
     "@vueuse/core": "^11.0.3",
     "await-to-js": "^3.0.0",
     "axios": "^1.6.8",
     "crypto-js": "^4.2.0",
     "element-plus": "^2.8.5",
+    "file-saver": "^2.0.5",
     "jsencrypt": "^3.3.2",
     "nanoid": "^5.0.7",
     "normalize.css": "^8.0.1",
@@ -31,6 +34,7 @@
     "ol": "^10.2.1",
     "pinia": "^2.1.7",
     "proj4": "^2.12.1",
+    "uuid": "^10.0.0",
     "vant": "^4.9.0",
     "vue": "^3.4.27",
     "vue-router": "^4.3.2"

+ 33 - 0
pnpm-lock.yaml

@@ -11,6 +11,12 @@ importers:
       '@amap/amap-jsapi-loader':
         specifier: ^1.0.1
         version: 1.0.1
+      '@types/file-saver':
+        specifier: ^2.0.7
+        version: 2.0.7
+      '@types/uuid':
+        specifier: ^10.0.0
+        version: 10.0.0
       '@vueuse/core':
         specifier: ^11.0.3
         version: 11.1.0(vue@3.5.11(typescript@5.4.5))
@@ -26,6 +32,9 @@ importers:
       element-plus:
         specifier: ^2.8.5
         version: 2.8.5(vue@3.5.11(typescript@5.4.5))
+      file-saver:
+        specifier: ^2.0.5
+        version: 2.0.5
       jsencrypt:
         specifier: ^3.3.2
         version: 3.3.2
@@ -47,6 +56,9 @@ importers:
       proj4:
         specifier: ^2.12.1
         version: 2.12.1
+      uuid:
+        specifier: ^10.0.0
+        version: 10.0.0
       vant:
         specifier: ^4.9.0
         version: 4.9.7(vue@3.5.11(typescript@5.4.5))
@@ -786,6 +798,9 @@ packages:
   '@types/estree@1.0.6':
     resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
 
+  '@types/file-saver@2.0.7':
+    resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
+
   '@types/json-schema@7.0.15':
     resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
 
@@ -813,6 +828,9 @@ packages:
   '@types/svgo@2.6.4':
     resolution: {integrity: sha512-l4cmyPEckf8moNYHdJ+4wkHvFxjyW6ulm9l4YGaOxeyBWPhBOT0gvni1InpFPdzx1dKf/2s62qGITwxNWnPQng==}
 
+  '@types/uuid@10.0.0':
+    resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
+
   '@types/web-bluetooth@0.0.16':
     resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
 
@@ -1945,6 +1963,9 @@ packages:
     resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
     engines: {node: '>=16.0.0'}
 
+  file-saver@2.0.5:
+    resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
+
   filelist@1.0.4:
     resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
 
@@ -3811,6 +3832,10 @@ packages:
   util-deprecate@1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
+  uuid@10.0.0:
+    resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
+    hasBin: true
+
   validate-npm-package-license@3.0.4:
     resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
 
@@ -4612,6 +4637,8 @@ snapshots:
 
   '@types/estree@1.0.6': {}
 
+  '@types/file-saver@2.0.7': {}
+
   '@types/json-schema@7.0.15': {}
 
   '@types/lodash-es@4.17.12':
@@ -4636,6 +4663,8 @@ snapshots:
     dependencies:
       '@types/node': 20.16.11
 
+  '@types/uuid@10.0.0': {}
+
   '@types/web-bluetooth@0.0.16': {}
 
   '@types/web-bluetooth@0.0.20': {}
@@ -5995,6 +6024,8 @@ snapshots:
     dependencies:
       flat-cache: 4.0.1
 
+  file-saver@2.0.5: {}
+
   filelist@1.0.4:
     dependencies:
       minimatch: 5.1.6
@@ -7926,6 +7957,8 @@ snapshots:
 
   util-deprecate@1.0.2: {}
 
+  uuid@10.0.0: {}
+
   validate-npm-package-license@3.0.4:
     dependencies:
       spdx-correct: 3.2.0

+ 25 - 0
src/api/duty/eventing.ts

@@ -43,3 +43,28 @@ export function closeEvent(data) {
     data: data
   });
 }
+// 列出应急预案
+export function listPlan() {
+  return request({
+    url: '/api/emergency_plan/event/avail_plan_list',
+    method: 'post'
+  });
+}
+
+// 启动应急预案
+export function launchPlan(data) {
+  return request({
+    url: '/api/event_management/event/lauch_emergency_plan',
+    method: 'post',
+    data: data
+  });
+}
+
+// 获取结构化文档
+export const getPlanDoc = (data) => {
+  return request({
+    url: '/api/emergency_plan/doc/detail',
+    method: 'post',
+    data: data
+  });
+}

+ 258 - 0
src/components/FileUpload/index.vue

@@ -0,0 +1,258 @@
+<template>
+  <div>
+    <van-uploader
+        multiple
+        :accept="fileType.toString()"
+        :max-count="limit"
+        :max-size="fileSize * 1024 * 1024"
+        @oversize="onOversize"
+        :before-read="beforeRead"
+        :after-read="afterRead">
+      <van-button icon="plus" type="primary" class="button">上传文件</van-button>
+    </van-uploader>
+    <div v-if="isShowTip" class="upload-tip">
+      <div v-if="fileType">1、支持{{ fileType.join('、') }}文件</div>
+      <div v-if="limit">2、最多支持上传{{limit}}个文件</div>
+    </div>
+    <!-- 文件列表 -->
+    <transition-group class="upload-file-list" name="el-fade-in-linear" tag="ul">
+      <li v-for="(file, index) in fileList" :key="file.uid" class="upload-list-item">
+        <div class="text-primary" @click="handleDownload(file)">
+          {{ getFileName(file.name) }}
+        </div>
+        <van-icon name="delete-o" class="delete-icon" @click="handleDelete(index)"/>
+      </li>
+    </transition-group>
+  </div>
+</template>
+
+<script lang="ts" setup name="FileUpload">
+import {showFailToast, showToast} from "vant";
+import {ref} from "vue";
+import { v1 as uuidv1 } from 'uuid';
+import {download2, globalHeaders} from "@/utils/request";
+import axios from "axios";
+
+const props = defineProps({
+  modelValue: {
+    type: [String, Object, Array],
+    default: () => []
+  },
+  buttonText: {
+    type: String,
+    default: '选取文件'
+  },
+  // 数量限制
+  limit: {
+    type: Number,
+    default: 5
+  },
+  // 大小限制(MB)
+  fileSize: {
+    type: Number,
+    default: 100
+  },
+  // 文件类型, 例如['png', 'jpg', 'jpeg']
+  fileType: {
+    type: Array,
+    default: ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'pdf']
+  },
+  // 是否显示提示
+  isShowTip: {
+    type: Boolean,
+    default: true
+  }
+});
+
+const emit = defineEmits(['update:modelValue']);
+const number = ref(0);
+const uploadList = ref<any[]>([]);
+
+const baseUrl = import.meta.env.VITE_BASE_API;
+const downLoadApi = import.meta.env.VITE_BASE_DOWNLOAD_API;
+const uploadFileUrl = ref(baseUrl + '/resource/oss/upload'); // 上传文件服务器地址
+const headers = ref(globalHeaders());
+
+const fileList = ref<any[]>([]);
+
+const onOversize = () => {
+  showToast('文件大小不能超过' + props.fileSize + 'MB');
+};
+// 上传前置处理
+const beforeRead = (file: any) => {
+  // 校检文件类型
+  if (props.fileType.length) {
+    const fileName = file.name.split('.');
+    const fileExt = fileName[fileName.length - 1];
+    const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
+    if (!isTypeOk) {
+      showToast(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`);
+      return false;
+    }
+  }
+  // 校检文件大小
+  if (props.fileSize) {
+    const isLt = file.size / 1024 / 1024 < props.fileSize;
+    if (!isLt) {
+      showToast(`上传文件大小不能超过 ${props.fileSize} MB!`);
+      return false;
+    }
+  }
+  number.value++;
+  return true;
+};
+
+// 上传
+const afterRead = async (file: any) => {
+  let url = baseUrl + '/file/upload/uploadfile'; //上传文件接口
+  try {
+    // 如果文件大于等于5MB,分片上传
+    const res = await uploadByPieces(url, file);
+    // 分片上传后操作
+    if (res.code === 200) {
+      uploadList.value.push({
+        name: res.originalName,
+        url: res.filename
+      });
+      uploadedSuccessfully();
+    } else {
+      number.value--;
+      showFailToast(res.msg)
+      // fileUploadRef.value?.handleRemove(file);
+      uploadedSuccessfully();
+    }
+  } catch (e) {
+    return e;
+  }
+};
+//分片上传
+const uploadByPieces = async (url, { file }) => {
+  // 上传过程中用到的变量
+  const chunkSize = 5 * 1024 * 1024; // 5MB一片
+  const chunkCount = Math.ceil(file.size / chunkSize); // 总片数
+  // 获取当前chunk数据
+  const identifier = uuidv1();
+  const getChunkInfo = (file, index) => {
+    let start = index * chunkSize;
+    let end = Math.min(file.size, start + chunkSize);
+    let chunknumber = file.slice(start, end);
+    const fileName = file.name;
+    return { chunknumber, fileName };
+  };
+  // 分片上传接口
+  const uploadChunk = (data, params) => {
+    return new Promise((resolve, reject) => {
+      axios({
+        url,
+        method: 'post',
+        data,
+        params,
+        headers: {
+          'Content-Type': 'multipart/form-data'
+        }
+      })
+          .then((res) => {
+            return resolve(res.data);
+          })
+          .catch((err) => {
+            return reject(err);
+          });
+    });
+  };
+  // 针对单个文件进行chunk上传
+  const readChunk = (index) => {
+    const { chunknumber } = getChunkInfo(file, index);
+    let fetchForm = new FormData();
+    fetchForm.append('file', chunknumber);
+    return uploadChunk(fetchForm, {
+      chunknumber: index,
+      identifier: identifier
+    });
+  };
+  // 针对每个文件进行chunk处理
+  const promiseList = [];
+  try {
+    for (let index = 0; index < chunkCount; ++index) {
+      promiseList.push(readChunk(index));
+    }
+    await Promise.all(promiseList);
+    // 文件分片上传完成后,调用合并接口
+    const res = await mergeChunks(identifier, file.name);
+    res.originalName = file.name;
+    return res;
+  } catch (e) {
+    return e;
+  }
+};
+const mergeChunks = async (identifier: string, filename: string) => {
+  try {
+    const response = await axios.post(baseUrl + '/file/upload/mergefile', null, {
+      params: {
+        identifier: identifier,
+        filename: filename,
+        chunkstar: 0 // 假设所有分片的开始序号为0
+      }
+    });
+
+    return response.data;
+  } catch (error) {
+    throw new Error('合并请求失败');
+  }
+};
+// 上传结束处理
+const uploadedSuccessfully = () => {
+  if (number.value > 0 && uploadList.value.length === number.value) {
+    fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
+    uploadList.value = [];
+    number.value = 0;
+    emit('update:modelValue', fileList.value);
+  }
+};
+// 删除文件
+const handleDelete = (index: number) => {
+  fileList.value.splice(index, 1);
+  emit('update:modelValue', fileList.value);
+};
+// 获取文件名称
+const getFileName = (name: string) => {
+  // 如果是url那么取最后的名字 如果不是直接返回
+  if (name.lastIndexOf('/') > -1) {
+    return name.slice(name.lastIndexOf('/') + 1);
+  } else {
+    return name;
+  }
+};
+
+// 下载方法
+const handleDownload = (file: any) => {
+  download2(baseUrl + downLoadApi + file.url, file.name);
+};
+</script>
+
+<style lang="scss" scoped>
+.button {
+  padding: 0 10px;
+  height: 26px;
+  background: #2C81FF;
+  border-radius: 2px;
+  margin-bottom: 6px;
+}
+.upload-tip {
+  font-size: 14px;
+}
+.upload-file-list {
+  .upload-list-item {
+    display: flex;
+    align-items: center;
+    .text-primary {
+      color: #2c81ff;
+      font-size: 14px;
+    }
+    .delete-icon {
+      color: #969799;
+      font-size: 20px;
+      margin-left: 5px;
+    }
+  }
+}
+</style>

+ 21 - 0
src/styles/index.less

@@ -43,6 +43,7 @@ html {
     background-color: #f4f8fa;
     height: 100%;
     overflow-y: auto;
+    margin: 0 auto;
   }
 }
 
@@ -76,3 +77,23 @@ a:hover {
 .amap-logo, .amap-copyright {
   display: none !important;
 }
+
+.dialog-footer, .popup-footer {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-top: 16px;
+  .cancel-btn {
+    margin-right: 24px;
+    color: #B3B3B3;
+  }
+  .cancel-btn, .confirm-btn {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 90px;
+    height: 40px;
+    font-size: 16px;
+    border-radius: 2px;
+  }
+}

+ 18 - 0
src/styles/vant-ui.less

@@ -38,6 +38,24 @@
 .van-button {
   border-radius: 2px;
 }
+.van-popup {
+  border-radius: 8px;
+  .van-doc-block__title {
+    font-size: 14px;
+    color: #445267;
+    font-weight: bold;
+    text-align: center;
+    margin: 0;
+    padding: 32px 16px 16px;
+    border-bottom: 1px solid #eeeeee;
+  }
+  .popup-footer {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-bottom: 24px;
+  }
+}
 /* vant4的 Toast 和 Popup 样式冲突,会导致 Toast 变成白底 */
 .van-popup.van-toast{
   background: var(--van-toast-background) !important;

+ 5 - 6
src/utils/request.ts

@@ -1,7 +1,8 @@
 import axios, {AxiosResponse, InternalAxiosRequestConfig} from 'axios';
 import {useUserStore} from '@/store/modules/user';
 import {getToken} from '@/utils/auth';
-import {tansParams} from '@/utils/ruoyi';
+import {blobValidate, tansParams} from '@/utils/ruoyi';
+import FileSaver from 'file-saver';
 import cache from '@/plugins/cache';
 import {HttpStatus} from '@/enums/RespEnum';
 import {errorCode} from '@/utils/errorCode';
@@ -164,7 +165,6 @@ service.interceptors.response.use(
 );
 // 通用下载方法
 export function download2(url: string, fileName: string) {
-    downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
     return service
         .get(
             url,
@@ -176,6 +176,7 @@ export function download2(url: string, fileName: string) {
         )
         .then(async (resp: any) => {
             const isLogin = blobValidate(resp);
+            debugger
             if (isLogin) {
                 const blob = new Blob([resp]);
                 FileSaver.saveAs(blob, fileName);
@@ -183,14 +184,12 @@ export function download2(url: string, fileName: string) {
                 const resText = await resp.data.text();
                 const rspObj = JSON.parse(resText);
                 const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'];
-                ElMessage.error(errMsg);
+                showFailToast(errMsg);
             }
-            downloadLoadingInstance.close();
         })
         .catch((r: any) => {
             console.error(r);
-            ElMessage.error('下载文件出现错误,请联系管理员!');
-            downloadLoadingInstance.close();
+            showFailToast('下载文件出现错误!')
         });
 }
 // 导出 axios 实例

+ 99 - 61
src/views/event/AssociationPlan.vue

@@ -1,93 +1,131 @@
 <template>
-  <van-popup
+  <van-dialog
       v-model:show="visible"
+      title="选择预案"
+      :show-confirm-button="false"
   >
-    <van-form @submit="on_submit">
-      <div class="van-doc-block__title">上报伤亡情况</div>
-      <van-cell-group inset>
-        <van-field
-            v-model="form.deaths"
-            label="死亡人数"
-            placeholder="填写死亡人数"
-            :rules="[{ required: true, message: '请填写死亡人数'  }]"
-        />
-        <van-field
-            v-model="form.injuries"
-            label="受伤人数"
-            placeholder="填写受伤人数"
-            :rules="[{ required: true, message: '请填写受伤人数'  }]"
-        />
-        <van-field
-            v-model="form.missing"
-            label="失联人数"
-            placeholder="填写失联人数"
-            :rules="[{ required: true, message: '请填写失联人数'  }]"
-        />
-      </van-cell-group>
-      <div style="margin: 3.0vmin;display: flex;
-            flex-direction: row;
-            justify-content: flex-end;">
-        <div style="display: flex;        flex-direction: row;  justify-content: space-between;">
-          <van-button @click="closeDialog(false)" style="margin-right:10px;">取 消</van-button>
-          <van-button type="primary" native-type="submit">确 定</van-button>
-        </div>
+    <van-form @submit="onSubmit">
+      <van-field
+          v-model="associationState.planName"
+          name="预案名称"
+          label="预案名称"
+          placeholder="请选择"
+          :rules="[{ required: true, message: '请选择预案名称' }]"
+          readonly
+          @click="associationState.showPicker = true"
+      />
+      <van-field
+          v-model="associationState.responseName"
+          name="响应等级"
+          label="响应等级"
+          placeholder="请选择"
+          :rules="[{ required: true, message: '请选择响应等级' }]"
+          readonly
+          @click="associationState.showPicker2 = true"
+      />
+      <div class="dialog-footer">
+        <van-button class="cancel-btn" @click="onCancel">
+          取消
+        </van-button>
+        <van-button class="confirm-btn" type="primary" native-type="submit">
+          确定
+        </van-button>
       </div>
     </van-form>
+  </van-dialog>
+  <van-popup v-model:show="associationState.showPicker" round position="bottom">
+    <van-picker
+        :columns="associationState.columns"
+        :columns-field-names="{ text: 'plan_name', value: 'plan_id' }"
+        @cancel="associationState.showPicker = false"
+        @confirm="onConfirm"
+    />
+  </van-popup>
+  <van-popup v-model:show="associationState.showPicker2" round position="bottom">
+    <van-picker
+        :columns="associationState.columns2"
+        :columns-field-names="{ text: 'name'}"
+        @cancel="associationState.showPicker2 = false"
+        @confirm="onConfirm2"
+    />
   </van-popup>
 </template>
 
 <script lang="ts" setup>
-import { ref, watch, defineEmits } from 'vue';
-import { uploadCasualties } from "@/api/event";
+import {ref, watch, defineEmits, reactive, onMounted} from 'vue';
+import {launchPlan, listPlan} from "@/api/duty/eventing";
+import {showSuccessToast} from "vant";
 
-interface Form {
-  event_id: string;
-  deaths: string;
-  injuries: string;
-  missing: string;
-}
 interface Props {
   modelValue: boolean;
-  data: Form;
+  eventId: string
 }
 
-const form = ref<Form>({
-  event_id: '',
-  deaths: '',
-  injuries: '',
-  missing: ''
-});
-
 const props = withDefaults(defineProps<Props>(), {
   modelValue: false
 });
 
-const emits = defineEmits(['update:modelValue']);
+const emits = defineEmits(['update:modelValue', 'confirm']);
 watch(
     () => props.modelValue,
     () => {
-      if (props.modelValue) {
-        form.value = props.data;
-      }
       visible.value = props.modelValue;
     }
 );
 
 const visible = ref(false);
 
-const on_submit = () => {
-  console.log('on_submit');
-  uploadCasualties({ ...form.value }).then((res) => {
-    closeDialog(true)
-  }).catch((err) => {
-    closeDialog(false);
-  });
-};
+const associationState = reactive({
+  showPicker: false,
+  showPicker2: false,
+  columns: [],
+  columns2: [
+    // 响应等级选项
+    { value: '1', name: '特重大(I级)' },
+    { value: '2', name: '重大(II级)' },
+    { value: '3', name: '较大(III级)' },
+    { value: '4', name: '一般(IV级)' }
+  ],
+  planName: '',
+  plan_id: '',
+  responseName: '',
+  response_level: ''
+});
+
+const onCancel = () => {
+  emits('update:modelValue', false);
+  associationState.plan_id = '';
+  associationState.planName = '';
+  associationState.response_level = '';
+}
+const onSubmit = () => {
+  const data = {
+    eventId: props.eventId,
+    plan_id: associationState.plan_id,
+    response_level: associationState.response_level
+  };
+  launchPlan(data).then((res) => {
+    showSuccessToast('关联预案成功')
+    emits('confirm', associationState.plan_id)
+  })
+}
+const onConfirm = ({selectedOptions}) => {
+  associationState.showPicker = false;
+  associationState.plan_id = selectedOptions[0].plan_id;
+  associationState.planName = selectedOptions[0].plan_name;
+}
+const onConfirm2 = ({selectedOptions}) => {
+  associationState.showPicker2 = false;
+  associationState.responseName = selectedOptions[0].name;
+  associationState.response_level = selectedOptions[0].value;
+}
 
-const closeDialog = (t) => {
-  emits('update:modelValue', t);
-};
 
+onMounted(() => {
+  listPlan().then((res) => {
+    associationState.columns = res.data;
+  })
+})
 </script>
 
 <style lang="scss" scoped>

+ 71 - 67
src/views/event/UploadEventCasualtiesDialog.vue

@@ -1,61 +1,65 @@
 <template>
-    <van-popup
-        v-model:show="visible"
-    >
+  <van-popup
+      v-model:show="visible"
+      @close="closeDialog"
+  >
     <van-form @submit="on_submit">
-        <div class="van-doc-block__title">上报伤亡情况</div>
-        <van-cell-group inset>
-            <van-field
-                v-model="form.deaths"
-                label="死亡人数"
-                placeholder="填写死亡人数"
-                :rules="[{ required: true, message: '请填写死亡人数'  }]"
-            />
-            <van-field
-                v-model="form.injuries"
-                label="受伤人数"
-                placeholder="填写受伤人数"
-                :rules="[{ required: true, message: '请填写受伤人数'  }]"
-            />
-            <van-field
-                v-model="form.missing"
-                label="失联人数"
-                placeholder="填写失联人数"
-                :rules="[{ required: true, message: '请填写失联人数'  }]"
-            />
-        </van-cell-group>
-        <div style="margin: 3.0vmin;display: flex;
-            flex-direction: row;
-            justify-content: flex-end;">
-            <div style="display: flex;        flex-direction: row;  justify-content: space-between;">
-                <van-button @click="closeDialog(false)" style="margin-right:10px;">取 消</van-button>
-                <van-button type="primary" native-type="submit">确 定</van-button>
-            </div>
-        </div>
-    </van-form>    
-</van-popup>
+      <div class="van-doc-block__title">上传总结报告</div>
+      <van-field
+          v-model="form.deaths"
+          label="死亡人数"
+          placeholder="填写死亡人数"
+          :rules="[{ required: true, message: '请填写死亡人数'  }]"
+      />
+      <van-field
+          v-model="form.injuries"
+          label="受伤人数"
+          placeholder="填写受伤人数"
+          :rules="[{ required: true, message: '请填写受伤人数'  }]"
+      />
+      <van-field
+          v-model="form.missing"
+          label="失联人数"
+          placeholder="填写失联人数"
+          :rules="[{ required: true, message: '请填写失联人数'  }]"
+      />
+      <van-field name="fileNames" label="总结报告" label-align="top">
+        <template #input>
+          <FileUpload v-model="form.fileNames" :fileType="['pdf', 'word', 'excel']"/>
+        </template>
+      </van-field>
+      <div class="popup-footer">
+          <van-button @click="closeDialog()" class="cancel-btn">取 消</van-button>
+          <van-button type="primary" native-type="submit" class="confirm-btn">确 定</van-button>
+      </div>
+    </van-form>
+  </van-popup>
 </template>
 
-<script lang="ts" setup>
+<script lang="ts" setup name="UploadEventCasualtiesDialog">
 import {getCurrentInstance, reactive, ref, toRefs, watch, defineEmits} from 'vue';
-import { uploadCasualties } from "@/api/event";
+import {uploadCasualties} from "@/api/event";
+import FileUpload from '@/components/FileUpload/index.vue';
 
 interface Form {
-    event_id: string;
-    deaths: string;
-    injuries: string;
-    missing: string;
+  event_id: string;
+  deaths: string;
+  injuries: string;
+  missing: string;
+  fileNames: string[];
 }
+
 interface Props {
-    modelValue: boolean;
-    data: Form;
+  modelValue: boolean;
+  data: Form;
 }
 
 const form = ref<Form>({
-    event_id: '',
-    deaths: '',
-    injuries: '',
-    missing: ''
+  event_id: '',
+  deaths: '',
+  injuries: '',
+  missing: '',
+  fileNames: []
 });
 
 const props = withDefaults(defineProps<Props>(), {
@@ -64,39 +68,39 @@ const props = withDefaults(defineProps<Props>(), {
 
 const emits = defineEmits(['update:modelValue']);
 watch(
-  () => props.modelValue,
-  () => {
-    if (props.modelValue) {
-      form.value = props.data;
+    () => props.modelValue,
+    () => {
+      if (props.modelValue) {
+        form.value = props.data;
+      }
+      visible.value = props.modelValue;
     }
-    visible.value = props.modelValue;
-  }
 );
 
 const visible = ref(false);
 
 const on_submit = () => {
-    console.log('on_submit');
-    uploadCasualties({ ...form.value }).then((res) => {
-        closeDialog(true)
-      }).catch((err) => {
-        closeDialog(false);
-      });
+  console.log('on_submit');
+  uploadCasualties({...form.value}).then((res) => {
+    closeDialog()
+  }).catch((err) => {
+    closeDialog();
+  });
 };
 
-const closeDialog = (t) => {
-  emits('update:modelValue', t);
+const closeDialog = () => {
+  emits('update:modelValue', false);
 };
 
 </script>
 
 <style lang="scss" scoped>
 .van-doc-block__title {
-    color: var(--van-doc-text-color-4);
-    margin: 0px;
-    padding: 3vmin;
-    font-size: 4.6vmin;
-    font-weight: 600;
-    line-height: 6.0vmin;
+  color: var(--van-doc-text-color-4);
+  margin: 0px;
+  padding: 3vmin;
+  font-size: 4.6vmin;
+  font-weight: 600;
+  line-height: 6.0vmin;
 }
-</style>
+</style>

+ 38 - 19
src/views/event/detail.vue

@@ -124,19 +124,27 @@
         </van-tab>
         <van-tab title="匹配预案">
           <div class="event_tab2">
-            <template v-if="eventInfo.plan_files && eventInfo.plan_files.length > 0">
+            <template v-if="!!eventInfo.plan_id && !!eventInfo.plan_files && eventInfo.plan_files.length > 0">
               <div class="plan-content">
                 <div class="content-header">
-                  <div class="plan-content-title">{{ eventInfo.plan_files[0].name }}</div>
+                  <div class="plan-content-title">{{ eventInfo.plan_files[0]?.name }}</div>
                   <i class="download-icon" @click="handleDownload(eventInfo.plan_files[0])"/>
                 </div>
-                <div class="content-text">茂名市自然灾害应急预案茂名市自然灾害应急预案</div>
+                <div class="content-text">
+                  <div v-for="(item, index) in planFiles" :key="index">
+                    <div>{{ item.title }}</div>
+                    <div v-for="(item2, index2) in item.items" :key="index2">
+                      <div>{{ item2.title }}</div>
+                      <div v-html="item2.value"></div>
+                    </div>
+                  </div>
+                </div>
               </div>
             </template>
             <template v-else>
               <div class="emptyIcon"/>
               <div class="emptyText">未关联到对应预案</div>
-              <van-button type="primary" size="small" @click="handleShowAssociation">手工关联</van-button>
+              <van-button type="primary" size="small" @click="associationShow = true">手工关联</van-button>
             </template>
           </div>
         </van-tab>
@@ -161,8 +169,7 @@
         </van-button>
       </div>
     </div>
-
-    <!--    <AssociationPlan v-model="uploadCasualtiesState.show" :data="uploadCasualtiesState.form" @update:model-value="onUploadCasualtiesDialogClose" />-->
+    <AssociationPlan v-model="associationShow" :eventId="eventId" @confirm="associationPlanConfirm" />
     <UploadEventCasualtiesDialog v-model="uploadCasualtiesState.show" :data="uploadCasualtiesState.form"
                                  @update:model-value="onUploadCasualtiesDialogClose"></UploadEventCasualtiesDialog>
     <StartEventDialog v-model="startEventState.show" :data="startEventState.form"
@@ -175,11 +182,12 @@ import {getCurrentInstance, reactive, ref, toRefs, onMounted} from 'vue';
 import {useRouter, useRoute} from 'vue-router'
 import {getEventDetail, closeEvent} from "@/api/event";
 import UploadEventCasualtiesDialog from "./UploadEventCasualtiesDialog.vue";
-import {showDialog, showConfirmDialog, showToast} from 'vant';
+import {showDialog, showConfirmDialog} from 'vant';
 import {useAMap} from "@/hooks/AMap/useAMap";
-import AssociationPlan from "@/views/event/AssociationPlan.vue";
 import StartEventDialog from "@/views/event/StartEventDialog.vue";
 import {download2} from "@/utils/request";
+import {getPlanDoc, listPlan} from "@/api/duty/eventing";
+import AssociationPlan from "@/views/event/AssociationPlan.vue";
 
 const router = useRouter()
 const route = useRoute()
@@ -274,14 +282,7 @@ const handleCloseEvent = () => {
   })
 };
 
-const associationState = reactive({
-  show: false,
-  form: {}
-});
-
-const handleShowAssociation = () => {
-  associationState.show = true;
-};
+let associationShow = ref(false);
 
 // 上报伤亡情况
 const uploadCasualtiesState = reactive({
@@ -322,8 +323,20 @@ const onStartEventDialogClose = (t) => {
     })
   }
 };
+let planFiles = ref([]);
+const getPlan = () => {
+  getPlanDoc({plan_id: eventInfo.value.plan_id}).then((res)=> {
+    planFiles.value = res.data;
+  });
+}
+const associationPlanConfirm = (plan_id) => {
+  associationShow.value = false;
+  eventInfo.value.plan_id = plan_id;
+  getPlan();
+}
 onMounted(() => {
   refreshData();
+
 })
 let hideMap = ref(false);
 let map;
@@ -332,6 +345,9 @@ const refreshData = () => {
   getEventDetail({event_id: eventId.value}).then((res) => {
     eventInfo.value = res.data
     eventTrackState.value = res.data.event_status_tracks;
+    if (!!eventInfo.value.plan_id) {
+      getPlan(eventInfo.value.plan_id);
+    }
     if (!eventInfo.value.longitude || !eventInfo.value.latitude) {
       hideMap.vlaue = true
     } else {
@@ -363,7 +379,7 @@ const refreshData = () => {
     }
   })
 }
-const baseUrl = import.meta.env.VITE_APP_BASE_API;
+const baseUrl = import.meta.env.VITE_BASE_API;
 // 下载方法
 const handleDownload = (file: any) => {
   download2(baseUrl + '/file/download/' + file.url, file.name);
@@ -597,8 +613,12 @@ const handleDownload = (file: any) => {
     font-size: 14px;
     color: #2C81FF;
     font-weight: bold;
-
+    padding: 5px 0;
+    .plan-content-title {
+      text-align: center;
+    }
     .download-icon {
+      flex-shrink: 0;
       display: inline-block;
       width: 15px;
       height: 14px;
@@ -615,7 +635,6 @@ const handleDownload = (file: any) => {
     overflow-y: auto;
   }
 }
-
 :deep(.van-tabs__wrap) {
   border-bottom: 1px solid #F6F6F6;
 }