WXL (wul)
14 小时以前 1aaa2392eea4694076c6a329a5b88436ceb50194
电话更新
已添加2个文件
已删除1个文件
已修改4个文件
928 ■■■■■ 文件已修改
src/api/AiCentre/index.js 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/AiCentre/phoneCall.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/CallButton/index.vue 80 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/sipService-cs.js 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/sipService-zs.js 253 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/sipService.js 317 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/record/detailpage/index.vue 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/AiCentre/index.js
@@ -1,14 +1,14 @@
export * from './indicator'
export * from './Problemspeaking'
export * from './Followup'
export * from './general'
export * from './publicity'
export * from './Qtemplate'
export * from './questionnaire'
export * from './SingleTask'
export * from './external'
export * from './patientexternal'
export * from './EChartsdata'
export * from './satisfactionse'
export * from './satisfaction'
export * from "./indicator";
export * from "./Problemspeaking";
export * from "./Followup";
export * from "./general";
export * from "./publicity";
export * from "./Qtemplate";
export * from "./questionnaire";
export * from "./SingleTask";
export * from "./external";
export * from "./patientexternal";
export * from "./EChartsdata";
export * from "./satisfactionse";
export * from "./satisfaction";
export * from "./phoneCall";
src/api/AiCentre/phoneCall.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
import request from "@/utils/request";
// åˆ é™¤å¤–部患者表
export function CallgetList() {
  return request({
    url: "/smartor/ServiceTelInfo/getList",
    method: "get",
  });
}
// æŸ¥è¯¢å¤–部患者表
export function CallsetState(data) {
  return request({
    url: "/smartor/ServiceTelInfo/setState",
    method: "get",
    params: data,
  });
}
src/components/CallButton/index.vue
@@ -28,6 +28,7 @@
<script>
import sipService from "@/utils/sipService";
import { CallsetState, CallgetList } from "@/api/AiCentre/index";
export default {
  props: {
@@ -37,15 +38,17 @@
    },
  },
  data() {
    const randomNum = Math.floor(Math.random() * 20) + 1000; // å†…部定义
    const randomNum = Math.floor(Math.random() * 20) + 1000; // å®šä¹‰éšæœºåˆ†æœºå·
    return {
      isCalling: false,
      randomNum: randomNum,
      randomID: null,
      callStatus: "idle", // idle, calling, connected, ended
      sipStatus: "未连接",
      sipStatusClass: "status-disconnected",
      sipConfig: {
        wsUrl: "wss://192.168.100.6:7443",
        sipUri: `${randomNum}` + "@192.168.100.6",
        sipUri: "",
        password: "Smartor@2023",
        displayName: "Web å°é¾™",
        // realm: "9.208.5.18:8090",
@@ -78,13 +81,22 @@
      return this.isCalling ? "通话中..." : "一键呼叫";
    },
  },
  mounted() {
    console.log('当前分机号',this.sipConfig);
  created() {
    // CallgetList();
  },
  async mounted() {
    await this.CallgetList();
    sipService.init(this.sipConfig);
    // è®¾ç½®çŠ¶æ€å›žè°ƒ
    sipService.onStatusChange = (status) => {
      this.sipStatus = status.text;
      this.sipStatusClass = `status-${status.type}`;
      // å¤„理注册失败和断开连接情况
      if (status.type === "failed" || status.type === "disconnected") {
        this.overCallsetState(); // é‡Šæ”¾åˆ†æœºå·
      }
    };
    // ç›‘听通话状态变化
@@ -115,7 +127,7 @@
        this.isCalling = true;
        console.log("开始呼叫:", sipService);
        await sipService.makeCall("0"+this.phoneNumber);
        await sipService.makeCall("0" + this.phoneNumber);
      } catch (error) {
        let registrationTime = Date.now(); // è®°å½•注销成功时间
        console.log(registrationTime, "呼叫失败时间");
@@ -123,7 +135,7 @@
        // this.callStatus = "ended";
        // this.isCalling = false;
        //this.$message.error(`呼叫失败: ${error.message}`);
          try {
        try {
          // å…ˆæ£€æŸ¥æ˜¯å¦å¯ä»¥å‘¼å«
          const { canCall, reason } = sipService.canMakeCall();
          if (!canCall) {
@@ -133,19 +145,69 @@
          this.isCalling = true;
          console.log("开始呼叫:", sipService);
          await sipService.makeCall("0"+this.phoneNumber);
          await sipService.makeCall("0" + this.phoneNumber);
        } catch (error) {
           this.callStatus = "ended";
           this.isCalling = false;
          this.callStatus = "ended";
          this.isCalling = false;
        }
      }
    },
    // æŸ¥è¯¢å¯ç”¨åˆ†æœºå·
    async CallgetList() {
      try {
        const res = await CallgetList();
        this.randomNum = res.data[0].tel;
        this.randomID = res.data[0].id;
        // æ­£ç¡®è®¾ç½® sipUri
        this.sipConfig.sipUri = `${this.randomNum}@192.168.100.6`;
        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);
      }
    },
    endCall() {
      sipService.endCall();
      this.callStatus = "ended";
      this.isCalling = false;
    },
    cleanupResources() {
      // ç»“束通话
      if (this.isCalling) {
        sipService.endCall();
      }
      // é‡Šæ”¾åˆ†æœºå·
      this.overCallsetState();
      // æ–­å¼€ SIP è¿žæŽ¥
      if (sipService.ua) {
        sipService.ua.stop();
      }
    },
  },
  beforeUnmount() {
    // ç»„件销毁时确保释放资源
    this.cleanupResources();
  },
};
</script>
src/utils/sipService-cs.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,201 @@
import JsSIP from "jssip";
class SipService {
  constructor() {
    this.ua = null;
    this.currentSession = null;
    this.onStatusChange = null; // çŠ¶æ€å˜åŒ–å›žè°ƒ
    this.onCallStatusChange = null; // æ–°å¢žé€šè¯çŠ¶æ€å›žè°ƒ
  }
  // åˆå§‹åŒ–SIP客户端
  init(config) {
    try {
      this.updateStatus("connecting", "连接中...");
console.log(config);
      this.ua = new JsSIP.UA({
        sockets: [new JsSIP.WebSocketInterface(config.wsUrl)],
        uri: config.sipUri,
        password: config.password,
        display_name: config.displayName,
        iceservers: [],
        // realm: config.realm,
        register: true,
        session_expires: 180,
        sessionTimersExpires: 300, // è®¾ç½® Session-Expires=120(必须 >= Min-SE)
        extraHeaders: [
          "Min-SE: 120", // å¯é€‰ï¼šæ˜¾å¼å‘Šè¯‰æœåŠ¡å™¨ä½ æ”¯æŒçš„æœ€å°å€¼
        ],
        register_expires: 300, // æ³¨å†Œæœ‰æ•ˆæœŸ(秒)
        connection_recovery_min_interval: 2, // æœ€å°é‡è¿žé—´éš”
        connection_recovery_max_interval: 30, // æœ€å¤§é‡è¿žé—´éš”
      });
      this.ua.start();
      // æ³¨å†Œäº‹ä»¶ç›‘听
      this.ua.on("registered", () => {
        this.updateStatus("registered", "已注册");
      });
      this.ua.on("registrationFailed", (e) => {
        this.updateStatus("failed", `注册失败: ${e.cause}`);
      });
      this.ua.on("disconnected", () => {
        this.updateStatus("disconnected", "连接断开");
      });
      this.ua.on("connected", () => {
        this.updateStatus("connecting", "重新连接中...");
      });
      // ç›‘听来电
      this.ua.on("newRTCSession", (data) => {
        this.handleIncomingCall(data.session);
      });
    } catch (error) {
      this.updateStatus("failed", `初始化失败: ${error.message}`);
      console.error("SIP初始化失败:", error);
    }
  }
  handleIncomingCall(session) {
    if (session.direction === "incoming") {
      console.log("来电:", session.remote_identity.uri.toString());
      // å¯ä»¥åœ¨è¿™é‡Œè§¦å‘ UI é€šçŸ¥
      if (this.onIncomingCall) {
        this.onIncomingCall(session);
      }
    }
  }
  // æ›´æ–°çŠ¶æ€å¹¶é€šçŸ¥UI
  updateStatus(type, text) {
    console.log(`SIP状态更新: ${type} - ${text}`);
    if (this.onStatusChange) {
      this.onStatusChange({ type, text });
    }
  }
  // ä¸€é”®æ‹¨å· - å¢žåŠ æ³¨å†ŒçŠ¶æ€æ£€æŸ¥
  makeCall(targetNumber) {
    if (!this.ua) {
      throw new Error("SIP客户端未初始化");
    }
    if (!this.ua.isRegistered()) {
      throw new Error("SIP未注册,无法呼叫");
    }
    const options = {
      sessionTimers: true,
      sessionTimersExpires: 300,
      extraHeaders: [
        "Min-SE: 120",
        "Route: <sip:@192.168.100.6>",
        "Accept: application/sdp",
        "Supported: replaces, timer",
        "Allow: INVITE, ACK, BYE, CANCEL, OPTIONS",
      ],
      eventHandlers: {
        progress: (e) => {
          this.updateCallStatus("calling", "呼叫中...");
        },
        failed: (e) => {
          this.updateCallStatus("ended", `呼叫失败: ${e.cause}`);
        },
        ended: (e) => {
          this.updateCallStatus("ended", "通话结束");
        },
        confirmed: (e) => {
          this.updateCallStatus("connected", "通话已接通");
        },
      },
      mediaConstraints: {
        audio: true,
        video: false,
      },
      rtcOfferConstraints: {
        offerToReceiveAudio: 1,
        offerToReceiveVideo: 0,
        mandatory: {
          OfferToReceiveAudio: true,
          OfferToReceiveVideo: false,
        },
      },
      pcConfig: {
        iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
        iceTransportPolicy: "all",
        bundlePolicy: "balanced",
        rtcpMuxPolicy: "require",
        codecs: {
          audio: [
            { name: "PCMU", clockRate: 8000, payloadType: 0 },
            { name: "PCMA", clockRate: 8000, payloadType: 8 },
          ],
          video: [],
        },
      },
    };
    this.currentSession = this.ua.call(
      `sip:${targetNumber}@192.168.100.6`,
      options
    );
    // åœ¨ä¼šè¯åˆ›å»ºåŽä¿®æ”¹ SDP
    this.currentSession.on("peerconnection", (pc) => {
       this.updateCallStatus('calling', '呼叫中...');
      pc.createOffer = (offerOptions) => {
        return RTCPeerConnection.prototype.createOffer
          .call(pc, offerOptions)
          .then((offer) => {
            const modifiedSdp = offer.sdp
              .replace(/c=IN IP4 192\.168\.100\.10/g, "c=IN IP4 192.168.100.6")
              .replace(/m=audio \d+ RTP\/AVP.*/, "m=audio 7078 RTP/AVP 0 8");
            return new RTCSessionDescription({
              type: "offer",
              sdp: modifiedSdp,
            });
          });
      };
    });
    this.currentSession.on('failed', (e) => {
        this.updateCallStatus('failed', `呼叫失败2: ${e}`);
      });
      this.currentSession.on('ended', () => {
        this.updateCallStatus('ended', '通话已结束');
      });
      this.currentSession.on('confirmed', () => {
        this.updateCallStatus('connected', '通话已接通');
      });
    this.setupAudio(this.currentSession);
  }
  setupAudio(session) {
    session.connection.addEventListener("addstream", (e) => {
      const audioElement = document.getElementById("remoteAudio");
      if (audioElement) {
        audioElement.srcObject = e.stream;
      }
    });
  }
  // æŒ‚断当前通话
  endCall() {
  if (this.currentSession) {
      this.currentSession.terminate();
      this.updateCallStatus('ended', '通话已结束');
      this.currentSession = null;
    }
  }
  // æ–°å¢žæ–¹æ³•:更新通话状态
  updateCallStatus(type, text) {
    console.log(`通话状态更新: ${type} - ${text}`);
    if (this.onCallStatusChange) {
      this.onCallStatusChange({ type, text });
    }
  }
}
export default new SipService();
src/utils/sipService-zs.js
ÎļþÒÑɾ³ý
src/utils/sipService.js
@@ -4,174 +4,211 @@
  constructor() {
    this.ua = null;
    this.currentSession = null;
    this.onStatusChange = null; // çŠ¶æ€å˜åŒ–å›žè°ƒ
    this.onCallStatusChange = null; // æ–°å¢žé€šè¯çŠ¶æ€å›žè°ƒ
    this.onStatusChange = null;
    this.onCallStatusChange = null;
    this.onIncomingCall = null;
    this.isRegistered = false; // æ–°å¢žæ³¨å†ŒçŠ¶æ€æ ‡å¿—
    this.registrationTime = null; // æ–°å¢žæ³¨å†ŒæˆåŠŸæ—¶é—´æˆ³
  }
  // åˆå§‹åŒ–SIP客户端
  init(config) {
    try {
      this.updateStatus("connecting", "连接中...");
      this.updateStatus("connecting", "连接中;...");
      this.ua = new JsSIP.UA({
        sockets: [new JsSIP.WebSocketInterface(config.wsUrl)],
        uri: config.sipUri,
        password: config.password,
        display_name: config.displayName,
        iceservers: [],
        // realm: config.realm,
        iceServers: [],
        register: true,
        session_expires: 180,
        sessionTimersExpires: 300, // è®¾ç½® Session-Expires=120(必须 >= Min-SE)
        extraHeaders: [
          "Min-SE: 120", // å¯é€‰ï¼šæ˜¾å¼å‘Šè¯‰æœåŠ¡å™¨ä½ æ”¯æŒçš„æœ€å°å€¼
        ],
        register_expires: 300, // æ³¨å†Œæœ‰æ•ˆæœŸ(秒)
        connection_recovery_min_interval: 2, // æœ€å°é‡è¿žé—´éš”
        connection_recovery_max_interval: 30, // æœ€å¤§é‡è¿žé—´éš”
        sessionExpires: 1800,
        minSessionExpires: 90,
        register_expires: 300,
      });
      this.ua.start();
      // æ³¨å†Œäº‹ä»¶ç›‘听
      // äº‹ä»¶ç›‘听
      this.ua.on("registered", () => {
        this.isRegistered = true;
        this.registrationTime = Date.now(); // è®°å½•注册成功时间
        console.log(this.registrationTime, "注册时间");
        this.updateStatus("registered", "已注册");
      });
      this.ua.on("registrationFailed", (e) => {
        this.isRegistered = false;
        this.updateStatus("failed", `注册失败: ${e.cause}`);
      });
      this.ua.on("disconnected", () => {
        this.updateStatus("disconnected", "连接断开");
      this.ua.on("unregistered", () => {
        this.isRegistered = false;
        let registrationTime = Date.now(); // è®°å½•注销成功时间
        console.log(registrationTime, "注销时间");
        this.updateStatus("disconnected", "已注销");
      });
      this.ua.on("connected", () => {
        this.updateStatus("connecting", "重新连接中...");
      });
      // ç›‘听来电
      this.ua.on("newRTCSession", (data) => {
        this.handleIncomingCall(data.session);
      });
      this.ua.on("disconnected", () =>
        this.updateStatus("disconnected", "连接断开")
      );
      this.ua.on("connected", () =>
        this.updateStatus("connecting", "重新连接中...")
      );
      this.ua.on("newRTCSession", (data) =>
        this.handleIncomingCall(data.session)
      );
    } catch (error) {
      this.updateStatus("failed", `初始化失败: ${error.message}`);
      console.error("SIP初始化失败:", error);
      throw error;
    }
  }
  handleIncomingCall(session) {
    if (session.direction === "incoming") {
      console.log("来电:", session.remote_identity.uri.toString());
      // å¯ä»¥åœ¨è¿™é‡Œè§¦å‘ UI é€šçŸ¥
      if (this.onIncomingCall) {
        this.onIncomingCall(session);
      }
  // æ–°å¢žæ–¹æ³•:检查是否可以呼叫
  canMakeCall(minDelay = 2000) {
    if (!this.isRegistered) {
      return { canCall: false, reason: "SIP未注册,无法呼叫" };
    }
  }
  // æ›´æ–°çŠ¶æ€å¹¶é€šçŸ¥UI
  updateStatus(type, text) {
    console.log(`SIP状态更新: ${type} - ${text}`);
    if (this.onStatusChange) {
      this.onStatusChange({ type, text });
    }
  }
    const now = Date.now();
    const timeSinceRegistration = now - this.registrationTime;
  // ä¸€é”®æ‹¨å· - å¢žåŠ æ³¨å†ŒçŠ¶æ€æ£€æŸ¥
    if (timeSinceRegistration < minDelay) {
      const remaining = minDelay - timeSinceRegistration;
      return {
        canCall: false,
        reason: `注册成功,请等待 ${Math.ceil(remaining / 1000)} ç§’后再呼叫`,
      };
    }
    return { canCall: true, reason: "" };
  }
  makeCall(targetNumber) {
    if (!this.ua) {
      throw new Error("SIP客户端未初始化");
    const { canCall, reason } = this.canMakeCall();
    if (!canCall) {
      return Promise.reject(new Error(reason));
    }
    return new Promise((resolve, reject) => {
      try {
        if (!this.ua) {
          throw new Error("SIP客户端未初始化");
        }
    if (!this.ua.isRegistered()) {
      throw new Error("SIP未注册,无法呼叫");
    }
        if (!this.ua.isRegistered()) {
          throw new Error("SIP未注册,无法呼叫");
        }
    const options = {
      sessionTimers: true,
      sessionTimersExpires: 300,
      extraHeaders: [
        "Min-SE: 120",
        "Route: <sip:@192.168.100.6>",
        "Accept: application/sdp",
        "Supported: replaces, timer",
        "Allow: INVITE, ACK, BYE, CANCEL, OPTIONS",
      ],
      eventHandlers: {
        progress: (e) => {
          this.updateCallStatus("calling", "呼叫中...");
        },
        failed: (e) => {
          this.updateCallStatus("ended", `呼叫失败: ${e.cause}`);
        },
        ended: (e) => {
          this.updateCallStatus("ended", "通话结束");
        },
        confirmed: (e) => {
          this.updateCallStatus("connected", "通话已接通");
        },
      },
      mediaConstraints: {
        audio: true,
        video: false,
      },
      rtcOfferConstraints: {
        offerToReceiveAudio: 1,
        offerToReceiveVideo: 0,
        mandatory: {
          OfferToReceiveAudio: true,
          OfferToReceiveVideo: false,
        },
      },
      pcConfig: {
        iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
        iceTransportPolicy: "all",
        bundlePolicy: "balanced",
        rtcpMuxPolicy: "require",
        codecs: {
          audio: [
            { name: "PCMU", clockRate: 8000, payloadType: 0 },
            { name: "PCMA", clockRate: 8000, payloadType: 8 },
          ],
          video: [],
        },
      },
    };
        const options = {
          sessionTimers: true, // å¯ç”¨ä¼šè¯è®¡æ—¶å™¨
          sessionTimersExpires: 150,
          extraHeaders: ["Accept: application/sdp"],
          mediaConstraints: { audio: true, video: false },
          rtcOfferConstraints: {
            offerToReceiveAudio: true,
            offerToReceiveVideo: false,
          },
          eventHandlers: {
            progress: () => this.updateCallStatus("calling", "呼叫中..."),
            failed: (e) => {
              this.handleCallFailure(e, reject);
            },
            ended: () => this.updateCallStatus("ended", "通话结束"),
            confirmed: () => {
              this.updateCallStatus("connected", "通话已接通");
              resolve();
            },
          },
        };
    this.currentSession = this.ua.call(
      `sip:${targetNumber}@192.168.100.6`,
      options
    );
    // åœ¨ä¼šè¯åˆ›å»ºåŽä¿®æ”¹ SDP
    this.currentSession.on("peerconnection", (pc) => {
       this.updateCallStatus('calling', '呼叫中...');
      pc.createOffer = (offerOptions) => {
        return RTCPeerConnection.prototype.createOffer
          .call(pc, offerOptions)
          .then((offer) => {
            const modifiedSdp = offer.sdp
              .replace(/c=IN IP4 192\.168\.100\.10/g, "c=IN IP4 192.168.100.6")
              .replace(/m=audio \d+ RTP\/AVP.*/, "m=audio 7078 RTP/AVP 0 8");
            return new RTCSessionDescription({
              type: "offer",
              sdp: modifiedSdp,
            });
          });
        this.currentSession = this.ua.call(
          `sip:${targetNumber}@1192.170.66.107`,
          options
        );
        this.setupPeerConnection(this.currentSession);
        this.setupAudio(this.currentSession);
      } catch (error) {
        this.updateCallStatus("failed", `呼叫失败22: ${error.message}`);
        reject(error);
      }
    });
  }
  setupPeerConnection(session) {
    session.on("peerconnection", (pc) => {
      const originalCreateOffer = pc.createOffer.bind(pc);
      pc.createOffer = async (offerOptions) => {
        try {
          const offer = await originalCreateOffer(offerOptions);
          return this.normalizeSDP(offer);
        } catch (error) {
          console.error("创建Offer失败:", error);
          throw error;
        }
      };
    });
    this.currentSession.on('failed', (e) => {
        this.updateCallStatus('failed', `呼叫失败2: ${e}`);
      });
      this.currentSession.on('ended', () => {
        this.updateCallStatus('ended', '通话已结束');
      });
      this.currentSession.on('confirmed', () => {
        this.updateCallStatus('connected', '通话已接通');
      });
    this.setupAudio(this.currentSession);
  }
  normalizeSDP(offer) {
    let sdp = offer.sdp;
    // æ ‡å‡†åŒ–SDP
    sdp = sdp.replace(/c=IN IP4.*\r\n/, "c=IN IP4 0.0.0.0\r\n");
    sdp = sdp.replace(
      /m=audio \d+.*\r\n/,
      "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n"
    );
    // ç¡®ä¿åŒ…含基本编解码器
    if (!sdp.includes("PCMU/8000")) sdp += "a=rtpmap:0 PCMU/8000\r\n";
    if (!sdp.includes("PCMA/8000")) sdp += "a=rtpmap:8 PCMA/8000\r\n";
    // æ·»åŠ å¿…è¦å±žæ€§
    sdp += "a=rtcp-mux\r\n";
    sdp += "a=sendrecv\r\n";
    console.log("标准化后的SDP:", sdp);
    return new RTCSessionDescription({
      type: offer.type,
      sdp: sdp,
    });
  }
  handleCallFailure(e, reject) {
    if (e.response?.status_code === 422) {
      const serverMinSE = e.response.headers["Min-SE"]?.[0]?.raw || "未知";
      console.error(`服务器要求 Min-SE â‰¤ ${serverMinSE},当前设置: 120`);
    }
    console.error("呼叫失败详情:", {
      cause: e.cause,
      message: e.message,
      response: e.response && {
        status: e.response.status_code,
        reason: e.response.reason_phrase,
      },
    });
    let errorMessage = "呼叫失败";
    switch (e.cause) {
      case "Incompatible SDP":
        errorMessage = "媒体协商失败,请检查编解码器配置";
        break;
      case "488":
      case "606":
        errorMessage = "对方设备不支持当前媒体配置";
        break;
      case "422":
        errorMessage = "会话参数不满足服务器要求";
        break;
      default:
        errorMessage = `呼叫失败3: ${e.cause || e.message}`;
    }
    this.updateCallStatus("failed55", errorMessage);
    reject(new Error(errorMessage));
  }
  setupAudio(session) {
    session.connection.addEventListener("addstream", (e) => {
      const audioElement = document.getElementById("remoteAudio");
@@ -180,21 +217,37 @@
      }
    });
  }
  // æŒ‚断当前通话
  endCall() {
  if (this.currentSession) {
    if (this.currentSession) {
      this.currentSession.terminate();
      this.updateCallStatus('ended', '通话已结束');
      this.updateCallStatus("ended", "通话已结束");
      this.currentSession = null;
    }
  }
  // æ–°å¢žæ–¹æ³•:更新通话状态
  updateStatus(type, text) {
    console.log(`SIP状态更新: ${type} - ${text}`);
    if (this.onStatusChange) {
      this.onStatusChange({ type, text });
    }
  }
  updateCallStatus(type, text) {
    console.log(`通话状态更新: ${type} - ${text}`);
    if (this.onCallStatusChange) {
      this.onCallStatusChange({ type, text });
    }
  }
  handleIncomingCall(session) {
    if (session.direction === "incoming") {
      console.log("来电:", session.remote_identity.uri.toString());
      if (this.onIncomingCall) {
        this.onIncomingCall(session);
      }
    }
  }
}
export default new SipService();
src/views/followvisit/record/detailpage/index.vue
@@ -1779,7 +1779,7 @@
      // è¿™é‡Œè°ƒç”¨ä½ çš„短信发送 API
      // å‡è®¾ API ä¸º sendMsg,参数可能需要根据实际情况调整
      sendMsg({
        phone:  this.userform.telcode, // ç¡®ä¿ç”µè¯å·ç å­—段正确
        phone: this.userform.telcode, // ç¡®ä¿ç”µè¯å·ç å­—段正确
        content: this.smsContent,
      })
        .then((res) => {
@@ -2152,6 +2152,16 @@
      this.getTaskservelist();
    },
  },
  // deactivated() {
  //   console.log(11);
  // },
  beforeRouteLeave(to, from, next) {
    this.$refs.callButton.cleanupResources();
    next(); // ç¡®ä¿è°ƒç”¨ nex
  },
  // beforeRouteUpdate() {
  //   console.log(33);
  // },
};
</script>
@@ -2554,7 +2564,8 @@
  @media screen and (max-width: 1200px), (min-resolution: 1.1dppx) {
    flex-direction: column;
    .call-action, .manual-action {
    .call-action,
    .manual-action {
      width: 100% !important;
    }
  }
@@ -2659,7 +2670,8 @@
    margin: 10px;
    padding: 15px;
    .topic-dev, .scriptTopic-dev {
    .topic-dev,
    .scriptTopic-dev {
      margin-bottom: 15px;
    }
  }
@@ -2692,7 +2704,8 @@
/* ä¸ºç§»åŠ¨è®¾å¤‡ä¼˜åŒ–æ»šåŠ¨ä½“éªŒ */
@media screen and (max-width: 768px) {
  .Followuserinfo, .Followuserinfos {
  .Followuserinfo,
  .Followuserinfos {
    padding: 15px;
    margin: 5px;
  }
@@ -2705,18 +2718,20 @@
/* ç¼©æ”¾æ£€æµ‹æ ·å¼ */
@media screen and (min-resolution: 1.1dppx),
       screen and (-webkit-min-device-pixel-ratio: 1.1),
       screen and (max-width: 1200px) {
  screen and (-webkit-min-device-pixel-ratio: 1.1),
  screen and (max-width: 1200px) {
  .action-container {
    flex-direction: column;
  }
  .call-action, .manual-action {
  .call-action,
  .manual-action {
    width: 100%;
  }
  /* è°ƒæ•´å†…部元素间距 */
  .call-container, .Followuserinfos {
  .call-container,
  .Followuserinfos {
    margin-bottom: 20px;
  }
}