Pārlūkot izejas kodu

Merge branch 'feature/planManage' of maoming/yjhtgl-web into dev

黄文锋 11 mēneši atpakaļ
vecāks
revīzija
e622eb82e9

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

@@ -0,0 +1,26 @@
+import request from '@/utils/request';
+
+export function getEvent(params) {
+  return request({
+    url: '/api/event/list',
+    method: 'get',
+    params: params
+  });
+}
+
+// 获取路由
+export function addEvent(data) {
+  return request({
+    url: '/api/event/create',
+    method: 'post',
+    data: data
+  });
+}
+
+export function getEventDetail(params) {
+  return request({
+    url: '/api/event/detail',
+    method: 'get',
+    params: params
+  });
+}

+ 23 - 0
src/api/riskPrevention/planManage.ts

@@ -0,0 +1,23 @@
+import request from '@/utils/request';
+
+export function getDrillList(params) {
+  return request({
+    url: '/api/emergency_plan/drill/list',
+    method: 'get',
+    params: params
+  });
+}
+export function getTrainingList(params) {
+  return request({
+    url: '/api/emergency_plan/training/list',
+    method: 'get',
+    params: params
+  });
+}
+
+export function getPlanDetail(planId) {
+  return request({
+    url: '/api/emergency_plan/plan/' + planId,
+    method: 'get'
+  });
+}

+ 148 - 5
src/api/system/user/index.ts

@@ -2,7 +2,24 @@ import { DeptVO } from './../dept/types';
 import { RoleVO } from '@/api/system/role/types';
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
-import { UserForm, UserQuery, UserVO, UserInfoVO } from './types';
+import {
+  UserForm,
+  UserQuery,
+  UserVO,
+  UserInfoVO,
+  PlanForm,
+  PlanVO,
+  PlanQuery,
+  PlanInfoVO,
+  DrillForm,
+  FetchDrillResponse,
+  DrillQuery,
+  DrillInfoVO,
+  ResponseForm,
+  ResponseVO,
+  ResponseQuery,
+  ResponseInfoVO
+} from './types';
 import { parseStrEmpty } from '@/utils/ruoyi';
 
 /**
@@ -17,6 +34,31 @@ export const listUser = (query): AxiosPromise<UserVO[]> => {
   });
 };
 
+export const listPlan = (query: PlanQuery): AxiosPromise<PlanVO[]> => {
+  return request({
+    url: '/system/user/list',
+    method: 'get',
+    params: query
+  });
+};
+export const listDrill = (params: DrillForm): AxiosPromise<FetchDrillResponse> => {
+  return request({
+    url: '/api/emergency_plan/drill/list',
+    method: 'get',
+    params: {
+      page: params.pageNum, // 分页参数中的页码
+      page_size: params.pageSize, // 分页参数中的每页大小
+      planNum: params.planId // 预案编号
+    }
+  });
+};
+export const listResponse = (query: DrillQuery): AxiosPromise<ResponseVO[]> => {
+  return request({
+    url: '/system/user/list',
+    method: 'get',
+    params: query
+  });
+};
 /**
  * 通过用户ids查询用户
  * @param userIds
@@ -29,7 +71,7 @@ export const optionSelect = (userIds: (number | string)[]): AxiosPromise<UserVO[
 };
 
 /**
- * 获取用户详情
+ * 获取培训详情
  * @param userId
  */
 export const getUser = (userId?: string | number): AxiosPromise<UserInfoVO> => {
@@ -38,9 +80,38 @@ export const getUser = (userId?: string | number): AxiosPromise<UserInfoVO> => {
     method: 'get'
   });
 };
-
 /**
- * 新增用户
+ * 获取预案详情
+ * @param planId
+ */
+export const getPlan = (planId?: string | number): AxiosPromise<PlanInfoVO> => {
+  return request({
+    url: '/system/user/' + parseStrEmpty(planId),
+    method: 'get'
+  });
+};
+/**
+ * 获取预案详情
+ * @param drillId
+ */
+export const getDrill = (drillId?: string | number): AxiosPromise<DrillInfoVO> => {
+  return request({
+    url: '/system/user/' + parseStrEmpty(drillId),
+    method: 'get'
+  });
+};
+/**
+ * 获取响应详情
+ * @param drillId
+ */
+export const getResponse = (drillId?: string | number): AxiosPromise<ResponseInfoVO> => {
+  return request({
+    url: '/system/user/' + parseStrEmpty(drillId),
+    method: 'get'
+  });
+};
+/**
+ * 新增培训
  */
 export const addUser = (data: UserForm) => {
   return request({
@@ -49,9 +120,29 @@ export const addUser = (data: UserForm) => {
     data: data
   });
 };
+/**
+ * 新增预案
+ */
+export const addPlan = (data: PlanForm) => {
+  return request({
+    url: '/system/user',
+    method: 'post',
+    data: data
+  });
+};
+/**
+ * 新增演练
+ */
+export const addDrill = (data: DrillForm) => {
+  return request({
+    url: '/system/user',
+    method: 'post',
+    data: data
+  });
+};
 
 /**
- * 修改用户
+ * 修改培训
  */
 export const updateUser = (data: UserForm) => {
   return request({
@@ -60,6 +151,26 @@ export const updateUser = (data: UserForm) => {
     data: data
   });
 };
+/**
+ * 修改预案
+ */
+export const updatePlan = (data: PlanForm) => {
+  return request({
+    url: '/system/user',
+    method: 'put',
+    data: data
+  });
+};
+/**
+ * 修改演练
+ */
+export const updateDrill = (data: DrillForm) => {
+  return request({
+    url: '/system/user',
+    method: 'put',
+    data: data
+  });
+};
 
 /**
  * 删除用户
@@ -71,6 +182,26 @@ export const delUser = (userId: Array<string | number> | string | number) => {
     method: 'delete'
   });
 };
+/**
+ * 删除预案
+ * @param planId 用户ID
+ */
+export const delPlan = (planId: Array<string | number> | string | number) => {
+  return request({
+    url: '/system/user/' + planId,
+    method: 'delete'
+  });
+};
+/**
+ * 删除演练
+ * @param planId 用户ID
+ */
+export const delDrill = (drillId: Array<string | number> | string | number) => {
+  return request({
+    url: '/system/user/' + drillId,
+    method: 'delete'
+  });
+};
 
 /**
  * 用户密码重置
@@ -211,11 +342,23 @@ export const deptTreeSelect = (): AxiosPromise<DeptVO[]> => {
 
 export default {
   listUser,
+  listPlan,
+  listDrill,
+  listResponse,
   getUser,
+  getPlan,
+  getDrill,
+  getResponse,
   optionSelect,
   addUser,
+  addPlan,
+  addDrill,
   updateUser,
+  updatePlan,
+  updateDrill,
   delUser,
+  delPlan,
+  delDrill,
   resetUserPwd,
   changeUserStatus,
   getUserProfile,

+ 44 - 0
src/api/system/user/planList.ts

@@ -0,0 +1,44 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { PlanForm, PlanVO, FetchPlanResponse } from './types';
+
+// 获取预案列表
+export const fetchReports = (params: PlanForm): AxiosPromise<FetchPlanResponse> => {
+  return request({
+    url: '/api/emergency_plan/plan/list',
+    method: 'get',
+    params: {
+      page: params.pageNum, // 分页参数中的页码
+      page_size: params.pageSize, // 分页参数中的每页大小
+      plan_type: params.planType, // 预案类型(可选)
+      publish_date: params.publish_date, // 发布日期范围(可选)
+      plan_name: params.planName // 预案名称(可选)
+    }
+  });
+};
+
+// 新增预案
+export const addReport = (data: PlanVO): AxiosPromise<void> => {
+  return request({
+    url: '/api/emergency_plan/plan/create',
+    method: 'post',
+    data: data
+  });
+};
+
+// 修改预案
+export const updateReport = (data: PlanVO): AxiosPromise<void> => {
+  return request({
+    url: `/api/reports/update/${data.reportNumber}`,
+    method: 'put',
+    data: data
+  });
+};
+
+// 删除预案
+export const deleteReport = (reportNumber: string): AxiosPromise<void> => {
+  return request({
+    url: `/api/reports/delete/${reportNumber}`,
+    method: 'delete'
+  });
+};

+ 102 - 66
src/api/system/user/types.ts

@@ -1,84 +1,120 @@
-import { RoleVO } from '@/api/system/role/types';
-import { PostVO } from '@/api/system/post/types';
-
+//培训记录
 /**
- * 用户信息
- */
-export interface UserInfo {
-  user: UserVO;
-  roles: string[];
-  permissions: string[];
-}
-
-/**
- * 用户查询对象类型
+ * 培训记录查询对象类型
  */
 export interface UserQuery extends PageQuery {
-  userName?: string;
-  phonenumber?: string;
-  status?: string;
-  deptId?: string | number;
-  roleId?: string | number;
+  theme?: string;
+  unitName?: string;
 }
-
 /**
- * 用户返回对象
+ * 培训记录返回对象
  */
-export interface UserVO extends BaseEntity {
-  userId: string | number;
-  tenantId: string;
-  deptId: number;
-  userName: string;
-  nickName: string;
-  userType: string;
-  email: string;
-  phonenumber: string;
-  sex: string;
-  avatar: string;
-  status: string;
-  delFlag: string;
-  loginIp: string;
-  loginDate: string;
-  remark: string;
-  deptName: string;
-  roles: RoleVO[];
-  roleIds: any;
-  postIds: any;
-  roleId: any;
-  admin: boolean;
+export interface UserVO {
+  textId: string;
+  theme: string;
+  unitName: string;
+  address: string;
+  startTime: string;
+  endTime: string;
+  peopleNum: string;
+  Content: string;
+  trainingWay: string;
 }
-
 /**
- * 用户表单类型
+ * 培训记录表单类型
  */
 export interface UserForm {
   id?: string;
-  userId?: string;
-  deptId?: number;
-  userName: string;
-  nickName?: string;
-  password: string;
-  phonenumber?: string;
-  email?: string;
-  sex?: string;
-  status: string;
-  remark?: string;
-  postIds: string[];
-  roleIds: string[];
+  textId?: string;
+  theme: string;
+  unitName?: string;
+  address?: string;
+  startTime?: string;
+  endTime?: string;
+  Content?: string;
+  peopleNum?: string;
+  trainingWay?: string;
 }
 
 export interface UserInfoVO {
   user: UserVO;
-  roles: RoleVO[];
-  roleIds: string[];
-  posts: PostVO[];
-  postIds: string[];
-  roleGroup: string;
-  postGroup: string;
 }
+//预案
+/**预案查询对象类型**/
 
-export interface ResetPwdForm {
-  oldPassword: string;
-  newPassword: string;
-  confirmPassword: string;
+export interface PlanQuery extends PageQuery {
+  planName?: string;
+}
+/**
+ * 预案返回对象
+ */
+export interface PlanVO {
+  planId: string;
+  planName: string;
+  planType: string;
+  publish_date: string;
+  organUnit: string;
+  document: string;
+}
+/**
+ * 预案表单类型
+ */
+export interface PlanForm {
+  pageNum: number;
+  pageSize: number;
+  publish_date?: [string, string];
+  planName: string;
+  planType?: string;
+}
+export interface PlanInfoVO {
+  plan: PlanVO;
+}
+// 定义预案列表接口响应的数据结构,total
+export interface FetchPlanResponse {
+  data: PlanVO[];
+  total: number;
+}
+//演练
+/**演练查询对象类型**/
+
+export interface DrillQuery {
+  planId?: string;
+}
+/**
+ * 演练返回对象
+ */
+export interface DrillVO {
+  drillId: string;
+  drillName: string;
+  drillUnit: string;
+  year: string;
+  drillTime: string;
+  drillAddress: string;
+  drillProject: string;
+  drillVideo: string;
+  drillPicture: string;
+}
+/**
+ * 演练表单类型
+ */
+export interface DrillForm {
+  pageNum: number;
+  pageSize: number;
+  planId?: string;
+}
+export interface DrillInfoVO {
+  drill: DrillVO;
+}
+// 定义演练列表接口响应的数据结构,total
+export interface FetchDrillResponse {
+  data: DrillVO[];
+  total: number;
+}
+// 定义获取预案列表接口的请求参数类型
+export interface QueryParams {
+  pageNum: number;
+  pageSize: number;
+  eventType?: string;
+  publishDate?: [string, string];
+  keyword?: string;
 }

+ 329 - 22
src/components/Map/company-map.vue

@@ -1,35 +1,342 @@
 <template>
-  <div>
-    <div id="map"></div>
-  </div>
+  <el-dialog v-model="mapPop" title="地图定位" width="80%" @close="handleClose">
+
+    <div class="map_box">
+      <div class="map" id="map"></div>
+      <div class="search">
+        <!-- @input="handleInput(0)" -->
+        <el-input placeholder="请输入地址" v-model="location" />
+        <el-button class="btn" type="primary" @click="handleInput(0)">搜索</el-button>
+      </div>
+
+      <div class="scroll_box" v-if="searchPop">
+        <div style="height: 30px; line-height: 30px;">
+          <span style="font-weight:bold">搜索结果列表</span>
+          <i class="el-icon-close" style="float: right;font-size: 20px;cursor: pointer;" @click="closeSearchList()" />
+        </div>
+
+        <el-scrollbar class="scroll" style="height: 250px;">
+          <div v-show="searchList.length" class="item" v-for="(item, index) in searchList" :key="index" @click="handlePanTo(index)">
+            <el-image class="img" :src="item.img" :alt="item.name" lazy>
+              <div slot="error" class="image-slot">
+                <i class="el-icon-picture-outline"></i>
+              </div>
+            </el-image>
+            <div>
+              <div class="text">{{ item.name }}</div>
+              <div>{{ item.address }}</div>
+            </div>
+          </div>
+          <div class="empty" v-show="!searchList.length" style="text-align: center;">没有搜索到内容</div>
+        </el-scrollbar>
+
+        <el-pagination background small :hide-on-single-page="true" layout="prev, pager, next" :total="total" :page-size="pageSize" :current-page="pageNum" style="margin-top: 10px;" @current-change="handleChangePage">
+        </el-pagination>
+      </div>
+
+      <span slot="footer" class="dialog-footer">
+          <el-button @click="handleClose">取 消</el-button>
+          <el-button type="primary" @click="sureMark">确 定</el-button>
+      </span>
+    </div>
+  </el-dialog>
 </template>
 
-<script setup>
-import { ref, onMounted } from 'vue';
-import AMapLoader from '@amap/amap-jsapi-loader';
+<script>
+
+import AMapLoader from '@amap/amap-jsapi-loader'
+
+export default {
+  props: {
+    address: {//公司地址
+      type: String,
+      default: () => {
+        return '';
+      },
+    },
+    latAndLong: {//经纬度
+      type: Array,
+      default: () => {
+        return [];
+      },
+    },
+    visible: {
+      type: Boolean,
+      default: () => {
+        return false;
+      },
+    },
+  },
+  data() {
+    return {
+      // 地图对象
+      map: {},
+      amap: {},
+      location: '',
+      marker: null,//地图上的点标记
+      contextMenu: null,
+      lnglatPosition: null,//选中的新坐标
+
+      pageNum: 1,
+      pageSize: 10,
+      total: 0,
 
-const map = ref(null);
+      searchList: [],
+      searchPop: false,
+      placeSearch: null,
+      form: {},
+      open: false,
+      mapPop: false,
+      geocoder: {}
+    };
+  },
+  watch: {
+    async visible(n) {
+      this.mapPop = n;
+      if (n) {
+        await this.initMap()
+        this.location = this.address;
+        this.handleInput(0)
+      }
+    }
+  },
+  destroyed() {
+    if (this.map) {
+      this.map.destroy()
+      this.map.off('rightclick')
+    }
+  },
+  methods: {
+    handleInput(flag) {
+      if (!this.location) return;
 
-onMounted(() => {
-  AMapLoader.load({
-    key: '30d3d8448efd68cb0b284549fd41adcf',
-    version: '2.0',
-    plugins: ['AMap.PlaceSearch']
-  }).then((AMap) => {
-    map.value = new AMap.Map('map', {
-      zoom: 10,
-      center: [110.919229,21.659751] // 茂名市
-    });
+      if (!flag) {//搜索
+        this.total = 0
+        this.pageNum = 1
+      }
 
+      const that = this;
+      if (!this.placeSearch) {
+        this.placeSearch = new this.amap.PlaceSearch({
+          pageSize: 10, // 每页条数,默认10,范围1-50
+          pageIndex: 1, // 页码
+          extensions: 'all' // 默认base,返回基本地址信息;all:返回详细信息
+        })
+      }
 
-  });
-});
+      this.searchPop = true;
+      this.placeSearch.setPageIndex(this.pageNum)
+      this.placeSearch.search(that.location, (status, result) => {
+        // console.log(result.poiList.pois, 'result')
+        if (!!result.poiList && result.poiList.pois && result.poiList.pois.length > 0) {
+          let arr = []
+          const pois = result.poiList.pois;
+          that.total = result.poiList ? result.poiList.count : 0;
+          arr = pois.map((item) => {
+            return {
+              name: item.name,
+              address: item.address,
+              img: item.photos[0]?.url,
+              lnglat: [item.location.lng, item.location.lat]
+            }
+          })
+          that.searchList = arr
 
+        } else {
+          that.total = 0
+          that.searchList = []
+        }
+
+      })
+    },
+    handleChangePage(newNum) {
+      if (!this.searchPop) return;
+      this.pageNum = newNum
+      this.handleInput(1)
+    },
+    closeSearchList() {
+      this.searchPop = false
+      this.location = ''
+      this.searchList = []
+      this.total = 0
+      this.pageNum = 1
+    },
+    // 地图中心的平移至指定点位置
+    handlePanTo(index) {
+      this.closeSearchList()
+      let lnglat = this.searchList[index].lnglat
+      this.map.panTo(lnglat)
+      this.setMarks(lnglat);
+    },
+    async initMap() {
+      let position = this.latAndLong.length ? this.latAndLong : [110.93154257997, 21.669064031332];
+      const AMap = await AMapLoader.load({
+        key: '30d3d8448efd68cb0b284549fd41adcf',     // 申请好的Web端开发者Key,首次调用 load 时必填
+        version: '2.0',      // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
+        plugins: ['AMap.PlaceSearch', 'AMap.ContextMenu', 'AMap.PolygonEditor', 'AMap.Geocoder']       // 插件列表
+      })
+      this.map = new AMap.Map('map', {
+        viewMode: '3D',    //是否为3D地图模式
+        center: position,
+        zoom: 15
+      })
+      this.amap = AMap
+
+      this.setMarks(position);
+      this.geocoder = new AMap.Geocoder({
+        // city 指定进行编码查询的城市,支持传入城市名、adcode 和 citycode
+        city: '010'
+      })
+      // 创建右键菜单
+      this.ContextMenu()
+      this.map.on('rightclick', this.handleRightclick);
+
+    },
+    ContextMenu() {
+      this.contextMenu = new AMap.ContextMenu();
+      this.contextMenu.addItem('选择标点', () => {
+        this.form = {
+          longitude: this.lnglatPosition[0],
+          latitude: this.lnglatPosition[1]
+        }
+        this.contextMenu.close()
+        let lnglat = [this.form.longitude, this.form.latitude];
+        this.setMarks(lnglat);
+      }, 1)
+    },
+    // 右键事件
+    handleRightclick(e) {
+      let lnglat = [e.lnglat.getLng(), e.lnglat.getLat()];
+      this.contextMenu.open(this.map, e.lnglat);
+      this.lnglatPosition = lnglat;
+    },
+    sureMark() {
+      let lnglat = [this.form.longitude, this.form.latitude];
+      this.geocoder.getAddress(lnglat, (status, result) => {
+        if (status === 'complete' && result.info === 'OK') {
+          // result为对应的地理位置详细信息
+          this.$emit("confirm", { lnglat: lnglat, address: result.regeocode.formattedAddress });
+        }
+      })
+
+    },
+    setMarks(lnglat) {//添加标记
+      if (this.marker) this.map.remove(this.marker);
+      let marker = new AMap.Marker({
+        position: lnglat,
+        icon: new AMap.Icon({
+          size: new AMap.Size(22, 28),  //图标所处区域大小
+          imageSize: new AMap.Size(22, 28), //图标大小
+          image: "//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png",
+        }),
+        anchor: 'bottom-center',
+        offset: new AMap.Pixel(0, 0)
+      });
+      marker.setMap(this.map);
+      this.marker = marker;
+    },
+    handleClose() {
+      this.$emit('update:visible', false)
+    }
+  }
+};
 </script>
 
-<style scoped>
-#map {
+<style lang="scss" scoped>
+.map_box {
+  position: relative;
   width: 100%;
-  height: 400px; /* 根据需要调整地图高度 */
+  height: 450px;
+  background: rgba(0, 0, 0, 0.3);
+  margin-bottom: 20px;
+}
+.map {
+  width: 100%;
+  height: 100%;
+}
+.search {
+  width: 50%;
+  position: absolute;
+  right: 2%;
+  top: 10px;
+  background: #fff;
+  padding: 8px 8px;
+  border-radius: 3px;
+  display: flex;
+}
+.btn {
+  margin-left: 10px;
+}
+
+.scroll_box {
+  width: 50%;
+  padding-top: 30px;
+  padding-bottom: 20px;
+  background: #fff;
+  position: absolute;
+  right: 2%;
+  top: 70px;
+  padding: 15px;
+  border-radius: 3px;
+  .close {
+    position: absolute;
+    right: 2%;
+    top: 10px;
+    cursor: pointer;
+    font-size: 20px;
+  }
+}
+.scroll {
+  width: 100%;
+  max-height: 250px;
+
+  .item {
+    display: flex;
+    font-size: 14px;
+    cursor: pointer;
+    padding: 8px;
+
+    &:hover {
+      background-color: #f6f6f6;
+    }
+
+    .img {
+      width: 50px;
+      height: 45px;
+      min-width: 50px;
+      margin-right: 10px;
+    }
+
+    &::v-deep {
+      .image-slot {
+        width: 100%;
+        height: 100%;
+        background-color: #f5f7fa;
+        text-align: center;
+        line-height: 50px;
+      }
+
+      .el-icon-picture-outline {
+        font-size: 25px;
+      }
+    }
+
+    .text {
+      color: #3385ff;
+      margin-bottom: 6px;
+    }
+  }
+}
+::v-deep {
+  .el-scrollbar__wrap {
+    overflow-x: hidden !important;
+  }
+
+  .is-horizontal {
+    display: none;
+  }
+}
+.empty {
+  margin: 20px 0;
 }
 </style>

+ 20 - 4
src/types/components.d.ts

@@ -16,16 +16,23 @@ declare module 'vue' {
     CompanyMap: typeof import('./../components/Map/company-map.vue')['default']
     DictTag: typeof import('./../components/DictTag/index.vue')['default']
     Editor: typeof import('./../components/Editor/index.vue')['default']
+    ElAnchor: typeof import('element-plus/es')['ElAnchor']
+    ElAnchorLink: typeof import('element-plus/es')['ElAnchorLink']
+    ElAnchor: typeof import('element-plus/es')['ElAnchor']
+    ElAnchorLink: typeof import('element-plus/es')['ElAnchorLink']
     ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
     ElBadge: typeof import('element-plus/es')['ElBadge']
     ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
     ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
     ElButton: typeof import('element-plus/es')['ElButton']
+    ElCard: typeof import('element-plus/es')['ElCard']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
+    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
+    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
     ElDrawer: typeof import('element-plus/es')['ElDrawer']
@@ -36,8 +43,8 @@ declare module 'vue' {
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElIcon: typeof import('element-plus/es')['ElIcon']
+    ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
-    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElLink: typeof import('element-plus/es')['ElLink']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
@@ -45,19 +52,28 @@ declare module 'vue' {
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPopover: typeof import('element-plus/es')['ElPopover']
     ElProgress: typeof import('element-plus/es')['ElProgress']
-    ElRadio: typeof import('element-plus/es')['ElRadio']
-    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
     ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElStep: typeof import('element-plus/es')['ElStep']
+    ElSteps: typeof import('element-plus/es')['ElSteps']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTabPane: typeof import('element-plus/es')['ElTabPane']
+    ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
+    ElText: typeof import('element-plus/es')['ElText']
+    ElTimeline: typeof import('element-plus/es')['ElTimeline']
+    ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElTree: typeof import('element-plus/es')['ElTree']
-    ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
+    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
+    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
+    ElText: typeof import('element-plus/es')['ElText']
+    ElTimeline: typeof import('element-plus/es')['ElTimeline']
+    ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
     ElUpload: typeof import('element-plus/es')['ElUpload']
     FileUpload: typeof import('./../components/FileUpload/index.vue')['default']
     Hamburger: typeof import('./../components/Hamburger/index.vue')['default']

+ 22 - 9
src/views/duty/eventing/eventDetails.vue

@@ -5,7 +5,7 @@
       返回上一级
     </div>
     <div class="line">
-      <div class="title">xxx公交站西侧出现积水倒灌情况</div>
+      <div class="title">{{ detailData.eventTitle }}</div>
       <div class="flex">
         <el-button type="primary">编辑</el-button>
         <el-button type="primary">开始指挥</el-button>
@@ -17,11 +17,13 @@
     <div class="line2">
       <div class="line-item">
         <div class="item-label">事件编号:</div>
-        <div class="item-value">YJSJ00000000001</div>
+        <div class="item-value">{{ detailData.eventId }}</div>
       </div>
       <div class="line-item">
         <div class="item-label">事件类型:</div>
-        <div class="item-value">自然灾害</div>
+        <div class="item-value">
+          <dict-tag
+        </div>
       </div>
       <div class="line-item">
         <div class="item-label">事件等级:</div>
@@ -38,15 +40,15 @@
     <div class="line2">
       <div class="line-item">
         <div class="item-label">事发地点:</div>
-        <div class="item-value link">茂名市茂南区xxxx路道路左侧路面</div>
+        <div class="item-value link">{{ detailData.address }}</div>
       </div>
       <div class="line-item">
         <div class="item-label">事发时间:</div>
-        <div class="item-value">2024-08-18 17:18</div>
+        <div class="item-value">{{ detailData.eventTime }}</div>
       </div>
       <div class="line-item">
         <div class="item-label">上报时间:</div>
-        <div class="item-value">2024-08-18 17:25</div>
+        <div class="item-value">{{ detailData.reportTime }}</div>
       </div>
       <div class="line-item">
         <div class="item-label">伤亡情况:</div>
@@ -59,11 +61,11 @@
     <div class="line2">
       <div class="line-item">
         <div class="item-label">登记人:</div>
-        <div class="item-value">张三</div>
+        <div class="item-value">{{ detailData.reportedBy }}</div>
       </div>
       <div class="line-item">
         <div class="item-label">登记时间:</div>
-        <div class="item-value">2024-08-18 17:30</div>
+        <div class="item-value">{{ detailData.reportTime }}</div>
       </div>
       <div class="line-item">
         <div class="item-label">联系方式:</div>
@@ -71,7 +73,7 @@
       </div>
       <div class="line-item">
         <div class="item-label">事件来源:</div>
-        <div class="item-value">110报送</div>
+        <div class="item-value">{{detailData.eventSource}}</div>
       </div>
     </div>
     <div class="sub-title">事件概要</div>
@@ -120,6 +122,9 @@
 </template>
 
 <script lang="ts" setup>
+import { getEventDetail } from '@/api/duty/eventing';
+
+const route = useRoute();
 const router = useRouter();
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { mm_event_level } = toRefs<any>(proxy?.useDict('mm_event_level'));
@@ -183,6 +188,14 @@ const eventTrackState = reactive({
 const handleBack = () => {
   router.go(-1);
 };
+let eventId = ref('')
+let detailData = ref({})
+onMounted(() => {
+  eventId.value = route.query.eventId
+  getEventDetail({eventId: eventId.value}).then(res => {
+    detailData.value = res.data
+  })
+});
 </script>
 
 <style lang="scss" scoped>

+ 217 - 0
src/views/riskPrevention/planManage/drill.vue

@@ -0,0 +1,217 @@
+<template>
+  <!-- 添加或修改培训记录配置对话框 -->
+  <el-dialog ref="formDialogRef" v-model="visible" :title="title" width="500px" append-to-body @close="closeDialog">
+    <el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
+      <el-row>
+        <el-col :span="20">
+          <el-form-item label="演练名称" prop="drillName">
+            <el-input v-model="form.drillName" placeholder="请输入演练名称" maxlength="30" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="20">
+          <el-form-item label="演练单位" prop="drillUnit">
+            <el-input v-model="form.drillUnit" placeholder="请输入演练单位" maxlength="30" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="20">
+          <el-form-item label="年度" prop="year">
+            <el-input v-model="form.year" placeholder="请输入演练年度" maxlength="11" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="20" class="block">
+          <el-form-item label="演练时间" prop="drillTime">
+            <el-date-picker v-model="form.drillTime" :shortcuts="shortcuts" placeholder="请选择开始时间" maxlength="11" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="20">
+          <el-form-item label="演练地点" prop="drillAddress">
+            <el-button>地图定位</el-button>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="20">
+          <el-form-item label="演练方案" prop="drillProject">
+            <el-button>上传</el-button>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="20">
+          <el-form-item label="演练视频" prop="drillVideo">
+            <el-button>上传</el-button>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="20">
+          <el-form-item label="演练图片" prop="drillPicture">
+            <el-button>上传</el-button>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel()">取 消</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+<script setup lang="ts">
+import { ref } from 'vue';
+import api from '@/api/system/user';
+import { DrillForm, DrillVO, DrillQuery } from '@/api/system/user/types';
+const userFormRef = ref<ElFormInstance>();
+const ids = ref<Array<number | string>>([]);
+const drillList = ref<DrillVO[]>();
+const loading = ref(true);
+const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const emits = defineEmits(['update:modelValue', 'getList']);
+const total = ref(0);
+import { to } from 'await-to-js';
+
+const props = defineProps({
+  modelValue: Boolean,
+  id: String
+});
+
+watch(
+  () => props.modelValue,
+  (val) => {
+    visible.value = val;
+  }
+);
+
+let visible = ref(false);
+const title = computed(() => {
+  return props.id ? '编辑演练' : '新建演练';
+});
+
+/***培训记录编辑界面时间选择 */
+
+const shortcuts = [
+  {
+    text: '今天',
+    value: new Date()
+  },
+  {
+    text: '昨天',
+    value: () => {
+      const date = new Date();
+      date.setDate(date.getDate() - 1);
+      return date;
+    }
+  },
+  {
+    text: '一周前',
+    value: () => {
+      const date = new Date();
+      date.setDate(date.getDate() - 7);
+      return date;
+    }
+  }
+];
+const initFormData: DrillForm = {
+  drillId: undefined,
+  drillName: undefined,
+  drillUnit: undefined,
+  year: undefined,
+  drillTime: undefined,
+  drillAddress: undefined,
+  drillProject: undefined,
+  drillVideo: undefined,
+  drillPicture: undefined
+};
+
+const initData: PageData<DrillForm, DrillQuery> = {
+  form: { ...initFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    planId: 'YJYA0000000001'
+  },
+  rules: {
+    drillName: [
+      { required: true, message: '演练名称不能为空', trigger: 'blur' },
+      {
+        min: 1,
+        message: '演练名称长度必须介于 1 和 20 之间',
+        trigger: 'blur'
+      }
+    ],
+    drillUnit: [
+      { required: true, message: '演练单位不能为空', trigger: 'blur' },
+      {
+        min: 1,
+        max: 20,
+        message: '演练单位长度必须介于 1 和 20 之间',
+        trigger: 'blur'
+      }
+    ],
+    year: [{ required: true, message: '演练年度不能为空', trigger: 'blur' }],
+    drillTime: [{ required: true, message: '演练时间不能为空', trigger: 'change' }]
+  }
+};
+
+const data = reactive<PageData<DrillForm, DrillQuery>>(initData);
+
+const { queryParams, form, rules } = toRefs<PageData<DrillForm, DrillQuery>>(data);
+
+const getList = async () => {
+  loading.value = true;
+  const res = await api.listDrill(proxy?.addDateRange(queryParams.value, dateRange.value));
+  loading.value = false;
+  drillList.value = res.rows;
+  total.value = res.total;
+};
+
+/** 重置操作表单 */
+const reset = () => {
+  form.value = { ...initFormData };
+  userFormRef.value?.resetFields();
+  form.value.drillId = undefined;
+};
+/** 取消按钮 */
+const cancel = () => {
+  emits('update:modelValue', false);
+  reset();
+};
+
+/**
+ * 关闭用户弹窗
+ */
+const closeDialog = () => {
+  emits('update:modelValue', false);
+  resetForm();
+};
+
+const resetForm = () => {
+  userFormRef.value?.resetFields();
+  userFormRef.value?.clearValidate();
+
+  form.value.drillId = undefined;
+};
+
+/**提交按钮 */
+const submitForm = () => {
+  userFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      form.value.drillId ? await api.updateDrill(form.value) : await api.addDrill(form.value);
+      proxy?.$modal.msgSuccess('操作成功');
+      emits('update:modelValue', true);
+      emits('getList');
+    }
+  });
+};
+</script>

+ 286 - 0
src/views/riskPrevention/planManage/index.vue

@@ -0,0 +1,286 @@
+<template>
+  <div class="app-container">
+    <transition name="fade">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-form ref="queryFormRef" :model="planForm" :inline="true">
+          <el-form-item style="width: 200px" label="预案类型" prop="planType">
+            <el-select v-model="planForm.planType" placeholder="全部" clearable>
+              <el-option label="总体应急预案" value="1" />
+              <el-option label="专项应急预案" value="2" />
+              <el-option label="部门应急预案" value="3" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="发布日期" prop="publish_date">
+            <el-date-picker
+              v-model="planForm.publish_date"
+              type="daterange"
+              range-separator="-"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              value-format="yyyy-MM-dd"
+            ></el-date-picker>
+          </el-form-item>
+          <el-form-item>
+            <el-input v-model="planForm.planName" placeholder="请输入预案名称/编制单位" clearable @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+          </el-form-item>
+          <el-form-item>
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </transition>
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate(selectedRow)">修改</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete(selectedRow)">删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="Download" @click="handleExport">导出</el-button>
+      </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @query-table="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 表格组件 -->
+    <el-table v-loading="loading" :data="demoList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="预案编号" align="center" prop="planId" />
+      <el-table-column label="预案名称" align="center" prop="planName" />
+      <el-table-column label="预案类型" align="center" prop="planType" />
+      <el-table-column label="发文字号" align="center" prop="document" />
+      <el-table-column label="编制单位" align="center" prop="organUnit" />
+      <el-table-column label="发布日期" align="center" prop="publishDate" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-tooltip content="查看" placement="top">
+            <el-button link type="primary" icon="View" @click="handleView(scope.row)"></el-button>
+          </el-tooltip>
+          <el-tooltip content="修改" placement="top">
+            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+          </el-tooltip>
+          <el-tooltip content="删除" placement="top">
+            <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+          </el-tooltip>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total > 0" v-model:page="planForm.pageNum" v-model:limit="planForm.pageSize" :total="total" @pagination="getList" />
+
+    <!-- 新增/修改弹窗 -->
+    <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
+      <el-form ref="demoFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="预案名称" prop="planName">
+          <el-input v-model="form.planName" placeholder="请输入预案名称" />
+        </el-form-item>
+        <el-form-item label="预案类型" prop="planType">
+          <el-select v-model="form.planType" placeholder="请选择预案类型" clearable>
+            <el-option label="总体应急预案" value="1" />
+            <el-option label="专项应急预案" value="2" />
+            <el-option label="部门应急预案" value="3" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="发布日期" prop="publishDate">
+          <el-date-picker v-model="form.publishDate" type="date" placeholder="选择发布日期" value-format="YYYY-MM-DD"></el-date-picker>
+        </el-form-item>
+        <el-form-item label="发文字号" prop="document">
+          <el-input v-model="form.document" placeholder="请输入发文字号" />
+        </el-form-item>
+        <el-form-item label="编制单位" prop="organizingUnit">
+          <el-input v-model="form.organizingUnit" placeholder="请输入编制单位" />
+        </el-form-item>
+        <el-col :span="1.5">
+          <!-- 使用分片上传组件,每个分片 -->
+          <chunk-upload :max-file-size="50 * 1024 * 1024" :max-files="5" />
+        </el-col>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确定</el-button>
+          <el-button @click="cancel">取消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue';
+import { ElMessage } from 'element-plus';
+import { fetchReports, addReport, updateReport, deleteReport } from '@/api/system/user/planList.ts';
+import { PlanForm, PlanVO } from '@/api/system/user/types';
+import ChunkUpload from '@/components/ChunkUpload/index.vue';
+import { getDicts } from '@/api/system/dict/data';
+
+import { useRouter } from 'vue-router';
+
+const router = useRouter();
+const demoFormRef = ref(null);
+const demoList = ref<PlanVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<string[]>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const selectedRow = ref<PlanVO | null>(null);
+let proxy = getCurrentInstance()?.proxy;
+const { mm_event_type } = toRefs<any>(proxy?.useDict('mm_event_type'));
+
+const planForm = reactive<PlanForm>({
+  pageNum: 1,
+  pageSize: 10,
+  planType: '',
+  publish_date: ['', ''],
+  planName: ''
+});
+
+const form = reactive({
+  planId: '',
+  planName: '',
+  planType: '',
+  document: '',
+  organizingUnit: '',
+  publishDate: ''
+});
+
+const rules = reactive({
+  planName: [{ required: true, message: '预案名称不能为空', trigger: 'blur' }],
+  planType: [{ required: true, message: '预案类型不能为空', trigger: 'blur' }],
+  document: [{ required: true, message: '发文字号不能为空', trigger: 'blur' }],
+  organizingUnit: [{ required: true, message: '编制单位不能为空', trigger: 'blur' }],
+  publishDate: [{ required: true, message: '发布日期不能为空', trigger: 'blur' }]
+});
+
+const planTypeSelection = ref([]);
+
+const dialog = reactive({
+  visible: false,
+  title: ''
+});
+
+const getList = async () => {
+  loading.value = true;
+  try {
+    const response = await fetchReports(planForm);
+    demoList.value = response.data;
+    total.value = response.total;
+  } catch (error) {
+    console.error('获取数据时出错:', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleQuery = () => {
+  planForm.pageNum = 1;
+  getList();
+};
+
+const resetQuery = () => {
+  planForm.pageNum = 1;
+  planForm.pageSize = 10;
+  queryParams.planType = '';
+  queryParams.publish_date = ['', ''];
+  queryParams.planName = '';
+  getList();
+};
+
+const handleSelectionChange = (selection: PlanVO[]) => {
+  ids.value = selection.map((item) => item.planId);
+  selectedRow.value = selection.length === 1 ? selection[0] : null;
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+
+const handleAdd = () => {
+  resetForm();
+  dialog.visible = true;
+  dialog.title = '添加预案';
+};
+
+const handleUpdate = (row: PlanVO) => {
+  if (row) {
+    resetForm();
+    Object.assign(form, row);
+    dialog.visible = true;
+    dialog.title = '修改预案';
+  }
+};
+
+const handleDelete = async (row: PlanVO) => {
+  try {
+    await deleteReport(row.planId);
+    ElMessage.success('删除成功');
+    getList();
+  } catch (error) {
+    ElMessage.error('删除失败');
+  }
+};
+
+const handleView = (row: PlanVO) => {
+  router.push({
+    path: '/riskPrevention/planManage/planList',
+    query: { planId: row.planId }
+  });
+};
+
+const submitForm = () => {
+  demoFormRef.value?.validate(async (valid) => {
+    if (valid) {
+      buttonLoading.value = true;
+      try {
+        if (form.planId) {
+          await updateReport(form);
+          ElMessage.success('更新成功');
+        } else {
+          await addReport(form);
+          ElMessage.success('添加成功');
+        }
+        dialog.visible = false;
+        getList();
+      } catch (error) {
+        ElMessage.error('操作失败');
+      } finally {
+        buttonLoading.value = false;
+      }
+    }
+  });
+};
+
+const resetForm = () => {
+  Object.assign(form, {
+    planId: '',
+    planName: '',
+    planType: '',
+    document: '',
+    organizingUnit: '',
+    publishDate: ''
+  });
+  demoFormRef.value?.resetFields();
+};
+
+const cancel = () => {
+  resetForm();
+  dialog.visible = false;
+};
+
+const handleExport = () => {
+  ElMessage.success('导出成功');
+};
+
+onMounted(() => {
+  getList();
+  getDicts('mm_event_type').then((res) => {
+    planTypeSelection.value = res.data;
+  });
+});
+</script>

+ 171 - 0
src/views/riskPrevention/planManage/plan.vue

@@ -0,0 +1,171 @@
+<template>
+  <!--添加或修改预案配置-->
+  <el-dialog ref="formDialogRef" v-model="visible" :title="title" width="500px" append-to-body @close="closeDialog">
+    <el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
+      <el-form-item label="预案编号" prop="planId">
+        <el-input v-model="form.planId" placeholder="请输入预案编号" />
+      </el-form-item>
+      <el-form-item label="预案名称" prop="planName">
+        <el-input v-model="form.planName" placeholder="请输入预案名称" />
+      </el-form-item>
+      <el-form-item label="预案类型" prop="planType">
+        <el-select v-model="form.planType" placeholder="请选择预案类型" maxlength="11">
+          <el-option label="总体应急预案" value="1" />
+          <el-option label="专项应急预案" value="2" />
+          <el-option label="部门应急预案" value="3" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="发文字号" prop="document">
+        <el-input v-model="form.document" placeholder="请输入发文字号" />
+      </el-form-item>
+      <el-form-item label="编制单位" prop="organUnit">
+        <el-input v-model="form.organUnit" placeholder="请输入编制单位" />
+      </el-form-item>
+      <el-form-item label="发布日期" prop="publish_date">
+        <el-date-picker v-model="form.publish_date" type="date" placeholder="选择发布日期" value-format="yyyy-MM-dd"></el-date-picker>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel()">取 消</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+<script setup lang="ts">
+import { ref } from 'vue';
+import api from '@/api/system/user';
+import { PlanForm, PlanVO, PlanQuery } from '@/api/system/user/types';
+const userFormRef = ref<ElFormInstance>();
+const ids = ref<Array<number | string>>([]);
+const planList = ref<PlanVO[]>();
+const loading = ref(true);
+const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const emits = defineEmits(['update:modelValue', 'getList']);
+const total = ref(0);
+import { to } from 'await-to-js';
+
+const props = defineProps({
+  modelValue: Boolean,
+  id: String
+});
+
+watch(
+  () => props.modelValue,
+  (val) => {
+    visible.value = val;
+  }
+);
+
+let visible = ref(false);
+const title = computed(() => {
+  return props.id ? '编辑演练' : '新建演练';
+});
+
+const initFormData: PlanForm = {
+  planId: undefined,
+  planName: undefined,
+  planType: undefined,
+  publish_date: undefined,
+  organUnit: undefined,
+  document: undefined
+};
+
+const initData: PageData<PlanForm, PlanQuery> = {
+  form: { ...initFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    planName: ''
+  },
+  rules: {
+    planId: [
+      { required: true, message: '预案编号不能为空', trigger: 'blur' },
+      {
+        min: 1,
+        message: '预案编号长度必须大于1',
+        trigger: 'blur'
+      }
+    ],
+    planName: [
+      { required: true, message: '预案名称不能为空', trigger: 'blur' },
+      {
+        min: 1,
+        max: 20,
+        message: '预案名称长度必须介于 1 和 20 之间',
+        trigger: 'blur'
+      }
+    ],
+    organUnit: [
+      { required: true, message: '编制单位不能为空', trigger: 'blur' },
+      {
+        min: 1,
+        max: 20,
+        message: '编制单位长度必须介于 1 和 20 之间1',
+        trigger: 'blur'
+      }
+    ],
+    planType: [{ required: true, message: '预案类型不能为空', trigger: 'blur' }],
+    document: [
+      { required: true, message: '发文字号不能为空', trigger: 'blur' },
+      {
+        min: 1,
+        message: '发文字号长度必须大于1',
+        trigger: 'blur'
+      }
+    ],
+    publish_date: [{ required: true, message: '发布时间不能为空', trigger: 'change' }]
+  }
+};
+const data = reactive<PageData<PlanForm, PlanQuery>>(initData);
+
+const { queryParams, form, rules } = toRefs<PageData<PlanForm, PlanQuery>>(data);
+
+const getList = async () => {
+  loading.value = true;
+  const res = await api.listPlan(proxy?.addDateRange(queryParams.value, dateRange.value));
+  loading.value = false;
+  planList.value = res.rows;
+  total.value = res.total;
+};
+
+/** 重置操作表单 */
+const reset = () => {
+  form.value = { ...initFormData };
+  userFormRef.value?.resetFields();
+};
+/** 取消按钮 */
+const cancel = () => {
+  emits('update:modelValue', false);
+  reset();
+};
+
+/**
+ * 关闭用户弹窗
+ */
+const closeDialog = () => {
+  emits('update:modelValue', false);
+  resetForm();
+};
+
+const resetForm = () => {
+  userFormRef.value?.resetFields();
+  userFormRef.value?.clearValidate();
+
+  form.value.planId = undefined;
+};
+
+/**提交按钮 */
+const submitForm = () => {
+  userFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      form.value.planId ? await api.updatePlan(form.value) : await api.addPlan(form.value);
+      proxy?.$modal.msgSuccess('操作成功');
+      emits('update:modelValue', true);
+      emits('getList');
+    }
+  });
+};
+</script>

+ 1022 - 0
src/views/riskPrevention/planManage/planList.vue

@@ -0,0 +1,1022 @@
+<template>
+  <div class="app-container p-2">
+    <el-row :gutter="20">
+      <!-- 基础信息 -->
+      <el-col :lg="30" :xs="24" style="">
+        <el-form-item>
+          <el-button type="primary" icon="ArrowLeft" @click="goBack">返回上一级</el-button>
+        </el-form-item>
+        <el-row :span="24" :gutter="10">
+          <el-col :span="18" label="预案名称">
+            <h2 key="planName" style="font-weight: bolder">{{ detailData.planName }}</h2>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['system:plan:add']" type="primary" plain icon="Plus" @click="planAdd()">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['system:plan:add']" type="success" plain icon="Edit" @click="planUpdate()"> 编辑 </el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['system:plan:delete']" type="danger" plain icon="Delete" @click="planDelete()"> 删除预案 </el-button>
+          </el-col>
+        </el-row>
+        <plan :id="dialog0.id" v-model="dialog0.visible" />
+        <div class="line" style="background: #e7e7e7; width: auto; height: 0.6px; position: relative"></div>
+        <h3>基础信息</h3>
+        <div style="white-space: nowrap">
+          <el-text :lg="15" class="mx-1">附件</el-text>
+          <el-text :lg="15" class="mx-2" type="primary">
+            <a v-if="detailData.file_list && detailData.file_list[0]" :href="detailData.file_list[0].file_url">{{ detailData.file_list[0].file_name_desc }}</a>
+          </el-text>
+        </div>
+        <el-card style="margin-top: 10px" shadow="hover">
+          <el-descriptions title="">
+            <el-descriptions-item key="planId" label="预案编号:">{{ detailData.planId }}</el-descriptions-item>
+            <el-descriptions-item key="planType" label="预案类型:">{{ detailData.planType }}</el-descriptions-item>
+            <el-descriptions-item key="publish_date" label="发布日期:">{{ detailData.publish_date }}</el-descriptions-item>
+            <el-descriptions-item key="organUnit" label="编制单位:">{{ detailData.organUnit }}</el-descriptions-item>
+            <el-descriptions-item key="document" label="发文字号:">{{ detailData.document }}</el-descriptions-item>
+          </el-descriptions>
+        </el-card>
+      </el-col>
+      <!-- 预案内容 -->
+      <el-col :lg="30" :xs="24" style="">
+        <h3>预案内容</h3>
+        <el-card shadow="hover">
+          <el-tabs v-model="activeName" type="border-card" class="demo-tabs" @tab-click="handleClick2">
+            <el-tab-pane label="总则" name="first">
+              <div>
+                <el-row>
+                  <el-col :span="4">
+                    <el-anchor :container="containerRef" direction="vertical" type="default" :offset="30" @click="handleClick1">
+                      <el-anchor-link href="#part1" title="编制目的" />
+                      <el-anchor-link href="#part2" title="编制依据" />
+                      <el-anchor-link href="#part3" title="适用范围" />
+                      <el-anchor-link href="#part4" title="工作原则" />
+                    </el-anchor>
+                  </el-col>
+                  <el-col :span="20">
+                    <div ref="containerRef" style="height: 300px; overflow-y: auto">
+                      <div id="part1" style="height: auto; margin-top: 20px; font-size: 14px">
+                        <h3 style="font-weight: 600">1.1 编制目的</h3>
+                        <span style="text-indent: 2em"
+                          >这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容</span
+                        >
+                      </div>
+                      <div id="part2" style="height: auto; margin-top: 15px; font-size: 14px">
+                        <h3 style="font-weight: 600">1.2 编制依据</h3>
+                        <span style="text-indent: 28px"
+                          >这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容</span
+                        >
+                      </div>
+                      <div id="part3" style="height: auto; margin-top: 15px; font-size: 14px">
+                        <h3 style="font-weight: 600">1.3 适用范围</h3>
+                        <span style="text-indent: 28px"
+                          >这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容</span
+                        >
+                      </div>
+                      <div id="part4" style="height: auto; margin-top: 15px; font-size: 14px">
+                        <h3 style="font-weight: 600">1.4 工作原则</h3>
+                        <span style="text-indent: 28px"
+                          >这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容</span
+                        >
+                      </div>
+                    </div>
+                  </el-col>
+                </el-row>
+              </div>
+            </el-tab-pane>
+            <el-tab-pane label="组织体系" name="second">组织体系</el-tab-pane>
+            <el-tab-pane label="运行机制" name="third">运行机制</el-tab-pane>
+            <el-tab-pane label="应急保障" name="fourth">应急保障</el-tab-pane>
+            <el-tab-pane label="监督管理" name="fifth">监督管理</el-tab-pane>
+            <el-tab-pane label="附则" name="sixth">附则</el-tab-pane>
+            <el-tab-pane label="附件" name="seventh">附件</el-tab-pane>
+          </el-tabs>
+        </el-card>
+      </el-col>
+      <el-col :lg="30" :xs="24">
+        <!-- 培训记录 -->
+        <h3>培训记录</h3>
+        <el-card shadow="hover">
+          <template #header>
+            <el-row :gutter="10">
+              <el-col :span="1.5">
+                <el-button v-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
+              </el-col>
+              <el-col :span="1.5">
+                <el-button v-has-permi="['system:user:add']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">
+                  修改
+                </el-button>
+              </el-col>
+              <el-col :span="1.5">
+                <el-button v-has-permi="['system:user:delete']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">
+                  删除
+                </el-button>
+              </el-col>
+              <right-toolbar v-model:showSearch="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
+            </el-row>
+          </template>
+
+          <el-table
+            v-loading="loading3"
+            :default-sort="{ prop: 'startTime,endTime', order: 'descending' }"
+            :data="userList"
+            @selection-change="handleSelectionChange"
+          >
+            <el-table-column type="selection" width="50" align="center" />
+            <el-table-column v-if="columns[0].visible" key="textId" label="记录编号" align="center" prop="textId" />
+            <el-table-column v-if="columns[1].visible" key="theme" label="培训主题" align="center" prop="theme" :show-overflow-tooltip="true" />
+            <el-table-column v-if="columns[2].visible" key="unitName" label="培训单位" align="center" prop="unitName" :show-overflow-tooltip="true" />
+            <el-table-column v-if="columns[3].visible" key="Content" label="培训内容" align="center" prop="Content" :show-overflow-tooltip="true" />
+            <el-table-column v-if="columns[4].visible" key="peopleNum" label="参与人数" align="center" prop="peopleNum" width="120" />
+            <el-table-column v-if="columns[4].visible" key="trainingWay" label="培训方式" align="center" prop="trainingWay" width="120" />
+            <el-table-column v-if="columns[5].visible" label="开始时间" sortable align="center" prop="startTime" width="160">
+              <template #default="scope">
+                <span>{{ scope.row.startTime }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column v-if="columns[6].visible" label="结束时间" sortable align="center" prop="endTime" width="160">
+              <template #default="scope">
+                <span>{{ scope.row.endTime }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column v-if="columns[7].visible" key="address" label="培训地点" align="center" prop="address" :show-overflow-tooltip="true" />
+            <el-table-column fixed="right" label="操作" width="100">
+              <template #default="scope">
+                <el-tooltip v-if="scope.row.textId !== 1" content="修改" placement="top">
+                  <el-button v-hasPermi="['system:role:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+                </el-tooltip>
+                <el-tooltip v-if="scope.row.textId !== 1" content="删除" placement="top">
+                  <el-button v-hasPermi="['system:role:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+                </el-tooltip>
+              </template>
+            </el-table-column>
+          </el-table>
+          <pagination
+            v-show="total3 > 0"
+            v-model:page="queryParams3.page"
+            v-model:limit="queryParams3.pageSize"
+            :total="total3"
+            @pagination="getList3"
+          />
+        </el-card>
+      </el-col>
+      <el-col :lg="30" :xs="24">
+        <!-- 演练记录 -->
+        <h3>演练记录</h3>
+        <el-card shadow="hover">
+          <template #header>
+            <el-row :gutter="10">
+              <el-col :span="1.5">
+                <el-button v-has-permi="['system:drill:add']" type="primary" plain icon="Plus" @click="drillAdd()">新增</el-button>
+              </el-col>
+              <el-col :span="1.5">
+                <el-button v-has-permi="['system:drill:add']" type="success" plain :disabled="single" icon="Edit" @click="drillUpdate()">
+                  修改
+                </el-button>
+              </el-col>
+              <el-col :span="1.5">
+                <el-button v-has-permi="['system:drill:delete']" type="danger" plain :disabled="multiple" icon="Delete" @click="drillDelete()">
+                  删除
+                </el-button>
+              </el-col>
+              <right-toolbar v-model:showSearch="showSearch" :columns="columns0" :search="true" @query-table="getList"></right-toolbar>
+            </el-row>
+            <drill :id="dialog3.id" v-model="dialog3.visible" @getlist="getList2" />
+          </template>
+          <el-table v-loading="loading2" :data="drillList" @selection-change="handleSelectionChangeDrill">
+            <el-table-column type="selection" width="50" align="center" />
+            <el-table-column v-if="columns0[0].visible" key="drillId" label="演练编号" align="center" prop="drillId" />
+            <el-table-column
+              v-if="columns0[1].visible"
+              key="drillName"
+              label="演练名称"
+              align="center"
+              prop="drillName"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column
+              v-if="columns0[2].visible"
+              key="drillUnit"
+              label="演练单位"
+              align="center"
+              prop="drillUnit"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column v-if="columns0[3].visible" key="year" label="年度" align="center" prop="year" :show-overflow-tooltip="true" />
+            <el-table-column v-if="columns0[4].visible" label="演练时间" align="center" prop="drillTime" width="160">
+              <template #default="scope">
+                <span>{{ scope.row.year }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column
+              v-if="columns0[5].visible"
+              key="drillAddress"
+              label="演练地点"
+              align="center"
+              prop="drillAddress"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column
+              v-if="columns0[6].visible"
+              key="drillProject"
+              label="演练方案"
+              align="center"
+              prop="drillProject"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column
+              v-if="columns0[7].visible"
+              key="drillVideo"
+              label="演练视频"
+              align="center"
+              prop="drillVideo"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column
+              v-if="columns0[8].visible"
+              key="drillPicture"
+              label="演练图片"
+              align="center"
+              prop="drillPicture"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column fixed="right" label="操作" width="100">
+              <template #default="scope">
+                <el-tooltip v-if="scope.row.drillId !== 1" content="修改" placement="top">
+                  <el-button v-hasPermi="['system:drill:edit']" link type="primary" icon="Edit" @click="drillUpdate(scope.row)"></el-button>
+                </el-tooltip>
+                <el-tooltip v-if="scope.row.drillId !== 1" content="删除" placement="top">
+                  <el-button v-hasPermi="['system:drill:remove']" link type="primary" icon="Delete" @click="drillDelete(scope.row)"></el-button>
+                </el-tooltip>
+              </template>
+            </el-table-column>
+          </el-table>
+
+          <pagination
+            v-show="total2 > 0"
+            v-model:page="drillForm.pageNum"
+            v-model:limit="drillForm.pageSize"
+            :total="total2"
+            @pagination="getList2"
+          />
+        </el-card>
+      </el-col>
+      <el-col :lg="30" :xs="24">
+        <!-- 响应记录 -->
+        <h3>响应记录</h3>
+        <el-card shadow="hover">
+          <el-table v-loading="loading" @selection-change="handleSelectionChangeResponse">
+            <el-table-column type="selection" width="50" align="center" />
+            <el-table-column v-if="columns1[0].visible" key="eventId" label="事件编号" align="center" prop="eventId" />
+            <el-table-column
+              v-if="columns1[1].visible"
+              key="eventName"
+              label="事件名称"
+              align="center"
+              prop="eventName"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column
+              v-if="columns1[2].visible"
+              key="eventType"
+              label="事件类型"
+              align="center"
+              prop="eventType"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column
+              v-if="columns1[3].visible"
+              key="responseUnit"
+              label="响应单位"
+              align="center"
+              prop="responseUnit"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column
+              v-if="columns1[4].visible"
+              key="eventAddress"
+              label="事件地点"
+              align="center"
+              prop="eventAddress"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column
+              v-if="columns1[5].visible"
+              key="responseLevel"
+              label="响应级别"
+              align="center"
+              prop="responseLevel"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column v-if="columns1[6].visible" label="响应开始时间" align="center" prop="responseStartTime" width="160">
+              <template #default="scope">
+                <span>{{ scope.row.responseStartTime }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column v-if="columns1[7].visible" label="响应开始时间" align="center" prop="responseEndTime" width="160">
+              <template #default="scope">
+                <span>{{ scope.row.responseEndTime }}</span>
+              </template>
+            </el-table-column>
+          </el-table>
+          <pagination
+            v-show="total > 0"
+            v-model:page="queryParams.pageNum"
+            v-model:limit="queryParams.pageSize"
+            :total="total"
+            @pagination="getListResponse"
+          />
+        </el-card>
+      </el-col>
+    </el-row>
+    <!-- 添加或修改培训记录配置对话框 -->
+    <el-dialog ref="formDialogRef" v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body @close="closeDialog">
+      <el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-row>
+          <el-col :span="20">
+            <el-form-item label="培训主题" prop="theme">
+              <el-input v-model="form.theme" placeholder="请输入培训主题" maxlength="30" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20">
+            <el-form-item label="培训单位" prop="unitName">
+              <el-input v-model="form.unitName" placeholder="请输入培训单位" maxlength="30" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20">
+            <el-form-item label="培训方式" prop="trainingWay">
+              <el-select v-model="form.trainingWay" placeholder="请选择培训方式" maxlength="11">
+                <el-option label="线上培训" value="1" />
+                <el-option label="线下培训" value="2" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20">
+            <el-form-item label="参与人数" prop="peopleNum">
+              <el-input v-model="form.peopleNum" placeholder="请输入参与人数" maxlength="11" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20" class="block">
+            <el-form-item label="开始时间" prop="startTime">
+              <el-date-picker v-model="form.startTime" :shortcuts="shortcuts" placeholder="请选择开始时间" maxlength="11" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20" class="block">
+            <el-form-item label="结束时间" prop="endTime">
+              <el-date-picker v-model="form.endTime" :shortcuts="shortcuts" placeholder="请选择开始时间" maxlength="11" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20">
+            <el-form-item label="培训地点" prop="address">
+              <el-input v-model="form.address" placeholder="请输入培训地点" maxlength="30" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20">
+            <el-form-item label="培训内容" prop="Content">
+              <el-input v-model="form.Content" type="textarea" placeholder="请输入内容"></el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel()">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 用户导入对话框 -->
+    <el-dialog v-model="upload.open" :title="upload.title" width="400px" append-to-body>
+      <el-upload
+        ref="uploadRef"
+        :limit="1"
+        accept=".xlsx, .xls"
+        :headers="upload.headers"
+        :action="upload.url + '?updateSupport=' + upload.updateSupport"
+        :disabled="upload.isUploading"
+        :on-progress="handleFileUploadProgress"
+        :on-success="handleFileSuccess"
+        :auto-upload="false"
+        drag
+      >
+        <el-icon class="el-icon--upload">
+          <i-ep-upload-filled />
+        </el-icon>
+        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+        <template #tip>
+          <div class="text-center el-upload__tip">
+            <div class="el-upload__tip"><el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据</div>
+            <span>仅允许导入xls、xlsx格式文件。</span>
+            <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
+          </div>
+        </template>
+      </el-upload>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel()">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <!-- 添加或修改培训记录配置对话框 -->
+    <el-dialog ref="formDialogRef" v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body @close="closeDialog">
+      <el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-row>
+          <el-col :span="20">
+            <el-form-item label="培训主题" prop="theme">
+              <el-input v-model="form.theme" placeholder="请输入培训主题" maxlength="30" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20">
+            <el-form-item label="培训单位" prop="unitName">
+              <el-input v-model="form.unitName" placeholder="请输入培训单位" maxlength="30" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20">
+            <el-form-item label="培训方式" prop="trainingWay">
+              <el-select v-model="form.trainingWay" placeholder="请选择培训方式" maxlength="11">
+                <el-option label="线上培训" value="1" />
+                <el-option label="线下培训" value="2" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20">
+            <el-form-item label="参与人数" prop="peopleNum">
+              <el-input v-model="form.peopleNum" placeholder="请输入参与人数" maxlength="11" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20" class="block">
+            <el-form-item label="开始时间" prop="startTime">
+              <el-date-picker v-model="form.startTime" :shortcuts="shortcuts" placeholder="请选择开始时间" maxlength="11" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20" class="block">
+            <el-form-item label="结束时间" prop="endTime">
+              <el-date-picker v-model="form.endTime" :shortcuts="shortcuts" placeholder="请选择开始时间" maxlength="11" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20">
+            <el-form-item label="培训地点" prop="address">
+              <el-input v-model="form.address" placeholder="请输入培训地点" maxlength="30" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="20">
+            <el-form-item label="培训内容" prop="Content">
+              <el-input v-model="form.Content" type="textarea" placeholder="请输入内容"></el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel()">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="User" lang="ts">
+import api from '@/api/system/user';
+import { UserForm, UserQuery, UserVO, PlanVO, DrillVO, PlanForm } from "@/api/system/user/types";
+import { DeptVO } from '@/api/system/dept/types';
+import { RoleVO } from '@/api/system/role/types';
+import { PostQuery, PostVO } from '@/api/system/post/types';
+import { treeselect } from '@/api/system/dept';
+import { globalHeaders } from '@/utils/request';
+import { to } from 'await-to-js';
+import { optionselect } from '@/api/system/post';
+import type { TabsPaneContext } from 'element-plus';
+import { ArrowLeft } from '@element-plus/icons-vue';
+import { useRouter } from 'vue-router';
+import { reactive } from "vue";
+import { getDrillList, getPlanDetail, getTrainingList } from '@/api/riskPrevention/planManage';
+
+const router = useRouter();
+
+const goBack = () => {
+  router.go(-1);
+};
+
+const containerRef = ref<HTMLElement | null>(null);
+
+const handleClick1 = (e: MouseEvent) => {
+  e.preventDefault();
+};
+
+const activeName = ref('first');
+const handleClick2 = (tab: TabsPaneContext, event: Event) => {
+  console.log(tab, event);
+};
+
+const handleClick = (tab: TabsPaneContext, event: Event) => {
+  console.log(tab, event);
+};
+
+/***培训记录编辑界面时间选择 */
+
+const shortcuts = [
+  {
+    text: '今天',
+    value: new Date()
+  },
+  {
+    text: '昨天',
+    value: () => {
+      const date = new Date();
+      date.setDate(date.getDate() - 1);
+      return date;
+    }
+  },
+  {
+    text: '一周前',
+    value: () => {
+      const date = new Date();
+      date.setDate(date.getDate() - 7);
+      return date;
+    }
+  }
+];
+const route = useRoute();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { sys_normal_disable, sys_user_sex } = toRefs<any>(proxy?.useDict('sys_normal_disable', 'sys_user_sex'));
+const userList = ref<UserVO[]>();
+const loading3 = ref(false);
+const total3 = ref(0);
+const planList = ref<PlanVO[]>();
+const drillList = ref<DrillVO[]>();
+const loading2 = ref(false);
+const total2 = ref(0);
+const responseList = ref<ResponseVO[]>();
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<number | string>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
+const Content = ref('');
+const deptOptions = ref<DeptVO[]>([]);
+const initPassword = ref<string>('');
+const postOptions = ref<PostVO[]>([]);
+const roleOptions = ref<RoleVO[]>([]);
+/*** 用户导入参数 */
+const upload = reactive<ImportOption>({
+  // 是否显示弹出层(用户导入)
+  open: false,
+  // 弹出层标题(用户导入)
+  title: '',
+  // 是否禁用上传
+  isUploading: false,
+  // 是否更新已经存在的用户数据
+  updateSupport: 0,
+  // 设置上传的请求头部
+  headers: globalHeaders(),
+  // 上传的地址
+  url: import.meta.env.VITE_APP_BASE_API + '/system/user/importData'
+});
+// 培训列显隐信息
+const columns = ref<FieldOption[]>([
+  { key: 0, label: `培训编号`, visible: false, children: [] },
+  { key: 1, label: `培训主题`, visible: true, children: [] },
+  { key: 2, label: `培训单位`, visible: true, children: [] },
+  { key: 3, label: `培训内容`, visible: true, children: [] },
+  { key: 4, label: `参与人数`, visible: true, children: [] },
+  { key: 5, label: `开始时间`, visible: true, children: [] },
+  { key: 6, label: `结束时间`, visible: true, children: [] },
+  { key: 7, label: `培训地点`, visible: true, children: [] }
+]);
+
+// 演练列显隐信息
+const columns0 = ref<FieldOption[]>([
+  { key: 0, label: `演练编号`, visible: false, children: [] },
+  { key: 1, label: `演练名称`, visible: true, children: [] },
+  { key: 2, label: `演练单位`, visible: true, children: [] },
+  { key: 3, label: `年度`, visible: true, children: [] },
+  { key: 4, label: `演练时间`, visible: true, children: [] },
+  { key: 5, label: `演练地点`, visible: true, children: [] },
+  { key: 6, label: `演练方案`, visible: true, children: [] },
+  { key: 7, label: `演练视频`, visible: true, children: [] },
+  { key: 8, label: `演练图片`, visible: true, children: [] }
+]);
+// 响应列显隐信息
+const columns1 = ref<FieldOption[]>([
+  { key: 0, label: `事件编号`, visible: false, children: [] },
+  { key: 1, label: `事件名称`, visible: true, children: [] },
+  { key: 2, label: `事件类型`, visible: true, children: [] },
+  { key: 3, label: `响应单位`, visible: true, children: [] },
+  { key: 4, label: `事件地点`, visible: true, children: [] },
+  { key: 5, label: `响应级别`, visible: true, children: [] },
+  { key: 6, label: `响应开始时间`, visible: true, children: [] },
+  { key: 7, label: `响应结束时间`, visible: true, children: [] }
+]);
+const deptTreeRef = ref<ElTreeInstance>();
+const queryFormRef = ref<ElFormInstance>();
+const userFormRef = ref<ElFormInstance>();
+const uploadRef = ref<ElUploadInstance>();
+const formDialogRef = ref<ElDialogInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+const dialog0 = reactive<DialogOption>({
+  visible: false,
+  id: ''
+});
+const dialog3 = reactive<DialogOption>({
+  visible: false,
+  id: ''
+});
+
+const initFormData: UserForm = {
+  textId: undefined,
+  theme: '',
+  unitName: undefined,
+  peopleNum: undefined
+};
+
+const initData: PageData<UserForm, UserQuery> = {
+  form: { ...initFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    theme: '',
+    unitName: ''
+  },
+  rules: {
+    theme: [
+      { required: true, message: '培训主题不能为空', trigger: 'blur' },
+      {
+        min: 1,
+        max: 20,
+        message: '培训主题长度必须介于 1 和 20 之间',
+        trigger: 'blur'
+      }
+    ],
+    unitName: [
+      { required: true, message: '培训单位不能为空', trigger: 'blur' },
+      {
+        min: 1,
+        max: 20,
+        message: '培训主题长度必须介于 1 和 20 之间',
+        trigger: 'blur'
+      }
+    ],
+    peopleNum: [
+      { required: true, message: '参与人数不能为空', trigger: 'blur' },
+      {
+        min: 1,
+        message: '参与人数长度必须大于1',
+        trigger: 'blur'
+      }
+    ],
+    trainingWay: [{ required: true, message: '培训方式不能为空', trigger: 'blur' }],
+    address: [
+      { required: true, message: '培训地点不能为空', trigger: 'blur' },
+      {
+        min: 1,
+        max: 20,
+        message: '培训主题长度必须介于 1 和 20 之间',
+        trigger: 'blur'
+      }
+    ],
+    Content: [
+      { required: true, message: '培训内容不能为空', trigger: 'blur' },
+      {
+        min: 1,
+        message: '参与人数长度必须大于1',
+        trigger: 'blur'
+      }
+    ],
+    startTime: [{ required: true, message: '开始时间不能为空', trigger: 'change' }],
+    endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }]
+  }
+};
+const data = reactive<PageData<UserForm, UserQuery>>(initData);
+
+const { queryParams, form2, rules } = toRefs<PageData<UserForm, UserQuery>>(data);
+const drillForm = reactive({
+  page: 1,
+  pageSize: 10,
+  planNum: ''
+});
+const queryParams3 = reactive({
+  page: 1,
+  pageSize: 10,
+  planNum: ''
+});
+
+const form = reactive<DrillVO>({
+  drillId: '',
+  drillName: '',
+  drillUnit: '',
+  year: '',
+  drillTime: '',
+  drillAddress: '',
+  drillProject: '',
+  drillVideo: '',
+  drillPicture: ''
+});
+
+/** 通过条件过滤节点  */
+const filterNode = (value: string, data: any) => {
+  if (!value) return true;
+  return data.label.indexOf(value) !== -1;
+};
+/** 根据名称筛选培训内容树 */
+watchEffect(
+  () => {
+    deptTreeRef.value?.filter(Content.value);
+  },
+  {
+    flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
+  }
+);
+
+/** 查询培训内容下拉树结构 */
+const getTreeSelect = async () => {
+  const res = await api.deptTreeSelect();
+  deptOptions.value = res.data;
+};
+
+/** 查询培训列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await api.listUser(proxy?.addDateRange(queryParams.value, dateRange.value));
+  loading.value = false;
+  userList.value = res.rows;
+  total.value = res.total;
+};
+/** 查询演练列表 */
+const getList2 = async () => {
+  loading2.value = true;
+  const res = await getDrillList(drillForm);
+  loading2.value = false;
+  res.data.forEach(item => {
+    item.drillPicture = item.drillPicture[0].file_name_desc
+    item.drillVideo = item.drillVideo[0].file_name_desc
+  })
+  drillList.value = res.data;
+  total2.value = res.total;
+};
+/** 查询培训列表 */
+const getList3 = async () => {
+  loading3.value = true;
+  const res = await getTrainingList(queryParams3);
+  loading3.value = false;
+  // res.data.forEach(item => {
+  //   item.drillPicture = item.drillPicture[0].file_name_desc
+  //   item.drillVideo = item.drillVideo[0].file_name_desc
+  // })
+  userList.value = res.data;
+  total3.value = res.total;
+};
+/** 查询响应列表 */
+const getListResponse = async () => {
+  loading.value = true;
+  const res = await api.listResponse(proxy?.addDateRange(queryParams.value, dateRange.value));
+  loading.value = false;
+  responseList.value = res.rows;
+  total.value = res.total;
+};
+/** 删除按钮操作 */
+const handleDelete = async (row?: UserVO) => {
+  const textIds = row?.textId || ids.value;
+  const [err] = await to(proxy?.$modal.confirm('是否确认删除记录编号为"' + textIds + '"的数据项?') as any);
+  if (!err) {
+    await api.delUser(textIds);
+    await getList();
+    proxy?.$modal.msgSuccess('删除成功');
+  }
+};
+
+/** 选择条数  */
+const handleSelectionChange = (selection: UserVO[]) => {
+  ids.value = selection.map((item) => item.textId);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+const handleSelectionChangeDrill = (selection: DrillVO[]) => {
+  ids.value = selection.map((item) => item.drillId);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+const handleSelectionChangeResponse = (selection: ResponseVO[]) => {
+  ids.value = selection.map((item) => item.eventId);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+/** 导入按钮操作 */
+const handleImport = () => {
+  upload.title = '用户导入';
+  upload.open = true;
+};
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download(
+    'system/user/export',
+    {
+      ...queryParams.value
+    },
+    `user_${new Date().getTime()}.xlsx`
+  );
+};
+/** 下载模板操作 */
+const importTemplate = () => {
+  proxy?.download('system/user/importTemplate', {}, `user_template_${new Date().getTime()}.xlsx`);
+};
+
+/**文件上传中处理 */
+const handleFileUploadProgress = () => {
+  upload.isUploading = true;
+};
+/** 文件上传成功处理 */
+const handleFileSuccess = (response: any, file: UploadFile) => {
+  upload.open = false;
+  upload.isUploading = false;
+  uploadRef.value?.handleRemove(file);
+  ElMessageBox.alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + '</div>', '导入结果', {
+    dangerouslyUseHTMLString: true
+  });
+  getList();
+};
+
+/** 提交上传文件 */
+function submitFileForm() {
+  uploadRef.value?.submit();
+}
+
+/** 初始化培训内容数据 */
+const initTreeData = async () => {
+  // 判断培训内容的数据是否存在,存在不获取,不存在则获取
+  if (deptOptions.value === undefined) {
+    const { data } = await treeselect();
+    deptOptions.value = data;
+  }
+};
+
+/** 重置操作表单 */
+const reset = () => {
+  form.value = { ...initFormData };
+  userFormRef.value?.resetFields();
+};
+/** 取消按钮 */
+const cancel = () => {
+  dialog.visible = false;
+  reset();
+};
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: UserForm) => {
+  reset();
+  const textId = row?.textId || ids.value[0];
+  const { data } = await api.getUser(textId);
+  dialog.visible = true;
+  dialog.title = '修改培训记录';
+  await initTreeData();
+  Object.assign(form.value, data.user);
+};
+
+/** 提交培训按钮 */
+const submitForm = () => {
+  userFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      form.value.textId ? await api.updateUser(form.value) : await api.addUser(form.value);
+      proxy?.$modal.msgSuccess('操作成功');
+      dialog.visible = false;
+      await getList();
+    }
+  });
+};
+
+/**
+ * 关闭培训弹窗
+ */
+const closeDialog = () => {
+  dialog.visible = false;
+  resetForm();
+};
+
+/**
+ * 重置表单
+ */
+const resetForm = () => {
+  userFormRef.value?.resetFields();
+  userFormRef.value?.clearValidate();
+  form.value.textId = undefined;
+};
+/** 新增按钮操作 */
+const handleAdd = async () => {
+  reset();
+  const { data } = await api.getUser();
+  dialog.visible = true;
+  dialog.title = '新增培训记录';
+};
+let detailData =ref({
+  file_list: []
+})
+onMounted(() => {
+  const planId = route.query.planId
+  drillForm.planNum = planId
+  queryParams3.planNum = planId
+  getPlanDetail(planId).then(res => {
+    detailData.value = res.data
+  })
+  getTreeSelect(); // 初始化培训内容数据
+  getList(); // 初始化列表数据
+  getList2(); // 初始化列表数据
+  getList3(); // 初始化列表数据
+  proxy?.getConfigKey('sys.user.initPassword').then((response) => {
+    initPassword.value = response.data;
+  });
+});
+
+async function handleDeptChange(value: number | string) {
+  const response = await optionselect(value);
+  postOptions.value = response.data;
+}
+
+/** 新增预案按钮操作 */
+const planAdd = () => {
+  dialog0.visible = true;
+  dialog0.id = '';
+};
+/** 修改预案按钮操作 */
+const planUpdate = (row) => {
+  dialog1.visible = true;
+  dialog0.id = row.planId;
+};
+/** 删除预案按钮操作 */
+const planDelete = async (row?: PlanVO) => {
+  const planIds = row?.planId || ids.value;
+  const [err] = await to(proxy?.$modal.confirm('是否确认删除记录编号为"' + planIds + '"的数据项?') as any);
+  if (!err) {
+    await api.delPlan(planIds);
+    await getListResponse();
+    proxy?.$modal.msgSuccess('删除成功');
+  }
+};
+
+/** 新增演练按钮操作 */
+const drillAdd = () => {
+  dialog3.visible = true;
+  dialog3.id = '';
+};
+/** 修改演练按钮操作 */
+const drillUpdate = (row) => {
+  dialog3.visible = true;
+  dialog3.id = row.drillId;
+};
+/** 删除演练按钮操作 */
+const drillDelete = async (row?: DrillVO) => {
+  const drillIds = row?.drillId || ids.value;
+  const [err] = await to(proxy?.$modal.confirm('是否确认删除记录编号为"' + drillIds + '"的数据项?') as any);
+  if (!err) {
+    await api.delPlan(drillIds);
+    await getList();
+    proxy?.$modal.msgSuccess('删除成功');
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.demo-tabs > .el-tabs__content {
+  padding: 32px;
+  color: #6b778c;
+  font-size: 32px;
+  font-weight: 600;
+}
+</style>

+ 1 - 1
src/views/system/user/authRole.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="app-container">
+  <div class="p-2">
     <div class="panel">
       <h4 class="panel-title">基本信息</h4>
       <el-form :model="form" :inline="true">

+ 5 - 1
src/views/system/user/profile/index.vue

@@ -1,10 +1,13 @@
 <template>
-  <div class="app-container">
+  <div class="p-2">
     <el-row :gutter="20">
       <el-col :span="6" :xs="24">
+        <el-card class="box-card">
+          <template #header>
             <div class="clearfix">
               <span>个人信息</span>
             </div>
+          </template>
           <div>
             <div class="text-center">
               <userAvatar />
@@ -36,6 +39,7 @@
               </li>
             </ul>
           </div>
+        </el-card>
       </el-col>
       <el-col :span="18" :xs="24">
         <el-card>