dezhu-icall-vue 1.0.0

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.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # dezhu-icall-vue
2
+
3
+ 得助智能 iCall 外呼组件 - Vue 封装
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install dezhu-icall-vue
9
+ ```
10
+
11
+ ## 使用
12
+
13
+ ```vue
14
+ <template>
15
+ <div>
16
+ <DeZhuICall
17
+ ref="icall"
18
+ :server-url="serverUrl"
19
+ :token="token"
20
+ :visible="false"
21
+ @ready="onReady"
22
+ @error="onError"
23
+ @offline="onOffline"
24
+ @callFailed="onCallFailed"
25
+ @hangup="onHangup"
26
+ @peerHangup="onPeerHangup"
27
+ />
28
+ <button @click="makeCall" :disabled="!isReady">拨打电话</button>
29
+ </div>
30
+ </template>
31
+
32
+ <script>
33
+ import { DeZhuICall } from 'dezhu-icall-vue'
34
+
35
+ export default {
36
+ components: { DeZhuICall },
37
+ data() {
38
+ return {
39
+ serverUrl: '',
40
+ token: '',
41
+ isReady: false
42
+ }
43
+ },
44
+ async mounted() {
45
+ const config = await fetch('/api/call-config').then(r => r.json())
46
+ this.serverUrl = config.serverUrl
47
+ this.token = config.token
48
+ },
49
+ methods: {
50
+ onReady() {
51
+ this.isReady = true
52
+ },
53
+ onError(error) {
54
+ console.error('错误:', error.message)
55
+ },
56
+ onOffline() {
57
+ console.log('已离线,正在重连...')
58
+ },
59
+ onCallFailed(reason) {
60
+ alert('呼叫失败: ' + reason)
61
+ },
62
+ onHangup(params) {
63
+ console.log('通话结束:', params.isSelfHangup ? '自己挂断' : '对方挂断')
64
+ },
65
+ onPeerHangup() {
66
+ console.log('对方已挂断')
67
+ },
68
+ async makeCall() {
69
+ try {
70
+ await this.$refs.icall.call('13800138000')
71
+ } catch (error) {
72
+ alert(error.message)
73
+ }
74
+ }
75
+ }
76
+ }
77
+ </script>
78
+ ```
79
+
80
+ ## Props
81
+
82
+ | 属性 | 类型 | 默认值 | 说明 |
83
+ |------|------|--------|------|
84
+ | serverUrl | String | '' | 服务器地址 |
85
+ | token | String | '' | 认证 Token |
86
+ | autoInit | Boolean | true | 参数准备好后自动初始化 |
87
+ | visible | Boolean | false | 是否显示拨号盘界面 |
88
+
89
+ ## 事件
90
+
91
+ | 事件名 | 说明 | 参数 |
92
+ |--------|------|------|
93
+ | ready | 初始化完成 | - |
94
+ | error | 错误 | Error |
95
+ | offline | 连接断开/离线 | - |
96
+ | statusChange | 状态变化 | status, params |
97
+ | dialing | 正在拨号 | params |
98
+ | ringing | 响铃中 | params |
99
+ | connected | 通话接通 | params |
100
+ | hangup | 通话挂断 | params |
101
+ | peerHangup | 对方挂断 | params |
102
+ | callFailed | 呼叫失败 | reason |
103
+
104
+ ## 方法
105
+
106
+ | 方法名 | 说明 | 参数 |
107
+ |--------|------|------|
108
+ | init() | 手动初始化 | - |
109
+ | call(phoneNumber) | 拨打电话 | phoneNumber: String |
110
+ | hangup() | 挂断电话 | - |
111
+
112
+ ## 特性
113
+
114
+ - **防重复初始化** - `checkAndInit()` 确保不会创建多个拨号盘
115
+ - **自动重连** - 离线后自动重连(5秒间隔)
116
+ - **页面可见性监听** - 切换回页面后自动检查并重连
117
+ - **拨号盘显示控制** - `visible` 属性控制是否显示拨号盘
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "dezhu-icall-vue",
3
+ "version": "1.0.0",
4
+ "description": "得助智能 iCall 外呼组件 - Vue 封装",
5
+ "main": "src/index.js",
6
+ "keywords": [
7
+ "dezhu",
8
+ "icall",
9
+ "vue",
10
+ "component",
11
+ "call",
12
+ "外呼"
13
+ ],
14
+ "author": "",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": ""
19
+ },
20
+ "bugs": {
21
+ "url": ""
22
+ },
23
+ "homepage": "",
24
+ "peerDependencies": {
25
+ "vue": "^2.6.0 || ^3.0.0"
26
+ },
27
+ "files": [
28
+ "src/"
29
+ ]
30
+ }
@@ -0,0 +1,368 @@
1
+ <template>
2
+ <!-- 拨号盘容器,通过 visible 控制显示/隐藏 -->
3
+ <div ref="callContainer" :style="containerStyle" />
4
+ </template>
5
+
6
+ <script>
7
+ /**
8
+ * 得助外呼组件
9
+ * @description 封装得助智能 iCall 外呼功能
10
+ */
11
+
12
+ export default {
13
+ name: 'DeZhuICall',
14
+
15
+ props: {
16
+ /** 得助服务器地址,如:https://hnzz.dezhuyun.com */
17
+ serverUrl: {
18
+ type: String,
19
+ default: ''
20
+ },
21
+ /** 认证 Token,通过 SSO 接入获取 */
22
+ token: {
23
+ type: String,
24
+ default: ''
25
+ },
26
+ /** 是否自动初始化,当 serverUrl 和 token 都准备好后自动初始化 */
27
+ autoInit: {
28
+ type: Boolean,
29
+ default: true
30
+ },
31
+ /** 是否显示拨号盘界面,默认隐藏 */
32
+ visible: {
33
+ type: Boolean,
34
+ default: false
35
+ }
36
+ },
37
+
38
+ computed: {
39
+ /** 容器样式,根据 visible 控制显示/隐藏 */
40
+ containerStyle() {
41
+ return {
42
+ display: this.visible ? 'block' : 'none'
43
+ }
44
+ }
45
+ },
46
+
47
+ watch: {
48
+ /** 监听 serverUrl 变化,满足条件时自动初始化 */
49
+ serverUrl(newVal) {
50
+ if (newVal && this.token && this.autoInit) {
51
+ this.checkAndInit()
52
+ }
53
+ },
54
+ /** 监听 token 变化,满足条件时自动初始化 */
55
+ token(newVal) {
56
+ if (newVal && this.serverUrl && this.autoInit) {
57
+ this.checkAndInit()
58
+ }
59
+ }
60
+ },
61
+
62
+ data() {
63
+ return {
64
+ /** iCall 实例对象 */
65
+ iCall: null,
66
+ /** 是否已完成初始化 */
67
+ isInitialized: false,
68
+ /** 是否正在初始化中,用于防止重复初始化 */
69
+ isInitializing: false,
70
+ /** 是否正在通话中 */
71
+ isCalling: false,
72
+ /** 重连定时器 ID */
73
+ reconnectTimer: null
74
+ }
75
+ },
76
+
77
+ mounted() {
78
+ // 组件挂载后,如果开启自动初始化则检查并初始化
79
+ if (this.autoInit) {
80
+ this.checkAndInit()
81
+ }
82
+ // 监听页面可见性变化,用于页面切换回来后自动重连
83
+ document.addEventListener('visibilitychange', this.handleVisibilityChange)
84
+ },
85
+
86
+ beforeDestroy() {
87
+ // 组件销毁前清理事件监听和定时器
88
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange)
89
+ this.clearReconnectTimer()
90
+ // 销毁拨号盘实例
91
+ this.destroy()
92
+ },
93
+
94
+ methods: {
95
+ /**
96
+ * 检查并初始化
97
+ * @description 防止重复创建拨号盘,确保只有一个实例
98
+ */
99
+ async checkAndInit() {
100
+ if (this.isInitializing || this.isInitialized) {
101
+ return
102
+ }
103
+ await this.init()
104
+ },
105
+
106
+ /**
107
+ * 初始化外呼组件
108
+ * @description 加载 SDK 并初始化拨号盘,初始化完成后自动签入
109
+ * @emits ready - 初始化完成时触发
110
+ * @emits error - 初始化失败时触发
111
+ */
112
+ async init() {
113
+ // 检查必要参数
114
+ if (!this.serverUrl || !this.token) {
115
+ return
116
+ }
117
+
118
+ // 防止重复初始化
119
+ if (this.isInitializing || this.isInitialized) {
120
+ return
121
+ }
122
+
123
+ this.isInitializing = true
124
+
125
+ try {
126
+ await this.loadSDK()
127
+ await this.initICall()
128
+ this.isInitialized = true
129
+ this.$emit('ready')
130
+ } catch (error) {
131
+ this.$emit('error', error)
132
+ // 初始化失败,5秒后自动重试
133
+ this.scheduleReconnect()
134
+ } finally {
135
+ this.isInitializing = false
136
+ }
137
+ },
138
+
139
+ /**
140
+ * 加载 iCall SDK
141
+ * @description 动态创建 script 标签加载得助 SDK
142
+ * @returns {Promise} 加载成功或失败
143
+ */
144
+ loadSDK() {
145
+ // SDK 已加载则直接返回
146
+ if (window.DeZhuFrontIcall) {
147
+ return Promise.resolve()
148
+ }
149
+
150
+ return new Promise((resolve, reject) => {
151
+ const script = document.createElement('script')
152
+ script.src = `${this.serverUrl}/dzfront/icall/lib/icall.js?time=${Date.now()}`
153
+ script.async = true
154
+
155
+ // 15秒超时处理
156
+ const timeout = setTimeout(() => {
157
+ reject(new Error('SDK 加载超时'))
158
+ }, 15000)
159
+
160
+ script.onload = () => {
161
+ clearTimeout(timeout)
162
+ resolve()
163
+ }
164
+
165
+ script.onerror = () => {
166
+ clearTimeout(timeout)
167
+ reject(new Error('SDK 加载失败'))
168
+ }
169
+
170
+ document.head.appendChild(script)
171
+ })
172
+ },
173
+
174
+ /**
175
+ * 初始化拨号盘
176
+ * @description 创建 iCall 实例并绑定事件
177
+ * @returns {Promise} 初始化成功或失败
178
+ */
179
+ initICall() {
180
+ return new Promise((resolve, reject) => {
181
+ const container = this.$refs.callContainer
182
+ if (!container) {
183
+ reject(new Error('找不到容器'))
184
+ return
185
+ }
186
+
187
+ // 如果已存在实例,先销毁防止重复创建
188
+ if (this.iCall) {
189
+ this.iCall.signOut()
190
+ this.iCall = null
191
+ }
192
+
193
+ // 创建 iCall 实例
194
+ this.iCall = new window.DeZhuFrontIcall(container, {
195
+ option: {
196
+ server: this.serverUrl,
197
+ token: this.token
198
+ },
199
+ event: {
200
+ /** 呼叫状态变化事件 */
201
+ onCallStatusChange: (status, params) => {
202
+ this.$emit('statusChange', status, params)
203
+
204
+ // 根据状态触发对应事件
205
+ switch (status) {
206
+ case 'MANUAL_DIALING':
207
+ this.$emit('dialing', params)
208
+ break
209
+ case 'MANUAL_EARLY_MEDIA':
210
+ this.$emit('ringing', params)
211
+ break
212
+ case 'MANUAL_ACTIVE':
213
+ this.$emit('connected', params)
214
+ break
215
+ case 'MANUAL_NOT_ACTIVE':
216
+ this.handleHangup(params)
217
+ break
218
+ }
219
+ },
220
+ /** 错误事件 */
221
+ onError: (error) => {
222
+ this.$emit('error', error)
223
+ },
224
+ /** 连接断开事件 */
225
+ onDisconnect: () => {
226
+ this.isInitialized = false
227
+ this.$emit('offline')
228
+ this.scheduleReconnect()
229
+ }
230
+ }
231
+ }, async () => {
232
+ // 初始化完成回调,自动签入
233
+ await this.iCall.signIn()
234
+ resolve()
235
+ })
236
+
237
+ // 10秒初始化超时处理
238
+ setTimeout(() => {
239
+ if (!this.isInitialized) {
240
+ reject(new Error('初始化超时'))
241
+ }
242
+ }, 10000)
243
+ })
244
+ },
245
+
246
+ /**
247
+ * 处理通话挂断
248
+ * @description 区分自己挂断和对方挂断,触发对应事件
249
+ * @param {Object} params - 挂断参数
250
+ * @param {boolean} params.isSelfHangup - 是否自己挂断
251
+ * @param {boolean} params.isPeerHangup - 是否对方挂断
252
+ * @param {boolean} params.isCallFail - 是否呼叫失败
253
+ * @param {string} params.callFailReason - 失败原因
254
+ */
255
+ handleHangup(params) {
256
+ this.isCalling = false
257
+
258
+ // 判断挂断方
259
+ const isSelfHangup = params?.isSelfHangup || params?.endReason === 1
260
+ const isPeerHangup = params?.isPeerHangup || params?.endReason === 2
261
+ const isFailed = params?.isCallFail
262
+
263
+ // 对方挂断时触发专用事件
264
+ if (isPeerHangup && !isSelfHangup) {
265
+ this.$emit('peerHangup', params)
266
+ }
267
+
268
+ // 触发挂断事件
269
+ this.$emit('hangup', {
270
+ ...params,
271
+ isSelfHangup,
272
+ isPeerHangup
273
+ })
274
+
275
+ // 呼叫失败时触发失败事件
276
+ if (isFailed) {
277
+ this.$emit('callFailed', params?.callFailReason || params?.failReason || '呼叫失败')
278
+ }
279
+ },
280
+
281
+ /**
282
+ * 处理页面可见性变化
283
+ * @description 页面从后台切换回来时,检查是否需要重连
284
+ */
285
+ handleVisibilityChange() {
286
+ // 页面重新可见且未初始化时,尝试重连
287
+ if (!document.hidden && this.autoInit && !this.isInitialized) {
288
+ this.checkAndInit()
289
+ }
290
+ },
291
+
292
+ /**
293
+ * 定时重连
294
+ * @description 离线后5秒自动尝试重连
295
+ */
296
+ scheduleReconnect() {
297
+ this.clearReconnectTimer()
298
+ this.reconnectTimer = setTimeout(() => {
299
+ this.reconnectTimer = null
300
+ // 只有未初始化且参数齐全时才重连
301
+ if (!this.isInitialized && this.serverUrl && this.token) {
302
+ this.init()
303
+ }
304
+ }, 5000)
305
+ },
306
+
307
+ /**
308
+ * 清除重连定时器
309
+ */
310
+ clearReconnectTimer() {
311
+ if (this.reconnectTimer) {
312
+ clearTimeout(this.reconnectTimer)
313
+ this.reconnectTimer = null
314
+ }
315
+ },
316
+
317
+ /**
318
+ * 拨打电话
319
+ * @description 发起外呼呼叫
320
+ * @param {string} phoneNumber - 电话号码
321
+ * @throws {Error} 拨号盘未初始化或正在通话中
322
+ */
323
+ async call(phoneNumber) {
324
+ if (!this.isInitialized || !this.iCall) {
325
+ throw new Error('拨号盘未初始化')
326
+ }
327
+
328
+ if (this.isCalling) {
329
+ throw new Error('正在通话中')
330
+ }
331
+
332
+ this.isCalling = true
333
+
334
+ try {
335
+ await this.iCall.call(phoneNumber)
336
+ } catch (error) {
337
+ this.isCalling = false
338
+ throw error
339
+ }
340
+ },
341
+
342
+ /**
343
+ * 挂断电话
344
+ * @description 主动挂断当前通话
345
+ */
346
+ async hangup() {
347
+ if (this.iCall && this.isCalling) {
348
+ await this.iCall.hangup()
349
+ this.isCalling = false
350
+ }
351
+ },
352
+
353
+ /**
354
+ * 销毁组件
355
+ * @description 清理定时器、签出并销毁拨号盘实例
356
+ */
357
+ destroy() {
358
+ this.clearReconnectTimer()
359
+ if (this.iCall) {
360
+ this.iCall.signOut()
361
+ this.iCall = null
362
+ }
363
+ this.isInitialized = false
364
+ this.isCalling = false
365
+ }
366
+ }
367
+ }
368
+ </script>
package/src/index.js ADDED
@@ -0,0 +1,20 @@
1
+ import DeZhuICall from './DeZhuICall.vue'
2
+
3
+ // 安装函数
4
+ const install = function(Vue) {
5
+ if (install.installed) return
6
+ install.installed = true
7
+ Vue.component(DeZhuICall.name, DeZhuICall)
8
+ }
9
+
10
+ // 自动安装
11
+ if (typeof window !== 'undefined' && window.Vue) {
12
+ install(window.Vue)
13
+ }
14
+
15
+ export default {
16
+ install,
17
+ DeZhuICall
18
+ }
19
+
20
+ export { DeZhuICall }