WXL (wul)
15 小时以前 91f78c7a3c325b7627f269524cdf92f006948cdf
src/views/followvisit/discharge/ClickCall.vue
@@ -1,21 +1,36 @@
<template>
  <div class="websocket-demo">
    <div>
      <h3>Websocket接口测试DEMO</h3>
      <h3>Websocket呼叫中心接口</h3>
      <div class="config-area">
        <div class="status-indicator">
          <span :class="['status-dot', connectionStatus]"></span>
          连接状态: {{ connectionText }}
          <span :class="['status-dot', seatStatus]"></span>
          座席状态: {{ seatStatusText }}
        </div>
        <div class="input-group">
          <label>CTI_WS_URL</label>
          <input
            type="text"
            v-model="config.cti_ws_url"
            placeholder="ws://40.78.0.169:6688"
            placeholder="wss://your-server.com"
          />
          <label>坐席工号</label>
          <input type="text" v-model="config.seatname" placeholder="8000" />
          <input
            type="text"
            v-model="config.seatname"
            :placeholder="randomNum"
          />
          <label>坐席分机</label>
          <input type="text" v-model="config.seatnum" placeholder="8000" />
          <input
            type="text"
            v-model="config.seatnum"
            :placeholder="randomNum"
          />
          <label>密码</label>
          <input type="text" v-model="config.password" placeholder="123456" />
@@ -23,129 +38,136 @@
        <div class="input-group">
          <label>外线号码</label>
          <input type="text" v-model="config.phone" placeholder="10086" />
          <label>UUID</label>
          <input type="text" v-model="config.uuid" />
          <label>其他坐席</label>
          <input type="text" v-model="config.other" placeholder="8001" />
          <input
            type="text"
            v-model="customerPhone"
            placeholder="请输入电话号码"
          />
          <label>技能组</label>
          <input type="text" v-model="config.group" placeholder="a3" />
          <label>外呼参数id</label>
          <input type="text" v-model="config.paramid" placeholder="3" />
        </div>
      </div>
      <!-- 操作按钮区域 -->
      <div class="button-area">
        <!-- 第一行按钮 -->
        <div class="button-row">
          <button @click="seatlogin">签入</button>
          <button @click="seatlogout">签出</button>
          <button @click="afk">示忙</button>
          <button @click="online">示闲</button>
          <button @click="pickup">代答</button>
          <button
            @click="handleSeatLogin"
            :disabled="!isConnected || isSeatLoggedIn"
          >
            签入
          </button>
          <button @click="handleSeatLogout" :disabled="!isSeatLoggedIn">
            签出
          </button>
          <button @click="callout" :disabled="!isSeatLoggedIn">外呼</button>
          <button @click="hangup" :disabled="!isSeatLoggedIn">挂机</button>
        </div>
        <!-- 第二行按钮 -->
        <div class="button-row">
          <button @click="hangup">挂机</button>
          <button @click="callout">外呼</button>
          <button @click="transfer">通话转移</button>
          <button @click="transferresume">通话转移收回</button>
          <button @click="hold">通话保持</button>
          <button @click="holdresume">通话保持收回</button>
          <button @click="remove">通话强拆</button>
          <button @click="insert">通话强插</button>
          <button @click="monitor">监听</button>
          <button @click="monitor_to_talk">监听转通话</button>
          <button @click="monitor_end">监听结束</button>
          <button @click="choosecall">选择</button>
          <button @click="replacecall">代接</button>
          <button @click="three">三方通话</button>
        </div>
        <!-- 第三行按钮 -->
        <div class="button-row">
          <button @click="handoff_ready">咨询开始</button>
          <button @click="handoff_call">咨询呼叫</button>
          <button @click="handoff_resume">咨询收回</button>
          <button @click="handoff_transfer">咨询转移</button>
          <button @click="handoff_three">咨询三方</button>
          <button @click="record_start">开始通话录音</button>
          <button @click="record_stop">停止通话录音</button>
        </div>
        <!-- 第四行按钮 -->
        <div class="button-row">
          <button @click="openseatlist">打开坐席状态</button>
          <button @click="closeseatlist">关闭坐席状态</button>
          <button @click="openqueues">打开队列信息</button>
          <button @click="closequeues">关闭队列信息</button>
          <button @click="opencalllist">打开通话信息</button>
          <button @click="closecalllist">关闭通话信息</button>
          <button @click="openroutelist">打开路由信息</button>
          <button @click="closeroutelist">关闭路由信息</button>
        </div>
        <!-- 第五行按钮 -->
        <div class="button-row">
          <button @click="seatlist">获取坐席信息</button>
          <button @click="queues">获取队列信息</button>
          <button @click="calllist">获取通话信息</button>
          <button @click="routelist">获取路由信息</button>
          <button @click="batch">获取外呼参数信息</button>
          <button @click="batch_start">开始外呼任务</button>
          <button @click="batch_stop">停止外呼任务</button>
          <button @click="afk" :disabled="!isSeatLoggedIn">示忙</button>
          <button @click="online" :disabled="!isSeatLoggedIn">示闲</button>
          <button @click="hold" :disabled="!isSeatLoggedIn">保持</button>
          <button @click="holdresume" :disabled="!isSeatLoggedIn">
            取消保持
          </button>
        </div>
      </div>
      <!-- 日志显示区域 -->
      <h3>协议日志区<button @click="testclear">清除</button></h3>
      <div id="msg" class="log-area">{{ logs }}</div>
      <h3>协议日志区 <button @click="testclear">清除</button></h3>
      <div class="log-area">{{ logs }}</div>
    </div>
  </div>
</template>
<script>
import { CallsetState, CallgetList } from "@/api/AiCentre/index";
export default {
  name: "WebsocketDemo",
  emits: ["status-change", "call-status", "error"],
  props: {
    customerPhone: {
      type: String,
      default: "",
    },
    autoLogin: {
      type: Boolean,
      default: true,
    },
  },
  data() {
    return {
      config: {
        cti_ws_url: "wss://9.208.2.190:8092/cal-api/",
        seatname: "8000",
        seatnum: "8000",
        cti_ws_url: "",
        seatname: "",
        seatnum: "",
        password: "123456",
        phone: "10086",
        phone: "",
        uuid: "",
        other: "8001",
        group: "a3",
        paramid: "3",
      },
      randomNum: "",
      randomID: "",
      logs: "",
      ws: null,
      isConnected: false,
      isSeatLoggedIn: false,
      currentCallStatus: "idle", // idle, calling, connected
      seatResourceAcquired: false,
      reconnectAttempts: 0,
      maxReconnectAttempts: 5,
      heartbeatTimer: null,
    };
  },
  mounted() {
  computed: {
    connectionStatus() {
      return this.isConnected ? "connected" : "disconnected";
    },
    connectionText() {
      return this.isConnected ? "已连接" : "未连接";
    },
    seatStatus() {
      return this.isSeatLoggedIn ? "logged-in" : "logged-out";
    },
    seatStatusText() {
      return this.isSeatLoggedIn ? "已签入" : "未签入";
    },
  },
  watch: {
    customerPhone(newVal) {
      this.config.phone = newVal;
    },
    isSeatLoggedIn(newVal) {
      this.$emit("status-change", {
        isLoggedIn: newVal,
        seatNumber: this.config.seatnum,
        status: newVal ? "ready" : "offline",
      });
    },
  },
  async mounted() {
    await this.initializeSeatResource();
    this.initializeWebSocket();
  },
  beforeUnmount() {
    this.disconnectWebSocket();
    this.cleanup();
  },
  methods: {
    // 初始化WebSocket连接
    initializeWebSocket() {
      try {
        // 根据当前页面协议自动选择WS协议
        const isHttps = window.location.protocol === "https:";
        this.config.cti_ws_url = isHttps
          ? "wss://9.208.2.190:8092/cal-api/"
@@ -159,22 +181,70 @@
        this.connectWebSocket();
      } catch (error) {
        this.addLog(`初始化WebSocket错误: ${error.message}`);
        // 尝试使用备用地址
        this.config.cti_ws_url = "wss://9.208.2.190:8092/cal-api/";
        setTimeout(() => this.connectWebSocket(), 2000);
      }
    },
    // 初始化座席号资源
    async initializeSeatResource() {
      try {
        const res = await CallgetList();
        if (res.data && res.data.length > 0) {
          // this.randomNum = res.data[0].tel;
          // this.randomID = res.data[0].id;
          this.randomNum = 8000;
          this.randomID = 8000;
          // 设置默认座席号
          this.config.seatname = this.randomNum;
          this.config.seatnum = this.randomNum;
          // 立即占用座席号资源
          await this.startCallsetState();
          this.seatResourceAcquired = true;
          this.addLog(`座席号资源获取成功: ${this.randomNum}`);
        }
      } catch (error) {
        console.error("获取座席号失败:", error);
        this.addLog("错误: 获取座席号资源失败");
        this.$emit("error", { type: "seat_acquisition_failed", error });
      }
    },
    // 占用座席号
    async startCallsetState() {
      try {
        await CallsetState({ id: this.randomID, state: 1 });
        this.addLog("座席号状态更新为使用中");
      } catch (error) {
        console.error("更新座席号状态失败:", error);
        throw error;
      }
    },
    // 释放座席号
    async releaseSeatResource() {
      if (this.seatResourceAcquired && this.randomID) {
        try {
          await CallsetState({ id: this.randomID, state: 0 });
          this.addLog("座席号资源已释放");
          this.seatResourceAcquired = false;
        } catch (error) {
          console.error("释放座席号失败:", error);
        }
      }
    },
    // 连接WebSocket
    // 连接WebSocket
    connectWebSocket() {
      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
        this.addLog("WebSocket已连接");
        return;
      }
      if (this.reconnectAttempts >= this.maxReconnectAttempts) {
        this.addLog("错误: 达到最大重连次数,停止重连");
        return;
      }
      try {
        let wsUrl = this.config.cti_ws_url;
        // 确保HTTPS页面使用WSS
        if (
          window.location.protocol === "https:" &&
          wsUrl.startsWith("ws://")
@@ -186,7 +256,14 @@
        this.ws.onopen = () => {
          this.isConnected = true;
          this.reconnectAttempts = 0;
          this.addLog("WebSocket连接成功");
          this.startHeartbeat();
          // 连接成功后自动签入
          if (this.autoLogin && this.seatResourceAcquired) {
            setTimeout(() => this.handleSeatLogin(), 500);
          }
        };
        this.ws.onmessage = (event) => {
@@ -195,54 +272,202 @@
        this.ws.onclose = (event) => {
          this.isConnected = false;
          this.isSeatLoggedIn = false;
          this.stopHeartbeat();
          this.addLog(`WebSocket连接关闭: ${event.code} ${event.reason}`);
          // 自动重连
          setTimeout(() => this.connectWebSocket(), 3000);
          if (this.reconnectAttempts < this.maxReconnectAttempts) {
            this.reconnectAttempts++;
            setTimeout(() => this.connectWebSocket(), 3000);
          }
        };
        this.ws.onerror = (error) => {
          this.addLog(`WebSocket错误: ${error.message}`);
          // 尝试备用URL
          if (!wsUrl.includes("9.208.2.190")) {
            this.config.cti_ws_url = "wss://9.208.2.190:8092/cal-api/";
            setTimeout(() => this.connectWebSocket(), 3000);
          }
        };
      } catch (error) {
        this.addLog(`连接WebSocket失败: ${error.message}`);
        // 尝试备用URL
        this.config.cti_ws_url = "wss://9.208.2.190:8092/cal-api/";
        setTimeout(() => this.connectWebSocket(), 3000);
      }
    },
    // 处理WebSocket消息
    handleWebSocketMessage(event) {
      const reader = new FileReader();
      reader.onloadend = (e) => {
        const message = reader.result;
        this.addLog(`收到消息: ${message}`);
        try {
          const obj = JSON.parse(message);
          // 处理心跳包
          if (obj.cmd === "system" && obj.action === "keepalive") {
            this.keepalive(obj.seatname, obj.seatnum);
          }
          // 自动设置UUID
          if (obj.cmd === "control" && obj.action === "tp_callin") {
            this.config.uuid = obj.uuid;
            this.addLog(`自动设置UUID: ${obj.uuid}`);
          }
        } catch (error) {
          this.addLog(`消息解析错误: ${error.message}`);
      try {
        // 检查数据类型:可能是字符串或Blob
        if (event.data instanceof Blob) {
          // 处理二进制数据(Blob)
          const reader = new FileReader();
          reader.onload = () => {
            try {
              const textData = reader.result;
              this.addLog(`收到Blob消息: ${textData}`);
              this.processWebSocketData(textData);
            } catch (error) {
              this.addLog(`Blob数据处理错误: ${error.message}`);
            }
          };
          reader.readAsText(event.data);
        } else if (typeof event.data === "string") {
          // 直接处理文本数据
          this.addLog(`收到文本消息: ${event.data}`);
          this.processWebSocketData(event.data);
        } else {
          this.addLog(`未知数据类型: ${typeof event.data}`);
        }
      } catch (error) {
        this.addLog(`消息处理错误: ${error.message}`);
      }
    },
    // 专门处理解析后的WebSocket数据
    processWebSocketData(messageText) {
      console.log(messageText,'消息1');
      try {
        const obj = JSON.parse(messageText);
      console.log(obj,'消息2');
        // 处理心跳包
        if (obj.cmd === "system" && obj.action === "keepalive") {
          this.keepalive(obj.seatname, obj.seatnum);
        }
        // 处理挂断
         if (obj.action === "calloutend") {
          this.hangup();
        }
        // 处理签入响应
        if (obj.cmd === "system" && obj.action === "seatlogin") {
            this.isSeatLoggedIn = true;
            this.addLog("座席签入成功");
            this.$emit("status-change", {
              isLoggedIn: true,
              seatNumber: this.config.seatnum,
              status: "ready",
            });
        }
        // 处理签出响应
        if (obj.cmd === "system" && obj.action === "seatlogout") {
          this.isSeatLoggedIn = false;
          this.addLog("座席签出成功");
          this.$emit("status-change", {
            isLoggedIn: false,
            status: "offline",
          });
        }
        // 自动设置UUID(来电事件)
        if (obj.cmd === "control" && obj.action === "tp_callin") {
          this.config.uuid = obj.uuid;
          this.addLog(`自动设置UUID: ${obj.uuid}`);
          this.$emit("call-status", {
            status: "incoming",
            uuid: obj.uuid,
            phone: obj.phone || "未知号码",
          });
        }
        // 处理外呼响应
        if (obj.cmd === "control" && obj.action === "callout") {
            this.$emit("call-status", {
              status: obj.status || "calling",
              uuid: obj.uuid,
              phone: this.config.phone,
            });
        }
        // 处理挂机响应
        if (obj.cmd === "control" && obj.action === "hangup") {
          this.$emit("call-status", {
            status: "idle",
            uuid: obj.uuid,
          });
        }
        // 处理通话状态变化
        if (obj.cmd === "control" && obj.status) {
          this.handleCallStatusChange(obj);
        }
      } catch (error) {
        this.addLog(`JSON解析错误: ${error.message}, 原始数据: ${messageText}`);
      }
    },
    // 处理呼叫状态变化
    handleCallStatusChange(obj) {
      const statusMap = {
        ringing: "振铃中",
        connected: "通话中",
        held: "已保持",
        ended: "通话结束",
      };
      reader.readAsText(event.data);
      this.addLog(`通话状态: ${statusMap[obj.status] || obj.status}`);
      this.$emit("call-status", {
        status: obj.status,
        uuid: obj.uuid,
        phone: obj.phone || this.config.phone,
      });
    },
    // 开始心跳检测
    startHeartbeat() {
      this.heartbeatTimer = setInterval(() => {
        if (this.isConnected && this.isSeatLoggedIn) {
          this.keepalive(this.config.seatname, this.config.seatnum);
        }
      }, 30000); // 30秒心跳
    },
    // 停止心跳检测
    stopHeartbeat() {
      if (this.heartbeatTimer) {
        clearInterval(this.heartbeatTimer);
        this.heartbeatTimer = null;
      }
    },
    // 座席签入
    async handleSeatLogin() {
      if (!this.seatResourceAcquired) {
        this.addLog("错误: 未获取座席号资源,无法签入");
        return;
      }
      const { seatname, seatnum, password } = this.config;
      if (!seatname || !seatnum) {
        this.addLog("错误: 座席工号和分机号不能为空");
        return;
      }
      const protocol = {
        cmd: "system",
        action: "seatlogin",
        seatname: seatname,
        seatnum: seatnum,
        password: password,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // 座席签出
    async handleSeatLogout() {
      const { seatname, seatnum } = this.config;
      const protocol = {
        cmd: "system",
        action: "seatlogout",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      if (this.sendWebSocketMessage(protocol)) {
        this.isSeatLoggedIn = false;
        // 延迟释放资源,确保签出完成
        setTimeout(() => this.releaseSeatResource(), 1000);
      }
    },
    // 断开WebSocket连接
    disconnectWebSocket() {
      if (this.ws) {
@@ -333,19 +558,11 @@
    // 示忙
    afk() {
      const { seatname, seatnum } = this.config;
      if (
        !this.validateParams({ seatname, seatnum }, ["seatname", "seatnum"])
      ) {
        return;
      }
      const protocol = {
        cmd: "system",
        action: "afk",
        seatname: seatname,
        seatnum: seatnum,
        seatname: this.config.seatname,
        seatnum: this.config.seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
@@ -353,19 +570,11 @@
    // 示闲
    online() {
      const { seatname, seatnum } = this.config;
      if (
        !this.validateParams({ seatname, seatnum }, ["seatname", "seatnum"])
      ) {
        return;
      }
      const protocol = {
        cmd: "system",
        action: "online",
        seatname: seatname,
        seatnum: seatnum,
        seatname: this.config.seatname,
        seatnum: this.config.seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
@@ -391,27 +600,28 @@
    // 挂机
    hangup() {
      const { seatname, seatnum } = this.config;
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "hangup",
        seatname: seatname,
        seatnum: seatnum,
        seatname: this.config.seatname,
        seatnum: this.config.seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // 外呼
    callout() {
      const { seatname, seatnum, phone } = this.config;
    // 外呼操作
    async callout(phoneNumber = null) {
      const phone = phoneNumber || this.customerPhone || this.config.phone;
      if (!phone) {
        this.addLog("错误: 被叫号码不能为空");
        this.$emit("error", { type: "phone_number_required" });
        return;
      }
      if (!this.validateParams({ seatnum, phone }, ["seatnum", "phone"])) {
      if (!this.isSeatLoggedIn) {
        this.addLog("错误: 座席未签入,无法外呼");
        return;
      }
@@ -419,13 +629,23 @@
        cmd: "control",
        action: "callout",
        phone: phone,
        seatname: seatname,
        seatnum: seatnum,
        seatname: this.config.seatname,
        seatnum: this.config.seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
      this.sendWebSocketMessage(protocol);
      this.$emit("call-status", { status: "calling", phone });
    },
    // 清理资源
    cleanup() {
      this.stopHeartbeat();
      if (this.ws) {
        this.ws.close();
        this.ws = null;
      }
      this.releaseSeatResource();
    },
    // 通话转移
    transfer() {
      const { seatname, seatnum, phone, uuid } = this.config;
@@ -480,18 +700,12 @@
    // 通话保持
    hold() {
      const { seatname, seatnum, uuid } = this.config;
      if (!this.validateParams({ seatnum, uuid }, ["seatnum", "uuid"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "hold",
        uuid: uuid,
        seatname: seatname,
        seatnum: seatnum,
        uuid: this.config.uuid,
        seatname: this.config.seatname,
        seatnum: this.config.seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
@@ -499,18 +713,12 @@
    // 通话保持收回
    holdresume() {
      const { seatname, seatnum, uuid } = this.config;
      if (!this.validateParams({ seatnum, uuid }, ["seatnum", "uuid"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "holdresume",
        uuid: uuid,
        seatname: seatname,
        seatnum: seatnum,
        uuid: this.config.uuid,
        seatname: this.config.seatname,
        seatnum: this.config.seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
@@ -1059,12 +1267,7 @@
      this.sendWebSocketMessage(protocol);
    },
    // 心跳包
    keepalive(seatname, seatnum) {
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "system",
        action: "keepalive",
@@ -1097,6 +1300,87 @@
</script>
<style scoped>
.status-indicator {
  margin-bottom: 15px;
  padding: 10px;
  background: #f5f5f5;
  border-radius: 4px;
  display: flex;
  align-items: center;
  gap: 10px;
}
.status-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  display: inline-block;
}
.status-dot.connected {
  background-color: #52c41a;
}
.status-dot.disconnected {
  background-color: #f5222d;
}
.status-dot.logged-in {
  background-color: #1890ff;
}
.status-dot.logged-out {
  background-color: #d9d9d9;
}
.button-row button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
.config-area {
  margin-bottom: 20px;
}
.input-group {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 10px;
}
.input-group label {
  font-weight: bold;
  min-width: 80px;
}
.input-group input {
  padding: 5px 10px;
  border: 1px solid #ccc;
  border-radius: 3px;
  width: 120px;
}
.button-area {
  margin-bottom: 20px;
}
.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-bottom: 10px;
}
.log-area {
  height: 200px;
  overflow-y: auto;
  border: 1px solid #ccc;
  padding: 10px;
  background: #f5f5f5;
  white-space: pre-wrap;
  font-family: monospace;
}
.websocket-demo {
  font-family: Arial, sans-serif;
  padding: 20px;