Procházet zdrojové kódy

Merge branch 'feature-hetong' into 'dev'

推送合同功能分支到dev分支测试

See merge request product-group-fe/sfy-safety-group/sfy-safety!260
毕欣怡 před 5 měsíci
rodič
revize
949779b5ea

+ 12 - 0
src/api/disaster-control/index.ts

@@ -17,6 +17,7 @@ import type {
   DisposalRectificationFormData,
   DisasterLossDetailQuery,
   DisasterLossDetailResponse,
+  UpdateDisasterHandleTaskQuery,
 } from '@/types/disaster-control';
 import type { QueryPageResponse, QueryPageRequest } from '@/types/basic-query';
 /**
@@ -49,6 +50,17 @@ export const createDisasterHandleTask = (query: DisposalManagementCreateQuery) =
   });
 };
 
+/**
+ * 更新灾害处置任务
+ */
+export const updateDisasterHandleTask = (query: UpdateDisasterHandleTaskQuery) => {
+  return http.request({
+    url: '/disasterHandle/updateDisasterHandleTask',
+    method: 'put',
+    data: query,
+  });
+};
+
 /**
  * 创建灾害处置上报任务
  */

+ 8 - 4
src/components/UploadFiles/UploadFiles.vue

@@ -2,7 +2,7 @@
   <div class="upload-wrapper">
     <!-- 上传按钮 -->
     <div class="upload-button-container">
-      <label for="fileInput" class="upload-button" :class="{ disabled: isUploadDisabled }">
+      <label :for="inputId" class="upload-button" :class="{ disabled: isUploadDisabled }">
         <el-icon><UploadFilled /></el-icon>
         <span>{{ label }}</span>
       </label>
@@ -10,7 +10,8 @@
     </div>
     <input
       type="file"
-      id="fileInput"
+      :id="inputId"
+      class="upload-input"
       multiple
       accept=".pdf,.docx,.xlsx,.pptx"
       @change="handleFileSelect"
@@ -59,6 +60,8 @@
     maxCount?: number;
   }>();
 
+  const inputId = `upload-file-${Math.random().toString(36).slice(2, 10)}`;
+
   // 常量定义
   const MAX_SIZE = computed(() => (props.maxSize || 5) * 1024 * 1024); // 默认5MB
   const MAX_COUNT = computed(() => props.maxCount || 9); // 默认最多9个文件
@@ -272,7 +275,8 @@
   .upload-button {
     @include flex-center;
     gap: 5px;
-    width: 100px;
+    // width: 100px;
+    padding: 0 10px;
     border: 1px solid rgba($text-color, 0.15);
     color: $primary-color;
     font-size: 14px;
@@ -298,7 +302,7 @@
     }
   }
 
-  #fileInput {
+  .upload-input {
     display: none;
   }
 

+ 2 - 2
src/router/router-guards.ts

@@ -45,10 +45,10 @@ export function createRouterGuards(router: Router) {
 
     // Whitelist can be directly entered
     if (whitePathList.includes(to.path as PageEnum)) {
-      next();
       if (token && !asyncRouteStore.getIsDynamicAddedRoute) {
-        setDynamicRoute(router);
+        await setDynamicRoute(router);
       }
+      next();
       return;
     }
 

+ 40 - 0
src/router/routers/disaster.ts

@@ -447,6 +447,46 @@ const disasterPreventionRoute = {
           path: 'disposal-rectification-item-detail/:id',
           redirect: '',
         },
+        {
+          component: '/disaster/disaster-control/PagePostDisaster',
+          id: 1046,
+          meta: {
+            activeMenu: '',
+            alwaysShow: false,
+            frameSrc: '',
+            hidden: false,
+            icon: '',
+            isFrame: 0,
+            isRoot: false,
+            noCache: false,
+            query: '',
+            title: '评估及重建',
+          },
+          name: 'disaster-control-post-disaster',
+          parentId: 1027,
+          path: 'post-disaster',
+          redirect: '',
+        },
+        {
+          component: '/disaster/disaster-control/PagePostDisasterItem',
+          id: 1047,
+          meta: {
+            activeMenu: '/disaster-prevention/disaster-control/post-disaster',
+            alwaysShow: false,
+            frameSrc: '',
+            hidden: false,
+            icon: '',
+            isFrame: 0,
+            isRoot: false,
+            noCache: false,
+            query: '',
+            title: '更新灾后评估及重建材料',
+          },
+          name: 'disaster-control-post-disaster-item',
+          parentId: 1027,
+          path: 'post-disaster-item',
+          redirect: '',
+        },
       ],
       component: '',
       id: 1032,

+ 34 - 0
src/store/modules/usePostDisasterMaterial.ts

@@ -0,0 +1,34 @@
+/**
+ * @description: 灾后评估及重建材料编辑和上传
+ * @return {
+ *  id: 任务id
+ *  type: 编辑或上传
+ *  disasterAssessMaterials: 灾后评估材料
+ *  disasterReconstructMaterials: 灾后重建材料
+ * }
+ */
+
+import { ref } from 'vue';
+import { defineStore } from 'pinia';
+
+export const usePostDisasterMaterial = defineStore('postDisasterMaterial', () => {
+  const id = ref<number>();
+  const type = ref<'edit' | 'upload'>('upload');
+  const disasterAssessMaterials = ref<string>('');
+  const disasterReconstructMaterials = ref<string>('');
+
+  const initData = () => {
+    id.value = undefined;
+    type.value = 'upload';
+    disasterAssessMaterials.value = '';
+    disasterReconstructMaterials.value = '';
+  };
+
+  return {
+    id,
+    type,
+    disasterAssessMaterials,
+    disasterReconstructMaterials,
+    initData,
+  };
+});

+ 13 - 0
src/types/disaster-control/index.ts

@@ -41,9 +41,22 @@ export interface DisposalManagementListResponse {
   id: number;
   handleTaskId: number;
   taskName: string;
+  createdAt: string;
   updatedAt: string;
   dueCompleteTime: string;
   deptId: number;
+  disasterAssessMaterials: string; // 灾后评估材料
+  disasterReconstructMaterials: string; // 灾后重建材料
+}
+
+export interface UpdateDisasterHandleTaskQuery {
+  id?: number;
+  taskName?: string;
+  disasterAssessMaterials?: string; // 灾后评估材料
+  disasterReconstructMaterials?: string; // 灾后重建材料
+  createdAt?: string;
+  updatedAt?: string;
+  isDeleted?: number;
 }
 
 export interface DisposalManagementCollapseListResponse<T> extends DisposalManagementListResponse {

+ 2 - 0
src/types/disaster-warning/index.ts

@@ -38,6 +38,8 @@ export interface WarningInfoDetailResponse
   extends BasicDetailResponse,
     Omit<WarningInfoListResponse, 'effectState' | 'isPush' | 'pushTime'> {
   source: string;
+  isEmergency: number;
+  eventType?: string;
 }
 
 export interface DefenseNoticeDetailResponse

+ 6 - 1
src/views/disaster/components/PreviewOnline.vue

@@ -66,7 +66,12 @@
     height: 100% !important;
     overflow-y: auto !important;
   }
+  :deep(.vue-office-pptx) {
+    height: 100vh !important;
+    overflow-y: auto !important;
+  }
   :deep(.pptx-preview-wrapper) {
-    height: 1080px !important;
+    height: 100% !important;
+    overflow-y: auto !important;
   }
 </style>

+ 6 - 0
src/views/disaster/constant/index.ts

@@ -8,6 +8,12 @@ export enum IS_PUSH {
   PUSH,
 }
 
+// 是否启动应急事件
+export enum IS_EMERGENCY {
+  NOT_EMERGENCY = 0,
+  EMERGENCY,
+}
+
 // 生效状态
 export enum ACTIVE_STATUS {
   INACTIVE = 0,

+ 166 - 0
src/views/disaster/disaster-control/PagePostDisaster.vue

@@ -0,0 +1,166 @@
+<template>
+  <div class="safety-platform-container">
+    <header class="safety-platform-container__header">
+      <span class="breadcrumb-title">灾后评估及重建</span>
+    </header>
+    <main class="safety-platform-container__main">
+      <BasicTable
+        :tableConfig="tableConfig"
+        :tableData="tableData"
+        @update:page-size="handleSizeChange"
+        @update:page-number="handleCurrentChange"
+      >
+        <template #disasterAssessMaterials="scope">
+          <div
+            class="file-container--div"
+            v-for="item in unformatAttachment(scope.row.disasterAssessMaterials)"
+            :key="item.fileId"
+          >
+            <img
+              class="file-container--div__icon"
+              @click="previewOnline(item.fileUrl, item.fileType as keyof typeof FILE_TYPE_ICON)"
+              :src="FILE_TYPE_ICON[item.fileType]"
+            />
+            <span
+              class="file-container--div__name"
+              @click="previewOnline(item.fileUrl, item.fileType as keyof typeof FILE_TYPE_ICON)"
+              >{{ item.fileName }}</span
+            >
+            <img
+              class="file-container--div__download"
+              :src="DownloadIcon"
+              @click="downloadFile(item.fileUrl, item.fileName)"
+            />
+          </div>
+        </template>
+        <template #disasterReconstructMaterials="scope">
+          <div
+            class="file-container--div"
+            v-for="item in unformatAttachment(scope.row.disasterReconstructMaterials)"
+            :key="item.fileId"
+          >
+            <img
+              class="file-container--div__icon"
+              @click="previewOnline(item.fileUrl, item.fileType as keyof typeof FILE_TYPE_ICON)"
+              :src="FILE_TYPE_ICON[item.fileType]"
+            />
+            <span
+              class="file-container--div__name"
+              @click="previewOnline(item.fileUrl, item.fileType as keyof typeof FILE_TYPE_ICON)"
+              >{{ item.fileName }}</span
+            >
+            <img
+              class="file-container--div__download"
+              :src="DownloadIcon"
+              @click="downloadFile(item.fileUrl, item.fileName)"
+            />
+          </div>
+        </template>
+        <template #action="scope">
+          <div class="action-container--div" style="justify-content: left">
+            <ActionButton
+              v-if="scope.row.disasterAssessMaterials || scope.row.disasterReconstructMaterials"
+              text="编辑"
+              @click="handleEditMaterials(scope.row)"
+            />
+            <ActionButton v-else text="上传" @click="handleUploadMaterials(scope.row)" />
+          </div>
+        </template>
+      </BasicTable>
+      <PreviewOnline ref="previewOnlineRef" />
+    </main>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+  import { useRouter } from 'vue-router';
+  import BasicTable from '@/components/BasicTable.vue';
+  import ActionButton from '@/components/ActionButton.vue';
+  import useTableConfig from '@/hooks/useTableConfigHook';
+  import { POST_DISASTER_TABLE_OPTIONS, POST_DISASTER_TABLE_COLUMNS } from './src/config/table';
+  import { getDisasterControlCollapseData } from '@/api/disaster-control';
+  import type { QueryPageRequest } from '@/types/basic-query';
+  import type { DisposalManagementListQuery, DisposalManagementListResponse } from '@/types/disaster-control';
+  import DownloadIcon from '@/views/disaster/disaster-control/src/svg/download.svg';
+  import { downloadFile } from '@/views/disaster/utils';
+  import PreviewOnline from '@/views/disaster/components/PreviewOnline.vue';
+  import { FILE_TYPE_ICON } from '@/components/UploadFiles/constants';
+  import { unformatAttachment } from '@/components/UploadFiles/utils';
+  import { usePostDisasterMaterial } from '@/store/modules/usePostDisasterMaterial';
+  import { storeToRefs } from 'pinia';
+
+  const router = useRouter();
+
+  const postDisasterMaterialStore = usePostDisasterMaterial();
+  const { id, type, disasterAssessMaterials, disasterReconstructMaterials } = storeToRefs(postDisasterMaterialStore);
+
+  const { tableConfig, pagination } = useTableConfig(POST_DISASTER_TABLE_COLUMNS, POST_DISASTER_TABLE_OPTIONS);
+
+  const tableData = ref<DisposalManagementListResponse[]>([]);
+
+  const tableQuery = ref<QueryPageRequest<DisposalManagementListQuery>>({
+    pageNumber: pagination.pageNumber,
+    pageSize: pagination.pageSize,
+    queryParam: {},
+  });
+
+  const handleSizeChange = (value: number) => {
+    pagination.pageSize = value;
+    tableQuery.value.pageSize = value;
+    getTableData();
+  };
+  const handleCurrentChange = (value: number) => {
+    pagination.pageNumber = value;
+    tableQuery.value.pageNumber = value;
+    getTableData();
+  };
+
+  async function getTableData() {
+    tableConfig.loading = true;
+    const res = await getDisasterControlCollapseData(tableQuery.value);
+    tableData.value = res.records;
+    pagination.total = res.totalRow;
+    tableConfig.loading = false;
+  }
+
+  // 预览
+  const previewOnlineRef = ref<InstanceType<typeof PreviewOnline>>();
+  const previewOnline = (url: string | undefined, type: keyof typeof FILE_TYPE_ICON) => {
+    if (url) {
+      previewOnlineRef.value?.open(url, type);
+    }
+  };
+
+  // 上传
+  const handleUploadMaterials = (row: DisposalManagementListResponse) => {
+    id.value = row.id;
+    type.value = 'upload';
+    disasterAssessMaterials.value = row.disasterAssessMaterials;
+    disasterReconstructMaterials.value = row.disasterReconstructMaterials;
+    router.push({
+      name: 'disaster-control-post-disaster-item',
+    });
+  };
+
+  // 编辑
+  const handleEditMaterials = (row: DisposalManagementListResponse) => {
+    id.value = row.id;
+    type.value = 'edit';
+    disasterAssessMaterials.value = row.disasterAssessMaterials;
+    disasterReconstructMaterials.value = row.disasterReconstructMaterials;
+    router.push({
+      name: 'disaster-control-post-disaster-item',
+    });
+  };
+
+  onMounted(() => {
+    getTableData();
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/basic-table-action.scss' as *;
+  @use '@/styles/basic-table-file.scss' as *;
+</style>

+ 128 - 0
src/views/disaster/disaster-control/PagePostDisasterItem.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="safety-platform-container">
+    <header class="safety-platform-container__header">
+      <BreadcrumbBack />
+      <span class="breadcrumb-title">{{ type === 'edit' ? '编辑' : '上传' }}灾后评估材料及重建方案</span>
+    </header>
+    <main class="safety-platform-container__main">
+      <BasicForm ref="basicFormRef" :formData="ruleFormData" :formRules="formRules" :formConfig="ruleFormConfig">
+        <template #disasterAssessMaterials>
+          <UploadFiles
+            label="上传文件"
+            ref="uploadFilesRef"
+            :fileList="ruleFormData.disasterAssessMaterials"
+            @upload-success="handleUploadSuccessAssessMaterials"
+          />
+        </template>
+        <template #disasterReconstructMaterials>
+          <UploadFiles
+            label="上传文件"
+            ref="uploadFilesRef"
+            :fileList="ruleFormData.disasterReconstructMaterials"
+            @upload-success="handleUploadSuccessReconstructMaterials"
+          />
+        </template>
+      </BasicForm>
+    </main>
+    <footer class="safety-platform-container__footer">
+      <el-button @click="router.back()">取消</el-button>
+      <el-button type="primary" @click="handleSubmit">提交</el-button>
+    </footer>
+    <UploadLoading :form-loading="formLoading" v-if="formLoading" />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import { useRouter } from 'vue-router';
+  import { ElMessage } from 'element-plus';
+  import { usePostDisasterMaterial } from '@/store/modules/usePostDisasterMaterial';
+  import BasicForm from '@/components/BasicForm.vue';
+  import UploadFiles from '@/components/UploadFiles/UploadFiles.vue';
+  import UploadLoading from '@/components/UploadLoading.vue';
+  import { useFormConfigHook } from '@/hooks/useFormConfigHook';
+  import { POST_DISASTER_MATERIAL_FORM_CONFIG, POST_DISASTER_MATERIAL_FORM_DATA } from './src/config/form';
+  import { FileItem } from '@/components/UploadFiles/types';
+  import { unformatAttachment, formatAttachmentList } from '@/components/UploadFiles/utils';
+  import type { UpdateDisasterHandleTaskQuery } from '@/types/disaster-control';
+  import { updateDisasterHandleTask } from '@/api/disaster-control';
+
+  interface PostDisasterMaterialFormData {
+    disasterAssessMaterials: FileItem[];
+    disasterReconstructMaterials: FileItem[];
+  }
+
+  const { ruleFormConfig, ruleFormData, formRules, cloneRuleFormData, beforeRouteLeave } =
+    useFormConfigHook<PostDisasterMaterialFormData>(
+      POST_DISASTER_MATERIAL_FORM_CONFIG,
+      POST_DISASTER_MATERIAL_FORM_DATA,
+    );
+
+  const postDisasterMaterialStore = usePostDisasterMaterial();
+  const { id, type, disasterAssessMaterials, disasterReconstructMaterials } = storeToRefs(postDisasterMaterialStore);
+
+  const router = useRouter();
+  const formLoading = ref(false);
+
+  const getDetail = async () => {
+    ruleFormData.disasterAssessMaterials = unformatAttachment(disasterAssessMaterials.value) || [];
+    ruleFormData.disasterReconstructMaterials = unformatAttachment(disasterReconstructMaterials.value) || [];
+    cloneRuleFormData();
+  };
+
+  const getFormData = async () => {
+    cloneRuleFormData();
+    const res: UpdateDisasterHandleTaskQuery = {
+      id: id.value,
+      disasterAssessMaterials: JSON.stringify(await formatAttachmentList(ruleFormData.disasterAssessMaterials)),
+      disasterReconstructMaterials: JSON.stringify(
+        await formatAttachmentList(ruleFormData.disasterReconstructMaterials),
+      ),
+    };
+    return res;
+  };
+
+  const handleUploadSuccessAssessMaterials = (fileList: FileItem[]) => {
+    ruleFormData.disasterAssessMaterials = fileList;
+  };
+
+  const handleUploadSuccessReconstructMaterials = (fileList: FileItem[]) => {
+    ruleFormData.disasterReconstructMaterials = fileList;
+  };
+
+  const handleSubmit = async () => {
+    const hasAssessMaterials = ruleFormData.disasterAssessMaterials?.length;
+    const hasReconstructMaterials = ruleFormData.disasterReconstructMaterials?.length;
+    if (!hasAssessMaterials && !hasReconstructMaterials) {
+      ElMessage.warning('请上传相关文件');
+      return;
+    }
+    try {
+      formLoading.value = true;
+      const params = await getFormData();
+      await updateDisasterHandleTask(params);
+      ElMessage.success(type.value === 'edit' ? '编辑成功' : '上传成功');
+      router.back();
+    } catch (e) {
+      console.log(e);
+    } finally {
+      formLoading.value = false;
+    }
+  };
+
+  onMounted(() => {
+    beforeRouteLeave();
+    getDetail();
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-details-layout.scss' as *;
+
+  .safety-platform-container__header {
+    flex-direction: row !important;
+    justify-content: flex-start !important;
+    gap: 8px !important;
+  }
+</style>

+ 19 - 0
src/views/disaster/disaster-control/src/config/form.ts

@@ -297,6 +297,20 @@ export const DISPOSAL_RECTIFICATION_FORM_CONFIG: FormConfig[] = [
   },
 ];
 
+// 灾后评估及重建材料表单信息
+export const POST_DISASTER_MATERIAL_FORM_CONFIG: FormConfig[] = [
+  {
+    label: '灾后评估材料:',
+    prop: 'disasterAssessMaterials',
+    slot: 'disasterAssessMaterials',
+  },
+  {
+    label: '灾后重建方案:',
+    prop: 'disasterReconstructMaterials',
+    slot: 'disasterReconstructMaterials',
+  },
+];
+
 // 通用表单数据
 const BASIC_FROM_DATA = {};
 const DISPOSAL_MANAGEMENT_BASIC_FROM_DATA = {
@@ -349,6 +363,11 @@ export const DISPOSAL_RECTIFICATION_FORM_DATA = {
   uploadFiles: [],
 };
 
+export const POST_DISASTER_MATERIAL_FORM_DATA = {
+  disasterAssessMaterials: [],
+  disasterReconstructMaterials: [],
+};
+
 // 通用表单规则
 const BASIC_FROM_RULES = {};
 

+ 41 - 0
src/views/disaster/disaster-control/src/config/table.ts

@@ -23,6 +23,12 @@ export const RECTIFICATION_INFO_TABLE_OPTIONS = {
   ...TABLE_OPTIONS,
 };
 
+// 灾后评估及重建表格配置
+export const POST_DISASTER_TABLE_OPTIONS = {
+  ...TABLE_OPTIONS,
+  maxHeight: 'calc(70vh - 30px)',
+};
+
 // 基础表格列配置项
 const BASIC_TABLE_COLUMNS = {
   INDEX: {
@@ -190,3 +196,38 @@ export const RECTIFICATION_INFO_TABLE_COLUMNS: TableColumnProps[] = [
     fixed: 'right',
   },
 ];
+
+// 灾后评估及重建表格列配置
+export const POST_DISASTER_TABLE_COLUMNS: TableColumnProps[] = [
+  BASIC_TABLE_COLUMNS.INDEX,
+  {
+    label: '灾害处置任务名称',
+    prop: 'taskName',
+    minWidth: '240px',
+  },
+  {
+    label: '创建时间',
+    prop: 'createdAt',
+    width: '200px',
+  },
+  {
+    label: '灾后评估材料',
+    prop: 'disasterAssessMaterials',
+    slot: 'disasterAssessMaterials',
+    minWidth: '300px',
+  },
+  {
+    label: '灾后重建方案',
+    prop: 'disasterReconstructMaterials',
+    slot: 'disasterReconstructMaterials',
+    minWidth: '300px',
+  },
+  {
+    label: '操作',
+    prop: 'action',
+    slot: 'action',
+    align: 'center',
+    width: '80px',
+    fixed: 'right',
+  },
+];

+ 4 - 0
src/views/disaster/disaster-warning/PageWarningInfoItem.vue

@@ -54,6 +54,8 @@
       warnTime: formData.warnTime,
       source: formData.source,
       content: formData.content,
+      isEmergency: formData.isEmergency,
+      eventType: formData.eventType,
       isPush: formData.isPush,
       userGroupList: formData.isPush ? formData.userGroupList : [],
     };
@@ -67,6 +69,8 @@
       warnTime: formData.warnTime,
       source: formData.source,
       content: formData.content,
+      isEmergency: formData.isEmergency,
+      eventType: formData.eventType,
       isPush: formData.isPush,
       userGroupList: formData.isPush ? formData.userGroupList : [],
     };

+ 1 - 1
src/views/disaster/disaster-warning/src/components/CreateDefenseNoticeItem.vue

@@ -22,7 +22,7 @@
         </el-select>
       </template>
       <template #attachmentListRes>
-        <UploadFiles label="上传文件" ref="uploadFilesRef" @uploadSuccess="handleUploadSuccess" />
+        <UploadFiles label="上传并导入文件" ref="uploadFilesRef" @uploadSuccess="handleUploadSuccess" />
       </template>
       <template #isPush>
         <el-radio-group v-model="ruleFormData.isPush">

+ 77 - 5
src/views/disaster/disaster-warning/src/components/CreateWarningInfoItem.vue

@@ -21,6 +21,22 @@
           />
         </el-select>
       </template>
+      <template #isEmergency>
+        <el-radio-group v-model="ruleFormData.isEmergency">
+          <el-radio :value="IS_EMERGENCY.EMERGENCY">是</el-radio>
+          <el-radio :value="IS_EMERGENCY.NOT_EMERGENCY">否</el-radio>
+        </el-radio-group>
+      </template>
+      <template #eventType>
+        <el-select v-model="ruleFormData.eventType" placeholder="请选择应急事件类型" filterable>
+          <el-option
+            v-for="item in emergencyEventDice"
+            :key="item.itemCode"
+            :label="item.itemValue"
+            :value="item.itemValue"
+          />
+        </el-select>
+      </template>
       <template #isPush>
         <el-radio-group v-model="ruleFormData.isPush">
           <el-radio :value="IS_PUSH.PUSH">是</el-radio>
@@ -29,8 +45,8 @@
         <SelectGroup
           v-if="ruleFormData.isPush"
           ref="selectGroupRef"
-          :userGroupList="ruleFormData.userGroupList || []"
-          @userGroupListChange="handleUserGroupListChange"
+          :user-group-list="ruleFormData.userGroupList || []"
+          @user-group-list-change="handleUserGroupListChange"
         />
       </template>
     </BasicForm>
@@ -38,27 +54,79 @@
 </template>
 
 <script setup lang="ts">
-  import { onMounted, ref } from 'vue';
+  import { onMounted, ref, watch } from 'vue';
   import BasicForm from '@/components/BasicForm.vue';
   import SelectGroup from '@/components/PersonGroup/SelectGroup.vue';
   import { useFormConfigHook } from '@/hooks/useFormConfigHook';
   import { useUserInfoHook } from '@/views/disaster/hooks';
   import { useDisasterWarningHook } from '../hook';
+  import { useEmergencyProcedureHook } from '@/views/emergency/emergency-procedure/hooks';
   import type { WarningInfoRuleForm } from '../type';
   import { WARNING_INFO_FROM_CONFIG, WARNING_INFO_FROM_DATA, WARNING_INFO_FROM_RULES } from '../config';
-  import { IS_PUSH } from '@/views/disaster/constant';
+  import { IS_PUSH, IS_EMERGENCY } from '@/views/disaster/constant';
 
   const { realname } = useUserInfoHook();
   const basicFormRef = ref<InstanceType<typeof BasicForm>>();
   const selectGroupRef = ref<InstanceType<typeof SelectGroup>>();
 
   const { warningTypeDice, disasterLevelDice, getWarningInfoDict, getDisasterLevelDict } = useDisasterWarningHook();
+  const { emergencyEventDice, getEmergencyEventDict } = useEmergencyProcedureHook();
 
   const { ruleFormConfig, ruleFormData, formRules, cloneRuleFormData, beforeRouteLeave } =
     useFormConfigHook<WarningInfoRuleForm>(WARNING_INFO_FROM_CONFIG, WARNING_INFO_FROM_DATA, WARNING_INFO_FROM_RULES);
 
+  // 应急事件类型配置
+  const EVENT_TYPE_PROP = 'eventType';
+  const initialFormConfig = [...ruleFormConfig.value];
+  const eventTypeIndex = initialFormConfig.findIndex((item) => item.prop === EVENT_TYPE_PROP);
+  const eventTypeConfig = eventTypeIndex > -1 ? initialFormConfig[eventTypeIndex] : null;
+  const eventTypeRules = formRules[EVENT_TYPE_PROP];
+
+  // 如果应急事件未启动,则默认启动应急事件
+  if (ruleFormData.isEmergency === undefined || ruleFormData.isEmergency === null) {
+    ruleFormData.isEmergency = IS_EMERGENCY.EMERGENCY;
+  }
+
+  watch(
+    () => ruleFormData.isEmergency,
+    (value) => {
+      const isEmergencyOpen = value === IS_EMERGENCY.EMERGENCY;
+
+      // 如果应急事件未启动,则清空应急事件类型
+      if (!isEmergencyOpen) {
+        if (ruleFormData.eventType) {
+          ruleFormData.eventType = '';
+        }
+        // 如果应急事件类型配置存在,则删除应急事件类型配置
+        if (eventTypeConfig && ruleFormConfig.value.some((item) => item.prop === EVENT_TYPE_PROP)) {
+          const withoutEventType = ruleFormConfig.value.filter((item) => item.prop !== EVENT_TYPE_PROP);
+          ruleFormConfig.value = withoutEventType;
+        }
+        // 如果应急事件类型规则存在,则删除应急事件类型规则
+        if (formRules[EVENT_TYPE_PROP]) {
+          delete formRules[EVENT_TYPE_PROP];
+        }
+        return;
+      }
+
+      // 如果应急事件已启动,则添加应急事件类型配置
+      if (eventTypeConfig && !ruleFormConfig.value.some((item) => item.prop === EVENT_TYPE_PROP)) {
+        const newConfig = [...ruleFormConfig.value];
+        const insertIndex =
+          eventTypeIndex > -1 && eventTypeIndex <= newConfig.length ? eventTypeIndex : newConfig.length;
+        newConfig.splice(insertIndex, 0, eventTypeConfig);
+        ruleFormConfig.value = newConfig;
+      }
+
+      if (eventTypeRules) {
+        formRules[EVENT_TYPE_PROP] = eventTypeRules;
+      }
+    },
+    { immediate: true },
+  );
+
   const handleUserGroupListChange = (userGroupList: number[]) => {
-    ruleFormData.userGroupList = userGroupList;
+    ruleFormData.userGroupList = userGroupList ?? [];
   };
 
   const handleValidate = async () => {
@@ -74,6 +142,9 @@
     if (!ruleFormData.isPush) {
       ruleFormData.userGroupList = [];
     }
+    if (ruleFormData.isEmergency !== IS_EMERGENCY.EMERGENCY) {
+      ruleFormData.eventType = '';
+    }
     cloneRuleFormData();
     return ruleFormData;
   };
@@ -85,6 +156,7 @@
   onMounted(() => {
     getWarningInfoDict();
     getDisasterLevelDict();
+    getEmergencyEventDict();
     ruleFormData.realname = realname;
     cloneRuleFormData();
     beforeRouteLeave();

+ 1 - 1
src/views/disaster/disaster-warning/src/components/EditDefenseNoticeItem.vue

@@ -23,7 +23,7 @@
       </template>
       <template #attachmentListRes>
         <UploadFiles
-          label="上传文件"
+          label="上传并导入文件"
           ref="uploadFilesRef"
           :fileList="ruleFormData.attachmentListRes"
           @uploadSuccess="handleUploadSuccess"

+ 69 - 5
src/views/disaster/disaster-warning/src/components/EditWarningInfoItem.vue

@@ -21,6 +21,22 @@
           />
         </el-select>
       </template>
+      <template #isEmergency>
+        <el-radio-group v-model="ruleFormData.isEmergency">
+          <el-radio :value="IS_EMERGENCY.EMERGENCY">是</el-radio>
+          <el-radio :value="IS_EMERGENCY.NOT_EMERGENCY">否</el-radio>
+        </el-radio-group>
+      </template>
+      <template #eventType>
+        <el-select v-model="ruleFormData.eventType" placeholder="请选择应急事件类型" filterable>
+          <el-option
+            v-for="item in emergencyEventDice"
+            :key="item.itemCode"
+            :label="item.itemValue"
+            :value="item.itemValue"
+          />
+        </el-select>
+      </template>
       <template #isPush>
         <el-radio-group v-model="ruleFormData.isPush">
           <el-radio :value="IS_PUSH.PUSH">是</el-radio>
@@ -29,8 +45,8 @@
         <SelectGroup
           v-if="ruleFormData.isPush"
           ref="selectGroupRef"
-          :userGroupList="ruleFormData.userGroupList || []"
-          @userGroupListChange="handleUserGroupListChange"
+          :user-group-list="ruleFormData.userGroupList || []"
+          @user-group-list-change="handleUserGroupListChange"
         />
       </template>
     </BasicForm>
@@ -38,15 +54,16 @@
 </template>
 
 <script setup lang="ts">
-  import { onMounted, ref } from 'vue';
+  import { onMounted, ref, watch } from 'vue';
   import BasicForm from '@/components/BasicForm.vue';
   import SelectGroup from '@/components/PersonGroup/SelectGroup.vue';
   import { useFormConfigHook } from '@/hooks/useFormConfigHook';
   import { useDisasterWarningHook } from '../hook';
+  import { useEmergencyProcedureHook } from '@/views/emergency/emergency-procedure/hooks';
   import type { WarningInfoRuleForm } from '../type';
   import { getWarningInfoDetail } from '@/api/disaster-warning';
   import { WARNING_INFO_FROM_CONFIG, WARNING_INFO_FROM_DATA, WARNING_INFO_FROM_RULES } from '../config';
-  import { IS_PUSH } from '@/views/disaster/constant';
+  import { IS_PUSH, IS_EMERGENCY } from '@/views/disaster/constant';
 
   const props = defineProps<{
     id: number;
@@ -56,10 +73,50 @@
   const selectGroupRef = ref<InstanceType<typeof SelectGroup>>();
 
   const { warningTypeDice, disasterLevelDice, getWarningInfoDict, getDisasterLevelDict } = useDisasterWarningHook();
+  const { emergencyEventDice, getEmergencyEventDict } = useEmergencyProcedureHook();
 
   const { ruleFormConfig, ruleFormData, formRules, cloneRuleFormData, beforeRouteLeave } =
     useFormConfigHook<WarningInfoRuleForm>(WARNING_INFO_FROM_CONFIG, WARNING_INFO_FROM_DATA, WARNING_INFO_FROM_RULES);
 
+  const EVENT_TYPE_PROP = 'eventType';
+  const initialFormConfig = [...ruleFormConfig.value];
+  const eventTypeIndex = initialFormConfig.findIndex((item) => item.prop === EVENT_TYPE_PROP);
+  const eventTypeConfig = eventTypeIndex > -1 ? initialFormConfig[eventTypeIndex] : null;
+  const eventTypeRules = formRules[EVENT_TYPE_PROP];
+
+  watch(
+    () => ruleFormData.isEmergency,
+    (value) => {
+      const isEmergencyOpen = value === IS_EMERGENCY.EMERGENCY;
+
+      if (!isEmergencyOpen) {
+        if (ruleFormData.eventType) {
+          ruleFormData.eventType = '';
+        }
+        if (eventTypeConfig && ruleFormConfig.value.some((item) => item.prop === EVENT_TYPE_PROP)) {
+          ruleFormConfig.value = ruleFormConfig.value.filter((item) => item.prop !== EVENT_TYPE_PROP);
+        }
+        if (formRules[EVENT_TYPE_PROP]) {
+          delete formRules[EVENT_TYPE_PROP];
+        }
+        return;
+      }
+
+      if (eventTypeConfig && !ruleFormConfig.value.some((item) => item.prop === EVENT_TYPE_PROP)) {
+        const newConfig = [...ruleFormConfig.value];
+        const insertIndex =
+          eventTypeIndex > -1 && eventTypeIndex <= newConfig.length ? eventTypeIndex : newConfig.length;
+        newConfig.splice(insertIndex, 0, eventTypeConfig);
+        ruleFormConfig.value = newConfig;
+      }
+
+      if (eventTypeRules) {
+        formRules[EVENT_TYPE_PROP] = eventTypeRules;
+      }
+    },
+    { immediate: true },
+  );
+
   const handleUserGroupListChange = (userGroupList: number[]) => {
     ruleFormData.userGroupList = userGroupList;
   };
@@ -68,8 +125,11 @@
     const res = await getWarningInfoDetail(props.id);
     for (const key in res) {
       if (key in ruleFormData) {
+        if (key === 'userGroupList') {
+          ruleFormData.userGroupList = JSON.parse(res.userGroupList as unknown as string);
+          continue;
+        }
         ruleFormData[key] = res[key as keyof typeof res];
-        ruleFormData.userGroupList = JSON.parse(res.userGroupList as unknown as string);
       }
     }
     cloneRuleFormData();
@@ -88,6 +148,9 @@
     if (!ruleFormData.isPush) {
       ruleFormData.userGroupList = [];
     }
+    if (ruleFormData.isEmergency !== IS_EMERGENCY.EMERGENCY) {
+      ruleFormData.eventType = '';
+    }
     cloneRuleFormData();
     return ruleFormData;
   };
@@ -100,6 +163,7 @@
     getWarningInfoDetailData();
     getWarningInfoDict();
     getDisasterLevelDict();
+    getEmergencyEventDict();
     beforeRouteLeave();
   });
 </script>

+ 2 - 2
src/views/disaster/disaster-warning/src/components/ViewDefenseNoticeItem.vue

@@ -36,7 +36,7 @@
   <section class="attachment" v-if="defenseNoticeDetail?.attachmentListRes.length">
     <span class="info-content" style="font-size: 14px">文件({{ defenseNoticeDetail?.attachmentListRes.length }})</span>
     &nbsp;
-    <a @click="downloadAll">下载全部</a>
+    <a @click="downloadAll">导出全部</a>
     <div class="attachment-list">
       <div
         class="attachment-item"
@@ -50,7 +50,7 @@
             <img :src="FILE_TYPE_ICON[item.fileType as keyof typeof FILE_TYPE_ICON]" />
             <span>{{ item.fileSize }}</span>
           </div>
-          <a @click.stop="downloadFile(item.fileUrl, item.fileName)">下载</a>
+          <a @click.stop="downloadFile(item.fileUrl, item.fileName)">导出</a>
         </div>
       </div>
     </div>

+ 27 - 5
src/views/disaster/disaster-warning/src/components/ViewWarningInfoItem.vue

@@ -14,31 +14,55 @@
       <p class="info-item">
         信息来源:<span class="info-content">{{ warningInfoDetail?.source }}</span>
       </p>
+      <p class="info-item">
+        启动应急事件:<span class="info-content">{{ getEmergencyStatusLabel }}</span>
+      </p>
+      <p v-if="warningInfoDetail?.isEmergency === IS_EMERGENCY.EMERGENCY" class="info-item">
+        应急事件类型:<span class="info-content">{{ emergencyEventLabel }}</span>
+      </p>
     </div>
   </header>
-  <section class="divider" />
+  <section class="divider"></section>
   <section class="content">
+    <!-- eslint-disable-next-line vue/no-v-html -->
     <div v-html="warningInfoDetail?.content"></div>
   </section>
 </template>
 
 <script setup lang="ts">
-  import { ref, onMounted } from 'vue';
+  import { ref, onMounted, computed } from 'vue';
   import { useDisasterWarningHook } from '../hook';
   import type { WarningInfoDetailResponse } from '@/types/disaster-warning';
   import { getWarningInfoDetail } from '@/api/disaster-warning';
+  import { useEmergencyProcedureHook } from '@/views/emergency/emergency-procedure/hooks';
+  import { IS_EMERGENCY } from '@/views/disaster/constant';
 
   const props = defineProps<{
     id: number;
   }>();
 
   const { getWarningInfoDict, getWarningType, getDisasterLevelDict, getDisasterLevel } = useDisasterWarningHook();
+  const { emergencyEventDice, getEmergencyEventDict } = useEmergencyProcedureHook();
 
   const warningInfoDetail = ref<WarningInfoDetailResponse>();
 
+  const getEmergencyStatusLabel = computed(() => {
+    if (!warningInfoDetail.value) return '-';
+    return warningInfoDetail.value.isEmergency === IS_EMERGENCY.EMERGENCY ? '是' : '否';
+  });
+
+  const emergencyEventLabel = computed(() => {
+    if (!warningInfoDetail.value || warningInfoDetail.value.isEmergency !== IS_EMERGENCY.EMERGENCY) {
+      return '-';
+    }
+    const eventType = warningInfoDetail.value.eventType || '';
+    return eventType || emergencyEventDice.value.find((item) => item.itemValue === eventType)?.itemValue || '-';
+  });
+
   onMounted(() => {
     getWarningInfoDict();
     getDisasterLevelDict();
+    getEmergencyEventDict();
     getWarningInfoDetail(props.id).then((res) => {
       warningInfoDetail.value = res;
     });
@@ -54,8 +78,6 @@
   }
   .info-item {
     width: 50%;
-    &:nth-child(1) {
-      margin-bottom: 16px;
-    }
+    margin-bottom: 16px;
   }
 </style>

+ 17 - 1
src/views/disaster/disaster-warning/src/config/form.ts

@@ -2,6 +2,8 @@
  * 灾害预警信息表单配置
  */
 import type { FormConfig } from '@/types/basic-form';
+import { IS_EMERGENCY } from '@/views/disaster/constant';
+
 // 通用表单信息
 const BASIC_FROM_CONFIG = {
   DISASTER_LEVEL: {
@@ -62,9 +64,19 @@ export const WARNING_INFO_FROM_CONFIG: FormConfig[] = [
     componentProps: {
       placeholder: '请输入发布内容',
       type: 'textarea',
-      rows: 5
+      rows: 5,
     },
   },
+  {
+    label: '启动应急事件:',
+    prop: 'isEmergency',
+    slot: 'isEmergency',
+  },
+  {
+    label: '应急事件类型:',
+    prop: 'eventType',
+    slot: 'eventType',
+  },
   BASIC_FROM_CONFIG.IS_PUSH,
   BASIC_FROM_CONFIG.CREATE_USER,
 ];
@@ -123,6 +135,8 @@ export const WARNING_INFO_FROM_DATA = {
   ...BASIC_FROM_DATA,
   warnTime: '',
   source: '',
+  isEmergency: IS_EMERGENCY.EMERGENCY,
+  eventType: '',
 };
 
 // 防御通知表单数据
@@ -153,6 +167,8 @@ export const WARNING_INFO_FROM_RULES = {
   ...BASIC_FROM_RULES,
   warnTime: [{ required: true, message: '请选择预警时间', trigger: 'change' }],
   source: [{ required: true, message: '请输入信息来源', trigger: 'blur' }],
+  isEmergency: [{ required: true, message: '请选择是否启动应急事件', trigger: 'change' }],
+  eventType: [{ required: true, message: '请选择应急事件类型', trigger: 'change' }],
 };
 
 // 防御通知表单规则

+ 30 - 1
src/views/home/src/components/IntroductionCard.vue

@@ -1,6 +1,11 @@
 <template>
   <div class="introduction-card-container">
-    <div class="introduction-card" v-for="item in COMPANY_INTRODUCTION_CARD_LIST" :key="item.title">
+    <div
+      class="introduction-card"
+      v-for="item in COMPANY_INTRODUCTION_CARD_LIST"
+      :key="item.title"
+      @click="handleCardClick(item)"
+    >
       <div class="introduction-card__header">
         <img :src="item.icon" />
         <p>{{ item.title }}</p>
@@ -17,7 +22,19 @@
   </div>
 </template>
 <script lang="ts" setup>
+  import router from '@/router';
+  import { NAV_LIST } from '@/constant/nav';
   import { COMPANY_INTRODUCTION_CARD_LIST } from '../constant';
+
+  const handleCardClick = (item) => {
+    const navItem = NAV_LIST.find((nav) => nav.meta.title === item.title);
+    if (!navItem || !navItem.path) {
+      router.replace({ name: 'StayTune' });
+      return;
+    }
+
+    router.push(navItem.path);
+  };
 </script>
 
 <style lang="scss" scoped>
@@ -27,12 +44,15 @@
     width: 100%;
     height: 100%;
   }
+
   .introduction-card {
     width: 361cpx;
     height: 100%;
     padding: 15cpx 34cpx 0 34cpx;
     background-color: $white-color;
     border-radius: 5cpx;
+    cursor: pointer;
+
     &__header {
       @include flex-center;
       flex-direction: column;
@@ -40,11 +60,13 @@
       font-weight: 550;
       font-size: 24cpx;
       color: #333;
+
       img {
         width: 80cpx;
         height: 80cpx;
       }
     }
+
     &__content {
       display: flex;
       justify-content: space-between;
@@ -52,6 +74,13 @@
       color: #666;
     }
   }
+
+  .introduction-card:hover {
+    transform: scale(1.02);
+    transition: all 0.3s ease-in-out;
+    box-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.1);
+  }
+
   .section-content {
     display: flex;
     flex-direction: column;