Browse Source

feat: init mobile

hi-cactus! 3 years ago
commit
6c2f583ef4
100 changed files with 4137 additions and 0 deletions
  1. 26 0
      .eslintrc.js
  2. 23 0
      .gitignore
  3. 9 0
      .prettierrc
  4. 24 0
      README.md
  5. 34 0
      index.d.ts
  6. 31 0
      index.html
  7. 57 0
      package.json
  8. BIN
      public/favicon.ico
  9. 37 0
      public/index.html
  10. 16 0
      src/App.tsx
  11. 63 0
      src/api/common.ts
  12. 80 0
      src/api/duty.ts
  13. 203 0
      src/api/incident.ts
  14. 104 0
      src/api/plan.ts
  15. 105 0
      src/api/resource.ts
  16. 4 0
      src/api/type.ts
  17. BIN
      src/assets/bg_jgxt.jpg
  18. 1 0
      src/assets/icons/duty/duty.svg
  19. 2 0
      src/assets/icons/duty/report.svg
  20. 2 0
      src/assets/icons/duty/roster.svg
  21. 8 0
      src/assets/icons/emergecyplan/department.svg
  22. 8 0
      src/assets/icons/emergecyplan/emergency.svg
  23. 15 0
      src/assets/icons/emergecyplan/highway.svg
  24. 21 0
      src/assets/icons/emergecyplan/navigation.svg
  25. 8 0
      src/assets/icons/emergecyplan/plan.svg
  26. 23 0
      src/assets/icons/emergecyplan/railway.svg
  27. 10 0
      src/assets/icons/emergecyplan/transportation.svg
  28. 8 0
      src/assets/icons/emergencyresources/car.svg
  29. 7 0
      src/assets/icons/emergencyresources/resource.svg
  30. 18 0
      src/assets/icons/emergencyresources/storehouse.svg
  31. 8 0
      src/assets/icons/emergencyresources/team.svg
  32. 2 0
      src/assets/icons/home/home.svg
  33. BIN
      src/assets/icons/home/icon_dczsj.png
  34. BIN
      src/assets/icons/home/icon_dpfsj.png
  35. BIN
      src/assets/icons/home/icon_map_dcz.png
  36. BIN
      src/assets/icons/home/icon_map_dcz@2x.png
  37. BIN
      src/assets/icons/home/icon_map_dpf.png
  38. BIN
      src/assets/icons/home/icon_map_dpf@2x.png
  39. BIN
      src/assets/icons/home/icon_map_location@2x.png
  40. BIN
      src/assets/icons/home/icon_map_spjk.png
  41. BIN
      src/assets/icons/home/icon_map_spjk@2x.png
  42. BIN
      src/assets/icons/home/icon_map_yjck.png
  43. BIN
      src/assets/icons/home/icon_map_yjck@2x.png
  44. BIN
      src/assets/icons/home/icon_map_yjcl.png
  45. BIN
      src/assets/icons/home/icon_map_yjcl@2x.png
  46. BIN
      src/assets/icons/home/icon_map_yjdw.png
  47. BIN
      src/assets/icons/home/icon_map_yjdw@2x.png
  48. BIN
      src/assets/icons/home/icon_map_yjsj.png
  49. BIN
      src/assets/icons/home/icon_map_yjsj@2x.png
  50. BIN
      src/assets/icons/home/icon_yjsj.png
  51. 5 0
      src/assets/icons/incident/archive.svg
  52. BIN
      src/assets/icons/incident/detail.png
  53. BIN
      src/assets/icons/incident/event.png
  54. 2 0
      src/assets/icons/incident/event.svg
  55. 5 0
      src/assets/icons/incident/handle.svg
  56. 5 0
      src/assets/icons/incident/ignore.svg
  57. BIN
      src/assets/icons/incident/plan.png
  58. 6 0
      src/assets/icons/incident/send.svg
  59. 2 0
      src/assets/icons/incident/warning.svg
  60. BIN
      src/assets/icons/incident/yjya.png
  61. 9 0
      src/assets/icons/index.js
  62. 22 0
      src/assets/icons/svgo.yml
  63. 31 0
      src/components/Card/index.scss
  64. 30 0
      src/components/Card/index.tsx
  65. 13 0
      src/components/Container/index.scss
  66. 183 0
      src/components/Container/index.tsx
  67. 41 0
      src/components/Dialog/index.scss
  68. 55 0
      src/components/Dialog/index.tsx
  69. 22 0
      src/components/EditTable/index.scss
  70. 54 0
      src/components/EditTable/index.tsx
  71. 36 0
      src/components/FormContainer/index.scss
  72. 38 0
      src/components/FormContainer/index.tsx
  73. 27 0
      src/components/MapView/index.tsx
  74. 198 0
      src/components/MapView/index.vue
  75. 157 0
      src/components/MarkerMap/constants.ts
  76. 184 0
      src/components/MarkerMap/dialog.ts
  77. 67 0
      src/components/MarkerMap/index.scss
  78. 448 0
      src/components/MarkerMap/index.tsx
  79. 83 0
      src/components/MediaUpload/index.scss
  80. 145 0
      src/components/MediaUpload/index.tsx
  81. 51 0
      src/components/Preview/index.scss
  82. 75 0
      src/components/Preview/index.tsx
  83. 3 0
      src/components/QueryForm/index.scss
  84. 118 0
      src/components/QueryForm/index.tsx
  85. 198 0
      src/components/QueryMap/index.tsx
  86. 165 0
      src/constants/constants.ts
  87. 46 0
      src/constants/icon.ts
  88. 14 0
      src/constants/incident.ts
  89. 102 0
      src/hooks/useMenus.ts
  90. 30 0
      src/layout/BaseLayout/NavBar/index.scss
  91. 46 0
      src/layout/BaseLayout/NavBar/index.tsx
  92. 55 0
      src/layout/BaseLayout/index.scss
  93. 18 0
      src/layout/BaseLayout/index.tsx
  94. 53 0
      src/layout/ManagementLayout/Menus/index.scss
  95. 158 0
      src/layout/ManagementLayout/Menus/index.tsx
  96. 17 0
      src/layout/ManagementLayout/index.scss
  97. 68 0
      src/layout/ManagementLayout/index.tsx
  98. 9 0
      src/main.ts
  99. 77 0
      src/router/index.ts
  100. 9 0
      src/store/index.ts

+ 26 - 0
.eslintrc.js

@@ -0,0 +1,26 @@
+// http://eslint.org/docs/user-guide/configuring
+
+module.exports = {
+  root: true,
+  env: {
+    browser: true,
+    node: true,
+  },
+  extends: [
+    'plugin:vue/vue3-essential',
+    'eslint:recommended',
+    '@vue/typescript/recommended',
+    '@vue/prettier',
+    '@vue/prettier/@typescript-eslint',
+  ],
+  plugins: ['@typescript-eslint'],
+  parserOptions: {
+    ecmaVersion: 7,
+    sourceType: 'module',
+  },
+  rules: {
+    '@typescript-eslint/explicit-module-boundary-types': 'off',
+    '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/no-empty-function': 'off',
+  },
+};

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 9 - 0
.prettierrc

@@ -0,0 +1,9 @@
+{
+  "arrowParens": "always",
+  "bracketSpacing": true,
+  "jsxBracketSameLine": true,
+  "printWidth": 80,
+  "proseWrap": "never",
+  "singleQuote": true,
+  "trailingComma": "all"
+}

+ 24 - 0
README.md

@@ -0,0 +1,24 @@
+# squi
+
+## Project setup
+```
+yarn install
+```
+
+### Compiles and hot-reloads for development
+```
+yarn serve
+```
+
+### Compiles and minifies for production
+```
+yarn build
+```
+
+### Lints and fixes files
+```
+yarn lint
+```
+
+### Customize configuration
+See [Configuration Reference](https://cli.vuejs.org/config/).

+ 34 - 0
index.d.ts

@@ -0,0 +1,34 @@
+declare module '*.svg' {
+  export default SVGAElement;
+}
+declare module '*.png';
+declare module '*.jpg';
+declare module '*.jpeg';
+declare module '*.gif';
+declare module '*.bmp';
+declare module '*.tiff';
+
+declare module '@element-plus/icons/lib/*.js' {
+  import { VNode } from 'vue';
+  export default VNode;
+}
+
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue';
+  const component: DefineComponent;
+  export default component;
+}
+
+export declare global {
+  interface Window {
+    minemap: {
+      Map: any;
+      Marker: any;
+      Popup: any;
+      LngLat: any;
+      util: {
+        getJSON(url: string, callback: (error: Error, data: any) => void): void;
+      };
+    };
+  }
+}

+ 31 - 0
index.html

@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>交通运输应急指挥系统</title>
+    <link
+      rel="stylesheet"
+      href="https://minedata.cn/minemapapi/v2.1.0/minemap.css"
+    />
+    <script src="https://minedata.cn/minemapapi/v2.1.0/minemap.js"></script>
+    <script>
+      minemap.domainUrl = 'https://minedata.cn';
+      minemap.dataDomainUrl = 'https://minedata.cn';
+      minemap.serverDomainUrl = 'https://minedata.cn';
+      minemap.spriteUrl = 'https://minedata.cn/minemapapi/v2.1.0/sprite/sprite';
+      minemap.serviceUrl = 'https://minedata.cn/service/';
+
+      /**
+       * key、solution设置
+       */
+      minemap.key = '77ef70465c2d4888b3a5132523494b94';
+      minemap.solution = 16857;
+    </script>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 57 - 0
package.json

@@ -0,0 +1,57 @@
+{
+  "name": "squi",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "vite serve",
+    "build": "vite build",
+    "lint": "eslint --fix --ext .ts,.tsx,.json src"
+  },
+  "dependencies": {
+    "@element-plus/icons": "^0.0.11",
+    "axios": "^0.24.0",
+    "clsx": "^1.1.1",
+    "core-js": "^3.6.5",
+    "echarts": "^5.2.1",
+    "element-plus": "^1.1.0-beta.24",
+    "nprogress": "^0.2.0",
+    "ol": "^6.9.0",
+    "pinia": "^2.0.0",
+    "vant": "3",
+    "vue": "^3.0.0",
+    "vue-router": "^4.0.12",
+    "vuex": "^4.0.0-0"
+  },
+  "devDependencies": {
+    "@types/lodash": "^4.14.176",
+    "@types/node": "^16.11.6",
+    "@types/nprogress": "^0.2.0",
+    "@typescript-eslint/eslint-plugin": "^5.2.0",
+    "@typescript-eslint/parser": "^5.2.0",
+    "@vitejs/plugin-vue": "^1.9.3",
+    "@vitejs/plugin-vue-jsx": "^1.2.0",
+    "@vue/babel-plugin-jsx": "^1.1.1",
+    "@vue/compiler-sfc": "^3.0.0",
+    "@vue/eslint-config-prettier": "^6.0.0",
+    "@vue/eslint-config-typescript": "^8.0.0",
+    "babel-eslint": "^10.1.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-prettier": "^4.0.0",
+    "eslint-plugin-vue": "^7.0.0",
+    "prettier": "^2.4.1",
+    "sass": "^1.27.0",
+    "sass-loader": "^10.0.4",
+    "typescript": "^4.4.4",
+    "unplugin-element-plus": "^0.1.3",
+    "unplugin-vue-components": "^0.16.0",
+    "vite": "^2.6.11",
+    "vite-plugin-style-import": "^1.4.0",
+    "vite-svg-loader": "^2.2.0",
+    "vue-cli-plugin-element-plus": "~0.0.13"
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}

BIN
public/favicon.ico


+ 37 - 0
public/index.html

@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html lang="">
+
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title>
+        <%= htmlWebpackPlugin.options.title %>
+    </title>
+    <link rel="stylesheet" href="https://minedata.cn/minemapapi/v2.1.0/minemap.css">
+    <script src="https://minedata.cn/minemapapi/v2.1.0/minemap.js"></script>
+    <script>
+        minemap.domainUrl = 'https://minedata.cn';
+        minemap.dataDomainUrl = 'https://minedata.cn';
+        minemap.serverDomainUrl = 'https://minedata.cn';
+        minemap.spriteUrl = 'https://minedata.cn/minemapapi/v2.1.0/sprite/sprite';
+        minemap.serviceUrl = 'https://minedata.cn/service/';
+
+        /**
+         * key、solution设置
+         */
+        minemap.key = '77ef70465c2d4888b3a5132523494b94';
+        minemap.solution = 16857;
+    </script>
+</head>
+
+<body>
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+</body>
+
+</html>

+ 16 - 0
src/App.tsx

@@ -0,0 +1,16 @@
+import { defineComponent } from 'vue';
+import zhCN from 'element-plus/lib/locale/lang/zh-cn';
+import { RouterView } from 'vue-router';
+
+export default defineComponent({
+  name: 'App',
+  setup() {
+    const locale = zhCN;
+
+    return () => (
+      <el-configProvider locale={locale}>
+        <RouterView />
+      </el-configProvider>
+    );
+  },
+});

+ 63 - 0
src/api/common.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { BaseResponse } from './type';
+
+/**
+ * - 预案类型: zhdd_plan_type;
+ * - 事件类型: zhdd_incident_type;
+ * - 事件状态 + 事件页面右侧菜单: zhdd_incident_status;
+ * - 事件等级: zhdd_incident_level;
+ * - 事件来源: zhdd_incident_source;
+ * - 上报单位 + 归属部门 + 预案页面右侧菜单 : zhdd_org_upload;
+ * - 应急资源 + 资源页面右侧菜单:zhdd_resource
+ * - 首页标记点: zhdd_screen_type
+ * - 车辆类型: zhdd_car_type
+ * - 处置方案用户角色: zhdd_incident_task_role
+ */
+export type DictType =
+  | 'zhdd_plan_type'
+  | 'zhdd_incident_type'
+  | 'zhdd_incident_status'
+  | 'zhdd_incident_level'
+  | 'zhdd_incident_source'
+  | 'zhdd_org_upload'
+  | 'zhdd_resource'
+  | 'zhdd_screen_type'
+  | 'zhdd_car_type'
+  | 'zhdd_incident_task_role';
+
+export interface GlobalDict {
+  dictLabel: string; // 字典标签
+  dictValue: string; // 字典键值
+}
+
+export interface DictReponse extends BaseResponse {
+  data: GlobalDict[];
+}
+
+/** 字典数据查询 */
+export const getGlobalDict = (dictType: DictType) =>
+  request<DictReponse>('GET', {
+    url: `/system/dict/data/type/${dictType}`,
+  });
+
+export interface UploadData {
+  /** 文件名 */
+  fileName?: string;
+  /** 文件相对路径 */
+  url?: string;
+}
+
+export interface UploadReponse extends BaseResponse {
+  data: UploadData;
+}
+export const upload = (file: File) => {
+  const form = new FormData();
+  form.append('file', file);
+  return request<UploadReponse>('POST', {
+    url: '/common/upload',
+    headers: {
+      'Content-Type': 'multipart/form-data',
+    },
+    data: form,
+  });
+};

+ 80 - 0
src/api/duty.ts

@@ -0,0 +1,80 @@
+import request from '@/utils/request';
+import { BaseResponse } from './type';
+
+export interface GetReportListParams {
+  /** 报告人 */
+  createBy?: string;
+  /** 报告开始时间 */
+  'params[beginTime]'?: string;
+  /** 报告截止时间 */
+  'params[endTime]'?: string;
+}
+
+export interface ReportItem {
+  id?: string;
+  /** 报告内容 */
+  reportContent?: string;
+  /** 1-保存。2-提交 */
+  status?: 1 | 2;
+  /** 值班日期 */
+  dutyDate?: string;
+  /** 报告人 */
+  createBy?: string;
+  /** 更新时间 */
+  updateTime?: string;
+}
+
+export interface ReportListResponse extends BaseResponse {
+  rows?: ReportItem[];
+  total?: number;
+}
+
+/** 值班报告列表查询 */
+export const getReportList = (params: GetReportListParams) =>
+  request<ReportListResponse>('GET', {
+    url: '/zhdd/dutyReport/list',
+    params: params,
+  });
+
+export interface ReporItemParams {
+  id?: string;
+  reportContent: string;
+  /** 1-保存。2-提交 */
+  status: 1 | 2;
+}
+
+/** 值班报告新增 */
+export const postReporItem = (params: ReporItemParams) =>
+  request<BaseResponse>('POST', {
+    url: '/zhdd/dutyReport',
+    data: params,
+  });
+/** 值班报告修改 */
+export const putReporItem = (params: ReporItemParams) =>
+  request<BaseResponse>('PUT', {
+    url: '/zhdd/dutyReport',
+    data: params,
+  });
+
+export interface RosterItem {
+  id?: number;
+  period?: number; // 周期,1-星期一
+  empName?: string; // 值班人姓名
+}
+
+export interface RosterListResponse extends BaseResponse {
+  data: RosterItem[];
+}
+
+/** 值班列表查询 */
+export const getRosterList = () =>
+  request<RosterListResponse>('GET', {
+    url: 'zhdd/dutyEmp/list',
+  });
+
+/** 修改值班人信息 */
+export const putRosterItem = (params: RosterItem) =>
+  request<BaseResponse>('PUT', {
+    url: '/zhdd/dutyEmp',
+    data: params,
+  });

+ 203 - 0
src/api/incident.ts

@@ -0,0 +1,203 @@
+import request from '@/utils/request';
+import { BaseResponse } from './type';
+
+export interface IncidentItem {
+  id?: string; // 非必须
+  type?: number; // 非必须 事件类型
+  level?: number; // 非必须 事件等级
+  addr?: string; // 非必须 地址
+  locations?: string; // 非必须 经纬度
+  createDept?: string; // 非必须 上报部门
+  source?: string; // 非必须 来源
+  des?: string; // 非必须 事件描述
+  pic?: string; // 非必须 图片
+  video?: string; // 非必须 视频
+  expr1?: null; // 非必须
+  expr2?: null; // 非必须
+  exprJson?: null; // 非必须
+  name?: string; // 非必须
+  status?: number; // 非必须 状态
+  madinDept?: string; // 非必须
+  assistDept?: string; // 非必须
+  createBy?: string; // 非必须
+  createTime?: string; // 非必须
+}
+
+export interface IncidentListResponse extends BaseResponse {
+  rows?: IncidentItem[];
+  total?: number;
+  pageSize?: number;
+  pageNum?: number;
+}
+
+export interface GetIncidentListParams {
+  name?: string;
+  type?: number | string;
+  level?: number | string;
+  status?: number | string;
+  'params[beginTime]'?: string; // yyyy-MM-dd
+  'params[endTime]'?: string; // yyyy-MM-dd
+  pageSize?: number;
+  pageNum?: number;
+}
+
+/** 事件列表查询 */
+export const getIncidentList = (params: GetIncidentListParams) =>
+  request<IncidentListResponse>('GET', {
+    url: '/zhdd/incident/list',
+    params: params,
+  });
+
+export interface IncidentItemDetailProcessItem {
+  id?: string;
+  incidentId?: string;
+  des?: string;
+  status?: null;
+  createTime?: string;
+  createBy?: null;
+}
+export interface IncidentItemDetailTaskItem {
+  id?: string;
+  incidentId?: string;
+  taskName?: string;
+  taskDes?: string;
+  source?: number;
+  delFlag?: string;
+  createTime?: string;
+  createBy?: null;
+}
+
+export interface IncidentItemPlanItem {
+  id?: string; // 非必须
+  planId?: string; // 非必须
+  level?: number; // 	非必须
+  taskName?: string; // 非必须
+  taskDes?: string; // 非必须 描述
+  createBy?: string; // 非必须
+  createTime?: string; //非必须
+}
+
+export interface IncidentItemTaskItem {
+  id?: string; // 必须
+  incidentId?: string; // 必须
+  taskName?: string; // 必须
+  taskDes?: string; // 必须
+  source?: number; // 必须
+  delFlag?: string; // 必须
+  createTime?: string; // 必须
+  createBy?: null; // 必须
+  taskCommandVos: {
+    id?: string;
+    incidentTaskId?: string;
+    command?: string;
+  }[]; // 指令列表
+  taskPersonVos?: {
+    // 人员列表
+    id?: string;
+    incidentTaskId?: string;
+    position: string;
+    person: string;
+  }[];
+}
+
+export interface IncidentItemDetail {
+  baseInfo?: IncidentItem; // 基本信息
+  process?: IncidentItemDetailProcessItem[]; //处置信息
+  task?: IncidentItemTaskItem[]; // 处置任务
+  baseTask?: {
+    [key: string]: IncidentItemPlanItem[];
+  }; // 处置预案
+}
+export interface IncidentItemDetailResponse extends BaseResponse {
+  data: IncidentItemDetail;
+}
+/** 事件详情 */
+export const getIncidentItem = (id: number | string) =>
+  request<IncidentItemDetailResponse>('GET', {
+    url: `/zhdd/incident/${id}`,
+  });
+
+/** 事件接报 */
+export const postIncidentItem = (params: IncidentItem) =>
+  request<BaseResponse>('POST', {
+    url: `/zhdd/incident`,
+    data: params,
+  });
+
+/**
+ * 事件修改
+ * ```
+ * 如果是更改状态的话,只需要传
+ *  { "id":"", "status":xx }
+ * ```
+ */
+export const putIncidentItem = (params: IncidentItem) =>
+  request<BaseResponse>('PUT', {
+    url: `/zhdd/incident`,
+    data: params,
+  });
+
+export interface IncidentTaskParam {
+  incidentId?: string; // 事件id
+  taskName?: string; // 任务名称
+  taskDes?: string; // 任务描述
+}
+
+/** 添加事件任务 TODO:// 什么接口?? */
+export const addIncidentTask = (params: IncidentItem) =>
+  request<BaseResponse>('POST', {
+    url: `/zhdd/incident/task`,
+    data: params,
+  });
+
+/** 删除事件任务 TODO:// 什么接口?? ids --> 1,2,3 ?? */
+export const deleteIncidentTask = (ids: string) =>
+  request<BaseResponse>('DELETE', {
+    url: `/zhdd/incident/task/${ids}`,
+  });
+
+export interface IncidentProcessParam {
+  incidentId?: string; // 事件id
+  des?: string; // 处置内容
+}
+
+/** 添加处置过程 */
+export const addIncidentProcess = (params: IncidentProcessParam) =>
+  request<BaseResponse>('POST', {
+    url: `/zhdd/incidentProcess`,
+    data: params,
+  });
+
+export interface IncidentPlanTaskParam {
+  id?: string;
+  incidentId?: string; // 事件id
+  taskName?: string;
+  incidentTaskCommands?: { id?: string; command: string }[]; // 指令
+  incidentTaskPeoples?: {
+    id?: string;
+    incidentTaskId?: string;
+    position: string;
+    person: string;
+  }[]; // 人员
+}
+
+/** 添加事件任务 --> 处置方案 */
+export const addIncidentPlanTask = (params: IncidentPlanTaskParam) =>
+  request<BaseResponse>('POST', {
+    url: `/zhdd/incident/task`,
+    data: params,
+  });
+
+export interface AllIncidentsResponse {
+  data: {
+    预警事件: NonNullable<IncidentItem>[];
+    待派发: NonNullable<IncidentItem>[];
+    待处置: NonNullable<IncidentItem>[];
+  };
+}
+
+/** 所有应急事件信息 */
+export const getAllIncidents = () =>
+  request<AllIncidentsResponse>('GET', {
+    url: `/zhdd/incident/location`,
+  });

+ 104 - 0
src/api/plan.ts

@@ -0,0 +1,104 @@
+import request from '@/utils/request';
+import { UploadFile } from 'element-plus/lib/components/upload/src/upload.type';
+import { UploadData } from './common';
+import { BaseResponse } from './type';
+
+export interface GetPlanListParams {
+  name?: string; // 预案名称
+  type?: string; // 预案类型
+  createDept?: string; // 归属部门
+}
+
+export interface PlanItem {
+  id?: string; // 非必须
+  name?: string; // 名称
+  type?: number; // 类型
+  createDept?: string; // 归属部门
+  createBy?: string; // 非必须
+  createTime?: string; // 非必须
+  updateTime?: string; // 非必须 更新时间
+  // TODO:// 缺少预案类型
+}
+
+export interface PlanListResponse extends BaseResponse {
+  rows?: PlanItem[];
+  total?: number;
+}
+
+/** 预案列表查询 */
+export const getPlanList = (params: GetPlanListParams) =>
+  request<PlanListResponse>('GET', {
+    url: '/zhdd/plan/list',
+    params: params,
+  });
+
+export interface PlanTask {
+  id?: string;
+  planId?: string;
+  level?: number; // 预案等级
+  taskDes?: string; // 任务描述
+  taskName?: string; // 任务名称
+  createBy?: string;
+  createTime?: string;
+}
+export interface PlanItemDetail extends PlanItem {
+  planTasks?: {
+    1?: PlanTask[];
+    2?: PlanTask[];
+    3?: PlanTask[];
+    4?: PlanTask[];
+  };
+  planFiles?: { id?: string; fileName: string; fileUrl: string }[];
+}
+
+export interface PlanItemDetailResponse extends BaseResponse {
+  data?: PlanItemDetail;
+}
+
+/** 获取预案详情 */
+export const getPlanItem = (id: string) =>
+  request<PlanItemDetailResponse>('GET', {
+    url: `/zhdd/plan/${id}`,
+  });
+
+export interface PlanItemParams extends PlanItem {
+  planTasks?: {
+    level: number; // 预案等级
+    taskDes: string; // 任务描述
+    taskName: string; // 任务名称
+  }[];
+  planFiles: UploadData[];
+}
+
+/** 新增预案 */
+export const addPlanItem = (params: PlanItemParams) =>
+  request<BaseResponse>('POST', {
+    url: '/zhdd/plan',
+    data: params,
+  });
+
+/** 修改预案 */
+export const editPlanItem = (params: PlanItemParams) =>
+  request<BaseResponse>('PUT', {
+    url: '/zhdd/plan',
+    data: params,
+  });
+
+/** 删除预案 */
+export const deletePlanItem = (ids: string) =>
+  request<BaseResponse>('DELETE', {
+    url: `/zhdd/plan/${ids}`,
+  });
+
+export interface PlanFilePatams {
+  planId?: string; // 应急预案id
+  planName?: string; // 文件名称
+  planUrl?: string; // 文件路径
+}
+
+/** 新增应急文件 */
+export const addPlanFile = (params: PlanFilePatams) =>
+  request<BaseResponse>('POST', {
+    url: '/zhdd/plan/file',
+    data: params,
+  });

+ 105 - 0
src/api/resource.ts

@@ -0,0 +1,105 @@
+import request from '@/utils/request';
+import { BaseResponse } from './type';
+
+export interface ResourceListItem {
+  id?: number;
+  resourceType?: number;
+  name?: string; //名称
+  address?: string; //地址
+  longitude?: string; // 经度
+  latitude?: string; //纬度
+  manageUnit?: string; //管理单位
+  num?: number; // 数量
+  carryGoods?: null; //携带物资(应急时)
+  contactName?: string; //联系人
+  contactPhone?: string; //联系电话
+  carType?: string;
+}
+
+export interface GetResourceListParams {
+  name?: string;
+  manageUnit?: string; // 管理单位
+  resourceType: 1 | 2 | 3; // 资源类型。1-仓库。2-队伍.3-车辆
+}
+
+export interface ResourceListResponse extends BaseResponse {
+  rows?: ResourceListItem[];
+  total?: number;
+  pageSize?: number;
+  pageNum?: number;
+}
+
+export const getResourceList = (params: GetResourceListParams) =>
+  request<ResourceListResponse>('GET', {
+    url: '/zhdd/resource/list',
+    params: params,
+  });
+
+export const deleteResourceItem = (id: number) =>
+  request<BaseResponse>('DELETE', {
+    url: `/zhdd/resource/${id}`,
+  });
+
+export interface ResourceItemDetail extends ResourceListItem {
+  resourceDetailList?: {
+    createBy?: string;
+    createTime?: string;
+    delFlag?: boolean;
+    id?: number;
+    model?: string;
+    name?: string;
+    num?: number;
+    resourceId?: number;
+    size?: string;
+    unit?: string;
+  }[];
+}
+export interface ResourceItemDetailResponse extends BaseResponse {
+  data: ResourceItemDetail;
+}
+
+export const getResourceItem = (id: string) =>
+  request<ResourceItemDetailResponse>('GET', {
+    url: `/zhdd/resource/${id}`,
+  });
+
+export const saveResourceItem = (data: ResourceItemDetail) =>
+  request<BaseResponse>('POST', {
+    url: `/zhdd/resource`,
+    data,
+  });
+
+export const putResourceItem = (data: ResourceItemDetail) =>
+  request<BaseResponse>('PUT', {
+    url: `/zhdd/resource`,
+    data,
+  });
+
+export interface ResourcesItem {
+  id?: string;
+  resourceType?: number;
+  name?: string;
+  address?: string;
+  longitude?: string;
+  latitude?: string;
+  manageUnit?: string;
+  carType?: null;
+  num?: number;
+  carryGoods?: null;
+  contactName?: string;
+  contactPhone?: string;
+  resourceDetailList?: null;
+}
+export interface AllResourcesResponse {
+  data: {
+    应急队伍: ResourcesItem[];
+    应急车队: ResourcesItem[];
+    应急仓库: ResourcesItem[];
+  };
+}
+
+/** 获取所有应急资源信息 */
+export const getAllResources = () =>
+  request<AllResourcesResponse>('GET', {
+    url: `/zhdd/resource/location`,
+  });

+ 4 - 0
src/api/type.ts

@@ -0,0 +1,4 @@
+export interface BaseResponse {
+  code?: number;
+  msg?: string;
+}

BIN
src/assets/bg_jgxt.jpg


File diff suppressed because it is too large
+ 1 - 0
src/assets/icons/duty/duty.svg


File diff suppressed because it is too large
+ 2 - 0
src/assets/icons/duty/report.svg


File diff suppressed because it is too large
+ 2 - 0
src/assets/icons/duty/roster.svg


File diff suppressed because it is too large
+ 8 - 0
src/assets/icons/emergecyplan/department.svg


File diff suppressed because it is too large
+ 8 - 0
src/assets/icons/emergecyplan/emergency.svg


+ 15 - 0
src/assets/icons/emergecyplan/highway.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>highway</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.65">
+        <g id="3_1_应急预案" transform="translate(-20.000000, -368.000000)" fill="#FFFFFF" fill-rule="nonzero">
+            <g id="左侧" transform="translate(0.000000, 107.000000)">
+                <g id="编组-2备份-5" transform="translate(0.000000, 248.000000)">
+                    <g id="公路" transform="translate(20.000000, 13.000000)">
+                        <path fill="currentColor" d="M3.07717116,0.00326888672 C2.78490582,-0.0321876495 2.4839057,0.224621151 2.41299263,0.587556082 L0.0133003411,13.1618743 C-0.057521427,13.5248396 0.163800403,13.8968746 0.45606574,13.9323007 C0.748300642,13.9677572 1.04945294,13.7109484 1.12021384,13.3478309 L3.51981482,0.773543115 C3.59075833,0.410577749 3.36934519,0.0474906439 3.07717116,0.00326888672 Z M7.16815544,0.596534347 C6.87595097,0.596534347 6.65465958,0.782430075 6.65465958,0.959499712 L6.65465958,3.81970088 C6.65465958,4.04111402 6.87607271,4.18266625 7.16815544,4.18266625 C7.46039034,4.18266625 7.68180348,3.99677052 7.68180348,3.81970088 L7.68180348,0.959499712 C7.68180348,0.738299622 7.46039034,0.596534347 7.16815544,0.596534347 Z M7.16815544,5.17447535 C6.87595097,5.17447535 6.65465958,5.36043195 6.65465958,5.58190595 L6.65465958,8.84071163 C6.65465958,9.06206389 6.87607271,9.24802049 7.16815544,9.24802049 C7.46039034,9.24802049 7.68180348,9.06209433 7.68180348,8.84071163 L7.68180348,5.58190595 C7.68180348,5.36058412 7.46039034,5.17447535 7.16815544,5.17447535 Z M7.16815544,10.3458644 C6.87595097,10.3458644 6.65465958,10.5317601 6.65465958,10.753295 L6.65465958,13.5336962 C6.65465958,13.7551093 6.87607271,13.9410659 7.16815544,13.9410659 C7.46039034,13.9410659 7.68180348,13.7551093 7.68180348,13.5336962 L7.68180348,10.753295 C7.68180348,10.5319123 7.46039034,10.3458644 7.16815544,10.3458644 Z M13.9867061,13.1973308 L11.586892,0.623043053 C11.516192,0.260077687 11.2149788,0.00329932152 10.9228048,0.0385428142 C10.5950829,0.109455887 10.4091263,0.446034285 10.4800394,0.808999651 L12.8798534,13.3831048 C12.9505839,13.7462832 13.2517058,14.0030616 13.5439711,13.967605 C13.8360538,13.9322702 14.0575278,13.5691527 13.9867061,13.1973308 Z" id="形状"></path>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 21 - 0
src/assets/icons/emergecyplan/navigation.svg

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg"
+    xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>navigation</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.65">
+        <g id="3_1_应急预案" transform="translate(-20.000000, -288.000000)" fill="#FFFFFF" fill-rule="nonzero">
+            <g id="左侧" transform="translate(0.000000, 107.000000)">
+                <g id="编组-2备份-3" transform="translate(0.000000, 168.000000)">
+                    <g id="港航局" transform="translate(20.000000, 13.000000)">
+                        <path fill="currentColor"
+                            d="M13.7696213,4.62832459 L12.1487165,3.49718958 L12.1487165,1.39584392 C12.1487165,1.13929784 11.9388151,0.929396497 11.682269,0.929396497 C11.4257229,0.929396497 11.2158216,1.13929784 11.2158216,1.39584392 L11.2158216,2.84649542 L7.23002835,0.0664687583 C7.22769611,0.0653026397 7.22419776,0.0641365211 7.22186552,0.061804284 C7.20320762,0.0501430983 7.18338361,0.0408141498 7.16355959,0.0326513198 C7.15423064,0.0279868456 7.14490169,0.0233223713 7.13440663,0.0209901342 C7.11808097,0.0151595414 7.10058919,0.0116611857 7.08309741,0.00816282996 C7.0702701,0.00583059283 7.0574428,0.00233223713 7.0446155,0.00116611857 C7.02945595,0 7.01429641,0 6.99913687,0 C6.98397733,0 6.96881779,0 6.95365825,0.00116611857 C6.94083094,0.00233223713 6.92800364,0.00583059283 6.91517634,0.00816282996 C6.89768456,0.0116611857 6.88019278,0.0151595414 6.86386712,0.0209901342 C6.85453817,0.0244884899 6.84520922,0.0291529641 6.83588027,0.0326513198 C6.81605626,0.0408141498 6.79623224,0.0501430983 6.77757434,0.061804284 C6.77524211,0.0629704026 6.77174375,0.0641365211 6.76941151,0.0653026397 L0.230984716,4.62715847 C0.00942218802,4.77059105 -0.0652094002,5.0877753 0.0618975235,5.33499244 C0.189004447,5.58337569 0.472371259,5.66733623 0.692767668,5.52506977 L7.00030299,1.12530442 L13.3066722,5.52506977 C13.5282347,5.66850235 13.8104354,5.58337569 13.9375423,5.33499244 C14.0658154,5.0877753 13.9900177,4.77059105 13.7696213,4.62832459 Z"
+                            id="路径"></path>
+                        <path fill="currentColor"
+                            d="M11.2927854,9.43273308 C11.4245568,9.48754065 11.2927854,9.5971558 11.2927854,9.76157851 C11.2927854,10.3597973 9.87361914,12.1836068 7.4842422,12.3258732 L7.4842422,8.39022308 L9.81647933,8.39022308 C10.0741915,8.39022308 10.2829268,8.18148786 10.2829268,7.92377566 C10.2829268,7.66606345 10.0741915,7.45732823 9.81647933,7.45732823 L7.4842422,7.45732823 L7.4842422,5.51224246 C8.02765345,5.3198329 8.41713705,4.80324237 8.41713705,4.19452848 C8.41713705,3.42139187 7.79093138,2.7951862 7.01779477,2.7951862 C6.24465816,2.7951862 5.61845249,3.42139187 5.61845249,4.19452848 C5.61845249,4.80324237 6.00793609,5.3198329 6.55134734,5.51224246 L6.55134734,7.45732823 L4.21911021,7.45732823 C3.96139801,7.45732823 3.75266278,7.66606345 3.75266278,7.92377566 C3.75266278,8.18148786 3.96139801,8.39022308 4.21911021,8.39022308 L6.55134734,8.39022308 L6.55134734,12.3258732 C4.47798853,12.1777762 2.74163799,10.3586312 2.67750147,9.76157851 C2.67750147,9.65196337 2.67750147,9.54234822 2.80927286,9.43273308 L3.07281566,9.26831036 L1.88920532,8.39022308 C1.82390268,12.2337499 5.44120247,13.1666447 6.36127002,13.385875 L7.01779477,13.9899244 L7.610183,13.4406826 C8.8602621,13.2214523 12.2140191,12.2325838 12.1487165,8.39022308 L11.0968775,9.26831036 L11.2927854,9.43273308 Z M7.01779477,3.72808106 C7.27138267,3.73308199 7.47452406,3.93975158 7.47515964,4.19338799 C7.47579225,4.4470244 7.27368673,4.6547071 7.02012701,4.66097591 L7.01546253,4.66097591 C6.76190281,4.6547071 6.55979729,4.4470244 6.56042989,4.19338799 C6.56106547,3.93975158 6.76420687,3.73308199 7.01779477,3.72808106 Z"
+                            id="形状"></path>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

File diff suppressed because it is too large
+ 8 - 0
src/assets/icons/emergecyplan/plan.svg


+ 23 - 0
src/assets/icons/emergecyplan/railway.svg

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg"
+    xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>railway</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.65">
+        <g id="3_1_应急预案" transform="translate(-20.000000, -328.000000)" fill="#FFFFFF" fill-rule="nonzero">
+            <g id="左侧" transform="translate(0.000000, 107.000000)">
+                <g id="编组-2备份-4" transform="translate(0.000000, 208.000000)">
+                    <g id="railway" transform="translate(20.000000, 13.000000)">
+                        <g id="铁路-(-2" transform="translate(1.000000, 0.000000)">
+                            <path fill="currentColor"
+                                d="M3.12082771,11.9847626 C3.28419043,12.0799829 3.48640974,12.0788336 3.64867955,11.9817625 C3.81094935,11.8846915 3.90759441,11.7070579 3.90095156,11.5180866 C3.8943087,11.3291153 3.785429,11.1587064 3.61674354,11.0732693 C1.63401146,9.999802 0.633156037,7.7172349 1.1868448,5.53160287 C1.74053356,3.34597084 3.70742001,1.81522531 5.96209485,1.81522531 C8.21676968,1.81522531 10.1836561,3.34597084 10.7373449,5.53160287 C11.2910337,7.7172349 10.2901782,9.999802 8.30744616,11.0732693 C8.06492174,11.2146311 7.97872943,11.5231935 8.11288741,11.7697759 C8.2470454,12.0163582 8.55293555,12.1116005 8.80336199,11.9847626 C11.1201661,10.7317768 12.3404398,8.10716872 11.805252,5.52819042 C11.2700643,2.94921213 9.10621195,1.02687546 6.48214526,0.799215598 L6.48214526,0.50563342 C6.47504211,0.224338558 6.24496713,0 5.9635826,0 C5.68219807,0 5.45212309,0.224338558 5.44501994,0.50563342 L5.44501994,0.799215598 C2.82211038,1.02860146 0.659930145,2.95073497 0.124873153,5.52871604 C-0.410183838,8.10669711 0.808487879,10.7305225 3.12347258,11.9847626 L3.12082771,11.9847626 Z"
+                                id="路径"></path>
+                            <path fill="currentColor"
+                                d="M10.3681418,12.9627087 L6.47916976,12.9627087 L6.47916976,9.33293535 L7.51629508,9.33293535 C7.79758994,9.3258322 8.0219285,9.09575722 8.0219285,8.81437269 C8.0219285,8.53298816 7.79758994,8.30291318 7.51629508,8.29581003 L4.40524973,8.29581003 C4.12395486,8.30291318 3.89961631,8.53298816 3.89961631,8.81437269 C3.89961631,9.09575722 4.12395486,9.3258322 4.40524973,9.33293535 L5.44237505,9.33293535 L5.44237505,12.9627087 L1.55307242,12.9627087 C1.36468613,12.9579516 1.18853803,13.0557355 1.09294482,13.2181361 C0.99735161,13.3805367 0.99735161,13.5820059 1.09294482,13.7444066 C1.18853803,13.9068072 1.36468613,14.0045911 1.55307242,13.999834 L10.3681418,13.999834 C10.5565281,14.0045911 10.7326762,13.9068072 10.8282694,13.7444066 C10.9238626,13.5820059 10.9238626,13.3805367 10.8282694,13.2181361 C10.7326762,13.0557355 10.5565281,12.9579516 10.3681418,12.9627087 L10.3681418,12.9627087 Z"
+                                id="路径"></path>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

File diff suppressed because it is too large
+ 10 - 0
src/assets/icons/emergecyplan/transportation.svg


File diff suppressed because it is too large
+ 8 - 0
src/assets/icons/emergencyresources/car.svg


File diff suppressed because it is too large
+ 7 - 0
src/assets/icons/emergencyresources/resource.svg


+ 18 - 0
src/assets/icons/emergencyresources/storehouse.svg

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg"
+    xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>storehouse</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.65">
+        <g id="4_1_应急资源" transform="translate(-20.000000, -168.000000)" fill="#FFFFFF" fill-rule="nonzero">
+            <g id="左侧" transform="translate(0.000000, 107.000000)">
+                <g id="编组-2" transform="translate(0.000000, 48.000000)">
+                    <g id="storehouse" transform="translate(20.000000, 13.000000)">
+                        <path fill="currentColor"
+                            d="M7.3235147,0.118387625 L13.8235033,5.63220337 C14.0344911,5.81037088 14.0594971,6.12607122 13.8813296,6.33705906 C13.7813057,6.45427454 13.6406471,6.51366371 13.4984257,6.51366371 C13.384336,6.51366371 13.2686834,6.47459188 13.174911,6.39488536 L13.004,6.25 L13.0045578,13.3434185 C13.002995,13.7044421 12.7107377,13.9982622 12.3497141,13.9982622 L1.65184879,13.9982622 C1.29082514,13.9982622 0.997005027,13.7044421 0.997005027,13.3434185 L0.997,6.246 L0.82352613,6.39488536 C0.612538283,6.57305288 0.296837949,6.54804691 0.118670433,6.33705906 C-0.0594970823,6.12607122 -0.0344911152,5.81037088 0.176496732,5.63220337 L6.6764853,0.118387625 C6.86246718,-0.0394625418 7.13753282,-0.0394625418 7.3235147,0.118387625 Z M7,1.15613526 L1.99717847,5.39896898 C1.99722191,5.40169058 1.99724371,5.40441753 1.99724371,5.40714966 L1.99724371,12.9964607 L12.0043192,12.9964607 L12.004,5.402 L7,1.15613526 Z M10.0038418,10.5005526 C10.2804703,10.5005526 10.5039611,10.7240434 10.5039611,11.0006719 C10.5039611,11.2773004 10.2804703,11.5007913 10.0038418,11.5007913 L3.99772108,11.5007913 C3.72109257,11.5007913 3.49760174,11.2773004 3.49760174,11.0006719 C3.49760174,10.7240434 3.72109257,10.5005526 3.99772108,10.5005526 L10.0038418,10.5005526 Z M10.0038418,8.50320096 C10.2804703,8.50320096 10.5039611,8.72669179 10.5039611,9.0033203 C10.5039611,9.27994882 10.2804703,9.50343965 10.0038418,9.50343965 L3.99772108,9.50343965 C3.72109257,9.50343965 3.49760174,9.27994882 3.49760174,9.0033203 C3.49760174,8.72669179 3.72109257,8.50320096 3.99772108,8.50320096 L10.0038418,8.50320096 Z M10.0038418,6.51835233 C10.2804703,6.51835233 10.5039611,6.74184316 10.5039611,7.01847167 C10.5039611,7.29510018 10.2804703,7.51859101 10.0038418,7.51859101 L3.99772108,7.51859101 C3.72109257,7.51859101 3.49760174,7.29510018 3.49760174,7.01847167 C3.49760174,6.74184316 3.72109257,6.51835233 3.99772108,6.51835233 L10.0038418,6.51835233 Z"
+                            id="形状结合"></path>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

File diff suppressed because it is too large
+ 8 - 0
src/assets/icons/emergencyresources/team.svg


File diff suppressed because it is too large
+ 2 - 0
src/assets/icons/home/home.svg


BIN
src/assets/icons/home/icon_dczsj.png


BIN
src/assets/icons/home/icon_dpfsj.png


BIN
src/assets/icons/home/icon_map_dcz.png


BIN
src/assets/icons/home/icon_map_dcz@2x.png


BIN
src/assets/icons/home/icon_map_dpf.png


BIN
src/assets/icons/home/icon_map_dpf@2x.png


BIN
src/assets/icons/home/icon_map_location@2x.png


BIN
src/assets/icons/home/icon_map_spjk.png


BIN
src/assets/icons/home/icon_map_spjk@2x.png


BIN
src/assets/icons/home/icon_map_yjck.png


BIN
src/assets/icons/home/icon_map_yjck@2x.png


BIN
src/assets/icons/home/icon_map_yjcl.png


BIN
src/assets/icons/home/icon_map_yjcl@2x.png


BIN
src/assets/icons/home/icon_map_yjdw.png


BIN
src/assets/icons/home/icon_map_yjdw@2x.png


BIN
src/assets/icons/home/icon_map_yjsj.png


BIN
src/assets/icons/home/icon_map_yjsj@2x.png


BIN
src/assets/icons/home/icon_yjsj.png


+ 5 - 0
src/assets/icons/incident/archive.svg

@@ -0,0 +1,5 @@
+<svg viewBox="0 0 14 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <path fill="currentColor"
+        d="M10.4493515,0.830182441 C11.1773297,0.830035971 11.8192123,1.30741801 12.0285324,2.00465347 L13.8657338,8.13072854 C13.9548464,8.42697428 14,8.73492649 14,9.04431216 L14,11.6527081 C14,12.0899079 13.826323,12.5092013 13.517176,12.8183483 C13.208029,13.1274953 12.7887357,13.3011722 12.3515358,13.3011722 L1.64846417,13.3011722 C1.21126429,13.3011722 0.791970979,13.1274953 0.482823978,12.8183483 C0.173676977,12.5092013 0,12.0899079 0,11.6527081 L0,9.04335652 C0,8.73420977 0.0453924919,8.42625755 0.134266209,8.13001182 L1.97146757,2.00465347 C2.17421267,1.32937602 2.78397075,0.857797754 3.48853242,0.831377004 L3.55064847,0.830182441 L10.4493515,0.830182441 Z M2.72784983,9.22779334 L1.00341298,9.22779334 L1.00341298,11.6527081 C1.00341298,11.9932231 1.26801042,12.2751249 1.60784983,12.2965647 L1.64846417,12.2977593 L12.3515358,12.2977593 C12.6920509,12.2978043 12.9739526,12.0331618 12.9953925,11.6933224 L12.996587,11.6527081 L12.996587,9.22779334 L11.2979522,9.22779334 C10.9644567,9.2276774 10.6595572,9.41611196 10.5105119,9.71444867 C10.2020441,10.3320306 9.58062236,10.7315553 8.89071672,10.755848 L8.82406143,10.7568036 L5.20102389,10.7568036 C4.48717648,10.7566525 3.83467462,10.3532205 3.51552901,9.71468758 C3.36625453,9.41641635 3.06138928,9.22796765 2.72784983,9.22779334 L2.72784983,9.22779334 Z M10.4493515,1.83359544 L3.55064847,1.83359544 C3.28128998,1.83359544 3.04028755,2.00096059 2.9462116,2.25335653 L2.93259385,2.29349305 L1.15392491,8.22438042 L2.72641638,8.22438042 C3.44040643,8.22441449 4.09317661,8.62759879 4.41286689,9.26601865 C4.55396428,9.54850821 4.83537442,9.73395823 5.15061434,9.75219613 L5.20030717,9.75339065 L8.82334471,9.75339065 C9.15781569,9.75339065 9.46290102,9.56465345 9.61102389,9.26601865 C9.91971106,8.64888083 10.5409705,8.24976815 11.2305802,8.22557496 L11.2974744,8.22438042 L12.8455973,8.22438042 L11.0664505,2.29277633 C10.9844894,2.02039928 10.7337928,1.83385693 10.4493515,1.83359544 L10.4493515,1.83359544 Z"
+        id="形状"></path>
+</svg>

BIN
src/assets/icons/incident/detail.png


BIN
src/assets/icons/incident/event.png


File diff suppressed because it is too large
+ 2 - 0
src/assets/icons/incident/event.svg


+ 5 - 0
src/assets/icons/incident/handle.svg

@@ -0,0 +1,5 @@
+<svg viewBox="0 0 14 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <path fill="currentColor"
+        d="M8.29818182,0.04 L11.76,3.75636364 L11.76,8.79636364 L10.8436364,8.79636364 L10.8436364,4.36727273 L7.38181818,4.36727273 L7.38181818,0.854545455 L0.967272727,0.854545455 L0.967272727,13.1236364 L6.87272727,13.1236364 L6.87272727,14.04 L0.814545455,14.04 C0.356363636,14.04 0,13.6836364 0,13.2254545 L0,13.2254545 L0,0.752727273 C0,0.396363636 0.356363636,0.04 0.814545455,0.04 L0.814545455,0.04 L8.29818182,0.04 Z M12.6763636,9.10181818 C12.8290909,8.89818182 13.1345455,8.89818182 13.3890909,9 C13.6436364,9.20363636 13.6436364,9.45818182 13.4909091,9.71272727 L13.4909091,9.71272727 L10.3345455,13.7854545 C10.1818182,13.9890909 9.87636364,14.1418182 9.62181818,13.8872727 L9.62181818,13.8872727 L7.43272727,12.0036364 C7.33090909,11.8509091 7.17818182,11.5454545 7.43272727,11.2909091 C7.58545455,11.1890909 7.89090909,11.0363636 8.14545455,11.2909091 L8.14545455,11.2909091 L9.87636364,12.8181818 Z M9.11272727,7.62545455 L9.11272727,8.54181818 L2.34181818,8.54181818 L2.34181818,7.62545455 L9.11272727,7.62545455 Z M6.31272727,5.48727273 L6.31272727,6.40363636 L2.34181818,6.40363636 L2.34181818,5.48727273 L6.31272727,5.48727273 Z M8.4,1.56727273 L8.4,3.45090909 L10.1309091,3.45090909 L8.4,1.56727273 Z"
+        id="形状结合"></path>
+</svg>

+ 5 - 0
src/assets/icons/incident/ignore.svg

@@ -0,0 +1,5 @@
+<svg viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <path fill="currentColor"
+        d="M7,0 C10.859375,0 14,3.140625 14,7 C14,10.859375 10.859375,14 7,14 C3.140625,14 0,10.859375 0,7 C0,3.140625 3.140625,0 7,0 Z M1.1125,7 C1.1125,10.2453125 3.753125,12.8875 7,12.8875 C8.35585587,12.8875 9.60643768,12.4265851 10.6030487,11.6531103 L2.31820476,3.43354834 C1.56203281,4.42389676 1.1125,5.66034521 1.1125,7 Z M7,1.1125 C5.49279792,1.1125 4.11623206,1.68150824 3.07379507,2.61603207 L11.4138062,10.8920614 C12.3304766,9.8537984 12.8875,8.49080372 12.8875,7 C12.8875,3.753125 10.2453125,1.1125 7,1.1125 Z"
+        id="形状结合"></path>
+</svg>

BIN
src/assets/icons/incident/plan.png


+ 6 - 0
src/assets/icons/incident/send.svg

@@ -0,0 +1,6 @@
+<svg viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg"
+    xmlns:xlink="http://www.w3.org/1999/xlink">
+    <path fill="currentColor"
+        d="M9.28712901,12.7520024 C9.50644992,12.8966462 9.78166053,12.9278524 10.0277993,12.8359872 C10.273938,12.7441221 10.4614051,12.5402329 10.5323267,12.2872633 L13.5960687,1.46124166 C13.7687133,0.853440594 13.418018,0.220326975 12.8111901,0.0442925487 C12.5485282,-0.0311954277 12.267476,-0.0096794727 12.0193656,0.104910693 L0.480449259,5.43046721 C0.207883538,5.55631273 0.0248989868,5.81995913 0.00234485285,6.11932592 C-0.0202092811,6.41869272 0.121207998,6.70677937 0.371841753,6.87204246 L2.7801501,8.46011155 C2.930848,8.56299685 3.12531815,8.57661474 3.28888425,8.49573597 C3.45245034,8.41485721 3.55968559,8.2520546 3.56941178,8.06984416 C3.57913798,7.88763372 3.48984315,7.71434056 3.33581642,7.61650904 L1.20281548,6.20966294 L12.4424297,1.02239155 C12.4704981,1.00932337 12.5023479,1.00685613 12.532094,1.01544572 C12.6039175,1.03789741 12.6446042,1.11365898 12.6236527,1.18593426 L9.63000038,11.7682205 L7.49257936,10.3582172 C7.20790576,10.1702866 6.82543767,10.2424187 6.62877081,10.5211284 L5.45934578,12.1754986 L5.45934578,8.8560238 L9.88699605,5.20756925 C10.0291744,5.09373879 10.0990679,4.91218537 10.0699244,4.73240022 C10.0407808,4.55261508 9.91711302,4.40243838 9.74625603,4.33935166 C9.57539904,4.27626493 9.38381046,4.3100373 9.24482259,4.42774209 L4.67951861,8.18985565 C4.53368964,8.30972608 4.44914008,8.48855421 4.44904339,8.67732656 L4.44904339,13.3689183 C4.44921872,13.6439962 4.6274526,13.8873099 4.88967041,13.9704345 C5.15188822,14.0535591 5.43774782,13.9573659 5.59636805,13.7326272 L7.24252951,11.4032487 L9.28776045,12.7526339 L9.28712901,12.7520024 Z"
+        id="路径"></path>
+</svg>

File diff suppressed because it is too large
+ 2 - 0
src/assets/icons/incident/warning.svg


BIN
src/assets/icons/incident/yjya.png


+ 9 - 0
src/assets/icons/index.js

@@ -0,0 +1,9 @@
+import Vue from 'vue'
+import SvgIcon from '@/components/SvgIcon'// svg component
+
+// register globally
+Vue.component('svg-icon', SvgIcon)
+
+const req = require.context('./svg', false, /\.svg$/)
+const requireAll = requireContext => requireContext.keys().map(requireContext)
+requireAll(req)

+ 22 - 0
src/assets/icons/svgo.yml

@@ -0,0 +1,22 @@
+# replace default config
+
+# multipass: true
+# full: true
+
+plugins:
+
+  # - name
+  #
+  # or:
+  # - name: false
+  # - name: true
+  #
+  # or:
+  # - name:
+  #     param1: 1
+  #     param2: 2
+
+- removeAttrs:
+    attrs:
+      - 'fill'
+      - 'fill-rule'

+ 31 - 0
src/components/Card/index.scss

@@ -0,0 +1,31 @@
+.card-content {
+  min-height: max-content;
+  transition: box-shadow 0.3s ease;
+  border-radius: 2px;
+  background: #ffffff;
+  &.always-shadow {
+    box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.06);
+  }
+  &.hover-shadow {
+    &:hover {
+      box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.06);
+    }
+  }
+  .card-content-title {
+    border-top-left-radius: 2px;
+    border-top-right-radius: 2px;
+    height: 55px;
+    line-height: 55px;
+    background: linear-gradient(180deg, #f4f8ff, #e7f1ff 100%);
+    padding: 0 24px;
+    > svg {
+      margin-right: 8px;
+    }
+  }
+  .card-content-body {
+    padding: 20px;
+    // height: calc(100% - 55px);
+    min-height: max-content;
+    box-sizing: border-box;
+  }
+}

+ 30 - 0
src/components/Card/index.tsx

@@ -0,0 +1,30 @@
+import getSlot from '@/utils/getSolt';
+import clsx from 'clsx';
+import { defineComponent, PropType, VNode } from 'vue';
+import './index.scss';
+
+export default defineComponent({
+  name: 'Card',
+  props: {
+    header: Function as PropType<() => VNode>,
+    body: Object as PropType<VNode>,
+    shadow: {
+      type: String as PropType<'always' | 'hover' | 'never'>,
+      default: 'always',
+    },
+  },
+  setup(props, ctx) {
+    return () => (
+      <div
+        class={clsx('card-content', {
+          'always-shadow': props.shadow === 'always' || !props.shadow,
+          'hover-shadow': props.shadow === 'hover',
+        })}>
+        <div class="card-content-title">{props.header && props.header()}</div>
+        <div class="card-content-body">
+          {ctx.slots.default && ctx.slots.default()}
+        </div>
+      </div>
+    );
+  },
+});

+ 13 - 0
src/components/Container/index.scss

@@ -0,0 +1,13 @@
+.table-form-container {
+  padding: 24px 20px;
+  min-height: calc(100vh - 60px - 88px);
+  background-color: #fff;
+  th.el-table__cell {
+    background-color: #fafafa;
+  }
+  .pagination-content {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 30px;
+  }
+}

+ 183 - 0
src/components/Container/index.tsx

@@ -0,0 +1,183 @@
+import {
+  defineComponent,
+  onMounted,
+  PropType,
+  reactive,
+  ref,
+  VNode,
+  watch,
+} from 'vue';
+import { useRoute } from 'vue-router';
+import QueryForm, { Schema } from '@/components/QueryForm';
+import keys from 'lodash/keys';
+import isEmpty from 'lodash/isEmpty';
+import './index.scss';
+import { pickBy } from 'lodash';
+
+export interface Column<T extends any> {
+  label?: string;
+  width?: string | number;
+  prop?: keyof T;
+  rules?: { type?: string; message: string; required?: boolean }[];
+  required?: boolean;
+  render?: (value: T[keyof T], row: T, index: number) => VNode;
+}
+
+export interface Data {
+  data: any[];
+  pageSize: number;
+  total: number;
+  pageNum: number;
+}
+
+export default defineComponent({
+  name: 'Container',
+  props: {
+    loading: Boolean,
+    refeshKey: String as PropType<string | null>,
+    // form props. copy from QueryForm
+    schema: Array as PropType<Schema[]>,
+    form: { type: Object, required: true },
+    suffix: {
+      type: [Object, String] as PropType<JSX.Element | string>,
+      default: undefined,
+    },
+    onReset: Function,
+    onQuery: Function as PropType<(params: any) => void>,
+    // table
+    data: Object,
+    columns: Array as PropType<Column<any>[]>,
+    params: Object,
+    transformQueryParams: {
+      type: Function,
+      default: (params: object) => pickBy(params, (p) => !isEmpty(p)),
+    },
+  },
+  setup(props) {
+    const state = reactive({
+      tableData: [] as any[],
+      loading: !!props.loading,
+    });
+
+    const route = useRoute();
+    const params = ref(route.params);
+    const form = ref(props.form);
+
+    const handleQueryData = (form: object) => {
+      props.onQuery && props.onQuery({ ...form });
+    };
+    const handleSubmit = (form: object) => {
+      handleQueryData({ ...params.value, ...props.transformQueryParams(form) });
+    };
+
+    const handlePageSizeChange = (pageSize: number) => {
+      handleQueryData({
+        ...params.value,
+        ...props.transformQueryParams(props.form),
+        pageSize,
+        pageNum: 1,
+      });
+    };
+
+    const handlePageNumChange = (pageNum: number) => {
+      handleQueryData({
+        ...params.value,
+        ...props.transformQueryParams(props.form),
+        pageSize: props.data?.pageSize ?? 10,
+        pageNum,
+      });
+    };
+
+    onMounted(() => {
+      handleQueryData({
+        ...params.value,
+        ...props.transformQueryParams(props.form),
+      });
+    });
+
+    watch(
+      () => props.refeshKey,
+      (next) => {
+        if (next)
+          handleQueryData({
+            ...params.value,
+            ...props.transformQueryParams(props.form),
+          });
+      },
+    );
+
+    watch(
+      () => route.params,
+      (nextParams, preParams) => {
+        if (
+          isEmpty(nextParams) ||
+          JSON.stringify(nextParams) === JSON.stringify(preParams) ||
+          Object.keys(nextParams).toString() !==
+            Object.keys(preParams).toString()
+        )
+          return;
+
+        const resetForm = keys(props.form).reduce(
+          (c, n) => ({ ...c, [n]: undefined }),
+          {},
+        );
+        form.value = resetForm;
+        params.value = nextParams;
+        handleQueryData({
+          ...nextParams,
+          ...resetForm,
+          pageSize: 10,
+          pageNum: 1,
+        });
+      },
+    );
+
+    return () => (
+      <div v-loading={props.loading} class="table-form-container">
+        <QueryForm
+          schema={props.schema}
+          form={form.value}
+          suffix={props.suffix}
+          onSubmit={handleSubmit}
+        />
+
+        <el-table
+          data={props.data?.rows ?? []}
+          height={'calc(100vh - 60px - 88px - 124px)'}>
+          {props.columns &&
+            props.columns.map((col, key) => (
+              <el-table-column
+                prop={col.prop}
+                label={col.label}
+                min-width={col.width}
+                v-slots={
+                  col.render && {
+                    default: (slotProps: any) =>
+                      col.render &&
+                      col.render(
+                        slotProps?.row?.[col.prop as string],
+                        slotProps?.row,
+                        slotProps.$index,
+                      ),
+                  }
+                }
+              />
+            ))}
+        </el-table>
+        <div class="pagination-content">
+          <el-pagination
+            hide-on-single-page={state.tableData?.length === 0}
+            background
+            page-sizes={[10, 20, 30, 40]}
+            page-size={props.data?.pageSize ?? 10}
+            total={props.data?.total ?? 0}
+            currentPage={props.data?.pageNum ?? 1}
+            layout="prev, pager, next,sizes, jumper"
+            onSizeChange={handlePageSizeChange}
+            onCurrentChange={handlePageNumChange}
+          />
+        </div>
+      </div>
+    );
+  },
+});

+ 41 - 0
src/components/Dialog/index.scss

@@ -0,0 +1,41 @@
+.dialog-container {
+  min-width: 430px;
+  margin: auto;
+  top: 50%;
+  transform: translateY(-50%);
+  .el-dialog__header {
+    padding: 13px 20px;
+    background: #003a8c;
+    border-radius: 2px 2px 0px 0px;
+    box-sizing: border-box;
+    .el-dialog__close {
+      color: #ffffff;
+    }
+    .el-dialog__title {
+      color: #fff;
+      line-height: 24px;
+      font-size: 16px;
+    }
+  }
+  .el-dialog__body {
+    padding: 24px 20px 0;
+  }
+  .footer-container {
+    height: 64px;
+    box-sizing: border-box;
+    align-items: center;
+    margin-top: 30px;
+    display: flex;
+    justify-content: right;
+    position: relative;
+    &::before {
+      content: '';
+      position: absolute;
+      background: #e4e7ed;
+      height: 1px;
+      width: calc(100% + 40px);
+      left: -20px;
+      top: 0;
+    }
+  }
+}

+ 55 - 0
src/components/Dialog/index.tsx

@@ -0,0 +1,55 @@
+import { defineComponent } from 'vue';
+import './index.scss';
+
+export default defineComponent({
+  name: 'Dialog',
+  props: {
+    title: String,
+    onPedding: Function,
+    onSave: Function,
+    onClose: Function,
+    visible: Boolean,
+    width: String,
+  },
+  setup(props, ctx) {
+    return () => (
+      <el-dialog
+        modelValue={props.visible}
+        title={props.title}
+        width={props.width ?? '30%'}
+        destroyOnClose
+        onClose={() => {
+          ctx.emit('update:visible', false);
+          props.onClose && props.onClose();
+        }}
+        custom-class="dialog-container">
+        {ctx.slots.default && ctx.slots.default()}
+
+        <div class="footer-container">
+          {ctx.slots.buttons ? (
+            ctx.slots.buttons()
+          ) : (
+            <>
+              <el-button
+                size="small"
+                onClick={() => ctx.emit('update:visible', false)}>
+                取消
+              </el-button>
+              <el-button
+                size="small"
+                type="primary"
+                onClick={async () => {
+                  const result = props.onPedding && (await props.onPedding());
+                  if (result) {
+                    ctx.emit('update:visible', false);
+                  }
+                }}>
+                确认
+              </el-button>
+            </>
+          )}
+        </div>
+      </el-dialog>
+    );
+  },
+});

+ 22 - 0
src/components/EditTable/index.scss

@@ -0,0 +1,22 @@
+.edit-table-container {
+  .el-form-item {
+    margin-bottom: 0;
+  }
+  .el-form-item__content {
+    margin-left: unset !important;
+    // line-height: unset;
+  }
+  .el-form-item__error {
+    position: unset;
+  }
+  .el-input-number {
+    .el-input-number__decrease,
+    .el-input-number__increase {
+      display: none;
+    }
+    .el-input__inner {
+      padding: 0 30px 0 15px;
+      text-align: left;
+    }
+  }
+}

+ 54 - 0
src/components/EditTable/index.tsx

@@ -0,0 +1,54 @@
+import isEmpty from 'lodash/isEmpty';
+import { defineComponent, PropType, ref, reactive, VNode } from 'vue';
+import type { Column } from '../Container';
+import './index.scss';
+
+export default defineComponent({
+  name: 'EditTable',
+  props: {
+    readonly: Boolean,
+    columns: Array as PropType<Column<any>[]>,
+    data: Object,
+    onAdd: Function,
+  },
+  setup(props) {
+    const handleAddResource = () => {};
+
+    return () => (
+      <>
+        {!props.readonly && (
+          <el-button
+            size="small"
+            icon="el-icon-plus"
+            onClick={props.onAdd}
+            readonly={props.readonly}>
+            新增
+          </el-button>
+        )}
+        {!isEmpty(props.data) && (
+          <el-table size="small" data={props.data} class="edit-table-container">
+            {props.columns &&
+              props.columns.map((col) => (
+                <el-table-column
+                  width={col.width}
+                  prop={col.prop}
+                  label={col.label}
+                  v-slots={
+                    col.render && {
+                      default: (slotProps: any) =>
+                        col.render &&
+                        col.render(
+                          slotProps?.row?.[col.prop as string],
+                          slotProps?.row,
+                          slotProps.$index,
+                        ),
+                    }
+                  }
+                />
+              ))}
+          </el-table>
+        )}
+      </>
+    );
+  },
+});

+ 36 - 0
src/components/FormContainer/index.scss

@@ -0,0 +1,36 @@
+.new-edit-form-container {
+  padding: 0 20px 45px;
+  min-height: calc(100vh - 60px - 85px);
+  background-color: #fff;
+  .form-title {
+    font-size: 16px;
+    font-family: PingFangSC, PingFangSC-Medium;
+    font-weight: 500;
+    height: 55px;
+    line-height: 55px;
+    border-bottom: 1px solid #e5e5e5;
+    margin-left: -20px;
+    margin-right: -20px;
+    display: flex;
+    justify-content: space-between;
+    padding-right: 20px;
+    > span {
+      position: relative;
+      margin-left: 40px;
+      &::before {
+        content: '';
+        display: inline-block;
+        position: absolute;
+        left: -12px;
+        width: 3px;
+        height: 16px;
+        background: #003a8c;
+        top: 50%;
+        transform: translateY(-50%);
+      }
+    }
+  }
+  .form-body {
+    padding: 40px 40px 0;
+  }
+}

+ 38 - 0
src/components/FormContainer/index.tsx

@@ -0,0 +1,38 @@
+import { reactive, defineComponent } from 'vue';
+import { useRouter } from 'vue-router';
+import './index.scss';
+
+export default defineComponent({
+  name: 'FormContainer',
+  props: {
+    title: String,
+  },
+  setup(props, ctx) {
+    const state = reactive({
+      loading: false,
+    });
+
+    const router = useRouter();
+
+    return () => (
+      <div v-loading={state.loading} class="new-edit-form-container">
+        <div class="form-title">
+          <span>{props.title}</span>
+          <el-button
+            type="text"
+            icon="el-icon-back"
+            onClick={() => router.back()}>
+            返回
+          </el-button>
+        </div>
+        <div class="form-body">
+          <el-row justify="center">
+            <el-col span={18}>
+              {ctx.slots.default && ctx.slots.default()}
+            </el-col>
+          </el-row>
+        </div>
+      </div>
+    );
+  },
+});

+ 27 - 0
src/components/MapView/index.tsx

@@ -0,0 +1,27 @@
+import { defineComponent, onMounted, ref } from 'vue';
+
+let ispro = process.env.NODE_ENV === 'production';
+export default defineComponent({
+  props: {
+    map: Object,
+  },
+  setup(props, ctx) {
+    const mapRef = ref<Element>();
+
+    onMounted(() => {
+      const map = new window.minemap.Map({
+        container: 'map',
+        style: 'https://minedata.cn/service/solu/style/id/16857' /*底图样式*/,
+        center: [118.29564, 33.97441] /*地图中心点*/,
+        zoom: 14 /*地图默认缩放等级*/,
+        pitch: 0 /*地图俯仰角度*/,
+        maxZoom: 17 /*地图最大缩放等级*/,
+        minZoom: 3 /*地图最小缩放等级*/,
+        // projection: 'MERCATOR',
+        logoControl: false,
+      });
+      ctx.emit('update:map', map);
+    });
+    return () => <div id="map" style={{ height: '100%' }} ref={mapRef} />;
+  },
+});

+ 198 - 0
src/components/MapView/index.vue

@@ -0,0 +1,198 @@
+<template>
+  <div>
+    <div id="map" ref="rootmap"></div>
+  </div>
+</template>
+
+<script>
+import 'ol/ol.css';
+import Map from 'ol/Map';
+import TileLayer from 'ol/layer/Tile';
+import View from 'ol/View';
+import WMTS from 'ol/source/WMTS';
+import WMTSTileGrid from 'ol/tilegrid/WMTS';
+import { fromLonLat, get as getProjection, transform } from 'ol/proj';
+import { getWidth } from 'ol/extent';
+
+import { Vector as SourceVec, OSM } from 'ol/source';
+import { Feature } from 'ol';
+import { Point, LineString } from 'ol/geom';
+import { Style, Icon, Stroke } from 'ol/style';
+import { Vector as LayerVec } from 'ol/layer';
+
+let ispro = process.env.NODE_ENV === 'production';
+//  ispro = false;
+export default {
+  props: {
+    src: {
+      type: String,
+      required: false,
+    },
+  },
+  data() {
+    return {
+      map: null,
+    };
+  },
+  mounted() {
+    var that = this;
+    this.initmap();
+    this.map.on('load', function () {
+      // 增加自定义数据源、自定义图层
+      that.map.on('click', function (e) {
+        console.log(e);
+      });
+    });
+
+    this.addpoint();
+    this.addline();
+  },
+  methods: {
+    initmap: function () {
+      this.map = new minemap.Map({
+        container: 'map',
+        style: 'https://minedata.cn/service/solu/style/id/16857' /*底图样式*/,
+        center: [118.29564, 33.97441] /*地图中心点*/,
+        zoom: 14 /*地图默认缩放等级*/,
+        pitch: 0 /*地图俯仰角度*/,
+        maxZoom: 17 /*地图最大缩放等级*/,
+        minZoom: 3 /*地图最小缩放等级*/,
+        projection: 'MERCATOR',
+      });
+    },
+
+    addline: function () {
+      var that = this;
+
+      function addSources() {
+        var center = that.map.getCenter();
+        var jsonData = {
+          type: 'FeatureCollection',
+          features: [
+            {
+              type: 'Feature',
+              geometry: {
+                type: 'LineString',
+                coordinates: [
+                  [center.lng - 0.005, center.lat + 0.005],
+                  [center.lng - 0.005, center.lat - 0.005],
+                ],
+              },
+              properties: {
+                title: '路线一',
+                kind: 1,
+              },
+            },
+            {
+              type: 'Feature',
+              geometry: {
+                type: 'LineString',
+                coordinates: [
+                  [center.lng - 0.004, center.lat],
+                  [center.lng, center.lat],
+                  [center.lng + 0.008, center.lat],
+                ],
+              },
+              properties: {
+                title: '路线二',
+                kind: 2,
+              },
+            },
+          ],
+        };
+        that.map.addSource('lineSource', {
+          type: 'geojson',
+          data: jsonData,
+        });
+      }
+
+      function addLayers() {
+        that.map.addLayer({
+          id: 'lineLayer',
+          type: 'line',
+          source: 'lineSource',
+          layout: {
+            'line-join': 'round',
+            'line-cap': 'round',
+            'border-visibility': 'visible', //是否开启线边框
+          },
+          paint: {
+            'line-width': 6,
+            'line-color': {
+              type: 'categorical',
+              property: 'kind',
+              stops: [
+                [1, '#ff0000'],
+                [2, '#00ff00'],
+              ],
+              default: '#ff0000',
+            },
+            'line-border-width': 2, //设置线边框宽度
+            'line-border-opacity': 1, //设置线边框透明度
+            'line-border-color': 'blue', //设置线边框颜色
+          },
+          minzoom: 7,
+          maxzoom: 17.5,
+        });
+
+        that.map.addLayer({
+          id: 'symbolLayer',
+          type: 'symbol',
+          source: 'lineSource',
+          layout: {
+            'text-field': '{title}',
+            'text-size': 10,
+            'symbol-placement': 'line',
+          },
+          paint: {
+            'text-color': '#000000',
+            'text-halo-color': '#ffffff',
+            'text-halo-width': 0.8,
+          },
+          minzoom: 7,
+          maxzoom: 17.5,
+        });
+      }
+      // addSources();
+      //   addLayers();
+      this.map.on('load', function () {
+        // 增加自定义数据源、自定义图层
+        addSources();
+        addLayers();
+      });
+    },
+
+    addpoint: function () {
+      var markers = [];
+
+      var lnglat = this.map.getCenter();
+
+      if (this.map) {
+        var _marker = new minemap.Marker({
+          draggable: true, //可以在初始化的时候决定是否可以拖拽
+          anchor: 'top-left', //锚点位置(默认值"top-left"),可选值有`'center'`, `'top'`, `'bottom'`, `'left'`, `'right'`, `'top-left'`, `'top-right'`, `'bottom-left'`, and `'bottom-right'`
+          color: '#111', //默认marker标记的颜色
+          rotation: 0, //marker的旋转角度,以锚点作为旋转中心点,顺时针为正
+          pitchAlignment: 'map', //倾斜对齐参数(默认值是‘auto’),此值决定marker标记是贴在地图平面上,还是立在地图平面上,当值为`map`时,marker标记贴在地图平面上,当值为`viewport`时,marker标记立在地图平面上,当值为 `auto`时,会根据 `rotationAlignment`参数的值,自动决定.
+          rotationAlignment: 'map', //旋转对齐参数(默认值是‘auto’),此值决定地图在旋转的时候,marker标记是否跟随旋转,当值为`map` 时,marker标记会固定在地图平面上,不会跟随地图的旋转而旋转;当值为`viewport`时,marker标记会跟随地图的旋转而旋转,保持正向面对观察者;当值为`auto`时,相当于值`viewport`.
+          scale: 1.5, //只有默认标记有这个参数,将默认标记放大1.5倍
+        })
+          .setLngLat([lnglat.lng + 0.005, lnglat.lat])
+          .addTo(this.map);
+        markers.push(_marker);
+      }
+    },
+  },
+};
+</script>
+
+<style>
+#map {
+  height: 100%;
+}
+/*隐藏ol的一些自带元素*/
+.ol-attribution,
+.ol-zoom {
+  display: none;
+}
+</style>

+ 157 - 0
src/components/MarkerMap/constants.ts

@@ -0,0 +1,157 @@
+const ICON_MAP_TYPES = {
+  WARNING_INCIDENT: { name: '预警事件' as const },
+  PENDING_INCIDENT: { name: '待派发事件' as const },
+  PENDING_DISPOSAL_INCIDENT: { name: '待处置事件' as const },
+  TRAFFIC_INFO: { name: '路况信息' as const },
+  VIDEO_SURVEILLANCE: { name: '视频监控' as const },
+  EMERGENCY_VEHICLES: { name: '应急车辆' as const },
+  EMERGENCY_TEAM: { name: '应急队伍' as const },
+  EMERGENCY_WAREHOUSE: { name: '应急仓库' as const },
+};
+
+export const WARNING_INCIDENT = [
+  {
+    name: '宿迁学院',
+    locations: '118.296459,33.929648',
+  },
+  {
+    locations: '118.504329,33.903868',
+    name: '宿迁市大兴派出所',
+  },
+  {
+    locations: '118.291631,33.884288',
+    name: '明珠派出所',
+  },
+  {
+    locations: '118.790799,33.705629',
+    name: '来安派出所',
+  },
+];
+
+export const PENDING_INCIDENT = [
+  {
+    locations: '118.288721,33.951047',
+    name: '宿迁宝龙城市广场',
+  },
+  {
+    locations: '118.285469,33.950549',
+    name: '用世水韵城',
+  },
+  {
+    locations: '118.290689,33.952558',
+    name: '金鹰天地广场(宿迁店)',
+  },
+  {
+    locations: '118.300139,33.955468',
+    name: '苏宁广场',
+  },
+  {
+    locations: '118.300149,33.953758',
+    name: '宿迁人民商场',
+  },
+  {
+    locations: '118.262051,33.956094',
+    name: '鑫鑫奶站',
+  },
+  {
+    locations: '118.27269,33.97476',
+    name: '恒茂商业广场',
+  },
+];
+
+export const PENDING_DISPOSAL_INCIDENT = [
+  {
+    name: '宿迁市中心血站',
+    locations: '118.273349,33.954288',
+  },
+  {
+    name: '早点中心',
+    locations: '118.296279,33.962388',
+  },
+  {
+    name: '宿迁市体育运动中心',
+    locations: '118.266809,33.953318',
+  },
+  {
+    name: '宿迁市体育运动中心',
+    locations: '118.268309,33.953778',
+  },
+  {
+    name: '宿迁市奥体中心',
+    locations: '118.290087,34.004411',
+  },
+  {
+    name: '中心大酒店',
+    locations: '118.245129,33.938378',
+  },
+  {
+    name: '中心庄',
+    locations: '118.752399,34.118339',
+  },
+  {
+    name: '中心桥',
+    locations: '118.404649,33.415169',
+  },
+  {
+    name: '国贸中心',
+    locations: '118.270089,33.895378',
+  },
+  {
+    name: '泗洪县中心医院',
+    locations: '118.243579,33.460968',
+  },
+];
+
+export const VIDEO_SURVEILLANCE = [
+  {
+    name: 'SUQIAN1234',
+    addr: '宿迁市第一人民医院',
+
+    locations: '118.269259,33.974288',
+  },
+  {
+    name: 'SUQIAN1234',
+    addr: '宿迁市宿城区人民医院',
+    locations: '118.300829,33.967868',
+  },
+  {
+    name: 'SUQIAN1234',
+    addr: '宿迁市工人医院(新)',
+    locations: '118.268819,33.939198',
+  },
+  {
+    name: 'SUQIAN1234',
+    addr: '南京鼓楼医院集团宿迁市人民医院',
+    locations: '118.296529,33.945168',
+  },
+  {
+    name: 'SUQIAN1234',
+    addr: '宿迁市儿童医院(旧)',
+    locations: '118.295179,33.947398',
+  },
+  {
+    name: 'SUQIAN1234',
+    addr: '宿迁市儿童医院',
+    locations: '118.255491,33.949056',
+  },
+  {
+    name: 'SUQIAN1234',
+    addr: '宿迁市第一人民医院-药学部',
+    locations: '118.268199,33.976638',
+  },
+  {
+    name: 'SUQIAN1234',
+    addr: '宿迁市钟吾医院',
+    locations: '118.275649,33.930958',
+  },
+  {
+    name: 'SUQIAN1234',
+    addr: '宿迁市骨科医院',
+    locations: '118.270939,33.959068',
+  },
+  {
+    name: 'SUQIAN1234',
+    addr: '钟吾医院-住院大楼',
+    locations: '118.275809,33.929618',
+  },
+];

+ 184 - 0
src/components/MarkerMap/dialog.ts

@@ -0,0 +1,184 @@
+import { IncidentItem } from '@/api/incident';
+import { ResourceItemDetail } from '@/api/resource';
+import { useCommonStore } from '@/store';
+
+export const GET_INCIDENT_DIALOG_HTML = (
+  item: IncidentItem,
+  callback = () => {},
+) => {
+  const commonStore = useCommonStore();
+  const el = document.createElement('div');
+
+  el.innerHTML = `
+<div>
+  <div class="title">事件信息</div>
+  <div class="content">
+        <div><span>标题:</span><span>${item?.name ?? '-'}</span></div>
+        <div><span>来源:</span><span>${
+          commonStore.globalDict['zhdd_incident_type']?.find(
+            (i) => i.dictValue.toString() === `${item?.source}`,
+          )?.dictLabel ?? '-'
+        }</span></div>
+        <div><span>类型:</span><span>${
+          commonStore.globalDict['zhdd_incident_type']?.find(
+            (i) => i.dictValue.toString() === `${item?.type}`,
+          )?.dictLabel ?? '-'
+        }</span></div>
+        <div><span>时间:</span><span>${item?.createTime ?? '-'}</span></div>
+        <div><span>地点:</span><span>${item?.addr ?? '-'}</span></div>
+        <div><span>描述:</span><span>${item?.des ?? '-'}</span></div>
+  </div>
+</div>
+`;
+
+  const action = document.createElement('div');
+  action.className = 'action';
+  const button = document.createElement('button');
+  button.className = 'el-button el-button--primary el-button--small';
+  button.innerHTML = '详情';
+  action.appendChild(button);
+  button.addEventListener('click', callback);
+
+  el.appendChild(action);
+  return el;
+};
+
+// •	视频打开后显示数据:
+// a.视频点位编号
+// b.视频点位地点
+// c.按钮可以打开一个窗口,然后窗口中显示摄像头拍摄内容 (目前还无该项目数据,暂定为该内容)
+
+export const GET_VIDEO_DIALOG_HTML = ({
+  name,
+  addr,
+  link,
+}: {
+  name?: string;
+  addr?: string;
+  link?: string;
+}) => {
+  const el = document.createElement('div');
+
+  el.innerHTML = `
+  <div>
+    <div class="title">视频监控信息</div>
+    <div class="content">
+        <div><span>编号:</span><span>${name ?? '-'}</span></div>
+        <div><span>地点:</span><span>${addr ?? '-'}</span></div>
+    </div>
+    <div class="action">
+        <button class="el-button el-button--primary el-button--small" type="button">
+            <span>查看</span>
+        </button>
+    </div>
+  </div>
+  `;
+  return el;
+};
+
+// •	应急仓库
+// a.应急仓库名称
+// b.应急仓库地点
+// c.应急仓库管理单位
+// d.联系人
+// e.联系电话
+// f.按钮(方式一:跳转应急仓库详情查看物资情况,方式二:在按钮旁边打开一个列表,该列表显示应急仓库的物资情况:物资名称、型号、数量)
+
+export const GET_WAREHOUSE_DIALOG_HTML = (
+  item: ResourceItemDetail,
+  callback = () => {},
+) => {
+  const el = document.createElement('div');
+
+  el.innerHTML = `
+<div>
+  <div class="title">应急仓库信息</div>
+  <div class="content">
+    <div><span>名称:</span><span>${item.name ?? '-'}</span></div>
+    <div><span>地点:</span><span>${item.address ?? '-'}</span></div>
+    <div><span>管理单位:</span><span>${item.manageUnit ?? '-'}</span></div>
+    <div><span>联系人:</span><span>${item.contactName ?? '-'}</span></div>
+    <div><span>联系方式:</span><span>${item.contactPhone ?? '-'}</span></div>
+  </div>
+</div>
+  `;
+
+  const action = document.createElement('div');
+  action.className = 'action';
+  const button = document.createElement('button');
+  button.className = 'el-button el-button--primary el-button--small';
+  button.innerHTML = '查看物资情况';
+  action.appendChild(button);
+  button.addEventListener('click', callback);
+
+  el.appendChild(action);
+  return el;
+};
+
+// •	应急车辆
+// a.车牌号
+// b.归属单位
+// c.车辆类型 (应急车辆为实时位置,目前暂无具体数据)
+
+export const GET_VEHICLES_DIALOG_HTML = (item: {
+  name?: string;
+  manageUnit?: string;
+  carType?: string;
+}) => {
+  const commonStore = useCommonStore();
+  const el = document.createElement('div');
+
+  el.innerHTML = `
+  <div>
+    <div class="title">应急车辆信息</div>
+    <div class="content">
+      <div><span>车牌号:</span><span>${item.name ?? '-'}</span></div>
+      <div><span>管理单位:</span><span>${item.manageUnit ?? '-'}</span></div>
+      <div><span>车辆类型:</span><span>${
+        commonStore.globalDict['zhdd_car_type']?.find(
+          (i) => i.dictValue.toString() === `${item?.carType}`,
+        )?.dictLabel ?? '-'
+      }</span></div>
+    </div>
+  </div>
+`;
+  return el;
+};
+
+// •	应急队伍
+// a.队伍名称
+// b.管理单位
+// c.携带物资
+// d.人数
+// e. 联系人
+// f.联系电话
+
+export const GET_TEAM_DIALOG_HTML = (item: ResourceItemDetail) => {
+  const el = document.createElement('div');
+  el.innerHTML = `
+<div>
+<div class="title">应急队伍信息</div>
+<div class="content">
+    <div><span>队伍名称:</span><span>${item.name ?? '-'}</span></div>
+    <div><span>管理单位:</span><span>${item.manageUnit ?? '-'}</span></div>
+    <div><span>携带物资:</span><span>${item.carryGoods ?? '-'}</span></div>
+    <div><span>人数:</span><span>${item.num ?? '-'}</span></div>
+    <div><span>联系人:</span><span>${item.contactName ?? '-'}</span></div>
+    <div><span>联系方式:</span><span>${item.contactPhone ?? '-'}</span></div>
+</div>
+</div>
+  `;
+  return el;
+};
+
+export const renderElement = (image: any) => {
+  var el = document.createElement('div');
+  el.id = 'marker';
+  el.style.backgroundImage = `url(${image})`;
+  el.style.backgroundSize = 'cover';
+  el.style.width = '24px';
+  el.style.height = '25px';
+  // el.innerHTML = '<div>123</div>';
+  // el.style.borderRadius = '50%';
+  return el;
+};

+ 67 - 0
src/components/MarkerMap/index.scss

@@ -0,0 +1,67 @@
+.task-map-container {
+  height: 100%;
+  .minemap-map {
+    .minemap-popup-tip {
+      display: none;
+    }
+    .minemap-popup-content {
+      max-width: max-content;
+      padding: unset;
+      width: 350px;
+
+      .title {
+        width: 350px;
+        height: 44px;
+        line-height: 44px;
+        padding: 0 20px;
+        box-sizing: border-box;
+        background: #003a8c;
+        border-radius: 1px;
+        font-size: 16px;
+        color: #fff;
+      }
+      .action {
+        padding: 0 24px 24px;
+      }
+      .content {
+        height: max-content;
+        padding: 24px;
+        background: #fff;
+        > div {
+          display: flex;
+          font-size: 14px;
+          line-height: 20px;
+          margin-bottom: 10px;
+          > span:first-child {
+            color: #666666;
+            min-width: max-content;
+          }
+          > span:last-child {
+            color: #333;
+            word-break: break-all;
+          }
+        }
+      }
+    }
+  }
+  .address-type-card {
+    position: absolute;
+    top: 30px;
+    right: 30px;
+    width: max-content;
+    min-width: 136px;
+    background: rgba(255, 255, 255, 0.95);
+    border-radius: 1px;
+    padding: 20px;
+    box-sizing: border-box;
+    .card-item {
+      display: flex;
+      align-items: center;
+      margin-right: 0;
+      & > span {
+        font-size: 14px;
+        font-weight: 400;
+      }
+    }
+  }
+}

+ 448 - 0
src/components/MarkerMap/index.tsx

@@ -0,0 +1,448 @@
+import {
+  defineComponent,
+  reactive,
+  PropType,
+  onMounted,
+  watchEffect,
+  watch,
+  computed,
+} from 'vue';
+import useMarkerStore, { MarkerType } from '@/store/useMarkerStore';
+import isString from 'lodash/isString';
+import MapView from '../MapView';
+import isEmpty from 'lodash/isEmpty';
+/** @ts-ignore */
+import icon_map_location from '@/assets/icons/home/icon_map_location@2x.png';
+/** @ts-ignore */
+import icon_map_yjcl from '@/assets/icons/home/icon_map_yjcl@2x.png';
+/** @ts-ignore */
+import icon_map_yjsj from '@/assets/icons/home/icon_map_yjsj@2x.png';
+/** @ts-ignore */
+import icon_map_yjdw from '@/assets/icons/home/icon_map_yjdw@2x.png';
+/** @ts-ignore */
+import icon_map_yjck from '@/assets/icons/home/icon_map_yjck@2x.png';
+/** @ts-ignore */
+import icon_map_spjk from '@/assets/icons/home/icon_map_spjk@2x.png';
+/** @ts-ignore */
+import icon_map_dcz from '@/assets/icons/home/icon_map_dcz@2x.png';
+/** @ts-ignore */
+import icon_map_dpf from '@/assets/icons/home/icon_map_dpf@2x.png';
+
+import './index.scss';
+import {
+  GET_INCIDENT_DIALOG_HTML,
+  GET_TEAM_DIALOG_HTML,
+  GET_VEHICLES_DIALOG_HTML,
+  GET_VIDEO_DIALOG_HTML,
+  GET_WAREHOUSE_DIALOG_HTML,
+  renderElement,
+} from './dialog';
+import { Object } from 'ol';
+import { useIncidentStore } from '@/store';
+import { useRoute, useRouter } from 'vue-router';
+import { ElMessage } from 'element-plus';
+
+const MARKER_MAP_TYPES = [
+  '预警事件',
+  '待派发事件',
+  '待处置事件',
+  '路况信息',
+  '视频监控',
+  '应急车辆',
+  '应急队伍',
+  '应急仓库',
+] as const;
+
+export type MarkerMapType = typeof MARKER_MAP_TYPES[number];
+
+interface State {
+  map: any;
+  types: MarkerMapType[];
+  trafficLayerIds: any[];
+  timer?: NodeJS.Timer | null;
+  trafficStatus: boolean;
+  markers: MarkerType[];
+  positions: string[];
+  hasTypes: string[];
+}
+
+interface ActionType {
+  type: MarkerMapType;
+  hasActioned: boolean;
+  action: Function;
+  remove: Function;
+}
+
+// 路况信息刷新间隔
+const REFESH_TRAFFIC_TIME = 60000;
+
+export default defineComponent({
+  name: 'MarkerMap',
+  props: {
+    adrsMapTypes: Array as PropType<string[]>,
+    incident: Object as PropType<MarkerType>,
+  },
+  setup(props, ctx) {
+    const state = reactive<State>({
+      map: null,
+      types: [],
+      trafficLayerIds: [],
+      timer: undefined,
+      trafficStatus: false,
+      markers: [],
+      positions: [],
+      hasTypes: [],
+    });
+
+    const router = useRouter();
+
+    const store = useMarkerStore();
+    const incidentStore = useIncidentStore();
+
+    const actionTypes = computed<ActionType[]>(() => [
+      {
+        type: '路况信息',
+        hasActioned: state.trafficStatus,
+        action: toggleControlTraffic,
+        remove: toggleControlTraffic,
+      },
+      {
+        type: '预警事件',
+        hasActioned: state.hasTypes.includes('预警事件'),
+        action: () =>
+          handleAddMarkers('预警事件', store.warningIncident, icon_map_yjsj),
+        remove: () => handleRemoveMarkers('预警事件', store.warningIncident),
+      },
+      {
+        type: '待派发事件',
+        hasActioned: state.hasTypes.includes('待派发事件'),
+        action: () =>
+          handleAddMarkers('待派发事件', store.pendingIncident, icon_map_dpf),
+        remove: () => handleRemoveMarkers('待派发事件', store.pendingIncident),
+      },
+      {
+        type: '待处置事件',
+        hasActioned: state.hasTypes.includes('待处置事件'),
+        action: () =>
+          handleAddMarkers(
+            '待处置事件',
+            store.pendingDisposalIncident,
+            icon_map_dcz,
+          ),
+        remove: () =>
+          handleRemoveMarkers('待处置事件', store.pendingDisposalIncident),
+      },
+      {
+        type: '视频监控',
+        hasActioned: state.hasTypes.includes('视频监控'),
+        action: () =>
+          handleAddMarkers('视频监控', store.videoSurveillance, icon_map_spjk),
+        remove: () => handleRemoveMarkers('视频监控', store.videoSurveillance),
+      },
+      {
+        type: '应急车辆',
+        hasActioned: state.hasTypes.includes('应急车辆'),
+        action: () =>
+          handleAddMarkers('应急车辆', store.emergencyVehicles, icon_map_yjcl),
+        remove: () => handleRemoveMarkers('应急车辆', store.emergencyVehicles),
+      },
+      {
+        type: '应急队伍',
+        hasActioned: state.hasTypes.includes('应急队伍'),
+        action: () =>
+          handleAddMarkers('应急队伍', store.emergencyTeam, icon_map_yjdw),
+        remove: () => handleRemoveMarkers('应急队伍', store.emergencyTeam),
+      },
+      {
+        type: '应急仓库',
+        hasActioned: state.hasTypes.includes('应急仓库'),
+        action: () =>
+          handleAddMarkers('应急仓库', store.emergencyWarehouse, icon_map_yjck),
+        remove: () => handleRemoveMarkers('应急仓库', store.emergencyWarehouse),
+      },
+    ]);
+
+    const getMarkerPopupHTML = (type: MarkerMapType, marker: MarkerType) => {
+      switch (type) {
+        case '待处置事件':
+        case '待派发事件':
+        case '预警事件':
+        default:
+          return GET_INCIDENT_DIALOG_HTML(marker, () =>
+            router.push(`/incident-management/status/${marker.status}`),
+          );
+        case '应急仓库':
+          return GET_WAREHOUSE_DIALOG_HTML(
+            {
+              name: marker.name,
+              address: marker.address,
+              manageUnit: marker.manageUnit,
+              contactName: marker.contactName,
+              contactPhone: marker.contactPhone,
+            },
+            () =>
+              router.push(
+                `/emergency-resources/resources/1/detail?id=${marker.id}&info=detail`,
+              ),
+          );
+        case '应急车辆':
+          return GET_VEHICLES_DIALOG_HTML({
+            name: marker.name,
+            manageUnit: marker.manageUnit,
+            carType: marker.carType,
+          });
+        case '应急队伍':
+          return GET_TEAM_DIALOG_HTML({
+            name: marker.name,
+            manageUnit: marker.manageUnit,
+            carryGoods: marker.carryGoods,
+            num: marker.num,
+            contactName: marker.contactName,
+            contactPhone: marker.contactPhone,
+          });
+
+        case '视频监控':
+          return GET_VIDEO_DIALOG_HTML({
+            name: marker.name,
+            addr: marker.addr,
+            link: '',
+          });
+      }
+    };
+    const updateTrafficSource = () => {
+      if (state.map.getSource('Traffic')) {
+        state.map.removeSource('Traffic');
+      }
+      state.map.addSource('Traffic', {
+        type: 'vector',
+        traffic: true,
+        tiles: [
+          'mineserver://data/dynamic-traffic/ertic?servicetype=0&z={z}&x={x}&y={y}',
+        ],
+      });
+    };
+    const updateTrafficLayerVisibility = (v: 'none' | 'visible') => {
+      state.trafficLayerIds.forEach(function (id) {
+        if (state.map?.getLayer(id)) {
+          state.map?.setLayoutProperty(id, 'visibility', v);
+        }
+      });
+    };
+    const toggleControlTraffic = () => {
+      if (state.trafficStatus) {
+        state.trafficStatus = false;
+        if (state.timer) {
+          clearInterval(state.timer);
+          state.timer = null;
+        }
+        updateTrafficLayerVisibility('none');
+      } else {
+        state.trafficStatus = true;
+        if (state.timer) {
+          clearInterval(state.timer);
+          state.timer = null;
+        }
+        updateTrafficSource();
+        state.timer = setInterval(function () {
+          updateTrafficSource();
+        }, REFESH_TRAFFIC_TIME);
+        updateTrafficLayerVisibility('visible');
+      }
+    };
+    const handleAddMarkers = (
+      type: MarkerMapType,
+      markers: State['markers'],
+      image: any,
+    ) => {
+      state.markers.push(
+        ...markers.map((i) => {
+          const popup = new window.minemap.Popup({
+            anchor: 'left',
+            closeOnClick: true,
+            closeButton: false,
+            offset: [10, 25],
+            maxWidth: 'max-content',
+            // autoPan: true,
+          }).setDOMContent(getMarkerPopupHTML(type, i));
+          return {
+            locations: i.locations,
+            popup,
+            marker:
+              i.locations &&
+              new window.minemap.Marker(renderElement(image), {
+                offset: [-25, -25],
+              })
+                .setLngLat({
+                  lng: i.locations?.split(',')[0],
+                  lat: i.locations?.split(',')[1],
+                })
+                .setPopup(popup)
+                .addTo(state.map),
+          };
+        }),
+      );
+      state.positions.push(...markers.map((i) => i.locations).filter(isString));
+
+      handleFitBounds();
+    };
+    const handleFitBounds = () => {
+      if (state.positions.length === 0) {
+        return;
+      }
+
+      const locations = state.positions.map((i) => i.split(',').map(Number));
+
+      const leftTop = locations.reduce((carry, next) => {
+        if (carry.length === 0) return next;
+        return [Math.min(carry[0], next[0]), Math.max(carry[1], next[1])];
+      }, []);
+
+      const rightBottom = locations.reduce((carry, next) => {
+        if (carry.length === 0) return next;
+        return [Math.max(carry[0], next[0]), Math.min(carry[1], next[1])];
+      }, []);
+
+      const leftTopBounds: number[][] = new window.minemap.LngLat(...leftTop)
+        .toBounds(10000)
+        .toArray();
+
+      const rightBottomBounds: number[][] = new window.minemap.LngLat(
+        ...rightBottom,
+      )
+        .toBounds(10000)
+        .toArray();
+
+      state.map.fitBounds([
+        rightBottomBounds.reduce((carry, next) => {
+          if (carry.length === 0) return next;
+          return [Math.max(carry[0], next[0]), Math.min(carry[1], next[1])];
+        }, []),
+        leftTopBounds.reduce((carry, next) => {
+          if (carry.length === 0) return next;
+          return [Math.min(carry[0], next[0]), Math.max(carry[1], next[1])];
+        }, []),
+      ]);
+    };
+    const handleRemoveMarkers = (
+      type: MarkerMapType,
+      markers: State['markers'],
+    ) => {
+      const locations = markers.map((i) => i.locations);
+      state.markers.forEach((m) => {
+        if (locations.includes(m.locations)) {
+          m.marker?.remove();
+          m.popup?.remove();
+          m.popup = null;
+          m.marker = null;
+        }
+      });
+      state.markers = state.markers.filter(
+        (m) => !locations.includes(m.locations),
+      );
+      state.positions = state.positions.filter((p) => !locations.includes(p));
+
+      handleFitBounds();
+    };
+    const handleSetDetailMarker = (marker: MarkerType) => {
+      const locations = marker.locations?.split(',').map(Number);
+
+      if (!locations) {
+        ElMessage.error({ message: '该点位无地址经纬度' });
+        return;
+      }
+      let type: MarkerMapType = '预警事件';
+      let icon = icon_map_yjsj;
+      switch (marker.status?.toString()) {
+        case '1':
+        default:
+          type = '预警事件' as const;
+          icon = icon_map_yjsj;
+          break;
+        case '2':
+          type = '待派发事件' as const;
+          icon = icon_map_dpf;
+          break;
+
+        case '3':
+          type = '待处置事件' as const;
+          icon = icon_map_dcz;
+          break;
+
+        case 'warehouse':
+          type = '应急仓库' as const;
+          icon = icon_map_yjck;
+          break;
+
+        case 'teams':
+          type = '应急队伍' as const;
+          icon = icon_map_yjdw;
+          break;
+        case 'vehicles':
+          type = '应急车辆' as const;
+          icon = icon_map_yjcl;
+          break;
+      }
+
+      handleAddMarkers(type, [marker], icon);
+      // handleAddMarkers('视频监控', videos, icon_map_spjk);
+      state.markers[0].marker.togglePopup();
+    };
+    onMounted(() => {
+      window.minemap.util.getJSON(
+        'https://minedata.cn/service/solu/style/id/16857',
+        function (error, data) {
+          data.layers.forEach(function (layer: any) {
+            //判断是否道路线图层
+            if (
+              layer.type == 'line' &&
+              layer.source == 'Traffic' &&
+              layer['source-layer'] == 'Trafficrtic'
+            ) {
+              state.trafficLayerIds.push(layer.id);
+            }
+          });
+        },
+      );
+
+      state.map.on('load', function () {
+        updateTrafficSource();
+        state.trafficStatus = false;
+        updateTrafficLayerVisibility('none');
+      });
+      store.getAllResources();
+      store.getAllIncidents();
+      if (!isEmpty(store.currentIncident)) {
+        handleSetDetailMarker(store.currentIncident);
+      }
+    });
+
+    watch(
+      () => state.types,
+      (next) => {
+        actionTypes.value.forEach((item) => {
+          if (next.includes(item.type) && !item.hasActioned) {
+            state.hasTypes.push(item.type);
+            item.action();
+          }
+          if (!next.includes(item.type) && item.hasActioned) {
+            state.hasTypes = state.hasTypes.filter((t) => t !== item.type);
+            item.remove();
+          }
+        });
+      },
+    );
+    return () => (
+      <div class="task-map-container">
+        <MapView v-model:map={state.map} />
+        <div class="address-type-card">
+          <el-checkbox-group v-model={state.types}>
+            {props.adrsMapTypes &&
+              props.adrsMapTypes.map((t) => (
+                <el-checkbox key={t} class="card-item" label={t} />
+              ))}
+          </el-checkbox-group>
+        </div>
+      </div>
+    );
+  },
+});

+ 83 - 0
src/components/MediaUpload/index.scss

@@ -0,0 +1,83 @@
+.upload-file-list-container {
+  margin-right: 4px;
+  flex-wrap: wrap;
+  display: flex;
+  .file-item {
+    width: 104px;
+    height: 104px;
+    margin-right: 4px;
+    margin-bottom: 4px;
+    border-radius: 2px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border: 1px dotted rgba(0, 0, 0, 0.15);
+    position: relative;
+    > video,
+    > img {
+      max-width: 100%;
+      max-height: 100%;
+      &:hover + .file-action {
+        opacity: 1;
+      }
+    }
+    .file-action {
+      width: 100%;
+      height: 100%;
+      background: rgba(0, 0, 0, 0.5);
+      position: absolute;
+      color: #fff;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 16px;
+      opacity: 0;
+      transition: opacity 0.4s;
+
+      > i {
+        padding: 0 8px;
+        cursor: pointer;
+        &:hover {
+          transition: color 0.4s;
+          color: #ccc;
+        }
+      }
+    }
+    .file-delete-btn {
+      position: absolute;
+      top: -5px;
+      right: -5px;
+      width: 18px;
+      height: 18px;
+      border-radius: 10px;
+      background-color: #ff0000;
+      font-size: 12px;
+      color: #fff;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      cursor: pointer;
+    }
+    &:hover {
+      .file-action {
+        opacity: 1;
+      }
+    }
+  }
+}
+.upload-btn {
+  width: 104px;
+  height: 104px;
+  background-color: rgba(0, 0, 0, 0.04);
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  border: 1px dotted rgba(0, 0, 0, 0.15);
+  border-radius: 2px;
+  > i {
+    font-size: 24px;
+    margin-top: 28px;
+    color: #003a8c;
+  }
+}

+ 145 - 0
src/components/MediaUpload/index.tsx

@@ -0,0 +1,145 @@
+import { defineComponent, reactive, computed, PropType } from 'vue';
+import { useCommonStore } from '@/store';
+import { UploadData } from '@/api/common';
+import './index.scss';
+import Preview from '../Preview';
+import { isImage, isVideo, sleep } from '@/utils';
+import { update } from 'lodash';
+import { BaseMediaUrl } from '@/constants/constants';
+import { Notify, Uploader } from 'vant';
+interface State {
+  dialog: boolean;
+  previewCurrentKey: number;
+  localFiles: UploadData[];
+  uploadFiles: UploadData[];
+  loading: boolean;
+}
+
+export default defineComponent({
+  props: {
+    media: String,
+    mediaType: {
+      type: String as PropType<'image' | 'video'>,
+      default: 'image',
+    },
+    accept: String,
+  },
+
+  setup(props, ctx) {
+    const commonStore = useCommonStore();
+
+    const state = reactive<State>({
+      dialog: false,
+      previewCurrentKey: 0,
+      localFiles: [],
+      uploadFiles:
+        props.media?.split(',').map((i) => ({ url: i, fileName: i })) ?? [],
+      loading: false,
+    });
+
+    const allFiles = computed(() =>
+      ([] as UploadData[])
+        .concat(
+          ...state.uploadFiles
+            ?.filter((media) =>
+              props.mediaType === 'image' ? isImage(media) : isVideo(media),
+            )
+            .concat(...state.localFiles),
+        )
+        .map((i) => ({ ...i, type: props.mediaType })),
+    );
+
+    const handlePreview = (idx: number) => {
+      state.previewCurrentKey = idx;
+      state.dialog = true;
+    };
+
+    const handleUpload = async (file: File) => {
+      if (file.name.length > 100) {
+        return Notify({ type: 'danger', message: '上传的文件名最长100个字符' });
+      }
+      state.localFiles.push({
+        url: window.URL.createObjectURL(file),
+        fileName: file.name,
+      });
+      state.loading = true;
+      console.log(window.URL.createObjectURL(file), '+++');
+
+      const data = await commonStore.upload(file);
+      await sleep(1000);
+      state.uploadFiles.push(data);
+      state.loading = false;
+      state.localFiles = [];
+      console.log(allFiles.value);
+
+      ctx.emit('update:media', allFiles.value.map((i) => i.url).join());
+    };
+
+    return () => (
+      <div class="upload-file-list-container">
+        {allFiles.value.map((file, idx) => {
+          return (
+            <div
+              key={`${file.fileName}_${idx}`}
+              class="file-item"
+              v-loading={file.url?.includes('blob:') && state.loading}>
+              {file.url && props.mediaType === 'image' ? (
+                <img
+                  src={
+                    file.url?.includes('blob:')
+                      ? file.url
+                      : BaseMediaUrl + file.url
+                  }
+                  alt={file.fileName}
+                />
+              ) : (
+                <video>
+                  <source
+                    src={
+                      file.url?.includes('blob:')
+                        ? file.url
+                        : BaseMediaUrl + file.url
+                    }
+                  />
+                </video>
+              )}
+              <div class="file-action">
+                <i class="el-icon-view" onClick={() => handlePreview(idx)} />
+              </div>
+              <div class="file-delete-btn">
+                {/* <el-popconfirm
+                  title="确定删除这个文件吗?"
+                  onConfirm={() => {}}
+                  v-slots={{
+                    reference: () => (
+                      <i class="el-icon-close" onClick={() => {}} />
+                    ),
+                  }}
+                /> */}
+                      <Icon name="cross" />
+
+              </div>
+            </div>
+          );
+        })}
+        <Uploader
+          multiple
+          action={''}
+          accept={props.accept}
+          beforeRead={(file: File) => {
+            handleUpload(file);
+            return false;
+          }}>
+          <div class="upload-btn">
+            {ctx.slots.default && ctx.slots.default()}
+          </div>
+        </el-upload>
+        <Preview
+          current={state.previewCurrentKey}
+          medias={allFiles.value}
+          v-model:dialog={state.dialog}
+        />
+      </div>
+    );
+  },
+});

+ 51 - 0
src/components/Preview/index.scss

@@ -0,0 +1,51 @@
+.preview-container {
+  position: relative;
+  height: 60vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  .preview-action-btn {
+    position: absolute;
+    z-index: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 50%;
+    opacity: 0.8;
+    cursor: pointer;
+    box-sizing: border-box;
+    user-select: none;
+    &.preview-action-left {
+      top: 50%;
+      transform: translateY(-50%);
+      left: 40px;
+      width: 44px;
+      height: 44px;
+      font-size: 24px;
+      color: #fff;
+      background-color: var(--el-text-color-regular);
+      border-color: #fff;
+    }
+    &.preview-action-right {
+      top: 50%;
+      transform: translateY(-50%);
+      right: 40px;
+      text-indent: 2px;
+      width: 44px;
+      height: 44px;
+      font-size: 24px;
+      color: #fff;
+      background-color: var(--el-text-color-regular);
+      border-color: #fff;
+    }
+  }
+
+  img,
+  video {
+    margin-left: 0px;
+    margin-top: 0px;
+    max-height: 100%;
+    max-width: 100%;
+    transition: transform 0.3s ease 0s;
+  }
+}

+ 75 - 0
src/components/Preview/index.tsx

@@ -0,0 +1,75 @@
+import { computed, defineComponent, PropType, ref, watch } from 'vue';
+import { UploadData } from '@/api/common';
+import './index.scss';
+import { BaseMediaUrl } from '@/constants/constants';
+
+export default defineComponent({
+  name: 'Preview',
+  props: {
+    dialog: Boolean,
+    current: {
+      type: Number,
+      default: 0,
+    },
+    medias: {
+      type: Array as PropType<(UploadData & { type: 'image' | 'video' })[]>,
+      default: [],
+    },
+  },
+  setup(props, ctx) {
+    const currentMediaKey = ref(props.current);
+
+    const currentMedia = computed(() => props.medias[currentMediaKey.value]);
+
+    watch(
+      () => props.current,
+      (next) => (currentMediaKey.value = next),
+    );
+
+    const handlePreviewLeftBtn = () => {
+      if (props.medias.length === 0) return;
+      if (currentMediaKey.value - 1 < 0) {
+        currentMediaKey.value = props.medias.length - 1;
+        return;
+      }
+      currentMediaKey.value -= 1;
+    };
+    const handlePreviewRightBtn = () => {
+      if (props.medias.length === 0) return;
+      if (currentMediaKey.value + 1 > props.medias.length - 1) {
+        currentMediaKey.value = 0;
+        return;
+      }
+      currentMediaKey.value += 1;
+    };
+
+    return () => (
+      <el-dialog
+        v-model={props.dialog}
+        destroyOnClose
+        onClose={() => ctx.emit('update:dialog', false)}>
+        <div class="preview-container">
+          {props.medias?.length > 1 && (
+            <>
+              <div
+                class="preview-action-btn preview-action-left"
+                onClick={handlePreviewLeftBtn}>
+                <i class="el-icon-arrow-left" />
+              </div>
+              <div
+                class="preview-action-btn preview-action-right"
+                onClick={handlePreviewRightBtn}>
+                <i class="el-icon-arrow-right" />
+              </div>
+            </>
+          )}
+          {currentMedia.value.type === 'image' ? (
+            <img src={BaseMediaUrl + currentMedia.value?.url} />
+          ) : (
+            <video controls src={BaseMediaUrl + currentMedia.value?.url} />
+          )}
+        </div>
+      </el-dialog>
+    );
+  },
+});

+ 3 - 0
src/components/QueryForm/index.scss

@@ -0,0 +1,3 @@
+.query-inline-form-container {
+    width: 100%;
+}

+ 118 - 0
src/components/QueryForm/index.tsx

@@ -0,0 +1,118 @@
+import { defineComponent, PropType, ref, watch } from 'vue';
+import getSlot from '@/utils/getSolt';
+import './index.scss';
+export interface Schema {
+  type: 'el-date-picker' | 'el-input' | 'el-select';
+  defaultValue?: unknown;
+  key: string;
+  options?: {
+    value: string | number | boolean | object;
+    label?: string | number;
+    disabled?: boolean;
+  }[];
+  placeholder?: string;
+  label?: string;
+}
+
+export interface QueryFormEvents {
+  onQuery: () => void;
+}
+
+export type FormModule<T extends object> = {
+  [key in keyof T]: T[keyof T];
+};
+
+export default defineComponent({
+  name: 'QueryForm',
+  props: {
+    schema: Array as PropType<Schema[]>,
+    onSubmit: Function,
+    onReset: Function,
+    form: { type: Object, required: true },
+    suffix: {
+      type: [Object, String] as PropType<JSX.Element | string>,
+      default: undefined,
+    },
+  },
+  setup(props, ctx) {
+    const suffix = getSlot({ props, ctx }, 'suffix');
+    const form = ref(props.form);
+
+    const handleSubmit = () => {
+      props.onSubmit && props.onSubmit(form.value);
+    };
+
+    if (!props.schema) {
+      return null;
+    }
+
+    watch(
+      () => props.form,
+      (next) => {
+        form.value = next;
+      },
+    );
+
+    return () => (
+      <div class="query-inline-form-container">
+        <el-form inline class="form-inline">
+          {props.schema &&
+            props.schema.map((item) => {
+              return (
+                <>
+                  {item.type === 'el-input' && (
+                    <el-form-item label={item.label}>
+                      <el-input
+                        size="small"
+                        v-model={form.value[item.key]}
+                        placeholder={item.placeholder || '请输入'}
+                        clearable
+                      />
+                    </el-form-item>
+                  )}
+                  {item.type === 'el-select' && (
+                    <el-form-item label={item.label}>
+                      <el-select
+                        clearable
+                        size="small"
+                        v-model={form.value[item.key]}
+                        placeholder={item.placeholder || '请选择'}>
+                        {item.options &&
+                          item.options.map((o) => (
+                            <el-option
+                              label={o.label}
+                              value={o.value}
+                              disabled={o.disabled}
+                            />
+                          ))}
+                      </el-select>
+                    </el-form-item>
+                  )}
+                  {item.type === 'el-date-picker' && (
+                    <el-form-item label={item.label}>
+                      <el-date-picker
+                        size="small"
+                        v-model={form.value[item.key]}
+                        style={{ width: '200px' }}
+                        type="daterange"
+                        start-placeholder="开始时间"
+                        end-placeholder="结束时间"
+                        format="YYYY-MM-DD"
+                        valueFormat="YYYY-MM-DD"
+                      />
+                    </el-form-item>
+                  )}
+                </>
+              );
+            })}
+          <el-form-item>
+            <el-button type="primary" size="small" onClick={handleSubmit}>
+              查询
+            </el-button>
+          </el-form-item>
+          <el-form-item>{suffix}</el-form-item>
+        </el-form>
+      </div>
+    );
+  },
+});

+ 198 - 0
src/components/QueryMap/index.tsx

@@ -0,0 +1,198 @@
+import { defineComponent, watch, reactive, PropType } from 'vue';
+import Map from '@/components/MapView';
+import { Notify, Button } from 'vant';
+/** @ts-ignore */
+import icon_map_location from '@/assets/icons/home/icon_map_location@2x.png';
+
+// const solution = 16857;
+const key = '77ef70465c2d4888b3a5132523494b94';
+
+export default defineComponent({
+  props: {
+    address: String,
+    latitude: String, // 纬度
+    longitude: String, // 经度
+    locations: String, // 经度,纬度
+    readonly: Boolean,
+    onRestPositon: Function,
+    onChoose: Function as PropType<(name?: string) => void>,
+  },
+  setup(props, ctx) {
+    const state = reactive({
+      map: null as any,
+      _marker: null as any,
+      loading: false,
+      postition: {
+        longitude: props.longitude,
+        latitude: props.latitude,
+        name: props.address,
+      },
+    });
+
+    watch(
+      () => props.address,
+      (next, pre) => {
+        if (!next) return;
+        if (next === pre) return;
+
+        if (!state.map) {
+          Notify({
+            type: 'danger',
+            message: '地图插件初始化异常, 请刷新页面 (Ctrl + R)',
+          });
+
+          return;
+        }
+        state.loading = true;
+
+        fetch(
+          `https://minedata.cn/service/lbs/search/v1/keywords?keywords=${next}&city=宿迁&citylimit=true&page_idx=1&page_size=10&key=${key}`,
+        )
+          .then((res) => res.json())
+          .then((data) => {
+            state.loading = false;
+
+            if (!data) {
+              Notify({
+                type: 'danger',
+                message: '宿迁市查询不到该地址, 请核对地址',
+              });
+              return;
+            }
+            // 经纬度坐标。经度在前,纬度在后,经度和纬度用“,”分割
+            const [longitude, latitude] = (
+              (data?.pois?.[0]?.location as string) ?? ''
+            ).split(',');
+
+            const name = data?.pois?.[0]?.name ?? '';
+
+            if (!longitude && !latitude) {
+              Notify({
+                type: 'danger',
+                message: '宿迁市查询不到该地址, 请核对地址',
+              });
+              return;
+            }
+
+            state.postition = {
+              longitude,
+              latitude,
+              name,
+            };
+
+            state.map.flyTo({
+              center: [Number(longitude), Number(latitude)],
+              zoom: 14,
+              bearing: 0,
+              pitch: 0,
+              duration: 2000,
+            });
+            if (state.map && state._marker) {
+              state._marker.remove();
+              state._marker = null;
+            }
+            if (state.map) {
+              var el = document.createElement('div');
+              el.id = 'marker';
+              el.style.backgroundImage = `url(${icon_map_location})`;
+              el.style.backgroundSize = 'cover';
+              el.style.width = '24px';
+              el.style.height = '24px';
+              el.style.borderRadius = '50%';
+
+              const popup = new window.minemap.Popup({
+                closeOnClick: false,
+                closeButton: false,
+                offset: [0, -15],
+              }).setText(name);
+
+              state._marker = new window.minemap.Marker(el, {
+                offset: [-12, -12],
+              })
+                .setLngLat([Number(longitude), Number(latitude)])
+                .setPopup(popup)
+                .addTo(state.map);
+            }
+            state._marker.togglePopup();
+          })
+          .catch((err) => {
+            console.log(err);
+            Notify({
+              type: 'danger',
+              message: '系统异常, 请稍后重试!',
+            });
+          });
+      },
+    );
+
+    const handleResetPosition = () => {
+      state._marker?.remove();
+      state._marker = null;
+
+      props.onRestPositon && props.onRestPositon();
+    };
+    const handleSavePosition = () => {
+      if (!state.postition.latitude || !state.postition.longitude) {
+        Notify({
+          type: 'danger',
+          message: '请输入地址!',
+        });
+        return;
+      }
+      Notify({
+        type: 'success',
+        message: '选择成功!',
+      });
+      ctx.emit('update:latitude', state.postition.latitude);
+      ctx.emit('update:longitude', state.postition.longitude);
+      props.onChoose && props.onChoose(state.postition.name);
+      ctx.emit(
+        'update:locations',
+        `${state.postition.longitude},${state.postition.latitude}`,
+      );
+    };
+
+    return () => (
+      <div
+        // v-loading={state.loading}
+        style={{
+          marginTop: '10px',
+          // marginRight: props.readonly ? '0' : '80px',
+          backgroundColor: '#fff',
+          padding: '10px 20px 10px',
+          position: 'relative',
+        }}>
+        <Map
+          v-model:map={state.map}
+          style={{
+            height: '240px',
+            borderRadius: '2px',
+            boxShadow: '0px 0px 0px 1px #d9d9d9;',
+          }}
+        />
+
+        {!props.readonly && (
+          <div
+            style={{
+              backgroundColor: '#fff',
+              padding: '10px 20px 10px',
+              marginLeft: '-20px',
+              // position: 'absolute',
+              // bottom: '0',
+              // right: '-80px',
+              // display: 'flex',
+              // flexDirection: 'column',
+            }}>
+            <Button size="small" onClick={handleResetPosition}>
+              重置
+            </Button>
+            <span style={{ padding: '8px ' }} />
+            <Button type="primary" size="small" onClick={handleSavePosition}>
+              确认
+            </Button>
+          </div>
+        )}
+      </div>
+    );
+  },
+});

+ 165 - 0
src/constants/constants.ts

@@ -0,0 +1,165 @@
+import {
+  IconArchive,
+  IconCar,
+  IconDepartment,
+  IconDuty,
+  IconEmergency,
+  IconHandle,
+  IconHighway,
+  IconHome,
+  IconIgnore,
+  IconIncident,
+  IconNavigation,
+  IconPlan,
+  IconRailway,
+  IconReport,
+  IconResource,
+  IconRoster,
+  IconSend,
+  IconStorehouse,
+  IconTeam,
+  IconTransportation,
+  IconWarning,
+} from '@/constants/icon';
+
+export const BaseMediaUrl = 'http://sqfile.xt.wenhq.top:8083';
+
+export const MENUS_INCIDENT = {
+  title: {
+    name: '事件状态',
+    path: '/incident-management/status' as const,
+  },
+  items: [
+    { name: '预警', path: '/incident-management/status/1', icon: IconWarning },
+    { name: '忽略', path: '/incident-management/status/2', icon: IconIgnore },
+    { name: '待派发', path: '/incident-management/status/3', icon: IconSend },
+    { name: '待处置', path: '/incident-management/status/4', icon: IconHandle },
+    {
+      name: '已归档',
+      path: '/incident-management/status/5',
+      icon: IconArchive,
+    },
+  ],
+};
+
+export const MENUS_EMERGENCY_PLAN = {
+  title: { name: '应急预案', path: null },
+  items: [
+    {
+      name: '归属部门',
+      path: '/emergency-plan/department/1',
+      icon: IconDepartment,
+    },
+    {
+      name: '应急安全处',
+      path: '/emergency-plan/department/2',
+      icon: IconEmergency,
+    },
+    {
+      name: '市运输服务中心',
+      path: '/emergency-plan/department/3',
+      icon: IconTransportation,
+    },
+    {
+      name: '市港航事业发展中心',
+      path: '/emergency-plan/department/4',
+      icon: IconNavigation,
+    },
+    {
+      name: '市铁路事业发展中心',
+      path: '/emergency-plan/department/5',
+      icon: IconRailway,
+    },
+    {
+      name: '市公路事业发展中心',
+      path: '/emergency-plan/department/6',
+      icon: IconHighway,
+    },
+  ],
+};
+
+export const MENUS_EMERGENCY_RESOURCES = {
+  title: { name: '应急资源', path: null },
+  items: [
+    {
+      name: '应急仓库',
+      path: '/emergency-resources/resources/1',
+      icon: IconStorehouse,
+    },
+    {
+      name: '应急队伍',
+      path: '/emergency-resources/resources/2',
+      icon: IconTeam,
+    },
+    {
+      name: '应急车辆',
+      path: '/emergency-resources/resources/3',
+      icon: IconCar,
+    },
+  ],
+};
+
+export const MENUS_DUTY = {
+  title: { name: '值班管理', path: null },
+  items: [
+    { name: '值班报告', path: '/duty-management/report', icon: IconReport },
+    { name: '值班排班', path: '/duty-management/roster', icon: IconRoster },
+  ],
+};
+
+export enum PATHS {
+  HOME = '/home',
+  INCIDENT_MANAGEMENT = '/incident-management',
+  EMERGENCY_PLAN = '/emergency-plan',
+  EMERGENCY_RESOURCES = '/emergency-resources',
+  DUTY_MANAGEMENT = '/duty-management',
+}
+
+export const PATH_LINK_NAV_BAR_MENU = {
+  'incident-management': 'MENUS_INCIDENT',
+  'emergency-plan': 'MENUS_EMERGENCY_PLAN',
+  'emergency-resources': 'MENUS_EMERGENCY_RESOURCES',
+  'duty-management': 'MENUS_DUTY',
+};
+
+export const PATH_LINK_NAV_BAR_MENU_DICT = [
+  {
+    path: PATHS.INCIDENT_MANAGEMENT,
+    subpath: 'status',
+    dict: 'zhdd_incident_status' as const,
+  },
+  {
+    path: PATHS.EMERGENCY_PLAN,
+    subpath: 'department',
+    dict: 'zhdd_org_upload' as const,
+  },
+  {
+    path: PATHS.EMERGENCY_RESOURCES,
+    subpath: 'resources',
+    dict: 'zhdd_resource' as const,
+  },
+];
+
+export const NAV_BAR_MENUS = [
+  { name: '首页', path: PATHS.HOME, icon: IconHome },
+  {
+    name: '事件管理',
+    path: PATHS.INCIDENT_MANAGEMENT,
+    icon: IconIncident,
+  },
+  {
+    name: '应急预案',
+    path: PATHS.EMERGENCY_PLAN,
+    icon: IconPlan,
+  },
+  {
+    name: '应急资源',
+    path: PATHS.EMERGENCY_RESOURCES,
+    icon: IconResource,
+  },
+  {
+    name: '值班管理',
+    path: PATHS.DUTY_MANAGEMENT,
+    icon: IconDuty,
+  },
+];

+ 46 - 0
src/constants/icon.ts

@@ -0,0 +1,46 @@
+/** @ts-ignore */
+export * as IconWarning from '@/assets/icons/incident/warning.svg';
+/** @ts-ignore */
+export * as IconIgnore from '@/assets/icons/incident/ignore.svg';
+/** @ts-ignore */
+export * as IconSend from '@/assets/icons/incident/send.svg';
+/** @ts-ignore */
+export * as IconHandle from '@/assets/icons/incident/handle.svg';
+/** @ts-ignore */
+export * as IconArchive from '@/assets/icons/incident/archive.svg';
+
+/** @ts-ignore */
+export * as IconDepartment from '@/assets/icons/emergecyplan/department.svg';
+/** @ts-ignore */
+export * as IconEmergency from '@/assets/icons/emergecyplan/emergency.svg';
+/** @ts-ignore */
+export * as IconTransportation from '@/assets/icons/emergecyplan/transportation.svg';
+/** @ts-ignore */
+export * as IconNavigation from '@/assets/icons/emergecyplan/navigation.svg';
+/** @ts-ignore */
+export * as IconRailway from '@/assets/icons/emergecyplan/railway.svg';
+/** @ts-ignore */
+export * as IconHighway from '@/assets/icons/emergecyplan/highway.svg';
+
+/** @ts-ignore */
+export * as IconStorehouse from '@/assets/icons/emergencyresources/storehouse.svg';
+/** @ts-ignore */
+export * as IconTeam from '@/assets/icons/emergencyresources/team.svg';
+/** @ts-ignore */
+export * as IconCar from '@/assets/icons/emergencyresources/car.svg';
+
+/** @ts-ignore */
+export * as IconReport from '@/assets/icons/duty/report.svg';
+/** @ts-ignore */
+export * as IconRoster from '@/assets/icons/duty/roster.svg';
+
+/** @ts-ignore */
+export * as IconHome from '@/assets/icons/home/home.svg';
+/** @ts-ignore */
+export * as IconIncident from '@/assets/icons/incident/event.svg';
+/** @ts-ignore */
+export * as IconPlan from '@/assets/icons/emergecyplan/plan.svg';
+/** @ts-ignore */
+export * as IconResource from '@/assets/icons/emergencyresources/resource.svg';
+/** @ts-ignore */
+export * as IconDuty from '@/assets/icons/duty/duty.svg';

+ 14 - 0
src/constants/incident.ts

@@ -0,0 +1,14 @@
+export const INCIDENT_TYPE = [
+  '自然灾害',
+  '事故灾难',
+  '公共卫生事件',
+  '社会安全事件',
+];
+
+export enum INCIDENT_ACTION_TYPE {
+  WARNING = 1, // 预警
+  PENDING = 2, // 待派发
+  PENDING_DISPOSAL = 3, //待处置
+  ARCHIVED = 4, //已归档
+  IGNORED = 5, //已忽略
+}

+ 102 - 0
src/hooks/useMenus.ts

@@ -0,0 +1,102 @@
+import {
+  IconWarning,
+  IconIgnore,
+  IconSend,
+  IconHandle,
+  IconArchive,
+  IconDepartment,
+  IconEmergency,
+  IconHighway,
+  IconNavigation,
+  IconRailway,
+  IconTransportation,
+  IconCar,
+  IconReport,
+  IconRoster,
+  IconStorehouse,
+  IconTeam,
+} from '@/constants/icon';
+import { useCommonStore } from '@/store';
+import { computed } from 'vue-demi';
+
+const INCIDENT_ICON = {
+  1: IconWarning,
+  2: IconSend,
+  3: IconHandle,
+  4: IconArchive,
+  5: IconIgnore,
+};
+const PLAN_ICON = {
+  1: IconDepartment,
+  2: IconEmergency,
+  3: IconTransportation,
+  4: IconNavigation,
+  5: IconRailway,
+  6: IconHighway,
+};
+
+const RESOURCE_ICON = {
+  1: IconStorehouse,
+  2: IconTeam,
+  3: IconCar,
+};
+
+const DUTY_ICON = {
+  1: IconReport,
+  2: IconRoster,
+};
+
+export default () => {
+  const store = useCommonStore();
+
+  const MENUS_INCIDENT = computed(() => {
+    const items = store.globalDict['zhdd_incident_status']?.map(
+      (item, idx) => ({
+        name: item.dictLabel,
+        path: `/incident-management/status/${item.dictValue}`,
+        icon: INCIDENT_ICON[
+          item.dictValue as unknown as keyof typeof INCIDENT_ICON
+        ],
+      }),
+    );
+    return {
+      title: {
+        name: '事件状态',
+        path: '/incident-management/status' as const,
+      },
+      items,
+    };
+  });
+
+  const MENUS_EMERGENCY_PLAN = computed(() => {
+    const items = store.globalDict['zhdd_org_upload']?.map((item) => ({
+      name: item.dictLabel,
+      path: `/emergency-plan/department/${item.dictValue}`,
+      icon: PLAN_ICON[item.dictValue as unknown as keyof typeof PLAN_ICON],
+    }));
+    return {
+      title: { name: '应急预案', path: null },
+      items,
+    };
+  });
+
+  const MENUS_EMERGENCY_RESOURCES = computed(() => {
+    const items = store.globalDict['zhdd_resource']?.map((item) => ({
+      name: item.dictLabel,
+      path: `/emergency-resources/resources/${item.dictValue}`,
+      icon: RESOURCE_ICON[
+        item.dictValue as unknown as keyof typeof RESOURCE_ICON
+      ],
+    }));
+    return {
+      title: { name: '应急资源', path: null },
+      items,
+    };
+  });
+
+  return {
+    MENUS_INCIDENT,
+    MENUS_EMERGENCY_PLAN,
+    MENUS_EMERGENCY_RESOURCES,
+  };
+};

+ 30 - 0
src/layout/BaseLayout/NavBar/index.scss

@@ -0,0 +1,30 @@
+.nav-bar-menu {
+  height: 60px;
+  background: transparent;
+  border-bottom: 0 none;
+  > .el-sub-menu,
+  .el-sub-menu__title,
+  > .el-menu-item {
+    background: transparent;
+    // height: 48px;
+    // line-height: 48px;
+    color: #fff !important;
+    // border-bottom: 0 none;
+    border-bottom: 3px solid transparent;
+    > svg {
+      width: 16px;
+      height: 16px;
+      margin-right: 10px;
+    }
+    &.is-active {
+      color: rgba(255, 255, 255, 0.6) !important;
+      border-bottom: 3px solid rgba(255, 255, 255, 0.6);
+    }
+    &:not(.is-disabled):focus,
+    &:not(.is-disabled):hover {
+      outline: 0;
+      color: rgba(255, 255, 255, 0.6) !important;
+      background-color: transparent !important;
+    }
+  }
+}

+ 46 - 0
src/layout/BaseLayout/NavBar/index.tsx

@@ -0,0 +1,46 @@
+import { defineComponent, ref, watchEffect } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import { NAV_BAR_MENUS } from '@/constants/constants';
+import './index.scss';
+
+export default defineComponent({
+  setup() {
+    const router = useRouter();
+    const route = useRoute();
+
+    const currentRouteInMenusPath = NAV_BAR_MENUS.map((m) => m.path).find((p) =>
+      route.path.includes(p),
+    );
+    const defaultActive = ref(currentRouteInMenusPath);
+
+    watchEffect(() => {
+      defaultActive.value = NAV_BAR_MENUS.map((m) => m.path).find((p) =>
+        route.path.includes(p),
+      );
+    });
+
+    return () => (
+      <el-menu
+        class="nav-bar-menu"
+        mode="horizontal"
+        ellipsis={false}
+        defaultActive={defaultActive.value}>
+        {NAV_BAR_MENUS.map((m) => (
+          <el-menuItem
+            index={m.path}
+            onClick={() => {
+              defaultActive.value = m.path;
+              router.push(m.path);
+            }}>
+            <m.icon
+              width="16px"
+              height="16px"
+              style={{ marginRight: '10px' }}
+            />
+            {m.name}
+          </el-menuItem>
+        ))}
+      </el-menu>
+    );
+  },
+});

+ 55 - 0
src/layout/BaseLayout/index.scss

@@ -0,0 +1,55 @@
+.base-layout-container {
+  header {
+    width: 100%;
+    height: 60px;
+    background: #003a8c;
+    box-shadow: 0px 1px 4px 0px rgba(0, 21, 41, 0.12);
+    box-sizing: border-box;
+    display: flex;
+    justify-content: space-between;
+    padding: 0 14px 0 30px;
+    align-items: center;
+    position: sticky;
+    top: 0;
+    z-index: 2001;
+    background-image: url('../../assets/bg_jgxt.jpg');
+    background-repeat: no-repeat;
+
+    .title-content {
+      display: flex;
+      align-items: center;
+      flex: 1;
+    }
+
+    .title {
+      font-size: 24px;
+      font-family: PingFangSC, PingFangSC-Semibold;
+      font-weight: 600;
+      text-align: left;
+      color: #ffffff;
+      line-height: 60px;
+      margin-right: 10px;
+    }
+    .action {
+      display: flex;
+      align-items: center;
+      color: #ffffff;
+      > span {
+        padding-right: 14px;
+      }
+      .el-icon {
+        padding: 10px;
+        cursor: pointer;
+        transition: color 0.3s;
+        &:hover {
+          color: #f0f0f0;
+        }
+      }
+    }
+  }
+  main {
+    display: flex;
+    flex-direction: column;
+    min-height: calc(100vh - 60px);
+  }
+}

+ 18 - 0
src/layout/BaseLayout/index.tsx

@@ -0,0 +1,18 @@
+import { defineComponent } from 'vue';
+import { RouterView } from 'vue-router';
+/** @ts-ignore */
+import SwitchButton from '@element-plus/icons/lib/SwitchButton.js';
+import NavBar from './NavBar';
+import './index.scss';
+
+export default defineComponent({
+  setup() {
+    return () => (
+      <section class="base-layout-container">
+        <main>
+          <RouterView />
+        </main>
+      </section>
+    );
+  },
+});

+ 53 - 0
src/layout/ManagementLayout/Menus/index.scss

@@ -0,0 +1,53 @@
+.sider-menus-container {
+  width: 200px;
+  background: #001529;
+  box-shadow: 2px 0px 6px 0px rgba(0, 21, 41, 0.35);
+
+  .sider-menu-title {
+    background: #051c41;
+    box-shadow: 2px 0px 6px 0px rgba(0, 21, 41, 0.35);
+    height: 48px;
+    font-size: 16px;
+    font-family: PingFangSC, PingFangSC-Regular;
+    font-weight: 400;
+    text-align: left;
+    color: #ffffff;
+    line-height: 48px;
+    padding: 0 20px;
+    cursor: pointer;
+  }
+  .el-skeleton.is-animated .el-skeleton__item {
+    background: linear-gradient(90deg, #0c2a42 25%, #183348 37%, #0c2a42 25%);
+    background-size: 400% 100%;
+  }
+  .sider-menu-item {
+    height: 40px;
+    font-size: 14px;
+    font-family: PingFangSC, PingFangSC-Regular;
+    font-weight: 400;
+    text-align: left;
+    color: rgba(255, 255, 255, 0.65);
+    line-height: 40px;
+    padding: 0 20px;
+    cursor: pointer;
+    transition: color 0.3s ease;
+
+    > svg,
+    > span {
+      width: 14px;
+      height: 14px;
+      margin-right: 10px;
+      text-align: center;
+      display: inline-block;
+    }
+    &:focus,
+    &:hover {
+      outline: 0 none;
+      color: rgba(255, 255, 255, 1);
+    }
+    &:active,
+    &.selected {
+      color: #195ac6;
+    }
+  }
+}

+ 158 - 0
src/layout/ManagementLayout/Menus/index.tsx

@@ -0,0 +1,158 @@
+import { defineComponent, ref, watch, PropType, computed } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import clsx from 'clsx';
+import { MENUS_DUTY, PATH_LINK_NAV_BAR_MENU } from '@/constants/constants';
+import './index.scss';
+import useMenus from '@/hooks/useMenus';
+import { useCommonStore } from '@/store';
+
+const MENUS_TYPE = {
+  MENUS_DUTY,
+};
+
+interface MenusProps {
+  type: 'MENUS_INCIDENT';
+  onChange: (status: string) => void;
+}
+
+export default defineComponent({
+  name: 'Menus',
+  props: {
+    type: {
+      type: String as PropType<keyof typeof PATH_LINK_NAV_BAR_MENU>,
+      required: true,
+    },
+    onChange: Function,
+  },
+  setup(props) {
+    const router = useRouter();
+    const route = useRoute();
+
+    const currentPath = computed(() => route.path);
+
+    const store = useCommonStore();
+
+    const { MENUS_INCIDENT, MENUS_EMERGENCY_PLAN, MENUS_EMERGENCY_RESOURCES } =
+      useMenus();
+
+    const handlerEnter = (item: { name: string; path: string }) => {
+      router.push(item.path);
+      props.onChange && props.onChange(item);
+    };
+
+    const menu = computed<{
+      title: { name: string; path?: string | null };
+      items?: {
+        name: string;
+        path: string;
+        icon?: string;
+      }[];
+    }>(() => {
+      switch (props.type) {
+        case 'duty-management':
+          return MENUS_DUTY;
+        default:
+        case 'incident-management':
+          return MENUS_INCIDENT.value;
+        case 'emergency-resources':
+          return MENUS_EMERGENCY_RESOURCES.value;
+        case 'emergency-plan':
+          return MENUS_EMERGENCY_PLAN.value;
+      }
+    });
+
+    return () => (
+      <div class="sider-menus-container">
+        <el-skeleton
+          rows={5}
+          animated
+          throttle={500}
+          loading={store.loading}
+          v-slots={{
+            template: () => (
+              <>
+                <div style={{ padding: '20px' }}>
+                  <el-skeleton-item
+                    variant="text"
+                    style={{
+                      height: '26px',
+                      width: '60%',
+                    }}
+                  />
+                  <div
+                    style={{
+                      display: 'flex',
+                      alignItems: 'center',
+                      justifyItems: 'space-between',
+                      margin: '9px 0',
+                      height: '22px',
+                    }}>
+                    <el-skeleton-item
+                      variant="text"
+                      style={{ width: '16px', marginRight: '10px' }}
+                    />
+
+                    <el-skeleton-item variant="text" style={{}} />
+                  </div>
+                  <div
+                    style={{
+                      display: 'flex',
+                      alignItems: 'center',
+                      justifyItems: 'space-between',
+                      margin: '9px 0',
+                      height: '22px',
+                    }}>
+                    <el-skeleton-item
+                      variant="text"
+                      style={{ width: '16px', marginRight: '10px' }}
+                    />
+
+                    <el-skeleton-item variant="text" style={{ width: '60%' }} />
+                  </div>
+                  <div
+                    style={{
+                      display: 'flex',
+                      alignItems: 'center',
+                      justifyItems: 'space-between',
+                      margin: '9px 0',
+                      height: '22px',
+                    }}>
+                    <el-skeleton-item
+                      variant="text"
+                      style={{ width: '16px', marginRight: '10px' }}
+                    />
+
+                    <el-skeleton-item variant="text" style={{}} />
+                  </div>
+                </div>
+              </>
+            ),
+          }}>
+          <div
+            class="sider-menu-title"
+            onClick={() => {
+              if (menu.value.title.path) {
+                router.push(menu.value.title.path);
+              }
+            }}>
+            {menu.value.title.name}
+          </div>
+
+          {menu.value.items?.map((item) => (
+            <div
+              class={clsx('sider-menu-item', {
+                selected: currentPath.value.includes(item.path),
+              })}
+              role="menuitem"
+              tabindex="0"
+              onKeypress={() => handlerEnter(item)}
+              onClick={() => handlerEnter(item)}>
+              {item.icon ? <item.icon /> : <span>•</span>}
+              {item.name}
+            </div>
+          ))}
+        </el-skeleton>
+      </div>
+    );
+  },
+});

+ 17 - 0
src/layout/ManagementLayout/index.scss

@@ -0,0 +1,17 @@
+.management-container {
+  min-height: calc(100vh - 60px);
+  background: #f0f2f5;
+  .sider-menus-container {
+    top: 60px;
+    position: fixed;
+    height: calc(100vh - 60px);
+  }
+  .main-content {
+    // background: #ffffff;
+    border-radius: 2px;
+    margin: 20px;
+    margin-left: 220px;
+    // padding: 24px 20px;
+    min-height: calc(100vh - 60px - 40px);
+  }
+}

+ 68 - 0
src/layout/ManagementLayout/index.tsx

@@ -0,0 +1,68 @@
+import { defineComponent, onMounted, ref, watch, watchEffect } from 'vue';
+import { useRouter, RouterView, useRoute } from 'vue-router';
+import Menus from '@/layout/ManagementLayout/Menus';
+import {
+  PATH_LINK_NAV_BAR_MENU,
+  PATH_LINK_NAV_BAR_MENU_DICT,
+} from '@/constants/constants';
+import './index.scss';
+import { useCommonStore } from '@/store';
+
+export default defineComponent({
+  name: 'ManagementLayout',
+
+  setup() {
+    const router = useRouter();
+    const route = useRoute();
+    const currentRoute = router.currentRoute.value.path;
+    const paths = Object.keys(PATH_LINK_NAV_BAR_MENU);
+
+    const currentKey = ref(paths.find((p) => currentRoute.includes(p)));
+
+    const store = useCommonStore();
+
+    const handleRedirect = async (path: string) => {
+      let item = '1';
+
+      for (const menu of PATH_LINK_NAV_BAR_MENU_DICT) {
+        if (path === menu.path) {
+          await store.getGlobalDict(menu.dict);
+          item = store.globalDict[menu.dict]?.[0]?.dictValue ?? '';
+          item && router.replace(`${menu.path}/${menu.subpath}/${item}`);
+        }
+      }
+    };
+
+    onMounted(() => {
+      PATH_LINK_NAV_BAR_MENU_DICT.forEach((i) => {
+        store.getGlobalDict(i.dict);
+      });
+      handleRedirect(route.path);
+    });
+
+    watchEffect(() => {
+      handleRedirect(route.path);
+    });
+
+    watch(
+      () => route.path,
+      (next) => {
+        currentKey.value = paths.find((p) => next.includes(p));
+      },
+    );
+
+    return () => (
+      <div class="management-container">
+        <Menus
+          type={
+            (currentKey.value as keyof typeof PATH_LINK_NAV_BAR_MENU) ??
+            'incident-management'
+          }
+        />
+        <div class="main-content">
+          <RouterView />
+        </div>
+      </div>
+    );
+  },
+});

+ 9 - 0
src/main.ts

@@ -0,0 +1,9 @@
+import { createApp } from "vue";
+import ElementPlus from "element-plus";
+import { createPinia } from "pinia";
+import App from "./App";
+import router from "./router";
+import "nprogress/nprogress.css";
+// import './styles/element/index.scss';
+import "./styles/global.scss";
+createApp(App).use(createPinia()).use(router).mount("#app");

+ 77 - 0
src/router/index.ts

@@ -0,0 +1,77 @@
+import useMainStore from '@/store/useMainStore';
+import {
+  createRouter,
+  createWebHashHistory,
+  RouteLocation,
+  RouteRecordRaw,
+} from 'vue-router';
+import NProgress from 'nprogress';
+
+const BaseLayout = () =>
+  import(/* webpackChunkName: "BaseLayout" */ '@/layout/BaseLayout');
+const ManagementLayout = () =>
+  import(
+    /* webpackChunkName: "ManagementLayout" */ '@/layout/ManagementLayout'
+  );
+
+const routes: RouteRecordRaw[] = [
+  {
+    path: '/',
+    name: 'incident-management',
+    component: BaseLayout,
+    redirect: '/home',
+    children: [
+      {
+        path: '/home',
+        component: () =>
+          import(/* webpackChunkName: "home.page" */ '@/views/HomePage'),
+        name: '应急处置',
+        meta: { title: '应急处置', iconName: 'TableOutlined' },
+      },
+      {
+        path: '/status/:status/detail',
+        component: () => import('@/views/IncidentManagementDetail'),
+      },
+      {
+        path: '/status/:status/report',
+        component: () => import('@/views/IncidentManagementReport'),
+      },
+    ],
+  },
+  {
+    path: '/404',
+    name: 'Global404',
+    meta: { title: '页面不见啦.' },
+    component: () => import(/* webpackChunkName: "404" */ '@/views/About'),
+  },
+  {
+    path: '/:pathMatch(.*)*',
+    name: 'All',
+    redirect: (to: RouteLocation) => {
+      return { path: '/404', query: { from: to.path } };
+    },
+  },
+];
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes,
+  scrollBehavior(to, from, savedPosition) {
+    // 始终滚动到顶部
+    return { top: 0 };
+  },
+});
+
+router.beforeEach(() => {
+  NProgress.start();
+  const main = useMainStore();
+  main.clearReqToken();
+});
+router.afterEach((to) => {
+  NProgress.done();
+  document!.title = to.meta.title
+    ? to?.meta?.title + ' - 交通运输应急指挥系统'
+    : '交通运输应急指挥系统';
+});
+
+export default router;

+ 9 - 0
src/store/index.ts

@@ -0,0 +1,9 @@
+import useMainStore from './useMainStore';
+
+export { default as useMainStore } from './useMainStore';
+export { default as useResourceStore } from './useResourceStore';
+export { default as useUserStore } from './useUserStore';
+export { default as useIncidentStore } from './useIncidentStore';
+export { default as usePlanStore } from './usePlanStore';
+export { default as useCommonStore } from './useCommonStore';
+export { default as useDutyStore } from './useDutyStore';

Some files were not shown because too many files changed in this diff