|  |  | 
 |  |  | 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"; | 
 
| ¶Ô±ÈÐÂÎļþ | 
 |  |  | 
 |  |  | 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, | 
 |  |  |   }); | 
 |  |  | } | 
 
 |  |  | 
 |  |  |  | 
 |  |  | <script> | 
 |  |  | import sipService from "@/utils/sipService"; | 
 |  |  | import { CallsetState, CallgetList } from "@/api/AiCentre/index"; | 
 |  |  |  | 
 |  |  | export default { | 
 |  |  |   props: { | 
 |  |  | 
 |  |  |     }, | 
 |  |  |   }, | 
 |  |  |   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", | 
 |  |  | 
 |  |  |       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(); // éæ¾åæºå· | 
 |  |  |       } | 
 |  |  |     }; | 
 |  |  |  | 
 |  |  |     // çå¬éè¯ç¶æåå | 
 |  |  | 
 |  |  |         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, "å¼å«å¤±è´¥æ¶é´"); | 
 |  |  | 
 |  |  |         // this.callStatus = "ended"; | 
 |  |  |         // this.isCalling = false; | 
 |  |  |         //this.$message.error(`å¼å«å¤±è´¥: ${error.message}`); | 
 |  |  |           try { | 
 |  |  |         try { | 
 |  |  |           // å
æ£æ¥æ¯å¦å¯ä»¥å¼å« | 
 |  |  |           const { canCall, reason } = sipService.canMakeCall(); | 
 |  |  |           if (!canCall) { | 
 |  |  | 
 |  |  |           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> | 
 
| ¶Ô±ÈÐÂÎļþ | 
 |  |  | 
 |  |  | 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(); | 
 
 |  |  | 
 |  |  |   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"); | 
 |  |  | 
 |  |  |       } | 
 |  |  |     }); | 
 |  |  |   } | 
 |  |  |   // ææå½åéè¯ | 
 |  |  |  | 
 |  |  |   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(); | 
 
 |  |  | 
 |  |  |       // è¿éè°ç¨ä½ ççä¿¡åé API | 
 |  |  |       // å设 API ä¸º sendMsgï¼åæ°å¯è½éè¦æ ¹æ®å®é
æ
åµè°æ´ | 
 |  |  |       sendMsg({ | 
 |  |  |         phone:  this.userform.telcode, // ç¡®ä¿çµè¯å·ç å段æ£ç¡® | 
 |  |  |         phone: this.userform.telcode, // ç¡®ä¿çµè¯å·ç å段æ£ç¡® | 
 |  |  |         content: this.smsContent, | 
 |  |  |       }) | 
 |  |  |         .then((res) => { | 
 |  |  | 
 |  |  |       this.getTaskservelist(); | 
 |  |  |     }, | 
 |  |  |   }, | 
 |  |  |   // deactivated() { | 
 |  |  |   //   console.log(11); | 
 |  |  |   // }, | 
 |  |  |   beforeRouteLeave(to, from, next) { | 
 |  |  |     this.$refs.callButton.cleanupResources(); | 
 |  |  |     next(); // ç¡®ä¿è°ç¨ nex | 
 |  |  |   }, | 
 |  |  |   // beforeRouteUpdate() { | 
 |  |  |   //   console.log(33); | 
 |  |  |   // }, | 
 |  |  | }; | 
 |  |  | </script> | 
 |  |  |  | 
 |  |  | 
 |  |  |   @media screen and (max-width: 1200px), (min-resolution: 1.1dppx) { | 
 |  |  |     flex-direction: column; | 
 |  |  |  | 
 |  |  |     .call-action, .manual-action { | 
 |  |  |     .call-action, | 
 |  |  |     .manual-action { | 
 |  |  |       width: 100% !important; | 
 |  |  |     } | 
 |  |  |   } | 
 |  |  | 
 |  |  |     margin: 10px; | 
 |  |  |     padding: 15px; | 
 |  |  |  | 
 |  |  |     .topic-dev, .scriptTopic-dev { | 
 |  |  |     .topic-dev, | 
 |  |  |     .scriptTopic-dev { | 
 |  |  |       margin-bottom: 15px; | 
 |  |  |     } | 
 |  |  |   } | 
 |  |  | 
 |  |  |  | 
 |  |  | /* ä¸ºç§»å¨è®¾å¤ä¼åæ»å¨ä½éª */ | 
 |  |  | @media screen and (max-width: 768px) { | 
 |  |  |   .Followuserinfo, .Followuserinfos { | 
 |  |  |   .Followuserinfo, | 
 |  |  |   .Followuserinfos { | 
 |  |  |     padding: 15px; | 
 |  |  |     margin: 5px; | 
 |  |  |   } | 
 |  |  | 
 |  |  |  | 
 |  |  | /* ç¼©æ¾æ£æµæ ·å¼ */ | 
 |  |  | @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; | 
 |  |  |   } | 
 |  |  | } |