Quellcode durchsuchen

reactor:关于坐席监控大屏新增监听等功能;

zhangchong vor 9 Monaten
Ursprung
Commit
ceee0ab0e2

+ 2 - 2
.env.development

@@ -1,9 +1,9 @@
 # 本地环境
 VITE_MODE_NAME=development
 # socket API
-VITE_API_SOCKET_URL=http://110.188.24.28:50300/hubs/hotline
+VITE_API_SOCKET_URL=http://110.188.24.28:50100/hubs/hotline
 # 基础请求地址
-VITE_API_URL=http://110.188.24.28:50300
+VITE_API_URL=http://110.188.24.28:50100
 # 防止部署多套系统到同一域名不同目录时,变量共用的问题 设置不同的前缀
 VITE_STORAGE_NAME=dev
 # 当前地州市

+ 1 - 1
index.html

@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="en" class="dark">
+<html lang="en">
   <head>
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />

+ 2 - 0
src/App.vue

@@ -16,6 +16,8 @@ import { RouterView, useRoute } from "vue-router";
 import { getImageUrl, useTitle } from "@/utils/tools";
 import { useFavicon } from "@vueuse/core";
 import { getCurrentCityConfig } from "@/utils/appConfig";
+import { useDark } from "@vueuse/core";
+useDark();
 //  signalR 初始化signalr
 signalR.init();
 signalR.joinGroup("BigScreen-DataShow"); //index

+ 100 - 88
src/api/home.ts

@@ -2,16 +2,16 @@
  * @Author: zc
  * @description 查询数据
  */
-import request from '@/utils/request';
+import request from "@/utils/request";
 
 /**
  * @description 查询数据
  */
 export const getData = () => {
-    return request({
-        url: `/api/v1/Bs/datashow`,
-        method: 'get'
-    });
+  return request({
+    url: `/api/v1/Bs/datashow`,
+    method: "get",
+  });
 };
 /**
  * @description 分机列表
@@ -19,11 +19,11 @@ export const getData = () => {
  * @return {*}
  */
 export const extensionPaged = (params?: object) => {
-    return request({
-        url: `/api/v1/IPPbx/query-telstate`,
-        method: 'get',
-        params,
-    });
+  return request({
+    url: `/api/v1/IPPbx/query-telstate`,
+    method: "get",
+    params,
+  });
 };
 /**
  * @description 坐席数据查询
@@ -31,12 +31,12 @@ export const extensionPaged = (params?: object) => {
  * @return {*}
  */
 export const agentPaged = (params?: object) => {
-    return request({
-        url: `/api/v1/Seat/base_data`,
-        method: 'get',
-        params,
-    });
-}
+  return request({
+    url: `/api/v1/Seat/base_data`,
+    method: "get",
+    params,
+  });
+};
 
 /**
  * @description 工单统计数据
@@ -44,141 +44,153 @@ export const agentPaged = (params?: object) => {
  * @return {*}
  */
 export const workOrder = (params?: object) => {
-    return request({
-        url: `/api/v1/DataScreen/order-statistics`,
-        method: 'get',
-        params,
-    });
-}
+  return request({
+    url: `/api/v1/DataScreen/order-statistics`,
+    method: "get",
+    params,
+  });
+};
 /**
  * @description 知识库统计
  * @param {object} params
  * @return {*}
  */
 export const knowledge = (params?: object) => {
-    return request({
-        url: `/api/v1/DataScreen/knowledge-statistics`,
-        method: 'get',
-        params,
-    });
-}
+  return request({
+    url: `/api/v1/DataScreen/knowledge-statistics`,
+    method: "get",
+    params,
+  });
+};
 /**
  * @description 受理类型办件分析
  * @param {object} params
  * @return {*}
  */
 export const acceptType = (params?: object) => {
-    return request({
-        url: `/api/v1/DataScreen/ordertype-statistics`,
-        method: 'get',
-        params,
-    });
-}
+  return request({
+    url: `/api/v1/DataScreen/ordertype-statistics`,
+    method: "get",
+    params,
+  });
+};
 /**
  * @description 热点预警
  * @param {object} params
  * @return {*}
  */
 export const hotSpot = (params?: object) => {
-    return request({
-        url: `/api/v1/DataScreen/earlywarning-statistics`,
-        method: 'get',
-        params,
-    });
-}
+  return request({
+    url: `/api/v1/DataScreen/earlywarning-statistics`,
+    method: "get",
+    params,
+  });
+};
 /**
  * @description 工单当日统计及环比
  * @param {object} params
  * @return {*}
  */
 export const orderDay = (params?: object) => {
-    return request({
-        url: `/api/v1/DataScreen/ordercount-statistics`,
-        method: 'get',
-        params,
-    });
-}
+  return request({
+    url: `/api/v1/DataScreen/ordercount-statistics`,
+    method: "get",
+    params,
+  });
+};
 /**
  * @description 区域受理排行
  * @param {object} params
  * @return {*}
  */
 export const areaAccept = (params?: object) => {
-    return request({
-        url: `/api/v1/DataScreen/orderarea-accept-statistics`,
-        method: 'get',
-        params,
-    });
-}
+  return request({
+    url: `/api/v1/DataScreen/orderarea-accept-statistics`,
+    method: "get",
+    params,
+  });
+};
 /**
  * @description 区域明细数据
  * @param {object} params
  * @return {*}
  */
 export const areaDetail = (params?: object) => {
-    return request({
-        url: `/api/v1/DataScreen/orderareaaccept-query`,
-        method: 'get',
-        params,
-    });
-}
+  return request({
+    url: `/api/v1/DataScreen/orderareaaccept-query`,
+    method: "get",
+    params,
+  });
+};
 /**
  * @description 办理中工单阅览
  * @param {object} params
  * @return {*}
  */
 export const orderView = (params?: object) => {
-    return request({
-        url: `/api/v1/DataScreen/order-handling-query`,
-        method: 'get',
-        params,
-    });
-}
+  return request({
+    url: `/api/v1/DataScreen/order-handling-query`,
+    method: "get",
+    params,
+  });
+};
 /**
  * @description 近30天高频事项预警
  * @param {object} params
  * @return {*}
  */
 export const highFrequency = (params?: object) => {
-    return request({
-        url: `/api/v1/DataScreen/highmatter-warning`,
-        method: 'get',
-        params,
-    });
-}
+  return request({
+    url: `/api/v1/DataScreen/highmatter-warning`,
+    method: "get",
+    params,
+  });
+};
 /**
  * @description 部门满意度排行
  * @param {object} params
  * @return {*}
  */
 export const departmentSatisfaction = (params?: object) => {
-    return request({
-        url: `/api/v1/DataScreen/ordervisit-orgsatisfaction-rank`,
-        method: 'get',
-        params,
-    });
-}
+  return request({
+    url: `/api/v1/DataScreen/ordervisit-orgsatisfaction-rank`,
+    method: "get",
+    params,
+  });
+};
 /**
  * @description 占比分析
  * @param {object} params
  * @return {*}
  */
 export const proportionAnalysis = (params?: object) => {
-    return request({
-        url: `/api/v1/DataScreen/order-source-accepttype-statistics`,
-        method: 'get',
-        params,
-    });
-}
+  return request({
+    url: `/api/v1/DataScreen/order-source-accepttype-statistics`,
+    method: "get",
+    params,
+  });
+};
 /**
  * @description 获取区域信息
  * @param {object} params
  * @return {*}
  */
 export const getArea = (params?: object) => {
-    return request({
-        url: `/api/v1/DataScreen/get_system_area`,
-        method: 'get',
-        params,
-    });
-}
+  return request({
+    url: `/api/v1/DataScreen/get_system_area`,
+    method: "get",
+    params,
+  });
+};
+/**
+ * @description 根据分机号查询当前分机通话信息
+ * @param {string} params
+ * @return {*}
+ */
+export const getExtensionStatus = (params: object) => {
+  return request({
+    url: `/api/v1/IPPbx/query-telstatebyno`,
+    method: "get",
+    params,
+  });
+};

+ 20 - 0
src/utils/appConfig.ts

@@ -1,3 +1,8 @@
+/** @description 获取当前的环境变量名称 是否是正式环境
+ * */
+export const isProduction = () => {
+  return ["yibin", "zigong"].includes(import.meta.env.VITE_MODE_NAME);
+};
 /**
  * @description 获取当前市州配置
  * @return
@@ -18,6 +23,12 @@ export const getCurrentCityConfig = () => {
         recordNumber: "蜀ICP备19035032号-36", // 城市备案号
         locationCenter: [104.643, 28.751694], // 城市地图中心点
         favicon: "yibin/favicon.ico", // 浏览器图标
+        callCenterSocketUrl: isProduction()
+          ? "ws://218.6.151.146:50104/ola_socket"
+          : " ws://222.213.23.229:29003/ola_socket", // 呼叫中心socket地址
+        telNo: "1009",
+        telPwd: "!@#123Qw",
+        telGroup: "10020",
       };
     case "zigong":
       return {
@@ -27,6 +38,9 @@ export const getCurrentCityConfig = () => {
         recordNumber: "蜀ICP备2024053169号-1",
         locationCenter: [104.778001, 29.339399],
         favicon: "zigong/favicon.ico",
+        callCenterSocketUrl: isProduction()
+          ? "ws://171.94.154.2:7681"
+          : " ws://171.94.154.2:7681", // 呼叫中心socket地址
       };
     default:
       return {
@@ -36,6 +50,12 @@ export const getCurrentCityConfig = () => {
         recordNumber: "蜀ICP备19035032号-36", // 备案号
         locationCenter: [104.643, 28.751694], // 地图中心点
         favicon: "yibin/favicon.ico",
+        callCenterSocketUrl: isProduction()
+          ? "ws://218.6.151.146:50104/ola_socket"
+          : " ws://222.213.23.229:29003/ola_socket", // 呼叫中心socket地址
+        telNo: "1009",
+        telPwd: "!@#123Qw",
+        telGroup: "10020",
       };
   }
 };

+ 92 - 0
src/utils/callCenter.ts

@@ -0,0 +1,92 @@
+import { ref } from "vue";
+import { getCurrentCityConfig } from "@/utils/appConfig";
+import { getNowDateTime } from "@/utils/constants";
+import { ElMessage } from "element-plus";
+
+/**
+ * @description 呼叫中心配置 方法
+ * @return
+ *     cityName: 'name', // 中文名称
+ *     cityCode: 'code', // 6位区号
+ *     cityAbbr: 'abbr', // 简写
+ *     recordNumber: 'recordNumber', // 备案号
+ *     locationCenter: [lat,lng], // 地图中心点
+ *     loginBg: 'loginBg', // 登录背景
+ *     isShowLogo: true, // 是否显示左上角logo
+ */
+export const callCenterWs = ref<any>(null); // 全局变量 当前呼叫中心链接
+export const callCenterIsSignIn = ref<boolean>(false); // 当前呼叫中心是否登录
+export const currentTel = ref<any>({
+  telNo: "", // 分机号
+  jobNum: "", //工号
+  telGroup: "", // 技能组ID
+}); // 当前分机号
+const { cityName } = getCurrentCityConfig();
+// 发送消息
+const e_TelSendMsg = (strObj: Object) => {
+  // 客户端当前时间
+  const nowTime = getNowDateTime();
+  const strMsg = JSON.stringify(strObj);
+  console.log(nowTime + " 发送消息:" + strMsg, callCenterWs.value?.status);
+  if (callCenterWs.value?.ws?.readyState === 1) {
+    // 已经链接并且可以通讯,则发放文本消息
+    callCenterWs.value?.send(strMsg);
+  } else {
+    ElMessage.error("请先签入");
+  }
+};
+/*
+ * 外呼
+ * ReqMakeCall - 方法名
+ * Extension:分机号
+ * Called:被叫
+ * CustomerId:客户ID
+ */
+const callout = (strCallNumber: number | string) => {
+  if (!strCallNumber) {
+    ElMessage.error("电话号码不能为空");
+    return;
+  }
+  const obkMsg = {
+    Action: "ReqMakeCall",
+    Param: {
+      Extension: currentTel.value.telNo,
+      Called: strCallNumber,
+      CustomerId: "",
+    },
+  };
+  // 发送请求
+  e_TelSendMsg(obkMsg);
+};
+/*
+ * 签出
+ * ReqAgentLogout - 签出方法名称
+ * Extension - 分机号码
+ */
+const signOutFn = () => {
+  const objMsg = {
+    Action: "ReqAgentLogout",
+    Param: {
+      Extension: currentTel.value.telNo,
+    },
+  };
+  // 发送请求
+  e_TelSendMsg(objMsg);
+};
+// 统一调用退出登录并关闭链接方法 兼容不同的呼叫中心
+export function callCenterLogout() {
+  if (!callCenterWs.value) {
+    ElMessage.warning("请先签入");
+    return;
+  }
+  switch (cityName) {
+    case "宜宾市":
+      callCenterWs.value?.logout();
+      callCenterWs.value?.close();
+      break;
+    case "自贡市":
+      signOutFn();
+      break;
+    default:
+  }
+}

+ 5 - 10
src/utils/constants.ts

@@ -1,3 +1,5 @@
+import dayjs from "dayjs";
+
 export const loadingOptions = {
   text: "加载中...",
   color: "#c23531",
@@ -74,14 +76,7 @@ export const shortcuts = [
     },
   },
 ];
-// 获取当前市州地区编码
-export const getCurrentCityCode = () => {
-  switch (import.meta.env.VITE_CURRENT_CITY) {
-    case "yibin":
-      return "511500";
-    case "zigong":
-      return "510300";
-    default:
-      return "511500";
-  }
+// 获取当前时间(日志打印专用)
+export const getNowDateTime = () => {
+  return dayjs().format("YYYY-MM-DD HH:mm:ss");
 };

+ 647 - 0
src/utils/olaFn.ts

@@ -0,0 +1,647 @@
+/**
+ * @description 天润呼叫中心对接接口
+ */
+import {
+  Fn,
+  isClient,
+  isWorker,
+  MaybeRefOrGetter,
+  toRef,
+  tryOnScopeDispose,
+  useIntervalFn,
+} from "@vueuse/shared/index";
+import { ref, Ref, watch } from "vue";
+import { ElMessage } from "element-plus";
+import { getNowDateTime } from "@/utils/constants";
+const DEFAULT_PING_MESSAGE = "ping";
+let UUID = "73836387-0000-0000-0000-0000-0000000000";
+let _extn = ""; // 分机号
+function resolveNestedOptions<T>(options: T | true): T {
+  if (options === true) return {} as T;
+  return options;
+}
+export function olaFn(
+  url: MaybeRefOrGetter<string | URL | undefined>,
+  options: any
+) {
+  const {
+    onConnected,
+    onDisconnected,
+    onError,
+    onMessage,
+    immediate = true,
+    autoClose = true,
+    protocols = [],
+    username = "",
+    password = "",
+  } = options;
+
+  const data: Ref<any | null> = ref(null);
+  const status = ref<any>("CLOSED");
+  const wsRef = ref<WebSocket | undefined>();
+  const urlRef = toRef(url);
+
+  let heartbeatPause: Fn | undefined;
+  let heartbeatResume: Fn | undefined;
+
+  let explicitlyClosed = false;
+  let retried = 0;
+
+  let bufferedData: (string | ArrayBuffer | Blob)[] = [];
+
+  let pongTimeoutWait: ReturnType<typeof setTimeout> | undefined;
+
+  const _sendBuffer = () => {
+    if (bufferedData.length && wsRef.value && status.value === "OPEN") {
+      for (const buffer of bufferedData) wsRef.value.send(buffer);
+      bufferedData = [];
+    }
+  };
+
+  const resetHeartbeat = () => {
+    clearTimeout(pongTimeoutWait);
+    pongTimeoutWait = undefined;
+  };
+
+  // Status code 1000 -> Normal Closure https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
+  const close: WebSocket["close"] = (code = 1000, reason) => {
+    if (!isClient || !wsRef.value) return;
+    explicitlyClosed = true;
+    resetHeartbeat();
+    heartbeatPause?.();
+    wsRef.value.close(code, reason);
+    wsRef.value = undefined;
+  };
+
+  const send = (data: string | ArrayBuffer | Blob | any, useBuffer = true) => {
+    if (!wsRef.value || status.value !== "OPEN") {
+      if (useBuffer) bufferedData.push(data);
+      ElMessage.error("请先签入");
+      return false;
+    }
+    _sendBuffer();
+    data.uuid = next_uuid();
+    const sedMsg = JSON.stringify(data);
+    wsRef.value.send(sedMsg);
+    // console.log(`${getNowDateTime()}:向呼叫中心发送消息:${sedMsg}`);
+    return true;
+  };
+  const merge = (
+    target: { [x: string]: any },
+    additional: { [x: string]: any; hasOwnProperty: (arg0: string) => any }
+  ) => {
+    for (const i in additional) {
+      if (additional.hasOwnProperty(i)) {
+        target[i] = additional[i];
+      }
+    }
+  };
+
+  const next_uuid = () => {
+    let u = (parseFloat(UUID.substring(29)) + 1).toString();
+    u = u == "2147483647" ? "0" : u;
+    while (u.length < 12) {
+      u = "0" + u;
+    }
+    UUID = "73836387-0000-0000-0000-0000-" + u;
+    return UUID;
+  };
+
+  const auth = (username: any, password: any) => {
+    send({
+      cmd: "auth",
+      args: { username, password, accept: "application/json" },
+    });
+  };
+
+  const get_agent_state = (extn: any) => {
+    send({ action: "api", cmd: "get_agent_state", args: { extn } });
+  };
+
+  const get_trunk_state = () => {
+    send({ action: "api", cmd: "get_trunk_state" });
+  };
+
+  const login = (queue: any, extn: any, params: any) => {
+    const args = { queue, extn };
+    merge(args, params);
+    _extn = extn;
+    return send({ action: "api", cmd: "login", args: args });
+  };
+
+  const logout = (extn?: string) => {
+    const extns = extn || _extn;
+    send({ action: "api", cmd: "logout", args: { extn: extns } });
+  };
+
+  const collect_dtmf = (extn: any, soundfile: any) => {
+    send({ action: "api", cmd: "collect_dtmf", args: { extn, soundfile } });
+  };
+  const collect_judge = (extn: any) => {
+    send({ action: "api", cmd: "collect_judge", args: { extn: extn } });
+  };
+
+  const ping = () => {
+    return send({ cmd: "ping" }, false);
+  };
+
+  const subscribe = (k: any) => {
+    return send({ cmd: "subscribe", args: { key: k } });
+  };
+
+  const unsubscribe = (k: any) => {
+    return send({ cmd: "unsubscribe", args: { key: k } });
+  };
+
+  const go_ready = () => {
+    send({ action: "api", cmd: "go_ready", args: { extn: _extn } });
+  };
+
+  const go_ready2 = (extn: any) => {
+    send({ action: "api", cmd: "go_ready", args: { extn: extn } });
+  };
+
+  const go_break = (reason: any) => {
+    send({
+      action: "api",
+      cmd: "go_break",
+      args: { extn: _extn, reason: reason },
+    });
+  };
+
+  const go_break2 = (extn: any) => {
+    send({ action: "api", cmd: "go_break", args: { extn: extn, reason: "" } });
+  };
+
+  const toggle_ready = () => {
+    send({ action: "api", cmd: "toggle_ready", args: { extn: _extn } });
+  };
+
+  const answer = () => {
+    send({ action: "api", cmd: "answer", args: { extn: _extn } });
+  };
+
+  const hangup = () => {
+    send({ action: "api", cmd: "hangup_other", args: { extn: _extn } });
+  };
+
+  const dial = (dst: any, otherStr?: any, gateway?: any) => {
+    send({
+      action: "api",
+      cmd: "dial",
+      args: { extn: _extn, dest: dst, gateway: gateway, otherStr: otherStr },
+    });
+  };
+
+  const transfer = (dst: any, src?: any) => {
+    let extn = src;
+
+    if (extn == null || typeof extn == "undefined") {
+      extn = _extn;
+    }
+
+    send({ action: "api", cmd: "transfer", args: { extn: extn, dest: dst } });
+  };
+
+  const transfer_uuid = (uuid: any, dst: any) => {
+    send({
+      action: "api",
+      cmd: "transfer",
+      args: { channel_uuid: uuid, dest: dst },
+    });
+  };
+
+  const monitor = (dst: any, src: any) => {
+    let extn = src;
+    if (extn == null || typeof extn == "undefined") {
+      extn = _extn;
+    }
+
+    send({ action: "api", cmd: "monitor", args: { extn: extn, dest: dst } });
+  };
+
+  const monitor_uuid = (uuid: any) => {
+    send({
+      action: "api",
+      cmd: "monitor",
+      args: { extn: _extn, channel_uuid: uuid },
+    });
+  };
+
+  const intercept = (dst: any, src: any, gateway: any) => {
+    let extn = src;
+
+    if (extn == null || typeof extn == "undefined") {
+      extn = _extn;
+    }
+
+    send({ action: "api", cmd: "intercept", args: { extn: extn, dest: dst } });
+  };
+
+  const intercept_uuid = (uuid: any) => {
+    send({
+      action: "api",
+      cmd: "intercept",
+      args: { extn: _extn, channel_uuid: uuid },
+    });
+  };
+
+  const three_way = (dst: any, src: any, gateway?: any) => {
+    let extn = src;
+    if (extn == null || typeof extn == "undefined") {
+      extn = _extn;
+    }
+    send({
+      action: "api",
+      cmd: "monitor",
+      args: { extn: extn, dest: dst, three_way: "true", goip_gateway: gateway },
+    });
+  };
+  /* hangup the thrid party */
+  const exit_three_way = (ext: any) => {
+    send({
+      action: "api",
+      cmd: "unmonitor",
+      args: { extn: ext, three_way: "true" },
+    });
+  };
+  const three_way_uuid = (uuid: any) => {
+    send({
+      action: "api",
+      cmd: "monitor",
+      args: { extn: _extn, channel_uuid: uuid, three_way: "true" },
+    });
+  };
+
+  const unmonitor = (ext: any) => {
+    send({ action: "api", cmd: "unmonitor", args: { extn: ext } });
+  };
+
+  const whisper = (w: any) => {
+    // who = "agent" / "caller" / "both" / "none"
+    send({ action: "api", cmd: "whisper", args: { extn: _extn, who: w } });
+  };
+
+  const consult = (dst: any) => {
+    send({ action: "api", cmd: "consult", args: { extn: _extn, dest: dst } });
+  };
+  const unconsult = (dst: any) => {
+    send({ action: "api", cmd: "unconsult", args: { extn: _extn, dest: dst } });
+  };
+  const consult_to_three_way = (dst: any) => {
+    send({
+      action: "api",
+      cmd: "consult_to_three_way",
+      args: { extn: _extn, dest: dst },
+    });
+  };
+
+  const hold = () => {
+    send({ action: "api", cmd: "hold", args: { extn: _extn } });
+  };
+
+  const unhold = () => {
+    send({ action: "api", cmd: "unhold", args: { extn: _extn } });
+  };
+
+  const toggle_hold = () => {
+    send({ action: "api", cmd: "toggle_hold", args: { extn: _extn } });
+  };
+
+  const take_call = (channel_uuid: any) => {
+    send({
+      action: "api",
+      cmd: "take_call",
+      args: { extn: _extn, channel_uuid: channel_uuid },
+    });
+  };
+  const next_queue = (uuid: any) => {
+    send({
+      action: "api",
+      cmd: "next_queue",
+      args: { extn: _extn, channel_uuid: uuid },
+    });
+  };
+
+  const conference = (dst: any) => {
+    send({
+      action: "api",
+      cmd: "conference",
+      args: { extn: _extn, dest: dst },
+    });
+  };
+
+  const conference_uuid = (uuid: any) => {
+    send({
+      action: "api",
+      cmd: "conference",
+      args: { extn: _extn, channel_uuid: uuid },
+    });
+  };
+
+  const broadcast = (numbers: any, mute: any) => {
+    send({
+      action: "api",
+      cmd: "broadcast",
+      args: { extn: _extn, numbers, mute },
+    });
+  };
+
+  /* send chat to a queue or an agent
+      to = queue             send to queue
+      to = queue.agent       send to agent
+    */
+  const chat = (to: any, message: any, content_type: any) => {
+    send({
+      action: "api",
+      cmd: "chat",
+      args: { to: to, message: message, content_type: content_type },
+    });
+  };
+
+  const message = (from: any, to: any, message: any, content_type: any) => {
+    send({
+      action: "api",
+      cmd: "message",
+      args: {
+        from: from,
+        to: to,
+        message: message,
+        content_type: content_type,
+      },
+    });
+  };
+
+  const alarm = (queue: any, state: any) => {
+    send({ action: "api", cmd: "alarm", args: { queue, state } });
+  };
+
+  /* dispatching apis */
+
+  const dlogin = (ext: any) => {
+    send({ action: "api", cmd: "dlogin", args: { extn: ext } });
+  };
+
+  const dlogout = (ext: any) => {
+    send({ action: "api", cmd: "dlogout", args: { extn: ext } });
+  };
+
+  const inject = (ext: any, uuid: any) => {
+    send({
+      action: "api",
+      cmd: "inject",
+      args: { extn: ext, channel_uuid: uuid },
+    });
+  };
+
+  const kill = (uuid: any, extn: any) => {
+    send({
+      action: "api",
+      cmd: "kill",
+      args: { channel_uuid: uuid, extn: extn },
+    });
+  };
+
+  const eavesdrop = (uuid: any) => {
+    send({ action: "api", cmd: "eavesdrop", args: { channel_uuid: uuid } });
+  };
+
+  const conf = (name: any, action: any, member: any) => {
+    send({
+      action: "api",
+      cmd: "conf",
+      args: { name: name, action: action, member: member },
+    });
+  };
+
+  const answer_all = (ext: any, queue: any) => {
+    send({
+      action: "api",
+      cmd: "answer_all",
+      args: { extn: ext, queue: queue },
+    });
+  };
+
+  const group_call = (
+    ext: any,
+    queue: any,
+    numbers: any,
+    batch_accept: any
+  ) => {
+    send({
+      action: "api",
+      cmd: "group_call",
+      args: {
+        extn: ext,
+        queue: queue,
+        numbers: numbers,
+        batch_accept: batch_accept,
+      },
+    });
+  };
+
+  const sip_gateway = (profile: any, gateway: any, op: any) => {
+    send({
+      action: "api",
+      cmd: "sip_gateway",
+      args: { profile: profile, gateway: gateway, op: op },
+    });
+  };
+
+  const playback = (filename: any) => {
+    send({
+      action: "api",
+      cmd: "playback",
+      args: { extn: _extn, soundfile: filename },
+    });
+  };
+
+  const stop_playback = () => {
+    send({ action: "api", cmd: "stop_playback", args: { extn: _extn } });
+  };
+
+  const merge_call = (ext1: any, ext2: any) => {
+    send({
+      action: "api",
+      cmd: "merge_call",
+      args: { extn1: ext1, extn2: ext2 },
+    });
+  };
+
+  /* common apis*/
+
+  /*phone control api, only yealink support for now*/
+  const api_handfree = (ext: any) => {
+    send({ action: "api", cmd: "api_handfree", args: { extn: ext } });
+  };
+  const getStatus = () => {
+    // @ts-ignore
+    return wsRef.value.readyState;
+  };
+  const startSubscribe = () => {
+    subscribe(`ola.agent.${options.username}`);
+    subscribe(`ola.caller.${options.username}`);
+    get_agent_state(options.username);
+  };
+
+  const _init = () => {
+    if (explicitlyClosed || typeof urlRef.value === "undefined") return;
+
+    const ws = new WebSocket(urlRef.value, protocols);
+    wsRef.value = ws;
+    status.value = "CONNECTING";
+
+    ws.onopen = () => {
+      status.value = "OPEN";
+      heartbeatResume?.();
+      _sendBuffer();
+      auth(options.username, options.password);
+      startSubscribe();
+      onConnected?.(ws!);
+    };
+
+    ws.onclose = (ev) => {
+      status.value = "CLOSED";
+      onDisconnected?.(ev);
+
+      if (!explicitlyClosed && options.autoReconnect) {
+        const {
+          retries = -1,
+          delay = 1000,
+          onFailed,
+        } = resolveNestedOptions(options.autoReconnect);
+        retried += 1;
+
+        // @ts-ignore
+        if (retries < 0 || retried < retries) setTimeout(_init, delay);
+        else if (typeof retries === "function" && retries())
+          setTimeout(_init, delay);
+        else onFailed?.();
+      }
+    };
+
+    ws.onerror = (e) => {
+      onError?.(e);
+    };
+
+    ws.onmessage = (e: MessageEvent) => {
+      if (options.heartbeat) {
+        resetHeartbeat();
+        const { message = DEFAULT_PING_MESSAGE } = resolveNestedOptions(
+          options.heartbeat
+        );
+        if (e.data === message) return;
+      }
+
+      data.value = e.data;
+      onMessage?.(e.data);
+    };
+  };
+
+  if (options.heartbeat) {
+    const {
+      message = DEFAULT_PING_MESSAGE,
+      interval = 1000,
+      pongTimeout = 1000,
+    } = resolveNestedOptions(options.heartbeat);
+
+    const { pause, resume } = useIntervalFn(
+      () => {
+        ping();
+        if (pongTimeoutWait != null) return;
+        pongTimeoutWait = setTimeout(() => {
+          // auto-reconnect will be trigger with ws.onclose()
+          close();
+          explicitlyClosed = false;
+        }, pongTimeout);
+      },
+      interval,
+      { immediate: false }
+    );
+
+    heartbeatPause = pause;
+    heartbeatResume = resume;
+  }
+
+  if (autoClose) {
+    if (isClient) window.addEventListener("beforeunload", () => close());
+    tryOnScopeDispose(close);
+  }
+
+  const open = () => {
+    if (!isClient && !isWorker) return;
+    close();
+    explicitlyClosed = false;
+    retried = 0;
+    _init();
+  };
+
+  if (immediate) open();
+
+  watch(urlRef, open);
+
+  return {
+    data,
+    status,
+    close,
+    send,
+    open,
+    ws: wsRef,
+    login,
+    logout,
+    go_ready,
+    go_break,
+    hangup,
+    hold,
+    unhold,
+    dial,
+    exit_three_way,
+    transfer,
+    monitor,
+    collect_dtmf,
+    collect_judge,
+    get_trunk_state,
+    ping,
+    unsubscribe,
+    go_ready2,
+    toggle_ready,
+    answer,
+    transfer_uuid,
+    monitor_uuid,
+    intercept,
+    intercept_uuid,
+    three_way,
+    three_way_uuid,
+    unmonitor,
+    whisper,
+    consult,
+    unconsult,
+    consult_to_three_way,
+    toggle_hold,
+    take_call,
+    next_queue,
+    conference,
+    conference_uuid,
+    broadcast,
+    chat,
+    message,
+    alarm,
+    dlogin,
+    dlogout,
+    inject,
+    kill,
+    eavesdrop,
+    conf,
+    answer_all,
+    group_call,
+    sip_gateway,
+    playback,
+    stop_playback,
+    merge_call,
+    api_handfree,
+    getStatus,
+    username: options.username,
+    password: options.password,
+    next_uuid,
+  };
+}

+ 110 - 22
src/views/seats/index.vue

@@ -1,47 +1,135 @@
 <template>
   <scale-screen
-      width="1920"
-      height="1080"
-      :delay="500"
-      :fullScreen="true"
-      :boxStyle="{
+    width="1920"
+    height="1080"
+    :delay="500"
+    :fullScreen="true"
+    :boxStyle="{
       background: '#03050C',
       overflow: isScale ? 'hidden' : 'auto',
     }"
-      :wrapperStyle="wrapperStyle"
-      :autoScale="isScale"
+    :wrapperStyle="wrapperStyle"
+    :autoScale="isScale"
   >
     <div class="content_wrap">
-      <Headers :data="seatsList"/>
-      <Container :data="seatsList"/>
+      <Headers :data="seatsList" />
+      <Container :data="seatsList" />
     </div>
   </scale-screen>
-  <Setting/>
+  <Setting />
 </template>
 <script setup lang="ts" name="seats">
-import {defineAsyncComponent, onMounted, ref} from "vue";
-import {useSettingStore} from "@/stores/setting";
-import {storeToRefs} from "pinia";
-import {extensionPaged} from "api/home";
+import { defineAsyncComponent, onMounted, ref } from "vue";
+import { useSettingStore } from "@/stores/setting";
+import { storeToRefs } from "pinia";
+import { extensionPaged } from "api/home";
+import { olaFn } from "@/utils/olaFn";
+import { getCurrentCityConfig } from "@/utils/appConfig";
+import { getNowDateTime } from "@/utils/constants";
+import { callCenterIsSignIn, callCenterWs } from "@/utils/callCenter";
 
-const ScaleScreen = defineAsyncComponent(() => import('@/components/scale-screen'));
-const Headers = defineAsyncComponent(() => import('@/views/seats/header.vue'));
-const Container = defineAsyncComponent(() => import('@/views/seats/container.vue'));
-const Setting = defineAsyncComponent(() => import('@/views/index/setting.vue'));
+const ScaleScreen = defineAsyncComponent(
+  () => import("@/components/scale-screen")
+);
+const Headers = defineAsyncComponent(() => import("@/views/seats/header.vue"));
+const Container = defineAsyncComponent(
+  () => import("@/views/seats/container.vue")
+);
+const Setting = defineAsyncComponent(() => import("@/views/index/setting.vue"));
 
 const settingStore = useSettingStore();
-const {isScale} = storeToRefs(settingStore);
+const { isScale } = storeToRefs(settingStore);
 const wrapperStyle = {};
 
 const seatsList = ref<any[]>([]);
+const { callCenterSocketUrl, telNo, telPwd, telGroup } = getCurrentCityConfig();
+const olaRef = ref();
+const websocket_connect = () => {
+  olaRef.value = olaFn(callCenterSocketUrl, {
+    username: telNo,
+    password: telPwd,
+    onConnected: onConnected, // 连接成功
+    onDisconnected: onDisconnected, // 断开链接
+    onMessage: onMessage, // 接收消息
+    onError: onError, // 错误
+    autoReconnect: {
+      delay: 2000,
+    }, // 自动重连
+    heartbeat: {
+      message: JSON.stringify({ cmd: "ping" }),
+      interval: 5000,
+      // pongTimeout: 1000,
+    },
+  });
+  console.log(`${getNowDateTime()}:开始链接呼叫中心`);
+};
+// 呼叫中心链接成功
+const onConnected = () => {
+  olaRef.value.logout(telNo); //连接之后,先登出一次,防止其他地方已经登陆
+  let array_ola_queue = []; // 队列
+  // 普通模式
+  let array = telGroup.split(",");
+  for (let i = 0; i < array.length; i++) {
+    array_ola_queue[i] = array[i];
+  }
+  olaRef.value.login(array_ola_queue, telNo, {
+    type: "onhook",
+  });
+  callCenterWs.value = olaRef.value;
+  console.log(`${getNowDateTime()}:呼叫中心链接成功`);
+};
+// 呼叫中心链接关闭
+const onDisconnected = (event: any) => {
+  callCenterWs.value = null;
+  callCenterIsSignIn.value = false; // 签出状态
+  console.log(`${getNowDateTime()}:呼叫中心断开链接`, event);
+};
+// 呼叫中心链接错误
+const onError = (ws: any, e: any) => {
+  callCenterIsSignIn.value = false; // 签出状态
+  console.log(`${getNowDateTime()}:呼叫中心链接错误`, e);
+};
+// 呼叫中心消息
+const onMessage = async (event: any) => {
+  const data = JSON.parse(event);
+  // console.log(`${getNowDateTime()}:接收呼叫中心消息`, event);
+  if (data.event_type == "agent_state") {
+    switch (data.state) {
+      case "login": // 签入消息回调
+        callCenterIsSignIn.value = true; // 签入状态
+        setTimeout(() => {
+          // 设置示闲状态
+          olaRef.value.go_ready();
+        }, 300);
+        console.log(
+          `${getNowDateTime()}:接收消息:呼叫中心:已签入,当前分机:${telNo}`
+        );
+        break;
+      case "logout": // 签出消息回调
+        callCenterIsSignIn.value = false; // 签出状态
+        console.log(`${getNowDateTime()}:接收消息:呼叫中心:已签出`);
+        break;
+      case "acw": // 话后整理回到
+        // 调用示闲
+        olaRef.value.go_ready(); // 示闲
+        console.log(`${getNowDateTime()}:接收消息:呼叫中心:已示闲`);
+        break;
+      case "unready": // 示忙消息回调
+        // 调用示闲
+        olaRef.value.go_ready(); // 示闲
+        console.log(`${getNowDateTime()}:接收消息:呼叫中心:已示闲`);
+        break;
+    }
+  }
+};
 onMounted(async () => {
   try {
-    const {result} = await extensionPaged();
+    const { result } = await extensionPaged();
     seatsList.value = result ?? [];
+    websocket_connect();
     // seatsList.value = [...result, ...result, ...result, ...result, ...result, ...result, ...result, ...result, ...result, ...result, ...result, ...result, ...result, ...result,];
-
   } catch (e) {
-    console.log(e)
+    console.log(e);
   }
 });
 </script>

+ 1 - 1
src/views/seats/left-bottom.vue

@@ -18,7 +18,7 @@
         <CountUp :endVal="state.count.inNoOn" />
       </div>
       <div>
-        <span>呼接通数量:</span>
+        <span>呼接通数量:</span>
         <CountUp :endVal="state.count.outOn" />
       </div>
       <div>

+ 48 - 43
src/views/seats/right.vue

@@ -23,17 +23,24 @@
         <p class="seats_item_name" v-else>
           {{ item.workUserName ? item.workUserName : "&nbsp;" }}
         </p>
-        <!--        <el-dropdown @command="handleCommand($event,item)" class="seats_item_dropdown" trigger="click" v-if="item.state === 'busy'">
-                  <el-icon class="seats_item_more" size="18">
-                    <Operation/>
-                  </el-icon>
-                  <template #dropdown>
-                    <el-dropdown-menu>
-                      <el-dropdown-item command="listen">监 听</el-dropdown-item>
-                      <el-dropdown-item command="interject">插 话</el-dropdown-item>
-                    </el-dropdown-menu>
-                  </template>
-                </el-dropdown>-->
+        <span @click.stop>
+          <el-dropdown
+            @command="handleCommand($event, item)"
+            class="seats_item_dropdown"
+            trigger="click"
+            v-if="item.state === 'busy'"
+          >
+            <el-icon class="seats_item_more" size="18">
+              <Operation />
+            </el-icon>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item command="listen">监 听</el-dropdown-item>
+                <el-dropdown-item command="interject">插 话</el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </span>
         <div class="seats_item_state">
           <img :src="getImageUrl('seats/' + item.state + '.png')" alt="" />
           <span class="seats_item_telNo">
@@ -62,17 +69,17 @@
             <span>分机号:</span>{{ call.telNo }}
           </div>
           <div class="call_popover_item">
-            <span>通话时长:</span>{{ formatDuration(callTime) }}
+            <span>通话时长:</span>{{ formatDuration(talkTime) }}
           </div>
           <div class="call_popover_item">
             <span>呼入类型:</span
-            >{{ call.callDirection === "inbound" ? "呼入" : "呼" }}
+            >{{ call.callDirection === "inbound" ? "呼入" : "呼" }}
           </div>
           <div class="call_popover_item">
             <span>电话号码:</span>{{ call.otherNumber }}
           </div>
           <div class="call_popover_item">
-            <span>接听量:</span>{{ call.weight }}
+            <span>接听量:</span>{{ call.onStateCount }}
           </div>
         </div>
       </template>
@@ -83,10 +90,13 @@
 import { onMounted, ref, nextTick, onBeforeUnmount, watch } from "vue";
 import { getImageUrl } from "@/utils/tools";
 import signalR from "@/utils/signalR";
-import { extensionPaged } from "api/home";
 import { ClickOutside as vClickOutside } from "element-plus";
 import { formatDuration } from "@/utils/formatTime";
 import dayjs from "dayjs";
+import { getExtensionStatus } from "api/home";
+import { useIntervalFn } from "@vueuse/core";
+import { Operation } from "@element-plus/icons-vue";
+import { callCenterWs } from "@/utils/callCenter";
 
 const props = defineProps({
   data: {
@@ -101,35 +111,38 @@ const popoverRef = ref();
 const hidePopover = ref(false);
 const call = ref({});
 // 开始签入时长
-const callTime = ref<any>(0); // 通话时长
-const timer = ref<any>(null); // 通话时长定时器
+const talkTime = ref<any>(0); // 通话时长
+const talkTimer = ref<any>(null); // 通话时长定时器
 const startCallTimer = (second: any) => {
   if (second) {
     // 从后台获取签入时长
     if (second < 0) second = 0; // 防止后台返回的签入时间大于当前时间
-    callTime.value = second;
-    timer.value = setInterval(() => {
-      callTime.value++;
+    talkTime.value = second;
+    talkTimer.value = useIntervalFn(() => {
+      talkTime.value++;
     }, 1000);
   } else {
-    timer.value = setInterval(() => {
-      callTime.value++;
+    talkTimer.value = useIntervalFn(() => {
+      talkTime.value++;
     }, 1000);
   }
 };
 const handleClick = (index: string | number, item: { state: string }) => {
+  textRef.value = textRefs.value[index];
+  call.value = item;
+  console.log(`当前分机信息:`, call.value);
   if (["busy", "held"].includes(item.state)) {
-    textRef.value = textRefs.value[index];
-    call.value = item;
-    hidePopover.value = true;
-    // 获取现在时间与签入时间的秒数
-    callTime.value = dayjs().diff(dayjs(item.answeredAt), "second");
-    startCallTimer(callTime.value);
+    getExtensionStatus({ telno: item.telNo }).then((res: any) => {
+      hidePopover.value = true;
+      // 获取现在时间与签入时间的秒数
+      talkTime.value = dayjs().diff(dayjs(item.answeredAt), "second");
+      startCallTimer(talkTime.value);
+    });
   }
 };
 // 隐藏弹窗
 const popoverHide = () => {
-  clearInterval(timer.value);
+  if (talkTimer.value) talkTimer.value.pause();
 };
 const onClickOutside = () => {
   hidePopover.value = false;
@@ -157,18 +170,6 @@ watch(
   { immediate: true }
 );
 const loading = ref(false);
-// 获取分机列表
-const getSeatsList = async () => {
-  loading.value = true;
-  try {
-    const { result } = await extensionPaged();
-    seatsList.value = result ?? [];
-    loading.value = false;
-  } catch (e) {
-    console.log(e);
-    loading.value = false;
-  }
-};
 // 对当前状态进行排序
 const sortSeatsList = () => {
   // 通话中的排序
@@ -195,11 +196,11 @@ const sortSeatsList = () => {
   );
   seatsList.value = [
     ...busyData,
+    ...readyData,
     ...acwData,
     ...heldData,
     ...treeWayData,
     ...unreadyData,
-    ...readyData,
     ...logoutData,
   ];
 };
@@ -226,7 +227,11 @@ onMounted(async () => {
 const handleCommand = (command: string, item: any) => {
   console.log(command, item);
   if (command === "listen") {
+    // 监听
+    callCenterWs.value.monitor(item.telNo, callCenterWs.value.username);
   } else if (command === "interject") {
+    // 强插
+    callCenterWs.value.intercept(item.telNo, callCenterWs.value.username);
   }
 };
 onBeforeUnmount(() => {
@@ -252,7 +257,7 @@ onBeforeUnmount(() => {
       padding: 7px 0;
       animation: bounce-out 0.3s ease;
       position: relative;
-
+      cursor: pointer;
       &_service {
         margin: 0 auto;
         width: 40px;