Przeglądaj źródła

完成灾害预警通知 增删改查 缺少上传

chauncey 11 miesięcy temu
rodzic
commit
728761a1c0

Plik diff jest za duży
+ 17 - 2
mock/disaster-warning/info.ts


+ 6 - 1
src/types/disaster-warning/index.ts

@@ -1,3 +1,4 @@
+import type { UserInfo } from '@/types/push-object';
 interface BasicResponse {
   id: number;
   disasterType: string;
@@ -21,7 +22,7 @@ export interface DefenseNoticeAttachment {
   fileName: string;
   fileType: string;
   fileSize: string;
-  downloadUrl: string;
+  downloadUrl?: string;
 }
 export interface DefenseNoticeDetailResponse {
   id: number;
@@ -32,4 +33,8 @@ export interface DefenseNoticeDetailResponse {
   publishTime: string;
   createUser: string;
   noticeAttachment: DefenseNoticeAttachment[];
+  isPush: boolean;
+  recipientType?: number;
+  userGroupList?: number[];
+  customUserList?: UserInfo[];
 }

+ 7 - 1
src/views/disaster/WorkRecord.md

@@ -7,4 +7,10 @@
 - [x]  创建灾害防御通知页面
 - [x]  提取公共scss(新建页面)
 - [x]  文件上传组件(是否通用?提取公共组件)
-- [ ]  文件在线预览组件、文件在线下载组件
+- [x]  文件在线预览组件、文件在线下载组件
+
+### 25/5/14
+#### 灾害防范模块
+- [ ] 准备通用hook-form表单
+#### 灾害预警模块
+- [ ] 完成所有静态页面开发

+ 77 - 0
src/views/disaster/components/PreviewOnline.vue

@@ -0,0 +1,77 @@
+<template>
+  <div class="preview-online-container" v-if="showPreviewOnline">
+    <el-icon class="close-icon" @click="close"><CloseBold /></el-icon>
+    <component :is="dynamicComponent[documentType as keyof typeof dynamicComponent]" :src="documentUrl" />
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { ref, computed, defineAsyncComponent } from 'vue';
+  import { FILE_TYPE_ICON } from '@/views/disaster/constant';
+  import { CloseBold } from '@element-plus/icons-vue';
+  import '@vue-office/docx/lib/index.css';
+  import '@vue-office/excel/lib/index.css';
+  const showPreviewOnline = ref(false);
+  const documentUrl = ref('');
+  const documentType = ref<keyof typeof FILE_TYPE_ICON>();
+  const open = (url: string, type: keyof typeof FILE_TYPE_ICON) => {
+    showPreviewOnline.value = true;
+    documentUrl.value = url;
+    documentType.value = type;
+  };
+  const close = () => {
+    showPreviewOnline.value = false;
+  };
+  const dynamicComponent = computed(() => {
+    return {
+      word: defineAsyncComponent(() => import('@vue-office/docx')),
+      excel: defineAsyncComponent(() => import('@vue-office/excel')),
+      pdf: defineAsyncComponent(() => import('@vue-office/pdf')),
+      ppt: defineAsyncComponent(() => import('@vue-office/pptx')),
+    };
+  });
+  defineExpose({
+    open,
+  });
+</script>
+
+<style lang="scss" scoped>
+  .preview-online-container {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100vw;
+    height: 100vh;
+    background-color: rgba(0, 0, 0, 0.3);
+    z-index: 9999;
+  }
+  .close-icon {
+    position: absolute;
+    top: 20px;
+    right: 20px;
+    cursor: pointer;
+    font-size: 30px;
+    color: $text-color;
+    z-index: 9999;
+  }
+  :deep(.docx-wrapper) {
+    background: $white-color;
+    padding: 0;
+  }
+  :deep(.slide-master-wrapper) {
+    height: 100vh;
+  }
+  :deep(.vue-office-pdf) {
+    height: 100% !important;
+    overflow-y: auto !important;
+  }
+  :deep(.vue-office-pdf-wrapper) {
+    padding: 0 !important;
+    canvas {
+      top: 0 !important;
+    }
+  }
+  :deep(.pptx-preview-wrapper) {
+    height: 1080px !important;
+  }
+</style>

+ 96 - 57
src/views/disaster/components/PushObject.vue

@@ -1,73 +1,89 @@
 <template>
-  <el-form :model="ruleForm" ref="ruleFormRef">
-    <el-form-item>
-      <el-radio-group v-model="ruleForm.recipientType">
-        <el-radio :value="1">分组</el-radio>
-        <el-radio :value="2">自定义</el-radio>
-      </el-radio-group>
-    </el-form-item>
-    <div class="userList" v-if="ruleForm.recipientType === 1">
-      <el-form-item label="选择分组" prop="userGroupList" :rules="[{ required: true, message: '请选择分组' }]">
-        <el-select v-model="ruleForm.userGroupList" multiple placeholder="请选择分组">
-          <el-option v-for="item in groupOptions" :key="item.id" :value="item.id" :label="item.name" />
-        </el-select>
+  <div class="push-object-container">
+    <el-form :model="ruleForm" ref="ruleFormRef">
+      <el-form-item>
+        <el-radio-group v-model="ruleForm.recipientType">
+          <el-radio :value="1">分组</el-radio>
+          <el-radio :value="2">自定义</el-radio>
+        </el-radio-group>
       </el-form-item>
-      <span @click="showGroupInfo"> 人员详情 </span>
-    </div>
-    <div class="userList" v-else-if="ruleForm.recipientType === 2">
-      <el-form-item label="选择人员" prop="customUserList" :rules="[{ required: true, message: '请选择人员' }]">
-        <el-select
-          v-model="ruleForm.customUserList"
-          value-key="id"
-          multiple
-          placeholder="请选择人员"
-          @click="userInfo = true"
-        >
-          <el-option
-            v-for="user in userOptions"
-            :key="user.id"
-            :label="`${user.username}-${user.realname}`"
-            :value="user"
-          />
-        </el-select>
-      </el-form-item>
-    </div>
-    <el-dialog
-      v-model="groupInfo"
-      title="人员详情"
-      align-center
-      :close-on-click-modal="false"
-      :destroy-on-close="true"
-      class="customDialog--pushObject"
-    >
-      <Group :userGroupInfo="userGroupInfo" />
-    </el-dialog>
-    <el-dialog
-      v-model="userInfo"
-      title="添加人员"
-      align-center
-      :close-on-click-modal="false"
-      :destroy-on-close="true"
-      class="customDialog--pushObject"
-    >
-      <User @cancel="userInfo = false" />
-    </el-dialog>
-  </el-form>
+      <div class="userList" v-if="ruleForm.recipientType === 1">
+        <el-form-item label="选择分组" prop="userGroupList" :rules="[{ required: true, message: '请选择分组' }]">
+          <el-select v-model="ruleForm.userGroupList" multiple placeholder="请选择分组" filterable>
+            <el-option v-for="item in groupOptions" :key="item.id" :value="item.id" :label="item.name" />
+          </el-select>
+        </el-form-item>
+        <span @click="showGroupInfo"> 人员详情 </span>
+      </div>
+      <div class="userList" v-else-if="ruleForm.recipientType === 2">
+        <el-form-item label="选择人员" prop="customUserList" :rules="[{ required: true, message: '请选择人员' }]">
+          <el-select
+            v-model="ruleForm.customUserList"
+            value-key="id"
+            multiple
+            placeholder="请选择人员"
+            @click="userInfo = true"
+          >
+            <el-option
+              v-for="user in userOptions"
+              :key="user.id"
+              :label="`${user.username}-${user.realname}`"
+              :value="user"
+            />
+          </el-select>
+        </el-form-item>
+      </div>
+      <el-dialog
+        v-model="groupInfo"
+        title="人员详情"
+        align-center
+        :close-on-click-modal="false"
+        :destroy-on-close="true"
+        class="customDialog--pushObject"
+      >
+        <Group :userGroupInfo="userGroupInfo" />
+      </el-dialog>
+      <el-dialog
+        v-model="userInfo"
+        title="添加人员"
+        align-center
+        :close-on-click-modal="false"
+        :destroy-on-close="true"
+        class="customDialog--pushObject"
+      >
+        <User :customUserList="ruleForm.customUserList" @cancel="userInfo = false" @submit="handleSubmit" />
+      </el-dialog>
+    </el-form>
+  </div>
 </template>
 
 <script lang="ts" setup>
-  import { ref, reactive } from 'vue';
+  import { ref, reactive, watch } from 'vue';
   import Group from './Group.vue';
   import User from './User.vue';
-  import type { GroupOptionType } from '../types';
+  import type { GroupOptionType, TreeNodeData } from '../types';
   import type { UserInfo, UserGroupInfo } from '@/types/push-object';
   import { queryUserGroupDetailByIds } from '@/api/push-object';
+  const props = defineProps<{
+    recipientType: number | null;
+    userGroupList: number[];
+    customUserList: UserInfo[];
+  }>();
   interface RuleForm {
     recipientType: number;
     userGroupList: number[];
     customUserList: UserInfo[];
   }
-  const groupOptions = ref<GroupOptionType[]>([]);
+  const groupOptions = ref<GroupOptionType[]>([
+    {
+      id: 1,
+      name: '测试1',
+    },
+    {
+      id: 2,
+      name: '测试2',
+    },
+  ]);
   const groupInfo = ref(false);
   const userInfo = ref(false);
   const userOptions = ref<UserInfo[]>([]);
@@ -89,9 +105,32 @@
       isHidden: false,
     }));
   };
+  const handleSubmit = (selectedPersonList: TreeNodeData[]) => {
+    ruleForm.customUserList = selectedPersonList.map((user) => {
+      return {
+        id: user.id,
+        username: user.name.split('-')[0],
+        realname: user.name.split('-')[1],
+      };
+    });
+    userOptions.value = ruleForm.customUserList;
+  };
+  watch(
+    [() => props.recipientType, () => props.userGroupList, () => props.customUserList],
+    ([newRecipientType, newUserGroupList, newCustomUserList]) => {
+      ruleForm.recipientType = newRecipientType ?? 1;
+      ruleForm.userGroupList = newUserGroupList ?? [];
+      ruleForm.customUserList = newCustomUserList ?? [];
+      userOptions.value = ruleForm.customUserList;
+    },
+    { immediate: true },
+  );
 </script>
 
 <style lang="scss" scoped>
+  .push-object-container {
+    width: 100%;
+  }
   :deep(.el-select__selection) {
     min-height: 25px;
     max-height: 60px;
@@ -99,7 +138,7 @@
   }
   .userList {
     width: 100%;
-    padding: 12cpx;
+    padding: 12cpx 0 0 12cpx;
     span {
       cursor: pointer;
       font-size: 10cpx;

+ 22 - 15
src/views/disaster/components/Upload.vue

@@ -5,13 +5,7 @@
       <el-icon><UploadFilled /></el-icon>
       <span>{{ label }}</span>
     </label>
-    <input
-      type="file"
-      id="fileInput"
-      multiple
-      accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
-      @change="handleFileSelect"
-    />
+    <input type="file" id="fileInput" multiple accept=".pdf,.docx,.xls,.xlsx,.ppt,.pptx" @change="handleFileSelect" />
 
     <!-- 总体进度条 -->
     <div class="progress-container" v-show="showProgress">
@@ -33,24 +27,23 @@
       <div v-for="file in fileList" :key="file.id" class="file-item">
         <div class="file-info">
           <img :src="FILE_TYPE_ICON[file.fileType as keyof typeof FILE_TYPE_ICON]" />
-          <span class="file-name">{{ file.file.name }}</span>
+          <span class="file-name">{{ file.fileName }}</span>
         </div>
-        <button class="delete-button" @click="removeFile(file.id)">
-          <el-icon><Delete /></el-icon>
-        </button>
+        <el-icon class="delete-button" @click="removeFile(file.id)"><Delete /></el-icon>
       </div>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-  import { ref, computed, nextTick } from 'vue';
+  import { ref, computed, nextTick, watch } from 'vue';
   import { UploadFilled, Delete } from '@element-plus/icons-vue';
   import { ElMessage, ElMessageBox } from 'element-plus';
   import { FILE_TYPE_ICON } from '../constant';
   import type { FileItem } from '../types';
-  defineProps<{
+  const props = defineProps<{
     label: string;
+    fileList?: FileItem[];
   }>();
 
   // 常量定义
@@ -71,7 +64,7 @@
   // 检查文件是否已存在
   const isFileAlreadyUploaded = (newFile: File): boolean => {
     return fileList.value.some((item) => {
-      return item.file.name === newFile.name && item.file.size === newFile.size && item.file.type === newFile.type;
+      return item.file?.name === newFile.name && item.file.size === newFile.size && item.file?.type === newFile.type;
     });
   };
 
@@ -156,11 +149,15 @@
   const addFileToUI = (file: File) => {
     const fileId = Date.now() + Math.floor(Math.random() * 1000);
     const fileType = getFileType(file);
+    const fileSize = file.size;
+    const fileName = file.name;
 
     fileList.value.unshift({
       id: fileId,
       file: file,
       fileType: fileType,
+      fileName: fileName,
+      fileSize: `${Math.round(fileSize / 1024)}KB`,
     });
   };
 
@@ -214,6 +211,15 @@
     }, interval);
   };
   const emit = defineEmits(['uploadSuccess']);
+  watch(
+    () => props.fileList,
+    (newVal) => {
+      fileList.value = newVal || [];
+    },
+    {
+      immediate: true,
+    },
+  );
 </script>
 
 <style lang="scss" scoped>
@@ -285,6 +291,7 @@
   .file-item {
     @include flex-center;
     justify-content: space-between;
+    width: 100%;
     border: 1px solid #e5e7eb;
     border-radius: 6cpx;
     padding: 12cpx;
@@ -305,7 +312,7 @@
   }
 
   .file-name {
-    width: 200cpx;
+    flex: 1;
     overflow: hidden;
     text-overflow: ellipsis;
     white-space: nowrap;

+ 31 - 6
src/views/disaster/components/User.vue

@@ -45,18 +45,22 @@
       </div>
       <div class="footer">
         <el-button @click="emit('cancel')">取消</el-button>
-        <el-button type="primary">确定</el-button>
+        <el-button type="primary" @click="handleSubmit">确定</el-button>
       </div>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-  import { nextTick, ref } from 'vue';
+  import { nextTick, ref, watch } from 'vue';
   import empty from 'assets/images/empty@1X.png';
   import { queryAvailableUserList } from '@/api/push-object';
   import type { TreeNodeData } from '@/views/disaster/types';
+  import type { UserInfo } from '@/types/push-object';
   import type { ElTree } from 'element-plus';
+  const props = defineProps<{
+    customUserList: UserInfo[];
+  }>();
   const loading = ref(false);
   const queryContent = ref('');
   const treeRef = ref<InstanceType<typeof ElTree>>();
@@ -75,8 +79,7 @@
   };
   const searchOptions = ref([
     { value: 'realname', label: '姓名' },
-    { value: 'staffNo', label: '工号' },
-    { value: 'deptName', label: '部门' },
+    { value: 'username', label: '工号' },
   ]);
   const selectType = ref(searchOptions.value[0].value);
   const getUserList = async () => {
@@ -85,12 +88,14 @@
     searchResult.value = res.map((user) => {
       return {
         id: user.id,
-        name: `${user.realname}-${user.deptName}`,
+        name: `${user.username}-${user.realname}`,
         children: [],
       };
     });
     nodeData.value[0].children = searchResult.value;
+    const selectedIds = selectedPersonList.value.map((item) => item.id);
     await nextTick();
+    treeRef.value?.setCheckedKeys(selectedIds);
     loading.value = false;
   };
   const handleSearch = () => {
@@ -105,7 +110,8 @@
   const handleCheckChange = (node: TreeNodeData, checked: boolean) => {
     if (node.children.length) return;
     if (checked) {
-      console.log(node, checked);
+      const exists = selectedPersonList.value.find((item) => item.id === node.id);
+      if (exists) return;
       selectedPersonList.value.push(node);
     } else {
       removeSelectedPerson(node.id);
@@ -117,6 +123,25 @@
     treeRef.value.setChecked(id, false, true);
   };
   const emit = defineEmits(['cancel', 'submit']);
+  const handleSubmit = () => {
+    emit('submit', selectedPersonList.value);
+    emit('cancel');
+  };
+  watch(
+    () => props.customUserList,
+    (newVal) => {
+      selectedPersonList.value = newVal.map((item) => {
+        return {
+          id: item.id,
+          name: `${item.username}-${item.realname}`,
+          children: [],
+        };
+      });
+    },
+    {
+      immediate: true,
+    },
+  );
 </script>
 
 <style lang="scss" scoped>

+ 11 - 1
src/views/disaster/disaster-warning/PageDefenseNotice.vue

@@ -36,7 +36,11 @@
             </span>
           </template>
           <template #action="scope">
-            <ActionButton text="编辑" v-if="scope.row.activeStatus === ACTIVE_STATUS.NOT_EFFECTIVE" />
+            <ActionButton
+              text="编辑"
+              v-if="scope.row.activeStatus === ACTIVE_STATUS.NOT_EFFECTIVE"
+              @click="handleEditDefenseNotice(scope.row.id)"
+            />
             <ActionButton text="查看" @click="handleViewDefenseNotice(scope.row.id)" />
             <ActionButton
               text="发布"
@@ -81,6 +85,12 @@
   const tableData = ref<DefenseNoticeListResponse[]>([]);
   const router = useRouter();
   const defaultPath = '/disaster-prevention/disaster-warning/defense-notice-item';
+  const handleEditDefenseNotice = (id: number) => {
+    router.push({
+      path: defaultPath,
+      query: { id, operate: 'edit' },
+    });
+  };
   const handleCreateDefenseNotice = () => {
     router.push({
       path: defaultPath,

+ 3 - 1
src/views/disaster/disaster-warning/PageDefenseNoticeItem.vue

@@ -5,7 +5,7 @@
       <span class="disaster-precaution-container__title">{{ headerTitle }}</span>
     </header>
     <main class="disaster-precaution-container__main">
-      <component :is="dynamicComponent" :id="id"/>
+      <component :is="dynamicComponent" :id="id" />
     </main>
     <footer class="disaster-precaution-container__footer" v-if="operate">
       <el-button>取消</el-button>
@@ -34,6 +34,8 @@
   const dynamicComponent = computed(() => {
     if (operate === 'create') {
       return defineAsyncComponent(() => import('./src/components/CreateDefenseNoticeItem.vue'));
+    } else if (operate === 'edit') {
+      return defineAsyncComponent(() => import('./src/components/EditDefenseNoticeItem.vue'));
     } else {
       return defineAsyncComponent(() => import('./src/components/ViewDefenseNoticeItem.vue'));
     }

+ 26 - 89
src/views/disaster/disaster-warning/src/components/CreateDefenseNoticeItem.vue

@@ -1,106 +1,43 @@
 <template>
   <div class="info-container">
     <el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" label-width="auto">
-      <el-form-item label="灾害类型:" prop="disasterType">
-        <el-select v-model="ruleForm.disasterType" placeholder="请选择灾害类型" filterable>
-          <el-option v-for="item in DISASTER_TYPE" :key="item.value" :label="item.label" :value="item.value" />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="灾害等级:" prop="disasterLevel">
-        <el-select v-model="ruleForm.disasterLevel" placeholder="请选择灾害等级" filterable>
-          <el-option v-for="item in DISASTER_LEVEL" :key="item.value" :label="item.label" :value="item.value" />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="通知标题:" prop="noticeTitle">
-        <el-input v-model="ruleForm.noticeTitle" placeholder="请输入通知标题" maxlength="200" show-word-limit />
-      </el-form-item>
-      <el-form-item label="通知内容:" prop="noticeContent">
-        <el-input
-          v-model="ruleForm.noticeContent"
-          type="textarea"
-          placeholder="请输入通知内容"
-          :rows="3"
-          maxlength="1000"
-          show-word-limit
-        />
-      </el-form-item>
-      <el-form-item label="通知附件:" prop="noticeAttachment">
-        <Upload label="上传附件" @uploadSuccess="handleUploadSuccess" />
-      </el-form-item>
-      <el-form-item label="是否推送:" prop="isPush">
-        <el-radio-group v-model="ruleForm.isPush">
-          <el-radio :value="true">是</el-radio>
-          <el-radio :value="false">否</el-radio>
-        </el-radio-group>
-        <PushObject v-if="ruleForm.isPush" />
-      </el-form-item>
-      <el-form-item label="创建人:">
-        <el-input v-model="ruleForm.createUser" disabled />
+      <el-form-item v-for="item in formConfig" :key="item.prop" :label="item.label" :prop="item.prop">
+        <template v-if="item.slot">
+          <Upload v-if="item.slot === 'noticeAttachment'" label="上传附件" @uploadSuccess="handleUploadSuccess" />
+
+          <div v-else-if="item.slot === 'isPush'" class="push-container">
+            <el-radio-group v-model="ruleForm.isPush">
+              <el-radio :value="true">是</el-radio>
+              <el-radio :value="false">否</el-radio>
+            </el-radio-group>
+            <PushObject v-if="ruleForm.isPush" />
+          </div>
+        </template>
+
+        <component v-else :is="item.component" v-model="ruleForm[item.prop]" v-bind="item.componentProps">
+          <el-option
+            v-for="option in item.selectOptions"
+            :key="option.value"
+            :label="option.label"
+            :value="option.value"
+          />
+        </component>
       </el-form-item>
     </el-form>
   </div>
 </template>
 
 <script setup lang="ts">
-  import { reactive, ref } from 'vue';
   import PushObject from '@/views/disaster/components/PushObject.vue';
   import Upload from '@/views/disaster/components/Upload.vue';
-  import type { FormInstance, FormRules } from 'element-plus';
-  import { DISASTER_TYPE, DISASTER_LEVEL } from '@/views/disaster/constant';
-  import type { FileItem } from '@/views/disaster/types';
-
-  interface RuleForm {
-    disasterType: string;
-    disasterLevel: string;
-    noticeTitle: string;
-    noticeContent: string;
-    noticeAttachment: FileItem[];
-    isPush: string;
-    createUser: string;
-  }
-
-  const ruleFormRef = ref<FormInstance>();
-  const ruleForm = reactive<RuleForm>({
-    disasterType: '',
-    disasterLevel: '',
-    noticeTitle: '',
-    noticeContent: '',
-    noticeAttachment: [],
-    isPush: '',
-    createUser: 'XXX',
-  });
-  const handleUploadSuccess = (fileList: FileItem[]) => {
-    ruleForm.noticeAttachment = fileList;
-  };
+  import { useDefenseNoticeFormHook } from '@/views/disaster/disaster-warning/src/useFormHook';
 
-  const rules = reactive<FormRules<RuleForm>>({
-    disasterType: [{ required: true, message: '请选择灾害类型', trigger: 'change' }],
-    disasterLevel: [
-      {
-        required: true,
-        message: '请选择灾害等级',
-        trigger: 'change',
-      },
-    ],
-    noticeTitle: [
-      {
-        required: true,
-        message: '请输入通知标题',
-        trigger: 'blur',
-      },
-    ],
-    noticeContent: [
-      {
-        required: true,
-        message: '请输入通知内容',
-        trigger: 'blur',
-      },
-    ],
-    noticeAttachment: [{ required: true, message: '请上传通知附件', trigger: 'change' }],
-    isPush: [{ required: true, message: '请选择是否推送', trigger: 'change' }],
-  });
+  const { formConfig, ruleFormRef, ruleForm, rules } = useDefenseNoticeFormHook();
 </script>
 
 <style scoped lang="scss">
   @use '@/views/disaster/style/info-container.scss' as *;
+  .push-container {
+    width: 100%;
+  }
 </style>

+ 75 - 0
src/views/disaster/disaster-warning/src/components/EditDefenseNoticeItem.vue

@@ -0,0 +1,75 @@
+<template>
+  <div class="info-container">
+    <el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" label-width="auto">
+      <el-form-item v-for="item in formConfig" :key="item.prop" :label="item.label" :prop="item.prop">
+        <template v-if="item.slot">
+          <Upload
+            v-if="item.slot === 'noticeAttachment'"
+            label="上传附件"
+            :fileList="ruleForm.noticeAttachment"
+            @uploadSuccess="handleUploadSuccess"
+          />
+
+          <div v-else-if="item.slot === 'isPush'" class="push-container">
+            <el-radio-group v-model="ruleForm.isPush">
+              <el-radio :value="true">是</el-radio>
+              <el-radio :value="false">否</el-radio>
+            </el-radio-group>
+            <PushObject
+              v-if="ruleForm.isPush"
+              :recipientType="ruleForm.recipientType"
+              :userGroupList="ruleForm.userGroupList"
+              :customUserList="ruleForm.customUserList"
+            />
+          </div>
+        </template>
+
+        <component v-else :is="item.component" v-model="ruleForm[item.prop]" v-bind="item.componentProps">
+          <el-option
+            v-for="option in item.selectOptions"
+            :key="option.value"
+            :label="option.label"
+            :value="option.value"
+          />
+        </component>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { onMounted } from 'vue';
+  import PushObject from '@/views/disaster/components/PushObject.vue';
+  import Upload from '@/views/disaster/components/Upload.vue';
+  import { useDefenseNoticeFormHook } from '@/views/disaster/disaster-warning/src/useFormHook';
+  import { getDefenseNoticeDetail } from '@/api/disaster-warning';
+
+  const props = defineProps<{
+    id: number;
+  }>();
+
+  const { formConfig, ruleFormRef, ruleForm, rules, handleUploadSuccess } = useDefenseNoticeFormHook();
+  const getDefenseNoticeDetailData = async () => {
+    const res = await getDefenseNoticeDetail(props.id);
+    ruleForm.createUser = res.createUser;
+    ruleForm.disasterType = res.disasterType;
+    ruleForm.disasterLevel = res.disasterLevel;
+    ruleForm.noticeTitle = res.noticeTitle;
+    ruleForm.noticeContent = res.noticeContent;
+    ruleForm.isPush = res.isPush;
+    ruleForm.noticeAttachment = res.noticeAttachment;
+    ruleForm.recipientType = res.recipientType ?? null;
+    ruleForm.userGroupList = res.userGroupList ?? [];
+    ruleForm.customUserList = res.customUserList ?? [];
+  };
+  onMounted(() => {
+    getDefenseNoticeDetailData();
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/views/disaster/style/info-container.scss' as *;
+  .push-container {
+    width: 100%;
+  }
+</style>

+ 23 - 7
src/views/disaster/disaster-warning/src/components/ViewDefenseNoticeItem.vue

@@ -24,19 +24,26 @@
     </section>
     <section class="attachment">
       <span class="info-content">附件({{ defenseNoticeDetail?.noticeAttachment.length }})</span>
+      <a @click="downloadAll">下载全部</a>
       <div class="attachment-list">
-        <div class="attachment-item" v-for="item in defenseNoticeDetail?.noticeAttachment" :key="item.id">
+        <div
+          class="attachment-item"
+          v-for="item in defenseNoticeDetail?.noticeAttachment"
+          :key="item.id"
+          @click="previewOnline(item.downloadUrl, item.fileType as keyof typeof FILE_TYPE_ICON)"
+        >
           <span class="attachment-item--name">{{ item.fileName }}</span>
           <div class="attachment-item--footer">
             <div class="info">
               <img :src="FILE_TYPE_ICON[item.fileType as keyof typeof FILE_TYPE_ICON]" />
               <span>{{ item.fileSize }}</span>
             </div>
-            <a @click="handleDownload(item.downloadUrl)">下载</a>
+            <a @click.stop="downloadFile(item.downloadUrl, item.fileName)">下载</a>
           </div>
         </div>
       </div>
     </section>
+    <PreviewOnline ref="previewOnlineRef" />
   </div>
 </template>
 
@@ -44,17 +51,26 @@
   import { ref, onMounted } from 'vue';
   import { FILE_TYPE_ICON } from '@/views/disaster/constant';
   import { getDefenseNoticeDetail } from '@/api/disaster-warning';
-  import type { DefenseNoticeDetailResponse, DefenseNoticeAttachment } from '@/types/disaster-warning';
-  import VueOfficeDocx from '@vue-office/docx';
-  import '@vue-office/docx/lib/index.css';
+  import type { DefenseNoticeDetailResponse } from '@/types/disaster-warning';
+  import { downloadFile } from '@/views/disaster/utils/download';
+  import PreviewOnline from '@/views/disaster/components/PreviewOnline.vue';
 
   const props = defineProps<{
     id: number;
   }>();
 
+  const previewOnlineRef = ref<InstanceType<typeof PreviewOnline>>();
+
+  const previewOnline = (url: string, type: keyof typeof FILE_TYPE_ICON) => {
+    previewOnlineRef.value?.open(url, type);
+  };
+
   const defenseNoticeDetail = ref<DefenseNoticeDetailResponse>();
-  const handleDownload = (fileUrl: string) => {
-    window.open(fileUrl, '_blank');
+
+  const downloadAll = () => {
+    defenseNoticeDetail.value?.noticeAttachment.forEach((item) => {
+      downloadFile(item.downloadUrl, item.fileName);
+    });
   };
 
   onMounted(() => {

+ 142 - 0
src/views/disaster/disaster-warning/src/useFormHook.ts

@@ -0,0 +1,142 @@
+/**
+ * 灾害防御通知表单
+ */
+import { ref, reactive } from 'vue';
+import type { FormInstance, FormRules } from 'element-plus';
+import { cloneDeep } from 'lodash-es';
+import { DISASTER_TYPE, DISASTER_LEVEL } from '@/views/disaster/constant';
+import type { FileItem } from '@/views/disaster/types';
+import type { DefenseNoticeAttachment } from '@/types/disaster-warning';
+import type { UserInfo } from '@/types/push-object';
+export const useDefenseNoticeFormHook = () => {
+  interface RuleForm {
+    disasterType: string;
+    disasterLevel: string;
+    noticeTitle: string;
+    noticeContent: string;
+    noticeAttachment: DefenseNoticeAttachment[];
+    isPush: boolean | null;
+    recipientType: number | null;
+    userGroupList: number[];
+    customUserList: UserInfo[];
+    createUser: string;
+    [key: string]: any; // 添加索引签名
+  }
+  const formConfig = [
+    {
+      label: '灾害类型',
+      prop: 'disasterType',
+      component: 'ElSelect',
+      componentProps: {
+        placeholder: '请选择灾害类型',
+        filterable: true,
+        clearable: true,
+      },
+      selectOptions: DISASTER_TYPE,
+    },
+    {
+      label: '灾害等级',
+      prop: 'disasterLevel',
+      component: 'ElSelect',
+      componentProps: {
+        placeholder: '请选择灾害等级',
+        filterable: true,
+        clearable: true,
+      },
+      selectOptions: DISASTER_LEVEL,
+    },
+    {
+      label: '通知标题',
+      prop: 'noticeTitle',
+      component: 'ElInput',
+      componentProps: {
+        placeholder: '请输入通知标题',
+        maxlength: 200,
+        showWordLimit: true,
+      },
+    },
+    {
+      label: '通知内容',
+      prop: 'noticeContent',
+      component: 'ElInput',
+      componentProps: {
+        placeholder: '请输入通知内容',
+        type: 'textarea',
+        rows: 5,
+        maxlength: 1000,
+        showWordLimit: true,
+      },
+    },
+    {
+      label: '通知附件',
+      prop: 'noticeAttachment',
+      slot: 'noticeAttachment',
+    },
+    {
+      label: '是否推送',
+      prop: 'isPush',
+      slot: 'isPush',
+    },
+    {
+      label: '创建人',
+      prop: 'createUser',
+      component: 'ElInput',
+      componentProps: {
+        disabled: true,
+      },
+    },
+  ];
+  const ruleFormRef = ref<FormInstance>();
+  const ruleForm = reactive<RuleForm>({
+    disasterType: '',
+    disasterLevel: '',
+    noticeTitle: '',
+    noticeContent: '',
+    noticeAttachment: [],
+    isPush: null,
+    recipientType: null,
+    userGroupList: [],
+    customUserList: [],
+    createUser: '',
+  });
+  const cloneRuleForm = () => {
+    return cloneDeep(ruleForm);
+  };
+  const rules = reactive<FormRules<RuleForm>>({
+    disasterType: [{ required: true, message: '请选择灾害类型', trigger: 'change' }],
+    disasterLevel: [
+      {
+        required: true,
+        message: '请选择灾害等级',
+        trigger: 'change',
+      },
+    ],
+    noticeTitle: [
+      {
+        required: true,
+        message: '请输入通知标题',
+        trigger: 'blur',
+      },
+    ],
+    noticeContent: [
+      {
+        required: true,
+        message: '请输入通知内容',
+        trigger: 'blur',
+      },
+    ],
+    noticeAttachment: [{ required: true, message: '请上传通知附件', trigger: 'change' }],
+    isPush: [{ required: true, message: '请选择是否推送', trigger: 'change' }],
+  });
+  const handleUploadSuccess = (fileList: FileItem[]) => {
+    ruleForm.noticeAttachment = fileList;
+  };
+  return {
+    formConfig,
+    ruleFormRef,
+    ruleForm,
+    rules,
+    cloneRuleForm,
+    handleUploadSuccess,
+  };
+};

+ 3 - 4
src/views/disaster/types/index.ts

@@ -1,3 +1,4 @@
+import type { DefenseNoticeAttachment } from '@/types/disaster-warning';
 export interface GroupOptionType {
   id: number;
   name: string;
@@ -9,8 +10,6 @@ export interface TreeNodeData {
   children: TreeNodeData[];
 }
 
-export interface FileItem {
-  id: number;
-  file: File;
-  fileType: string;
+export interface FileItem extends DefenseNoticeAttachment {
+  file?: File;
 }

+ 46 - 0
src/views/disaster/utils/download.ts

@@ -0,0 +1,46 @@
+/**
+ * 根据url和name下载文件
+ * @param url 文件url
+ * @param name 文件名
+ */
+export const downloadFile = (url: string, name: string): void => {
+  // 创建XMLHttpRequest请求
+  const request = new XMLHttpRequest();
+  request.responseType = "blob";
+  
+  // 打开请求
+  request.open("GET", url);
+  
+  // 设置onload回调函数
+  request.onload = function () {
+    if (this.status === 200) {
+      // 创建Blob URL
+      const blobUrl = window.URL.createObjectURL(this.response);
+      
+      // 创建a标签
+      const link = document.createElement('a');
+      // 设置下载链接
+      link.href = blobUrl;
+      // 设置下载文件名
+      link.download = name;
+      // 设置链接样式为不可见
+      link.style.display = 'none';
+      // 添加到文档中
+      document.body.appendChild(link);
+      // 触发点击事件执行下载
+      link.click();
+      // 下载完成后移除该元素
+      document.body.removeChild(link);
+      // 释放blob URL
+      window.URL.revokeObjectURL(blobUrl);
+    }
+  };
+  
+  // 添加错误处理
+  request.onerror = function() {
+    console.error('下载文件失败');
+  };
+  
+  // 发送请求
+  request.send();
+};

+ 21 - 21
src/views/system-config/scene-manage/components/WorkshopDrawer.vue

@@ -50,7 +50,7 @@
 <script setup lang="ts">
   import { computed, reactive, ref, defineProps, defineEmits, watch } from 'vue';
   import { type FormInstance, type FormRules } from 'element-plus';
-  import { addWorkshop, editWorkshop } from '@/api/scene/scene.ts';
+  // import { addWorkshop, editWorkshop } from '@/api/scene/scene.ts';
 
   const props = defineProps<{
     detail: {
@@ -125,16 +125,16 @@
         // orderNum: props.detail.orderNum,
       };
 
-      addWorkshop(newAddData)
-        .then(() => {
-          console.log('成功添加车间');
-        })
-        .catch((err) => {
-          console.log(err);
-        })
-        .finally(() => {
-          emit('onOk');
-        });
+      // addWorkshop(newAddData)
+      //   .then(() => {
+      //     console.log('成功添加车间');
+      //   })
+      //   .catch((err) => {
+      //     console.log(err);
+      //   })
+      //   .finally(() => {
+      //     emit('onOk');
+      //   });
     });
   };
 
@@ -151,16 +151,16 @@
         // // isDisabled: ruleForm.isDisabled,
       };
 
-      editWorkshop(editNewWorkshopData)
-        .then(() => {
-          console.log('成功修改车间');
-        })
-        .catch((err) => {
-          console.log(err);
-        })
-        .finally(() => {
-          emit('onOk');
-        });
+      // editWorkshop(editNewWorkshopData)
+      //   .then(() => {
+      //     console.log('成功修改车间');
+      //   })
+      //   .catch((err) => {
+      //     console.log(err);
+      //   })
+      //   .finally(() => {
+      //     emit('onOk');
+      //   });
     });
   };