WXL (wul)
15 小时以前 91f78c7a3c325b7627f269524cdf92f006948cdf
景宁电话接入
已修改5个文件
已添加2个文件
2492 ■■■■■ 文件已修改
dist.zip 补丁 | 查看 | 原始文档 | blame | 历史
src/api/AiCentre/phoneCall.js 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/CallCenterLs/index.vue 614 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/ClickCall copy.vue 1258 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/ClickCall.vue 590 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/record/detailpage/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vue.config.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dist.zip
Binary files differ
src/api/AiCentre/phoneCall.js
@@ -1,11 +1,13 @@
import request from "@/utils/request";
// åˆ é™¤å¤–部患者表
export function CallgetList() {
  return request({
    url: "/smartor/ServiceTelInfo/getList",
    method: "get",
    params: {
      orgid: localStorage.getItem("orgid"),
    },
  });
}
// æŸ¥è¯¢å¤–部患者表
src/components/CallCenterLs/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,614 @@
<template>
  <div class="call-center-container">
    <!-- ä¸»æŽ§åˆ¶åŒº -->
    <div class="control-section">
      <div class="phone-control-card">
        <h3 class="section-title">电话控制</h3>
        <div class="input-group">
          <div class="form-field">
            <label class="form-label">客户电话号码</label>
            <input
              v-model="customerPhone"
              type="text"
              placeholder="请输入电话号码"
              :disabled="isCalling"
              class="phone-input"
            />
          </div>
          <div class="button-group">
            <button
              @click="handleCall"
              :class="['call-btn', callButtonClass]"
              :disabled="!canMakeCall"
            >
              <span class="btn-icon">📞</span>
              {{ callButtonText }}
            </button>
            <button
              @click="handleHangup"
              class="hangup-btn"
              :disabled="!canHangup"
            >
              <span class="btn-icon">📵</span>
              æŒ‚æ–­
            </button>
          </div>
        </div>
      </div>
      <!-- çŠ¶æ€æ˜¾ç¤ºåŒº -->
      <div class="status-card">
        <h3 class="section-title">状态监控</h3>
        <div class="status-grid">
          <div class="status-item">
            <span class="status-label">座席状态:</span>
            <span :class="['status-indicator', seatStatusClass]">
              <span class="status-dot"></span>
              {{ seatStatusText }}
            </span>
          </div>
          <div class="status-item">
            <span class="status-label">通话状态:</span>
            <span :class="['status-indicator', callStatusClass]">
              <span class="status-dot"></span>
              {{ callStatusText }}
            </span>
          </div>
          <div class="status-item" v-if="callDuration">
            <span class="status-label">通话时长:</span>
            <span class="duration-display">
              â±ï¸ {{ callDuration }}
            </span>
          </div>
        </div>
      </div>
    </div>
    <!-- è°ƒè¯•面板 -->
    <div class="debug-section">
      <el-collapse accordion>
        <el-collapse-item name="debug">
          <template slot="title">
            <div class="debug-header">
              <span class="debug-title">呼叫调试日志</span>
              <span class="debug-subtitle">点击查看详细通话信息</span>
            </div>
          </template>
          <div class="debug-content">
            <WebsocketDemo
              ref="callComponent"
              :customer-phone="customerPhone"
              :auto-login="true"
              @status-change="onSeatStatusChange"
              @call-status="onCallStatusChange"
              @error="onCallError"
              class="call-component"
            />
          </div>
        </el-collapse-item>
      </el-collapse>
    </div>
  </div>
</template>
<script>
import WebsocketDemo from "../../views/followvisit/discharge/ClickCall.vue";
export default {
  name: "CallCenterModal",
  components: {
    WebsocketDemo,
  },
  props: {
    initialPhone: {
      type: String,
      default: "",
    },
  },
  data() {
    return {
      customerPhone: "",
      isSeatLoggedIn: false,
      callStatus: "idle",
      callStartTime: null,
      callDuration: "00:00",
      durationTimer: null,
      lastError: null,
    };
  },
  computed: {
    isCalling() {
      return this.callStatus === "calling";
    },
    isInCall() {
      return this.callStatus === "connected";
    },
    canMakeCall() {
      return (
        this.isSeatLoggedIn &&
        this.customerPhone &&
        this.callStatus === "idle" &&
        !this.lastError
      );
    },
    canHangup() {
      return this.isCalling || this.isInCall;
    },
    callButtonClass() {
      if (!this.canMakeCall) return "disabled";
      return this.isCalling ? "calling" : "idle";
    },
    callButtonText() {
      if (this.isCalling) return "呼叫中...";
      if (this.isInCall) return "通话中";
      return "开始呼叫";
    },
    seatStatusClass() {
      return this.isSeatLoggedIn ? "success" : "error";
    },
    seatStatusText() {
      return this.isSeatLoggedIn ? "已签入" : "未签入";
    },
    callStatusClass() {
      switch (this.callStatus) {
        case "connected":
          return "success";
        case "calling":
          return "warning";
        default:
          return "idle";
      }
    },
    callStatusText() {
      switch (this.callStatus) {
        case "connected":
          return "通话中";
        case "calling":
          return "呼叫中";
        default:
          return "空闲";
      }
    },
  },
  watch: {
    initialPhone: {
      immediate: true,
      handler(newVal) {
        if (newVal) {
          this.customerPhone = newVal;
        }
      },
    },
  },
  methods: {
    handleCall() {
      if (!this.canMakeCall) return;
      this.$refs.callComponent.callout(this.customerPhone);
      this.startCallTimer();
    },
    handleHangup() {
      this.$refs.callComponent.hangup();
      this.stopCallTimer();
      this.callStatus = "idle";
    },
    onSeatStatusChange(status) {
      this.isSeatLoggedIn = status.isLoggedIn;
    },
    onCallStatusChange(status) {
      this.callStatus = status.status;
      if (status.status === "connected") {
        this.startCallTimer();
      } else if (status.status === "idle") {
        this.stopCallTimer();
      }
    },
    onCallError(error) {
      this.lastError = error;
      this.$emit("error", error);
    },
    startCallTimer() {
      this.callStartTime = new Date();
      this.durationTimer = setInterval(() => {
        if (this.callStartTime) {
          const duration = Math.floor((new Date() - this.callStartTime) / 1000);
          const minutes = Math.floor(duration / 60)
            .toString()
            .padStart(2, "0");
          const seconds = (duration % 60).toString().padStart(2, "0");
          this.callDuration = `${minutes}:${seconds}`;
        }
      }, 1000);
    },
    stopCallTimer() {
      if (this.durationTimer) {
        clearInterval(this.durationTimer);
        this.durationTimer = null;
      }
      this.callDuration = "00:00";
      this.callStartTime = null;
    },
    handleSeatBusy() {
      this.$refs.callComponent.afk();
    },
    handleSeatReady() {
      this.$refs.callComponent.online();
    },
    handleHold() {
      this.$refs.callComponent.hold();
    },
    handleResume() {
      this.$refs.callComponent.holdresume();
    },
    // æä¾›ç»™çˆ¶ç»„件调用的方法
    setPhoneNumber(phone) {
      this.customerPhone = phone;
    },
    autoCall(phone) {
      this.setPhoneNumber(phone);
      this.$nextTick(() => {
        this.handleCall();
      });
    },
  },
  beforeUnmount() {
    this.stopCallTimer();
  },
};
</script>
<style lang="scss" scoped>
.call-center-container {
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 0;
  background: #f8fafc;
}
// æŽ§åˆ¶åŒºåŸŸæ ·å¼
.control-section {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  padding: 20px;
    padding-bottom: 0;
  @media (max-width: 1024px) {
    grid-template-columns: 1fr;
  }
}
.section-title {
  font-size: 16px;
  font-weight: 600;
  color: #1e293b;
  margin-bottom: 16px;
  display: flex;
  align-items: center;
  &::before {
    content: "";
    width: 3px;
    height: 16px;
    background: #3b82f6;
    margin-right: 8px;
    border-radius: 2px;
  }
}
// å¡ç‰‡é€šç”¨æ ·å¼
.phone-control-card,
.status-card {
  background: white;
  padding: 20px;
  border-radius: 12px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  border: 1px solid #e2e8f0;
}
// è¡¨å•控件样式
.form-field {
  margin-bottom: 20px;
}
.form-label {
  display: block;
  font-weight: 500;
  color: #475569;
  margin-bottom: 8px;
  font-size: 14px;
}
.phone-input {
  width: 100%;
  padding: 12px;
  border: 2px solid #e2e8f0;
  border-radius: 8px;
  font-size: 14px;
  transition: all 0.3s ease;
  &:focus {
    outline: none;
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  }
  &:disabled {
    background-color: #f8fafc;
    color: #94a3b8;
    cursor: not-allowed;
  }
}
.button-group {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}
// æŒ‰é’®åŸºç¡€æ ·å¼
.call-btn,
.hangup-btn {
  padding: 12px 28px; /* å¢žåŠ å†…è¾¹è·ï¼Œä½¿æŒ‰é’®æ›´å¤§æ–¹ */
  border: none;
  border-radius: 12px; /* ä½¿ç”¨æ›´å¤§çš„圆角,创造“药丸”形 */
  cursor: pointer;
  font-size: 14px;
  font-weight: 600; /* å­—体加粗 */
  transition: all 0.3s ease; /* å¹³æ»‘过渡所有属性 */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px; /* å›¾æ ‡å’Œæ–‡å­—的间距 */
  min-width: 120px; /* è®¾ç½®æœ€å°å®½åº¦ */
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1); /* å¤šå±‚阴影增强立体感 */
}
.call-btn {
  background: linear-gradient(135deg, #10b981, #059669); /* ç»¿è‰²æ¸å˜ */
  color: white;
}
.call-btn:hover:not(.disabled) {
  transform: translateY(-2px); /* æ‚¬åœæ—¶è½»å¾®ä¸Šæµ® */
  box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4); /* æ‚¬åœæ—¶é˜´å½±æ›´æ˜Žæ˜¾ */
}
.call-btn.calling {
  background: linear-gradient(135deg, #f59e0b, #d97706); /* å‘¼å«ä¸­çŠ¶æ€æ”¹ä¸ºæ©™è‰²æ¸å˜ */
  animation: pulse 1.5s infinite; /* å‘¼å«ä¸­æ·»åŠ å‘¼å¸è„‰å†²åŠ¨ç”» */
}
.hangup-btn {
  background: linear-gradient(135deg, #ef4444, #dc2626); /* çº¢è‰²æ¸å˜ */
  color: white;
}
.hangup-btn:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
}
/* ç¦ç”¨çŠ¶æ€ */
.call-btn.disabled,
.hangup-btn:disabled {
  background: #cbd5e1 !important;
  cursor: not-allowed;
  transform: none !important;
  box-shadow: none !important;
}
/* è„‰å†²åŠ¨ç”»å®šä¹‰ */
@keyframes pulse {
  0% { box-shadow: 0 4px 12px rgba(245, 158, 11, 0.5); }
  50% { box-shadow: 0 4px 20px rgba(245, 158, 11, 0.8); }
  100% { box-shadow: 0 4px 12px rgba(245, 158, 11, 0.5); }
}
// çŠ¶æ€æ˜¾ç¤ºæ ·å¼
.status-grid {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.status-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px;
  background: #f8fafc;
  border-radius: 8px;
}
.status-label {
  font-size: 14px;
  color: #475569;
  font-weight: 500;
}
.status-indicator {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 500;
  padding: 4px 12px;
  border-radius: 20px;
}
.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  animation: pulse 2s infinite;
}
.status-indicator.success {
  background: #f0fdf4;
  color: #059669;
  .status-dot {
    background: #059669;
  }
}
.status-indicator.error {
  background: #fef2f2;
  color: #dc2626;
  .status-dot {
    background: #dc2626;
  }
}
.status-indicator.warning {
  background: #fffbeb;
  color: #d97706;
  .status-dot {
    background: #f59e0b;
  }
}
.status-indicator.idle {
  background: #f8fafc;
  color: #64748b;
  .status-dot {
    background: #94a3b8;
  }
}
.duration-display {
  font-family: 'Courier New', monospace;
  font-weight: 600;
  color: #059669;
  font-size: 14px;
}
// è°ƒè¯•面板样式
.debug-section {
  background: white;
  margin: 0 20px 20px;
  padding: 20px;
  border-radius: 12px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.debug-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  padding-right: 20px;
}
.debug-title {
 font-size: 16px;
  font-weight: 600;
  color: #1e293b;
  margin-bottom: 16px;
  display: flex;
  align-items: center;
  &::before {
    content: "";
    width: 3px;
    height: 16px;
    background: #3b82f6;
    margin-right: 8px;
    border-radius: 2px;
  }
}
.debug-subtitle {
  font-size: 12px;
  color: #64748b;
}
.debug-content {
  padding: 0;
}
.call-component {
  min-height: 200px;
  border-top: 1px solid #e2e8f0;
}
// åŠ¨ç”»å®šä¹‰
@keyframes pulse {
  0% { opacity: 1; }
  50% { opacity: 0.5; }
  100% { opacity: 1; }
}
// å“åº”式设计
@media (max-width: 768px) {
  .control-section {
    padding: 16px;
    grid-template-columns: 1fr;
  }
  .phone-control-card,
  .status-card {
    padding: 16px;
  }
  .button-group {
    flex-direction: column;
  }
  .status-item {
    flex-direction: column;
    gap: 8px;
    align-items: flex-start;
  }
  .debug-section {
    margin: 0 16px 16px;
  }
}
@media (max-width: 480px) {
  .call-center-container {
    gap: 12px;
  }
  .control-section {
    padding: 12px;
  }
}
</style>
src/views/followvisit/discharge/ClickCall copy.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1258 @@
<template>
  <div class="websocket-demo">
    <div>
      <h3>Websocket接口测试DEMO</h3>
      <div class="config-area">
        <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"
          />
          <label>坐席工号</label>
          <input type="text" v-model="config.seatname" placeholder="8000" />
          <label>坐席分机</label>
          <input type="text" v-model="config.seatnum" placeholder="8000" />
          <label>密码</label>
          <input type="text" v-model="config.password" placeholder="123456" />
        </div>
        <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" />
          <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>
        </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>
        </div>
      </div>
      <!-- æ—¥å¿—显示区域 -->
      <h3>协议日志区<button @click="testclear">清除</button></h3>
      <div id="msg" class="log-area">{{ logs }}</div>
    </div>
  </div>
</template>
<script>
import { CallsetState, CallgetList } from "@/api/AiCentre/index";
export default {
  name: "WebsocketDemo",
  data() {
    return {
      config: {
        cti_ws_url: "wss://9.208.2.190:8092/cal-api/",
        seatname: "8000",
        seatnum: "8000",
        password: "123456",
        phone: "10086",
        uuid: "",
        other: "8001",
        group: "a3",
        paramid: "3",
      },
      randomNum: "",
      randomID: "",
      logs: "",
      ws: null,
      isConnected: false,
    };
  },
  mounted() {
    this.CallgetList();
    this.initializeWebSocket();
  },
  beforeUnmount() {
    this.disconnectWebSocket();
  },
  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/"
          : "ws://40.78.0.169:6688";
        if (typeof window.WebSocket === "undefined") {
          this.addLog("错误: æµè§ˆå™¨ä¸æ”¯æŒWebSocket");
          return;
        }
        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 CallgetList() {
      try {
        const res = await CallgetList();
        this.randomNum = res.data[0].tel;
        this.randomID = res.data[0].id;
        // æ­£ç¡®è®¾ç½® sipUri
        this.config.seatname = randomNum;
        this.config.seatnum = randomNum;
        this.startCallsetState();
      } catch (error) {
        console.error("获取座席号失败:", error);
        this.updateStatus("failed", "获取座席号失败");
      }
    },
    //使用座席号
    async startCallsetState() {
      try {
        await CallsetState({ id: this.randomID, state: 1 });
        console.log("座席号状态更新为使用中");
      } catch (error) {
        console.error("更新座席号状态失败:", error);
      }
    },
    //释放座席号
    async overCallsetState() {
      try {
        if (this.randomID) {
          await CallsetState({ id: this.randomID, state: 0 });
          console.log("座席号状态更新为可用");
        }
      } catch (error) {
        console.error("释放座席号失败:", error);
      }
    },
    // è¿žæŽ¥WebSocket
    connectWebSocket() {
      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
        this.addLog("WebSocket已连接");
        return;
      }
      try {
        let wsUrl = this.config.cti_ws_url;
        // ç¡®ä¿HTTPS页面使用WSS
        if (
          window.location.protocol === "https:" &&
          wsUrl.startsWith("ws://")
        ) {
          wsUrl = wsUrl.replace("ws://", "wss://");
        }
        this.ws = new WebSocket(wsUrl);
        this.ws.onopen = () => {
          this.isConnected = true;
          this.addLog("WebSocket连接成功");
        };
        this.ws.onmessage = (event) => {
          this.handleWebSocketMessage(event);
        };
        this.ws.onclose = (event) => {
          this.isConnected = false;
          this.addLog(`WebSocket连接关闭: ${event.code} ${event.reason}`);
          // è‡ªåŠ¨é‡è¿ž
          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}`);
        }
      };
      reader.readAsText(event.data);
    },
    // æ–­å¼€WebSocket连接
    disconnectWebSocket() {
      if (this.ws) {
        this.ws.close();
        this.ws = null;
        this.isConnected = false;
        this.addLog("WebSocket已断开");
      }
    },
    // å‘送WebSocket消息
    sendWebSocketMessage(message) {
      if (!this.isConnected || !this.ws) {
        this.addLog("错误: WebSocket未连接");
        return false;
      }
      try {
        const messageStr =
          typeof message === "string" ? message : JSON.stringify(message);
        this.ws.send(messageStr);
        this.addLog(`发送消息: ${messageStr}`);
        return true;
      } catch (error) {
        this.addLog(`发送消息失败: ${error.message}`);
        return false;
      }
    },
    // éªŒè¯å‚æ•°
    validateParams(params, requiredFields) {
      for (const field of requiredFields) {
        if (!params[field] || params[field].toString().trim() === "") {
          this.addLog(`错误: ${field} ä¸èƒ½ä¸ºç©º`);
          return false;
        }
      }
      return true;
    },
    // ==================== WebSocket.js åŠŸèƒ½æ•´åˆ ====================
    // ç­¾å…¥
    seatlogin() {
      const { seatname, seatnum, password, cti_ws_url } = this.config;
      if (
        !this.validateParams({ seatname, seatnum }, ["seatname", "seatnum"])
      ) {
        return;
      }
      // é‡æ–°è¿žæŽ¥WebSocket(原js文件中的逻辑)
      this.connectWebSocket();
      setTimeout(() => {
        const protocol = {
          cmd: "system",
          action: "seatlogin",
          seatname: seatname,
          seatnum: seatnum,
          password: password,
          timestamp: Date.now(),
        };
        this.sendWebSocketMessage(protocol);
      }, 1000);
    },
    // ç­¾å‡º
    seatlogout() {
      const { seatname, seatnum } = this.config;
      if (
        !this.validateParams({ seatname, seatnum }, ["seatname", "seatnum"])
      ) {
        return;
      }
      const protocol = {
        cmd: "system",
        action: "seatlogout",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
      this.ws.close();
    },
    // ç¤ºå¿™
    afk() {
      const { seatname, seatnum } = this.config;
      if (
        !this.validateParams({ seatname, seatnum }, ["seatname", "seatnum"])
      ) {
        return;
      }
      const protocol = {
        cmd: "system",
        action: "afk",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // ç¤ºé—²
    online() {
      const { seatname, seatnum } = this.config;
      if (
        !this.validateParams({ seatname, seatnum }, ["seatname", "seatnum"])
      ) {
        return;
      }
      const protocol = {
        cmd: "system",
        action: "online",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // ä»£ç­”
    pickup() {
      const { seatname, seatnum } = this.config;
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "pickup",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // æŒ‚机
    hangup() {
      const { seatname, seatnum } = this.config;
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "hangup",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // å¤–呼
    callout() {
      const { seatname, seatnum, phone } = this.config;
      if (!this.validateParams({ seatnum, phone }, ["seatnum", "phone"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "callout",
        phone: phone,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // é€šè¯è½¬ç§»
    transfer() {
      const { seatname, seatnum, phone, uuid } = this.config;
      if (
        !this.validateParams({ seatnum, phone, uuid }, [
          "seatnum",
          "phone",
          "uuid",
        ])
      ) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "transfer",
        uuid: uuid,
        phone: phone,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // é€šè¯è½¬ç§»æ”¶å›ž
    transferresume() {
      const { seatname, seatnum, phone, uuid } = this.config;
      if (
        !this.validateParams({ seatnum, phone, uuid }, [
          "seatnum",
          "phone",
          "uuid",
        ])
      ) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "transferresume",
        uuid: uuid,
        phone: phone,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // é€šè¯ä¿æŒ
    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,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // é€šè¯ä¿æŒæ”¶å›ž
    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,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // é€šè¯å¼ºæ‹†
    remove() {
      const { seatname, seatnum, phone } = this.config;
      if (!this.validateParams({ seatnum, phone }, ["seatnum", "phone"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "remove",
        phone: phone,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // é€šè¯å¼ºæ’
    insert() {
      const { seatname, seatnum, phone } = this.config;
      if (!this.validateParams({ seatnum, phone }, ["seatnum", "phone"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "insert",
        phone: phone,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // ç›‘听
    monitor() {
      const { seatname, seatnum, phone } = this.config;
      if (!this.validateParams({ seatnum, phone }, ["seatnum", "phone"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "monitor",
        phone: phone,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // ç›‘听转通话
    monitor_to_talk() {
      const { seatname, seatnum, phone } = this.config;
      if (!this.validateParams({ seatnum, phone }, ["seatnum", "phone"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "monitor_to_talk",
        phone: phone,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // ç›‘听结束
    monitor_end() {
      const { seatname, seatnum, phone } = this.config;
      if (!this.validateParams({ seatnum, phone }, ["seatnum", "phone"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "monitor_end",
        phone: phone,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // é€‰æ‹©é€šè¯
    choosecall() {
      const { seatname, seatnum, uuid } = this.config;
      if (!this.validateParams({ seatnum, uuid }, ["seatnum", "uuid"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "choosecall",
        uuid: uuid,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // ä»£æŽ¥
    replacecall() {
      const { seatname, seatnum, phone } = this.config;
      if (!this.validateParams({ seatnum, phone }, ["seatnum", "phone"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "replacecall",
        phone: phone,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // ä¸‰æ–¹é€šè¯
    three() {
      const { seatname, seatnum, phone } = this.config;
      if (!this.validateParams({ seatnum, phone }, ["seatnum", "phone"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "three",
        phone: phone,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // å’¨è¯¢å¼€å§‹
    handoff_ready() {
      const { seatname, seatnum, uuid } = this.config;
      if (!this.validateParams({ seatnum, uuid }, ["seatnum", "uuid"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "handoff_ready",
        uuid: uuid,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // å’¨è¯¢å‘¼å«
    handoff_call() {
      const { seatname, seatnum, other, uuid } = this.config;
      if (
        !this.validateParams({ seatnum, other, uuid }, [
          "seatnum",
          "other",
          "uuid",
        ])
      ) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "handoff_call",
        uuid: uuid,
        phone: other,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // å’¨è¯¢æ”¶å›ž
    handoff_resume() {
      const { seatname, seatnum, uuid } = this.config;
      if (!this.validateParams({ seatnum, uuid }, ["seatnum", "uuid"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "handoff_resume",
        uuid: uuid,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // å’¨è¯¢è½¬ç§»
    handoff_transfer() {
      const { seatname, seatnum, other, uuid } = this.config;
      if (
        !this.validateParams({ seatnum, other, uuid }, [
          "seatnum",
          "other",
          "uuid",
        ])
      ) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "handoff_transfer",
        uuid: uuid,
        phone: other,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // å’¨è¯¢ä¸‰æ–¹
    handoff_three() {
      const { seatname, seatnum, uuid } = this.config;
      if (!this.validateParams({ seatnum, uuid }, ["seatnum", "uuid"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "handoff_three",
        uuid: uuid,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // å¼€å§‹é€šè¯å½•音
    record_start() {
      const { seatname, seatnum, uuid } = this.config;
      if (!this.validateParams({ seatnum, uuid }, ["seatnum", "uuid"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "record_start",
        uuid: uuid,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // åœæ­¢é€šè¯å½•音
    record_stop() {
      const { seatname, seatnum, uuid } = this.config;
      if (!this.validateParams({ seatnum, uuid }, ["seatnum", "uuid"])) {
        return;
      }
      const protocol = {
        cmd: "control",
        action: "record_stop",
        uuid: uuid,
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // æ‰“开坐席状态
    openseatlist() {
      const { seatname, seatnum } = this.config;
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "status",
        action: "openseatlist",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // å…³é—­åå¸­çŠ¶æ€
    closeseatlist() {
      const { seatname, seatnum } = this.config;
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "status",
        action: "closeseatlist",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // æ‰“开队列信息
    openqueues() {
      const { seatname, seatnum } = this.config;
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "status",
        action: "openqueues",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // å…³é—­é˜Ÿåˆ—信息
    closequeues() {
      const { seatname, seatnum } = this.config;
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "status",
        action: "closequeues",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // æ‰“开通话信息
    opencalllist() {
      const { seatname, seatnum } = this.config;
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "status",
        action: "opencalllist",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // å…³é—­é€šè¯ä¿¡æ¯
    closecalllist() {
      const { seatname, seatnum } = this.config;
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "status",
        action: "closecalllist",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // æ‰“开路由信息
    openroutelist() {
      const { seatname, seatnum } = this.config;
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "status",
        action: "openroutelist",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // å…³é—­è·¯ç”±ä¿¡æ¯
    closeroutelist() {
      const { seatname, seatnum } = this.config;
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "status",
        action: "closeroutelist",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // èŽ·å–åå¸­ä¿¡æ¯
    seatlist() {
      const { group } = this.config;
      if (!this.validateParams({ group }, ["group"])) {
        return;
      }
      const protocol = {
        cmd: "status",
        action: "seatlist",
        group: group,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // èŽ·å–é˜Ÿåˆ—ä¿¡æ¯
    queues() {
      const protocol = {
        cmd: "status",
        action: "queues",
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // èŽ·å–é€šè¯ä¿¡æ¯
    calllist() {
      const protocol = {
        cmd: "status",
        action: "calllist",
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // èŽ·å–è·¯ç”±ä¿¡æ¯
    routelist() {
      const protocol = {
        cmd: "status",
        action: "routelist",
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // èŽ·å–å¤–å‘¼å‚æ•°ä¿¡æ¯
    batch() {
      const { paramid } = this.config;
      if (!this.validateParams({ paramid }, ["paramid"])) {
        return;
      }
      const protocol = {
        cmd: "status",
        action: "batch",
        paramid: paramid,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // å¼€å§‹å¤–呼任务
    batch_start() {
      const { seatname, seatnum } = this.config;
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "system",
        action: "batch_start",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // åœæ­¢å¤–呼任务
    batch_stop() {
      const { seatname, seatnum } = this.config;
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "system",
        action: "batch_stop",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // å¿ƒè·³åŒ…
    keepalive(seatname, seatnum) {
      if (!this.validateParams({ seatnum }, ["seatnum"])) {
        return;
      }
      const protocol = {
        cmd: "system",
        action: "keepalive",
        seatname: seatname,
        seatnum: seatnum,
        timestamp: Date.now(),
      };
      this.sendWebSocketMessage(protocol);
    },
    // æ¸…除日志
    testclear() {
      this.logs = "";
      this.addLog("日志已清除");
    },
    // æ·»åŠ æ—¥å¿—
    addLog(message) {
      const timestamp = new Date().toLocaleTimeString();
      this.logs += `[${timestamp}] ${message}\n`;
      // é™åˆ¶æ—¥å¿—长度,防止内存溢出
      const logLines = this.logs.split("\n");
      if (logLines.length > 100) {
        this.logs = logLines.slice(-50).join("\n");
      }
    },
  },
};
</script>
<style scoped>
.websocket-demo {
  font-family: Arial, sans-serif;
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}
.config-area {
  margin-bottom: 20px;
  padding: 15px;
  border: 1px solid #ddd;
  border-radius: 4px;
  background-color: #f9f9f9;
}
.input-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  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;
}
.button-row button {
  padding: 8px 15px;
  border: 1px solid #ccc;
  border-radius: 3px;
  background-color: #f0f0f0;
  cursor: pointer;
  transition: background-color 0.3s;
  font-size: 12px;
}
.button-row button:hover {
  background-color: #e0e0e0;
}
.button-row button:active {
  background-color: #d0d0d0;
  transform: translateY(1px);
}
.log-area {
  height: 300px;
  overflow-y: auto;
  border: 1px solid #ccc;
  padding: 10px;
  background-color: #f5f5f5;
  white-space: pre-wrap;
  font-family: "Courier New", monospace;
  font-size: 12px;
  line-height: 1.4;
}
h3 {
  color: #333;
  border-bottom: 2px solid #eee;
  padding-bottom: 10px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
h3 button {
  padding: 5px 10px;
  font-size: 12px;
  background-color: #f0f0f0;
  border: 1px solid #ccc;
  border-radius: 3px;
  cursor: pointer;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .websocket-demo {
    padding: 10px;
  }
  .input-group {
    flex-direction: column;
    align-items: flex-start;
  }
  .input-group input {
    width: 100%;
  }
  .button-row {
    flex-direction: column;
  }
  .button-row button {
    width: 100%;
    margin-bottom: 5px;
  }
}
</style>
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>
      <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}`);
          // è‡ªåŠ¨é‡è¿ž
          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) {
      try {
        // æ£€æŸ¥æ•°æ®ç±»åž‹ï¼šå¯èƒ½æ˜¯å­—符串或Blob
        if (event.data instanceof Blob) {
          // å¤„理二进制数据(Blob)
      const reader = new FileReader();
      reader.onloadend = (e) => {
        const message = reader.result;
        this.addLog(`收到消息: ${message}`);
          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(message);
        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();
        }
          // è‡ªåŠ¨è®¾ç½®UUID
        // å¤„理签入响应
        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(`消息解析错误: ${error.message}`);
        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;
src/views/followvisit/record/detailpage/index.vue
@@ -1013,6 +1013,18 @@
        <el-button type="primary" @click="setupsubtask">确认创建服务</el-button>
      </div>
    </el-dialog>
    <div class="main-content" v-if="orgname == '景宁畲族自治县人民医院'">
      <!-- <el-button @click="CaldialogVisible = true">打开弹框</el-button> -->
      <!-- å¼¹æ¡†è°ƒç”¨ -->
      <el-dialog
        title="呼叫功能框"
        :visible.sync="CaldialogVisible"
        width="60%"
      >
        <CallCenterLs ref="callCenterModal" :initial-phone="currentPhoneNumber" />
      </el-dialog>
    </div>
  </div>
</template>
@@ -1038,10 +1050,12 @@
} from "@/api/patient/homepage";
import CallButton from "@/components/CallButton";
import MergeAndModify from "./MergeAndModify.vue";
import CallCenterLs from "@/components/CallCenterLs";
export default {
  components: {
    CallButton,
    MergeAndModify,
    CallCenterLs,
  },
  directives: {
    numericOnly: {
@@ -1125,6 +1139,7 @@
      // å·²æœ‰æ•°æ®...
      callStatus: "idle", // idle, calling, connected, ended, failed
      isEndingCall: false,
      CaldialogVisible: false,
      currentCall: null, // å½“前通话对象
      input: "今天身体还不错",
      radio: "2",
@@ -1673,8 +1688,13 @@
        this.$message.error("请输入正确的手机号码");
        return;
      }
      this.currentPhoneNumber = phone;
      // å‘¼å«åˆ¤æ–­
      if (this.orgname == "景宁畲族自治县人民医院") {
        this.CaldialogVisible = true;
        return
      }
      this.callType = type;
      this.callStatus = "calling";
vue.config.js
@@ -37,8 +37,8 @@
      [process.env.VUE_APP_BASE_API]: {
        // target: `https://www.health-y.cn/lssf`,
        // target: `http://192.168.100.129:8095`,
        target: `http://192.168.100.10:8096`,
        // target:`http://localhost:8095`,
        // target: `http://192.168.100.10:8096`,
        target:`http://localhost:8095`,
        // target:`http://35z1t16164.qicp.vip`,
        // target: `http://192.168.100.193:8095`,
        // target: `http://192.168.101.166:8093`,