| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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="handleSeatLogout" |
| | | :class="[ |
| | | 'seat-btn', |
| | | 'logout', |
| | | { 'in-call': isInCall || isCalling }, |
| | | ]" |
| | | :disabled="!canLogout" |
| | | > |
| | | <span class="btn-icon">ðª</span> |
| | | {{ isInCall || isCalling ? "强å¶ç¾åº" : "ç¾åº" }} |
| | | </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"; |
| | | }, |
| | | canLogout() { |
| | | // åªæå¨å·²ç¾å
¥ç¶ææå
许ç¾åºï¼æ 论æ¯å¦å¨éè¯ä¸ï¼ |
| | | return this.isSeatLoggedIn && !this.isSeatLoggingOut; |
| | | }, |
| | | 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(); |
| | | }, |
| | | async handleSeatLogout() { |
| | | if (!this.canLogout) return; |
| | | |
| | | try { |
| | | // 妿æ£å¨éè¯ä¸ï¼å
ææ |
| | | if (this.isInCall || this.isCalling) { |
| | | // æ¾ç¤ºç¡®è®¤å¯¹è¯æ¡ |
| | | const confirm = await this.$confirm( |
| | | "ç¡®å®è¦ç¾åºåï¼ç¾åºå°ç»æå½åéè¯", |
| | | "æç¤º", |
| | | { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | } |
| | | ); |
| | | await this.handleHangup(); |
| | | } |
| | | console.log(2); |
| | | |
| | | // æ§è¡ç¾åº |
| | | await this.$refs.callComponent.handleSeatLogout(); |
| | | console.log(3); |
| | | this.isSeatLoggedIn = false; |
| | | |
| | | this.$message.success("座å¸ç¾åºæå"); |
| | | } catch (error) { |
| | | if (error !== "cancel") { |
| | | // 忽ç¥ç¨æ·åæ¶çæ
åµ |
| | | console.error("ç¾åºå¤±è´¥:", error); |
| | | this.$message.error("座å¸ç¾åºå¤±è´¥"); |
| | | } |
| | | } |
| | | }, |
| | | handleHangup() { |
| | | this.$refs.callComponent.hangup(); |
| | | this.stopCallTimer(); |
| | | this.callStatus = "idle"; |
| | | }, |
| | | |
| | | onSeatStatusChange(status) { |
| | | this.isSeatLoggedIn = status.isLoggedIn; |
| | | |
| | | // å¦æåº§å¸ç¾åºï¼éç½®éè¯ç¶æ |
| | | if (!status.isLoggedIn) { |
| | | this.callStatus = "idle"; |
| | | this.stopCallTimer(); |
| | | } |
| | | }, |
| | | |
| | | 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(); |
| | | |
| | | // ç»ä»¶éæ¯åå°è¯ç¾åº |
| | | if (this.isSeatLoggedIn) { |
| | | this.handleSeatLogout().catch(console.error); |
| | | } |
| | | }, |
| | | }; |
| | | </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; |
| | | } |
| | | } |
| | | |
| | | .seat-btn.logout { |
| | | background: linear-gradient(135deg, #f97316, #ea580c); |
| | | color: white; |
| | | |
| | | &:hover:not(:disabled) { |
| | | box-shadow: 0 6px 16px rgba(249, 115, 22, 0.4); |
| | | } |
| | | |
| | | // éè¯ä¸çç¹æ®æ ·å¼ |
| | | &.in-call { |
| | | background: linear-gradient(135deg, #ef4444, #dc2626); |
| | | animation: pulse 1.5s infinite; |
| | | |
| | | &:hover:not(:disabled) { |
| | | box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // éè¯ä¸ç¾åºæé®çç¹æ®å¨ç» |
| | | @keyframes pulse-red { |
| | | 0% { |
| | | box-shadow: 0 4px 12px rgba(239, 68, 68, 0.5); |
| | | } |
| | | 50% { |
| | | box-shadow: 0 4px 20px rgba(239, 68, 68, 0.8); |
| | | } |
| | | 100% { |
| | | box-shadow: 0 4px 12px rgba(239, 68, 68, 0.5); |
| | | } |
| | | } |
| | | .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> |