浏览代码

通讯录、协同标绘

Hwf 8 月之前
父节点
当前提交
643e73538d

+ 9 - 0
src/api/globalMap/onlinePlotting.ts

@@ -40,3 +40,12 @@ export const createPattern = (data) => {
     data: data
   });
 };
+
+// 开启协同
+export const startCollaboration = (data) => {
+  return request({
+    url: '/api/pattern/ws/add_user',
+    method: 'post',
+    data: data
+  });
+};

+ 388 - 0
src/components/Contact/index.vue

@@ -0,0 +1,388 @@
+<template>
+  <Dialog custom-show type="lg" title="通讯录" @close="handleClose" @confirm="handleConfirm">
+    <div class="content">
+      <div class="left-content">
+        <el-input v-model="queryParams1.deptName" class="custom-input" placeholder="组织架构搜索">
+          <template #prefix>
+            <el-icon class="el-input__icon"><search /></el-icon>
+          </template>
+        </el-input>
+        <div class="tree-container">
+          <div class="tree-box">
+            <el-tree :data="treeData" :props="{ value: 'deptId', label: 'deptName', children: 'children' }" accordion @node-click="handleNodeClick" />
+          </div>
+        </div>
+      </div>
+      <div class="middle-content">
+        <div class="user-box">
+          <div class="user-table">
+            <div class="tr">
+              <div class="td2">
+                <div :class="getCheckedClass()" @click="handleChecked"></div>
+              </div>
+              <div class="td">姓名</div>
+              <div class="td3">职务</div>
+            </div>
+            <div class="table-content">
+              <div v-for="(item, index) in userList" :key="index" class="tr2">
+                <div class="td2">
+                  <div :class="item.checked ? 'common-checked-active' : 'common-checked'" @click="handleChecked2(item)"></div>
+                </div>
+                <div class="td">{{ item.nickName }}</div>
+                <div class="td3">
+                  {{ item.duty }}
+                  <div class="phone-icon"></div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="select-box2">
+        <div class="select-header">
+          <div class="left-item">
+            <div>已选择:</div>
+            <div class="text">{{ selectList.length }}</div>
+            <div>人</div>
+          </div>
+          <div class="clear-btn" @click="clearSelect">清空</div>
+        </div>
+        <div class="select-content">
+          <div v-for="(item, index) in selectList" :key="index" class="box-item">
+            <div class="box-left">
+              <div class="line">
+                <div class="text1">{{ item.nickName }}</div>
+                <div class="text2">{{ item.duty }}</div>
+              </div>
+              <div class="line" style="margin-top: 20px">
+                <div class="text2">{{ item.dept }}</div>
+              </div>
+            </div>
+            <div class="close-btn" @click="deleteItem(item)"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </Dialog>
+</template>
+
+<script lang="ts" setup name="Contact">
+import { Search } from '@element-plus/icons-vue';
+import { listDept } from '@/api/system/dept';
+import { DeptVO } from '@/api/system/dept/types';
+import { listUser } from '@/api/system/user';
+defineProps({
+  modelValue: Boolean
+});
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const emits = defineEmits(['update:modelValue', 'close', 'confirm']);
+let queryParams1 = ref({
+  pageNum: 1,
+  pageSize: 10,
+  deptName: undefined,
+  deptCategory: undefined,
+  status: undefined
+});
+let queryParams2 = ref({
+  pageNum: 1,
+  pageSize: 100,
+  deptId: ''
+});
+const treeData = ref([]);
+const userList = ref([]);
+const selectList = ref([]);
+const getCheckedClass = () => {
+  let res = 'common-checked';
+  const len = userList.value.length;
+  const len2 = selectList.value.length;
+  if (len2 > 0 && len2 === len) {
+    res = 'common-checked-active';
+  } else if (len2 > 0) {
+    res = 'common-checked-half';
+  }
+  return res;
+};
+const handleNodeClick = (data) => {
+  queryParams2.value.deptId = data.deptId;
+  getPerson();
+};
+const getPerson = () => {
+  listUser(queryParams2.value).then((res) => {
+    const data = res.rows;
+    data.forEach((item) => {
+      for (let i = 0; i < selectList.value.length; i++) {
+        if (selectList.value[i].nickName === item.nickName) {
+          item.checked = true;
+          break;
+        }
+      }
+    });
+    userList.value = data;
+  });
+};
+// 全选、全取消
+const handleChecked = () => {
+  const checkedClass = getCheckedClass();
+  let flag = true;
+  if (checkedClass === 'common-checked-active') {
+    flag = false;
+  }
+  userList.value.forEach((item) => {
+    item.checked = flag;
+  });
+  computedSelectList();
+};
+// 单个选中、取消选中
+const handleChecked2 = (item) => {
+  item.checked = !item.checked;
+  computedSelectList();
+};
+const computedSelectList = () => {
+  userList.value.forEach((item2) => {
+    let index = 0;
+    for (let i = 0; i < selectList.value.length; i++) {
+      if (!!item2.checked && selectList.value[i].userId !== item2.userId) {
+        index++;
+      } else if (!item2.checked && selectList.value[i].userId === item2.userId) {
+        selectList.value.splice(i, 1);
+        break;
+      }
+    }
+    if (!!item2.checked && selectList.value.length === index) {
+      selectList.value.push(item2);
+    }
+  });
+};
+// 清空
+const clearSelect = () => {
+  userList.value.forEach((item) => {
+    if (item.checked) {
+      item.checked = false;
+    }
+  });
+  selectList.value = [];
+};
+// 清空指定项
+const deleteItem = (item) => {
+  for (let i = 0; i < userList.value.length; i++) {
+    if (item.userId === userList.value[i].userId) {
+      userList.value[i].checked = false;
+      break;
+    }
+  }
+  computedSelectList();
+};
+
+// 弹窗关闭后
+const handleClose = () => {
+  emits('update:modelValue', false);
+  emits('close');
+};
+
+//
+const handleConfirm = () => {
+  emits('confirm', selectList.value);
+  emits('update:modelValue', false);
+};
+
+const initData = () => {
+  listDept(queryParams1.value).then((res: any) => {
+    const data = proxy?.handleTree<DeptVO>(res.data, 'deptId');
+    treeData.value = data;
+  });
+};
+onMounted(() => {
+  initData();
+});
+</script>
+
+<style lang="scss" scoped>
+.content {
+  display: flex;
+  margin-top: 12px;
+  .left-content {
+    width: 910px;
+    padding-right: 10px;
+    border-right: 1px solid #2187ff;
+  }
+  .middle-content {
+    width: 910px;
+    padding: 0 10px;
+    border-right: 1px solid #2187ff;
+    .search-box {
+      display: flex;
+      .select-box {
+        flex-shrink: 0;
+        width: 176px !important;
+        height: 56px;
+        line-height: 56px;
+        margin: 0 10px;
+        color: #83a3be;
+        font-size: 14px;
+      }
+    }
+    .user-box {
+      width: 100%;
+      height: 100%;
+      .user-table {
+        padding: 15px 0;
+        display: flex;
+        flex-direction: column;
+        font-size: 16px;
+        color: #fbffff;
+        .tr {
+          background-color: #102e76;
+        }
+        .tr,
+        .tr2 {
+          display: flex;
+          padding: 6px 0;
+          .td {
+            flex: 1;
+            display: flex;
+            align-items: center;
+          }
+          .td2 {
+            width: 30px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+          }
+          .td3 {
+            flex: 2;
+            display: flex;
+            align-items: center;
+          }
+        }
+        .table-content {
+          height: 858px;
+          overflow-y: auto;
+        }
+        .tr2 {
+          margin-top: 10px;
+          background-color: #122868;
+        }
+        .phone-icon {
+          width: 31px;
+          height: 31.5px;
+          background: url('@/assets/images/emergencyCommandMap/communication/phone.png');
+          background-size: 100% 100%;
+          cursor: pointer;
+        }
+      }
+    }
+  }
+
+  .custom-select-popper {
+    .el-scrollbar {
+      .el-select-dropdown__item {
+        color: #b1cae0;
+        font-size: 14px;
+        height: 56px;
+        line-height: 56px;
+      }
+    }
+  }
+  .input {
+    background: transparent;
+    color: #83a3be;
+    font-size: 14px;
+    outline: none;
+    appearance: none;
+    height: 100%;
+    border: none;
+    &::placeholder {
+      color: #83a3be;
+    }
+  }
+  .tree-container {
+    display: flex;
+    .tree-box {
+      width: 100%;
+      height: 920px;
+      overflow-y: auto;
+      padding: 15px 8px;
+      :deep(.el-tree) {
+        height: 100%;
+        background-color: transparent;
+        color: #fbffff;
+        font-size: 16px;
+        .el-tree-node__content {
+          height: auto;
+          padding-top: 10px;
+          padding-bottom: 10px;
+          white-space: normal;
+          word-break: break-all;
+        }
+        .el-tree-node__expand-icon {
+          color: #297cfc;
+        }
+        .el-tree-node:focus > .el-tree-node__content,
+        .el-tree-node__content:hover {
+          background-color: transparent !important;
+        }
+      }
+    }
+  }
+  .select-box2 {
+    margin-left: 30px;
+    width: 910px;
+    height: 100%;
+    .select-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      color: #fbffff;
+      font-size: 14px;
+      border-bottom: 1px solid #247dff;
+      padding: 20px;
+      .left-item {
+        display: flex;
+        align-items: center;
+        .text {
+          margin: 0 10px;
+          color: #00e8ff;
+          font-family: 'BEBAS-1';
+        }
+      }
+      .clear-btn {
+        color: #00e8ff;
+        cursor: pointer;
+      }
+    }
+    .select-content {
+      height: 858px;
+      overflow-y: auto;
+      .box-item {
+        border-bottom: 1px solid #247dff;
+        padding: 7px;
+        position: relative;
+        display: flex;
+        .box-left {
+          flex: 1;
+          margin-right: 5px;
+        }
+        .line {
+          color: #fff;
+          font-size: 16px;
+          display: flex;
+          .text1 {
+            margin-right: 35px;
+          }
+          .text2 {
+            color: #a7ccdf;
+          }
+        }
+        .close-btn {
+          flex-shrink: 0;
+          cursor: pointer;
+          width: 20px;
+          height: 20px;
+          background: url('@/assets/images/emergencyCommandMap/communication/close.png') no-repeat;
+          background-size: 100% 100%;
+        }
+      }
+    }
+  }
+}
+</style>

+ 1 - 1
src/layout/index.vue

@@ -28,7 +28,7 @@ import SideBar from './components/Sidebar/index.vue';
 import { AppMain, Navbar, Settings, TagsView } from './components';
 import useAppStore from '@/store/modules/app';
 import useSettingsStore from '@/store/modules/settings';
-import { initWebSocket } from '@/utils/websocket';
+import { initWebSocket } from '@/utils/websocket2';
 
 const settingsStore = useSettingsStore();
 const theme = computed(() => settingsStore.theme);

+ 27 - 0
src/store/modules/websocketStore.ts

@@ -0,0 +1,27 @@
+import { sendSock, createWebSocket, closeSock } from '@/utils/websocket';
+
+const websocketStore = defineStore('websocket', {
+  state: () => ({
+    webSocketList: {
+      bizType: null,
+      content: '',
+      id: '',
+      remindLevel: 0,
+      senderId: null,
+      type: 1,
+      userId: null
+    }
+  }),
+  actions: {
+    init() {
+      console.log('websocket init');
+      createWebSocket(this.handleData);
+    },
+    handleData(res) {
+      console.log('websocket handleData', res);
+      Object.assign(this.webSocketList, res);
+    }
+  }
+});
+
+export default websocketStore;

+ 1 - 0
src/types/components.d.ts

@@ -15,6 +15,7 @@ declare module 'vue' {
     Chart: typeof import('./../components/Chart/index.vue')['default']
     ChunkUpload: typeof import('./../components/ChunkUpload/index.vue')['default']
     CompanyMap: typeof import('./../components/Map/company-map.vue')['default']
+    Contact: typeof import('./../components/Contact/index.vue')['default']
     ContactSelect: typeof import('./../components/ContactSelect/index.vue')['default']
     Dialog: typeof import('./../components/Dialog/index.vue')['default']
     DictTag: typeof import('./../components/DictTag/index.vue')['default']

+ 123 - 119
src/utils/websocket.ts

@@ -1,139 +1,143 @@
-/**
- * @module initWebSocket 初始化
- * @module websocketonopen 连接成功
- * @module websocketonerror 连接失败
- * @module websocketclose 断开连接
- * @module resetHeart 重置心跳
- * @module sendSocketHeart 心跳发送
- * @module reconnect 重连
- * @module sendMsg 发送数据
- * @module websocketonmessage 接收数据
- * @module test 测试收到消息传递
- * @description socket 通信
- * @param {any} url socket地址
- * @param {any} websocket websocket 实例
- * @param {any} heartTime 心跳定时器实例
- * @param {number} socketHeart 心跳次数
- * @param {number} HeartTimeOut 心跳超时时间
- * @param {number} socketError 错误次数
- */
-
+import useUserStore from '@/store/modules/user';
 import { getToken } from '@/utils/auth';
-import { ElNotification } from 'element-plus';
-import useNoticeStore from '@/store/modules/notice';
-
-let socketUrl: any = ''; // socket地址
-let websocket: any = null; // websocket 实例
-let heartTime: any = null; // 心跳定时器实例
-let socketHeart = 0 as number; // 心跳次数
-const HeartTimeOut = 10000; // 心跳超时时间 10000 = 10s
-let socketError = 0 as number; // 错误次数
-
-// 初始化socket
-export const initWebSocket = (url: any) => {
-  if (import.meta.env.VITE_APP_WEBSOCKET === 'false') {
+
+let webSock = null;
+let connectState = false; // 标记 WebSocket 连接状态
+let rec; //断线重连后,延迟5秒重新创建WebSocket连接  rec用来存储延迟请求的代码
+let closeFlag = false; // 是否关闭socket
+let global_callback = null;
+
+const port = '9988'; // webSocket连接端口
+const useUser = useUserStore();
+const wsUrl = `${import.meta.env.VITE_APP_BASE_WEBSOCKET}/api/pattern/1/ws`;
+function createWebSocket(callback) {
+  if (webSock != null) {
     return;
   }
-  socketUrl = url;
-  // 初始化 websocket
-  websocket = new WebSocket(url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID);
-  websocketonopen();
-  websocketonmessage();
-  websocketonerror();
-  websocketclose();
-  sendSocketHeart();
-  return websocket;
-};
+  webSock = new WebSocket(wsUrl);
 
-// socket 连接成功
-export const websocketonopen = () => {
-  websocket.onopen = function () {
-    console.log('连接 websocket 成功');
-    resetHeart();
+  global_callback = callback;
+
+  webSock.onopen = function () {
+    // 连接打开时的处理
+    websocketOpen();
   };
-};
 
-// socket 连接失败
-export const websocketonerror = () => {
-  websocket.onerror = function (e: any) {
-    console.log('连接 websocket 失败', e);
+  webSock.onmessage = function (e) {
+    // 接收消息时的处理
+    websocketOnMessage(e);
+  };
+  webSock.onclose = function (e) {
+    // 连接关闭时的处理
+    websocketClose(e);
   };
-};
 
-// socket 断开链接
-export const websocketclose = () => {
-  websocket.onclose = function (e: any) {
-    console.log('断开连接', e);
+  // 连接发生错误的回调方法
+  webSock.onerror = function () {
+    websocketError();
   };
-};
+}
+
+//心跳设置
+const heartCheck = {
+  timeout: 30000, //每段时间发送一次心跳包 这里设置为20s
+  timeoutObj: null, //延时发送消息对象(启动心跳新建这个对象,收到消息后重置对象)
 
-// socket 重置心跳
-export const resetHeart = () => {
-  socketHeart = 0;
-  socketError = 0;
-  clearInterval(heartTime);
-  sendSocketHeart();
+  start: function () {
+    this.timeoutObj = setInterval(function () {
+      if (connectState) webSock.send(JSON.stringify({ type: 0 }));
+    }, this.timeout);
+  },
+
+  reset: function () {
+    clearTimeout(this.timeoutObj);
+    this.start();
+  }
 };
 
-// socket心跳发送
-export const sendSocketHeart = () => {
-  heartTime = setInterval(() => {
-    // 如果连接正常则发送心跳
-    if (websocket.readyState == 1) {
-      // if (socketHeart <= 30) {
-      websocket.send(
-        JSON.stringify({
-          type: 'ping'
-        })
-      );
-      socketHeart = socketHeart + 1;
-    } else {
-      // 重连
-      reconnect();
+//定义重连函数
+const reConnect = () => {
+  console.log('尝试重新连接');
+  if (connectState) return; //如果已经连上就不在重连了
+  rec && clearTimeout(rec);
+  rec = setTimeout(function () {
+    // 延迟5秒重连  避免过多次过频繁请求重连
+    if (!connectState) {
+      createWebSocket();
     }
-  }, HeartTimeOut);
+  }, 5000);
 };
 
-// socket重连
-export const reconnect = () => {
-  if (socketError <= 2) {
-    clearInterval(heartTime);
-    initWebSocket(socketUrl);
-    socketError = socketError + 1;
-    // eslint-disable-next-line prettier/prettier
-    console.log('socket重连', socketError);
+// 实际调用的方法
+function sendSock(agentData) {
+  if (webSock.readyState === webSock.OPEN) {
+    // 若是ws开启状态
+    webSock.send(agentData);
+  } else if (webSock.readyState === webSock.CONNECTING) {
+    // 若是 正在开启状态,则等待1s后重新调用
+    setTimeout(function () {
+      sendSock(agentData);
+    }, 1000);
   } else {
-    // eslint-disable-next-line prettier/prettier
-    console.log('重试次数已用完');
-    clearInterval(heartTime);
+    // 若未开启 ,则等待1s后重新调用
+    setTimeout(function () {
+      sendSock(agentData);
+    }, 1000);
   }
-};
+}
 
-// socket 发送数据
-export const sendMsg = (data: any) => {
-  websocket.send(data);
-};
+function websocketClose() {
+  connectState = false;
+  webSock = null;
+  global_callback = null;
+  closeFlag = false;
+  heartCheck.timeoutObj && clearTimeout(heartCheck.timeoutObj);
+  // 清除重连定时器
+  rec && clearTimeout(rec);
+}
 
-// socket 接收数据
-export const websocketonmessage = () => {
-  websocket.onmessage = function (e: any) {
-    if (e.data.indexOf('heartbeat') > 0) {
-      resetHeart();
-    }
-    if (e.data.indexOf('ping') > 0) {
-      return;
-    }
-    useNoticeStore().addNotice({
-      message: e.data,
-      read: false,
-      time: new Date().toLocaleString()
-    });
-    ElNotification({
-      title: '消息',
-      message: e.data,
-      type: 'success',
-      duration: 3000
-    });
-    return e.data;
-  };
+// 数据接收
+function websocketOnMessage(msg) {
+  if (!msg || !msg.data) {
+    // 可能得情况 - 心跳机制、无关信息接收
+    console.log('收到数据:空消息');
+    return;
+  }
+  console.log(msg);
+  // 收到信息为Blob类型时
+  const result = JSON.parse(msg.data);
+  if (result.type === 0 || result.type === '0') {
+    //自己的业务
+  } else {
+    global_callback(result);
+  }
+}
+
+function closeSock({ activeClose = false }) {
+  closeFlag = activeClose;
+  // 清除心跳定时器
+  heartCheck.timeoutObj && clearTimeout(heartCheck.timeoutObj);
+  // 清除重连定时器
+  rec && clearTimeout(rec);
+  if (closeFlag) {
+    // 关闭socket
+    webSock.close();
+  }
+  // 初始化相关变量
+  webSock = null;
+  connectState = false;
+}
+
+const websocketError = () => {
+  closeSock({ activeClose: true });
+  // 执行重连
+  reConnect();
 };
+
+function websocketOpen() {
+  webSock.send('Authorization: Bearer ' + getToken());
+  connectState = true;
+  heartCheck.start(); //发送心跳 看个人项目需求
+}
+
+export { sendSock, createWebSocket, closeSock };

+ 139 - 0
src/utils/websocket2.ts

@@ -0,0 +1,139 @@
+/**
+ * @module initWebSocket 初始化
+ * @module websocketonopen 连接成功
+ * @module websocketonerror 连接失败
+ * @module websocketclose 断开连接
+ * @module resetHeart 重置心跳
+ * @module sendSocketHeart 心跳发送
+ * @module reconnect 重连
+ * @module sendMsg 发送数据
+ * @module websocketonmessage 接收数据
+ * @module test 测试收到消息传递
+ * @description socket 通信
+ * @param {any} url socket地址
+ * @param {any} websocket2 websocket2 实例
+ * @param {any} heartTime 心跳定时器实例
+ * @param {number} socketHeart 心跳次数
+ * @param {number} HeartTimeOut 心跳超时时间
+ * @param {number} socketError 错误次数
+ */
+
+import { getToken } from '@/utils/auth';
+import { ElNotification } from 'element-plus';
+import useNoticeStore from '@/store/modules/notice';
+
+let socketUrl: any = ''; // socket地址
+let websocket2: any = null; // websocket2 实例
+let heartTime: any = null; // 心跳定时器实例
+let socketHeart = 0 as number; // 心跳次数
+const HeartTimeOut = 10000; // 心跳超时时间 10000 = 10s
+let socketError = 0 as number; // 错误次数
+
+// 初始化socket
+export const initWebSocket = (url: any) => {
+  if (import.meta.env.VITE_APP_WEBSOCKET === 'false') {
+    return;
+  }
+  socketUrl = url;
+  // 初始化 websocket2
+  websocket2 = new WebSocket(url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID);
+  websocketonopen();
+  websocketonmessage();
+  websocketonerror();
+  websocketclose();
+  sendSocketHeart();
+  return websocket2;
+};
+
+// socket 连接成功
+export const websocketonopen = () => {
+  websocket2.onopen = function () {
+    console.log('连接 websocket2 成功');
+    resetHeart();
+  };
+};
+
+// socket 连接失败
+export const websocketonerror = () => {
+  websocket2.onerror = function (e: any) {
+    console.log('连接 websocket2 失败', e);
+  };
+};
+
+// socket 断开链接
+export const websocketclose = () => {
+  websocket2.onclose = function (e: any) {
+    console.log('断开连接', e);
+  };
+};
+
+// socket 重置心跳
+export const resetHeart = () => {
+  socketHeart = 0;
+  socketError = 0;
+  clearInterval(heartTime);
+  sendSocketHeart();
+};
+
+// socket心跳发送
+export const sendSocketHeart = () => {
+  heartTime = setInterval(() => {
+    // 如果连接正常则发送心跳
+    if (websocket2.readyState == 1) {
+      // if (socketHeart <= 30) {
+      websocket2.send(
+        JSON.stringify({
+          type: 'ping'
+        })
+      );
+      socketHeart = socketHeart + 1;
+    } else {
+      // 重连
+      reconnect();
+    }
+  }, HeartTimeOut);
+};
+
+// socket重连
+export const reconnect = () => {
+  if (socketError <= 2) {
+    clearInterval(heartTime);
+    initWebSocket(socketUrl);
+    socketError = socketError + 1;
+    // eslint-disable-next-line prettier/prettier
+    console.log('socket重连', socketError);
+  } else {
+    // eslint-disable-next-line prettier/prettier
+    console.log('重试次数已用完');
+    clearInterval(heartTime);
+  }
+};
+
+// socket 发送数据
+export const sendMsg = (data: any) => {
+  websocket2.send(data);
+};
+
+// socket 接收数据
+export const websocketonmessage = () => {
+  websocket2.onmessage = function (e: any) {
+    if (e.data.indexOf('heartbeat') > 0) {
+      resetHeart();
+    }
+    if (e.data.indexOf('ping') > 0) {
+      return;
+    }
+    useNoticeStore().addNotice({
+      message: e.data,
+      read: false,
+      time: new Date().toLocaleString()
+    });
+    ElNotification({
+      title: '消息',
+      message: e.data,
+      type: 'success',
+      duration: 3000
+    });
+    return e.data;
+  };
+};

+ 0 - 1
src/views/globalMap/LeftMenu.vue

@@ -58,7 +58,6 @@
                 :key="index"
                 class="list-item2"
                 style="margin-top: 5px"
-                @click="selectSearchMarker(item)"
               >
                 <div class="text1" :title="item.name" @click="handleShowDialog(item)">{{ item.name }}</div>
                 <div class="tags">

+ 75 - 5
src/views/globalMap/RightMenu/OnlinePlotting/index.vue

@@ -7,11 +7,16 @@
           {{ item.name }}
         </div>
       </div>
-      <div class="btn1" @click="handleScreenshot">
-        <div class="icon1"></div>
-        当前地图截图导出
+      <div class="btn-box">
+        <div v-show="!collaboration" class="btn2" @click="handleShare('1')">协同标绘</div>
+        <div v-show="collaboration" class="btn2" @click="handleCloseCollaboration">关闭协同</div>
+        <div v-show="collaboration" class="btn2" style="margin-left: 20px">保存</div>
       </div>
     </div>
+    <div class="btn1" @click="handleScreenshot">
+      <div class="icon1"></div>
+      当前地图截图导出
+    </div>
     <div v-if="menuActive1 === 0" class="content">
       <div class="box1">
         <div class="box-item">
@@ -130,23 +135,28 @@
   <div v-show="tipTitle !== ''" class="tipTitle">{{ tipTitle }}</div>
   <!--保存修改弹窗-->
   <EditDialog v-if="showEdit" v-model="showEdit" :edit-data="editData" @submit="handleSubmit" />
+  <Contact v-if="shareState.showShare" v-model="shareState.showShare" @close="handleCloseShare" @confirm="handleShareConfirm" />
 </template>
 
 <script lang="ts" setup name="OnlinePlotting">
 import { nanoid } from 'nanoid';
 import { deepClone } from '@/utils';
 import { useHistory } from '@/hooks/useHistory';
-import { deletePatternById, getPatternInfo, getPatternList } from '@/api/globalMap/onlinePlotting';
+import { deletePatternById, getPatternInfo, getPatternList, startCollaboration } from '@/api/globalMap/onlinePlotting';
 import TextEdit from '@/views/globalMap/RightMenu/OnlinePlotting/TextEdit.vue';
 import EditDialog from '@/views/globalMap/RightMenu/OnlinePlotting/EditDialog.vue';
 import { Search } from '@element-plus/icons-vue';
 import html2canvas from 'html2canvas';
+import websocketStore from '@/store/modules/websocketStore';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const userWebsocket = websocketStore();
 const getDrawTool = inject('getDrawTool');
 const getMap = inject('getMap');
+const getMapUtils = inject('getMapUtils');
 const containerScale = inject('containerScale');
 const { currentState, commit, undo, history, future } = useHistory();
+const emits = defineEmits(['getCollaborationData']);
 const getImageUrl = (name) => {
   return new URL(`../../../../assets/images/map/rightMenu/onlinePlotting/icon/${name}.png`, import.meta.url).href;
 };
@@ -321,6 +331,15 @@ const lineWidthOptions = reactive([
 ]);
 let showTextEdit = ref();
 let lnglat = ref([]);
+// 协同
+let collaboration = ref(false);
+// 分享
+let shareState = reactive({
+  type: '',
+  showShare: false,
+  id: ''
+});
+let shareId = ref('');
 const overlays = [];
 const overlaysData = [];
 watch(
@@ -645,6 +664,37 @@ const handleEdit = (id) => {
   });
   showEdit.value = true;
 };
+const handleShare = (type, id?: string) => {
+  if (type === '1') {
+    // 创建协同
+    collaboration.value = true;
+  }
+  shareState.type = type;
+  shareState.id = id;
+  shareState.showShare = true;
+};
+const handleCloseShare = () => {
+  shareState.type = '';
+  shareState.id = '';
+};
+const handleCloseCollaboration = () => {
+  collaboration.value = false;
+};
+const handleShareConfirm = (data) => {
+  if (shareState.type === '1') {
+    // 协同标绘
+    startCollaboration();
+    userWebsocket.init();
+  } else {
+    // 分享
+  }
+  shareState.type = '';
+  shareState.id = '';
+};
+watch(userWebsocket.webSocketList, (newVal) => {
+  console.log('监听数据变化');
+  console.log(newVal);
+});
 const handleShowDialog = () => {
   editData.value = {
     id: '',
@@ -759,8 +809,24 @@ onMounted(() => {
     margin-right: 6px;
   }
 }
+.btn2 {
+  width: 104px;
+  height: 29px;
+  background: url('@/assets/images/map/rightMenu/btn.png') no-repeat;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+  font-size: 16px;
+  color: #edfaff;
+  .icon1 {
+    width: 42px;
+    height: 42px;
+    background: url('@/assets/images/map/rightMenu/onlinePlotting/screenshot.png') no-repeat;
+    margin-right: 12px;
+  }
+}
 .box1 {
-  margin-top: 20px;
   display: flex;
   justify-content: space-between;
   align-items: center;
@@ -1083,4 +1149,8 @@ onMounted(() => {
     }
   }
 }
+.btn-box {
+  display: flex;
+  align-items: center;
+}
 </style>