cloud-web-corejs 1.0.54-dev.637 → 1.0.54-dev.638

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,768 +0,0 @@
1
- <template>
2
- <div class="call-center-float" :style="panelStyle">
3
- <div class="call-center-float__header" @mousedown="onDragStart">
4
- <div class="call-center-float__status-row">
5
- <span class="call-center-float__label">座席状态:</span>
6
- <span class="call-center-float__status" :class="statusClass">{{
7
- agentStatusText
8
- }}</span>
9
- <span
10
- class="call-center-float__label call-center-float__label--incoming"
11
- >来电号码:</span
12
- >
13
- <span class="call-center-float__incoming">{{
14
- incomingNumber || "—"
15
- }}</span>
16
- </div>
17
- <button
18
- type="button"
19
- class="call-center-float__toggle"
20
- :title="collapsed ? '展开' : '收起'"
21
- @mousedown.stop
22
- @click="collapsed = !collapsed"
23
- >
24
- {{ collapsed ? "▲" : "▼" }}
25
- </button>
26
- </div>
27
-
28
- <div v-show="!collapsed" class="call-center-float__body">
29
- <input
30
- v-model.trim="dialNumber"
31
- class="call-center-float__input"
32
- type="text"
33
- placeholder="请输入外呼号码"
34
- :disabled="!isOnWork"
35
- @keyup.enter="handleDial"
36
- />
37
- <button
38
- type="button"
39
- class="call-center-float__btn call-center-float__btn--dial"
40
- :disabled="!canDial"
41
- title="外呼"
42
- @click="handleDial"
43
- >
44
- <i class="call-center-float__phone-icon" />
45
- </button>
46
- <button
47
- v-for="btn in actionButtons"
48
- :key="btn.key"
49
- type="button"
50
- class="call-center-float__btn call-center-float__btn--action"
51
- :disabled="btn.disabled"
52
- @click="btn.handler"
53
- >
54
- {{ btn.label }}
55
- </button>
56
- </div>
57
-
58
- <audio id="webcam" autoplay style="display: none" />
59
- </div>
60
- </template>
61
-
62
- <script>
63
- import {
64
- getAgentStateMeta,
65
- canAgentDial,
66
- isRestAgentState,
67
- resolveAgentStateFromPayload,
68
- } from "./agentState";
69
-
70
- const RING_MSG_TYPES = [
71
- "PhoneStateUserRing",
72
- "PhoneStateAgentRing",
73
- "PhoneStateRing",
74
- "StartService",
75
- ];
76
-
77
- const CALL_ACTIVE_MSG_TYPES = [
78
- "StartService",
79
- "PhoneStateAgentRing",
80
- "PhoneStateAgentConnected",
81
- "PhoneStateUserConnected",
82
- "PhoneStateUserRing",
83
- "PhoneStateRing",
84
- "PhoneStateConnected",
85
- ];
86
-
87
- const CALL_END_MSG_TYPES = [
88
- "StopService",
89
- "AgentHangUp",
90
- "PhoneStateDisConnected",
91
- ];
92
-
93
- export default {
94
- name: "CallCenterFloat",
95
- props: {
96
- option: Object,
97
- },
98
- data() {
99
- return {
100
- sdkReady: false,
101
- configReady: false,
102
- collapsed: false,
103
- agentState: "-1",
104
- incomingNumber: "",
105
- dialNumber: "",
106
- currentCall: null,
107
- isOnWork: false,
108
- position: { left: null, right: 24, bottom: 24 },
109
- dragging: false,
110
- dragOffset: { x: 0, y: 0 },
111
- pendingAgentState: null,
112
- pendingAgentStateTimer: null,
113
- };
114
- },
115
- computed: {
116
- agentStatusText() {
117
- if (!this.configReady) {
118
- return "未配置";
119
- }
120
- if (!this.sdkReady) {
121
- return "未就绪";
122
- }
123
- return getAgentStateMeta(this.agentState).label;
124
- },
125
- statusClass() {
126
- return getAgentStateMeta(this.agentState).className;
127
- },
128
- panelStyle() {
129
- const style = {
130
- bottom: `${this.position.bottom}px`,
131
- zIndex: 10000,
132
- };
133
- if (this.position.left !== null) {
134
- style.left = `${this.position.left}px`;
135
- style.right = "auto";
136
- } else {
137
- style.right = `${this.position.right}px`;
138
- style.left = "auto";
139
- }
140
- return style;
141
- },
142
- canDial() {
143
- return (
144
- this.isOnWork &&
145
- !!this.dialNumber &&
146
- canAgentDial(this.agentState)
147
- );
148
- },
149
- actionButtons() {
150
- return [
151
- {
152
- key: "hangup",
153
- label: "挂机",
154
- disabled: !this.currentCall,
155
- handler: this.handleHangUp,
156
- },
157
- {
158
- key: "onWork",
159
- label: "上班",
160
- disabled: this.isOnWork,
161
- handler: this.handleOnWork,
162
- },
163
- {
164
- key: "offWork",
165
- label: "下班",
166
- disabled: !this.isOnWork,
167
- handler: this.handleOffWork,
168
- },
169
- {
170
- key: "idle",
171
- label: "空闲",
172
- disabled: !this.isOnWork,
173
- handler: () => this.handleUpdateState("1"),
174
- },
175
- {
176
- key: "busy",
177
- label: "忙碌",
178
- disabled: !this.isOnWork,
179
- handler: () => this.handleUpdateState("0"),
180
- },
181
- {
182
- key: "rest",
183
- label: "小休",
184
- disabled: !this.isOnWork,
185
- handler: () => this.handleUpdateState("114"),
186
- },
187
- {
188
- key: "meal",
189
- label: "用餐",
190
- disabled: !this.isOnWork,
191
- handler: () => this.handleUpdateState("118"),
192
- },
193
- ];
194
- },
195
- },
196
- mounted() {
197
- this.initCallCenter();
198
- document.addEventListener("mousemove", this.onDragMove);
199
- document.addEventListener("mouseup", this.onDragEnd);
200
- },
201
- beforeDestroy() {
202
- document.removeEventListener("mousemove", this.onDragMove);
203
- document.removeEventListener("mouseup", this.onDragEnd);
204
- this.clearPendingAgentStateTimer();
205
- },
206
- methods: {
207
- getServ() {
208
- return window.zcVertoServ;
209
- },
210
- isConfigReady() {
211
- const cfg = this.option;
212
- return !!(
213
- cfg.client_id &&
214
- cfg.client_secret &&
215
- cfg.companyId &&
216
- cfg.agentId
217
- );
218
- },
219
- initCallCenter() {
220
- this.configReady = this.isConfigReady();
221
- if (!this.configReady) {
222
- return;
223
- }
224
- const serv = this.getServ();
225
- if (!serv || typeof serv.ininServ !== "function") {
226
- console.warn(
227
- "[CallCenter] zcVertoServ 未加载,请检查 index.html 脚本引用"
228
- );
229
- return;
230
- }
231
-
232
- this.sdkReady = true;
233
- const option = this.option;
234
- const params = {
235
- client_id: option.client_id,
236
- client_secret: option.client_secret,
237
- companyId: option.companyId,
238
- appId: option.appId,
239
-
240
- agentId: option.agentId,
241
- groupId: option.groupId,
242
- voipAccount: option.voipAccount,
243
- callWay: option.callWay,
244
- };
245
-
246
- serv.ininServ(params, this.onSocketMessage, this.onSdkError);
247
- serv.getToken((res) => {
248
- if (!res || !res.access_token) {
249
- this.notifyError("获取呼叫中心 Token 失败");
250
- return;
251
- }
252
- serv.queryAgentState((rs) => {
253
- if (rs && String(rs.retCode) === "000000" && rs.item) {
254
- const state = resolveAgentStateFromPayload(rs.item);
255
- if (state !== null) {
256
- this.applyAgentState(state);
257
- }
258
- if (Number(rs.item.agentState) >= 0) {
259
- this.isOnWork = true;
260
- serv.reConnect();
261
- }
262
- }
263
- });
264
- });
265
- /* serv.queryAgentState((rs) => {
266
- debugger;
267
- if (rs && rs.retCode === "000000" && rs.item) {
268
- this.applyAgentState(rs.item.agentState);
269
- if (Number(rs.item.agentState) >= 0) {
270
- this.isOnWork = true;
271
- serv.reConnect();
272
- }
273
- }
274
- }); */
275
- },
276
- onSdkError(msg) {
277
- this.notifyError(msg || "呼叫中心操作失败");
278
- },
279
- onSocketMessage(res) {
280
- if (!res || !res.data) {
281
- return;
282
- }
283
- let data;
284
- try {
285
- data = JSON.parse(res.data);
286
- } catch (e) {
287
- return;
288
- }
289
-
290
- if (!data.msgType) {
291
- return;
292
- }
293
-
294
- let option = this.option;
295
-
296
- switch (data.msgType) {
297
- case "onLine":
298
- break;
299
- case "offLine":
300
- case "repeat":
301
- this.isOnWork = false;
302
- this.applyAgentState("-1");
303
- this.currentCall = null;
304
- if (data.msgType === "repeat") {
305
- this.notifyError("呼叫中心账号已在其他窗口登录");
306
- }
307
- break;
308
- case "AgentStateIdle":
309
- this.applyAgentState(
310
- this.resolveAgentStateFromSocket(data) || "1"
311
- );
312
- break;
313
- case "AgentStateBusy":
314
- this.applyAgentStateFromBusyMessage(data);
315
- break;
316
- case "AgentStateAfterWorking":
317
- this.applyAgentState(
318
- this.resolveAgentStateFromSocket(data) || "115"
319
- );
320
- break;
321
- case "AgentStateTalking":
322
- this.applyCallActiveState(data);
323
- break;
324
- case "AgentStateLogOut":
325
- this.isOnWork = false;
326
- this.applyAgentState("-1");
327
- this.currentCall = null;
328
- break;
329
- case "StopService":
330
- case "AgentHangUp":
331
- this.restoreAgentStateAfterCall(data);
332
- break;
333
- case "PhoneStateDisConnected":
334
- if (this.currentCall) {
335
- this.restoreAgentStateAfterCall(data);
336
- }
337
- break;
338
- default:
339
- if (CALL_ACTIVE_MSG_TYPES.includes(data.msgType)) {
340
- this.applyCallActiveState(data);
341
- }
342
- break;
343
- }
344
-
345
- if (RING_MSG_TYPES.includes(data.msgType)) {
346
- this.incomingNumber = this.resolveIncomingNumber(data);
347
- if (
348
- String(this.option.callWay) === "1" &&
349
- data.direction === 1 &&
350
- this.voipAccount
351
- ) {
352
- this.incomingNumber = this.voipAccount;
353
- }
354
- }
355
-
356
- if (data.callId && !CALL_END_MSG_TYPES.includes(data.msgType)) {
357
- this.currentCall = data;
358
- if (!this.incomingNumber) {
359
- this.incomingNumber = this.resolveIncomingNumber(data);
360
- }
361
- if (
362
- data.msgType === "PhoneStateAgentRing" &&
363
- String(this.option.callWay) === "1"
364
- ) {
365
- this.getServ().agentListen();
366
- }
367
- }
368
- },
369
- resolveIncomingNumber(data) {
370
- const direction = Number(data.direction);
371
- if (direction === 0) {
372
- return data.caller || data.callee || "";
373
- }
374
- if (direction === 1) {
375
- return data.callee || data.caller || "";
376
- }
377
- return data.caller || data.callee || "";
378
- },
379
- applyAgentState(state) {
380
- this.agentState =
381
- state === undefined || state === null ? "-1" : String(state);
382
- },
383
- resolveAgentStateFromSocket(data) {
384
- return resolveAgentStateFromPayload(data);
385
- },
386
- applyCallActiveState(data) {
387
- this.pendingAgentState = null;
388
- this.clearPendingAgentStateTimer();
389
- const socketState = data ? this.resolveAgentStateFromSocket(data) : null;
390
- this.applyAgentState(socketState === "3" ? socketState : "3");
391
- },
392
- restoreAgentStateAfterCall(data) {
393
- this.currentCall = null;
394
- this.incomingNumber = "";
395
- const socketState = data ? this.resolveAgentStateFromSocket(data) : null;
396
- if (socketState !== null && socketState !== "3") {
397
- this.applyAgentState(socketState);
398
- return;
399
- }
400
- this.syncAgentStateAfterCall();
401
- },
402
- syncAgentStateAfterCall() {
403
- const serv = this.getServ();
404
- if (!serv || !this.isOnWork) {
405
- return;
406
- }
407
- serv.queryAgentState((rs) => {
408
- if (rs && String(rs.retCode) === "000000" && rs.item) {
409
- const state = resolveAgentStateFromPayload(rs.item);
410
- if (state !== null && state !== "3") {
411
- this.applyAgentState(state);
412
- return;
413
- }
414
- }
415
- this.applyAgentState("1");
416
- });
417
- },
418
- applyAgentStateFromBusyMessage(data) {
419
- if (this.currentCall || String(this.agentState) === "3") {
420
- const talkingState = this.resolveAgentStateFromSocket(data);
421
- if (talkingState === "3") {
422
- this.applyAgentState("3");
423
- }
424
- return;
425
- }
426
- let socketState = this.resolveAgentStateFromSocket(data);
427
- if (socketState === "0") {
428
- if (
429
- this.pendingAgentState &&
430
- isRestAgentState(this.pendingAgentState)
431
- ) {
432
- socketState = this.pendingAgentState;
433
- } else if (isRestAgentState(this.agentState)) {
434
- return;
435
- }
436
- }
437
- if (socketState !== null) {
438
- this.applyAgentState(socketState);
439
- return;
440
- }
441
- if (isRestAgentState(this.agentState)) {
442
- return;
443
- }
444
- this.applyAgentState("0");
445
- },
446
- setPendingAgentState(state) {
447
- this.pendingAgentState = String(state);
448
- this.clearPendingAgentStateTimer();
449
- this.pendingAgentStateTimer = setTimeout(() => {
450
- this.pendingAgentState = null;
451
- this.pendingAgentStateTimer = null;
452
- }, 4000);
453
- },
454
- clearPendingAgentStateTimer() {
455
- if (this.pendingAgentStateTimer) {
456
- clearTimeout(this.pendingAgentStateTimer);
457
- this.pendingAgentStateTimer = null;
458
- }
459
- },
460
- handleOnWork() {
461
- const serv = this.getServ();
462
- if (!serv || !this.sdkReady) {
463
- return;
464
- }
465
- const option = this.option;
466
- const payload = {
467
- displayNumber: option.displayNumber || undefined,
468
- isRecordStereo: false,
469
- agentState: "1",
470
- };
471
- serv.agentLogin(payload, (res) => {
472
- this.handleApiResult(res, "上班", () => {
473
- this.isOnWork = true;
474
- this.applyAgentState("1");
475
- });
476
- });
477
- },
478
- handleOffWork() {
479
- const serv = this.getServ();
480
- if (!serv) {
481
- return;
482
- }
483
- serv.agentLoginout((res) =>
484
- this.handleApiResult(res, "下班", () => {
485
- this.isOnWork = false;
486
- this.applyAgentState("-1");
487
- this.currentCall = null;
488
- this.incomingNumber = "";
489
- })
490
- );
491
- },
492
- handleDial() {
493
- const serv = this.getServ();
494
- if (!serv || !this.sdkReady) {
495
- this.notifyError("呼叫中心未就绪");
496
- return;
497
- }
498
- if (!this.isOnWork) {
499
- this.notifyError("请先点击「上班」");
500
- return;
501
- }
502
- if (!this.dialNumber) {
503
- this.notifyError("请输入外呼号码");
504
- return;
505
- }
506
- if (!canAgentDial(this.agentState)) {
507
- this.notifyError("当前座席状态不可外呼");
508
- return;
509
- }
510
- const option = this.option;
511
- serv.agentCallOut(
512
- {
513
- displayNumber: option.displayNumber || undefined,
514
- destinationNumber: this.dialNumber,
515
- params: "",
516
- },
517
- (res) =>
518
- this.handleApiResult(res, "外呼", () => {
519
- if (res && res.callId) {
520
- this.currentCall = { callId: res.callId, direction: 1 };
521
- this.applyCallActiveState();
522
- }
523
- })
524
- );
525
- },
526
- handleHangUp() {
527
- const serv = this.getServ();
528
- if (!serv || !this.currentCall || !this.currentCall.callId) {
529
- return;
530
- }
531
- serv.agentHangUp({ callId: this.currentCall.callId }, (res) =>
532
- this.handleApiResult(res, "挂机", () => {
533
- this.restoreAgentStateAfterCall();
534
- })
535
- );
536
- },
537
- handleUpdateState(agentState) {
538
- const serv = this.getServ();
539
- if (!serv || !this.isOnWork) {
540
- return;
541
- }
542
- const targetState = String(agentState);
543
- this.setPendingAgentState(targetState);
544
- serv.updateAgentState({ agentState: targetState }, (res) => {
545
- if (!res || String(res.retCode) !== "000000") {
546
- this.pendingAgentState = null;
547
- this.clearPendingAgentStateTimer();
548
- }
549
- this.handleApiResult(res, "修改状态", () => {
550
- this.applyAgentState(targetState);
551
- });
552
- });
553
- },
554
- handleApiResult(res, action, onSuccess) {
555
- if (res && String(res.retCode) === "000000") {
556
- if (typeof onSuccess === "function") {
557
- onSuccess(res);
558
- }
559
- return;
560
- }
561
- const msg = (res && (res.retMsg || res.msg)) || `${action}失败`;
562
- this.notifyError(msg);
563
- },
564
- notifyError(message) {
565
- if (this.$message) {
566
- this.$message.error(message);
567
- } else {
568
- console.error("[CallCenter]", message);
569
- }
570
- },
571
- onDragStart(event) {
572
- if (event.button !== 0) {
573
- return;
574
- }
575
- const rect = this.$el.getBoundingClientRect();
576
- this.dragging = true;
577
- this.dragOffset = {
578
- x: event.clientX - rect.left,
579
- y: event.clientY - rect.top,
580
- };
581
- if (this.position.left === null) {
582
- this.position.left = rect.left;
583
- this.position.bottom = window.innerHeight - rect.bottom;
584
- }
585
- },
586
- onDragMove(event) {
587
- if (!this.dragging) {
588
- return;
589
- }
590
- const panelWidth = this.$el ? this.$el.offsetWidth : 720;
591
- const panelHeight = this.$el ? this.$el.offsetHeight : 80;
592
- const edge = 8;
593
- const maxLeft = Math.max(edge, window.innerWidth - panelWidth - edge);
594
- const left = Math.min(
595
- maxLeft,
596
- Math.max(edge, event.clientX - this.dragOffset.x)
597
- );
598
- const maxTop = window.innerHeight - panelHeight - edge;
599
- const top = Math.min(
600
- maxTop,
601
- Math.max(edge, event.clientY - this.dragOffset.y)
602
- );
603
- const bottom = window.innerHeight - top - panelHeight;
604
- this.position.left = left;
605
- this.position.bottom = bottom;
606
- },
607
- onDragEnd() {
608
- this.dragging = false;
609
- },
610
- },
611
- };
612
- </script>
613
-
614
- <style lang="scss" scoped>
615
- .call-center-float {
616
- position: fixed;
617
- min-width: 680px;
618
- max-width: calc(100vw - 32px);
619
- border-radius: 4px;
620
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
621
- background: linear-gradient(90deg, #3d8fd8 0%, #2b6cb8 55%, #245da8 100%);
622
- color: #fff;
623
- font-size: 13px;
624
- user-select: none;
625
- }
626
-
627
- .call-center-float__header {
628
- display: flex;
629
- align-items: center;
630
- justify-content: space-between;
631
- padding: 8px 10px 4px;
632
- cursor: move;
633
- }
634
-
635
- .call-center-float__status-row {
636
- display: flex;
637
- align-items: center;
638
- flex-wrap: wrap;
639
- gap: 4px 12px;
640
- flex: 1;
641
- min-width: 0;
642
- }
643
-
644
- .call-center-float__label {
645
- color: rgba(255, 255, 255, 0.92);
646
- white-space: nowrap;
647
-
648
- &--incoming {
649
- margin-left: 8px;
650
- }
651
- }
652
-
653
- .call-center-float__status {
654
- font-weight: 600;
655
- margin-right: 4px;
656
-
657
- &.is-idle {
658
- color: #b8f06a;
659
- }
660
-
661
- &.is-busy,
662
- &.is-talking {
663
- color: #ffd666;
664
- }
665
-
666
- &.is-rest {
667
- color: #91d5ff;
668
- }
669
-
670
- &.is-offline {
671
- color: #d9d9d9;
672
- }
673
- }
674
-
675
- .call-center-float__incoming {
676
- color: #fff;
677
- font-weight: 600;
678
- min-width: 80px;
679
- }
680
-
681
- .call-center-float__toggle {
682
- border: none;
683
- background: rgba(255, 255, 255, 0.2);
684
- color: #fff;
685
- width: 24px;
686
- height: 24px;
687
- line-height: 22px;
688
- border-radius: 2px;
689
- cursor: pointer;
690
- flex-shrink: 0;
691
- margin-left: 8px;
692
-
693
- &:hover {
694
- background: rgba(255, 255, 255, 0.35);
695
- }
696
- }
697
-
698
- .call-center-float__body {
699
- display: flex;
700
- align-items: center;
701
- gap: 6px;
702
- padding: 6px 10px 10px;
703
- flex-wrap: wrap;
704
- }
705
-
706
- .call-center-float__input {
707
- width: 140px;
708
- height: 30px;
709
- padding: 0 8px;
710
- border: none;
711
- border-radius: 2px;
712
- outline: none;
713
- font-size: 13px;
714
- color: #333;
715
-
716
- &:disabled {
717
- background: #f0f0f0;
718
- color: #999;
719
- }
720
- }
721
-
722
- .call-center-float__btn {
723
- height: 30px;
724
- border: none;
725
- border-radius: 2px;
726
- cursor: pointer;
727
- font-size: 13px;
728
- color: #fff;
729
- white-space: nowrap;
730
- padding: 0 10px;
731
- transition: opacity 0.15s;
732
-
733
- &:disabled {
734
- opacity: 0.45;
735
- cursor: not-allowed;
736
- }
737
-
738
- &--dial {
739
- width: 34px;
740
- padding: 0;
741
- background: #f5a623;
742
- display: inline-flex;
743
- align-items: center;
744
- justify-content: center;
745
-
746
- &:not(:disabled):hover {
747
- background: #e69512;
748
- }
749
- }
750
-
751
- &--action {
752
- min-width: 52px;
753
- background: #52a845;
754
-
755
- &:not(:disabled):hover {
756
- background: #46963b;
757
- }
758
- }
759
- }
760
-
761
- .call-center-float__phone-icon {
762
- display: inline-block;
763
- width: 16px;
764
- height: 16px;
765
- background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M6.6 10.8c1.4 2.8 3.8 5.1 6.6 6.6l2.2-2.2c.3-.3.7-.4 1-.2 1.1.4 2.3.6 3.6.6.6 0 1 .4 1 1V20c0 .6-.4 1-1 1C10.3 21 3 13.7 3 4c0-.6.4-1 1-1h3.5c.6 0 1 .4 1 1 0 1.3.2 2.5.6 3.6.1.3 0 .7-.2 1L6.6 10.8z'/%3E%3C/svg%3E")
766
- center / contain no-repeat;
767
- }
768
- </style>