<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>
|