yimi-webrtc-phone
v1.1.30
Published
基于 Vue3 开发的 WebRTC 电话组件,封装为 Web Component 以实现跨框架复用。主要功能包括:
Readme
YimiWebRTC Phone
基于 Vue3 开发的 WebRTC 电话组件,封装为 Web Component 以实现跨框架复用。主要功能包括:
- 坐席状态管理(空闲、忙碌、通话中、话后处理等)
- 通话操作(拨打、接听、挂断、静音)
- 自定义主题和 UI 尺寸
- 本地日志记录
- 提供完整API和事件系统
支持 ES Module 和 IIFE 两种引入方式,可在任意前端项目中使用。适用于呼叫中心等场景的在线通话系统开发。
安装及引入
安装
npm i yimi-webrtc-phone引入
使用有两种方式,一种是 ES module,一种是 IIFE。
ES module 方式示例:
<!DOCTYPE html>
<html lang="en">
......
<script type="module">
import { register } from "../dist/yimi-webrtc-phone.es.js";
register();
</script>
</head>
<body>
<yimi-webrtc-phone id="phone"></yimi-webrtc-phone>
<script>
customElements.whenDefined("yimi-webrtc-phone").then(() => {
const phoneBar = document.getElementById("phone");
// 自定义事件监听
phoneBar.addEventListener("phone-message", (message) => {
console.log("message", message.detail || message);
});
// 使用组件的 API
phoneBar.handleCall("10010");
})
</script>
</body>
</html>IIFE 方式示例:
<!DOCTYPE html>
<html lang="en">
......
<body>
<div id="wrap"></div>
<script src="../dist/yimi-webrtc-phone.iife.js"></script>
<!-- 引入之后 window 上会有一个属性叫做 yimiWebrtcPhone -->
<script>
const register = window.yimiWebrtcPhone.register;
register();
// register 的作用是将自定义元素注册到 window.customElements 上,和下面的代码等价
// const CustomElement = window.yimiWebrtcPhone.YimiWebrtcPhone;
// window.customElements.define("yimi-webrtc-phone", CustomElement);
// 创建自定义元素并添加到 DOM 中
const phoneElement = document.createElement("yimi-webrtc-phone");
document.getElementById("wrap").appendChild(phoneElement);
try {
phoneElement.initConfig({
server: "",
officeNumber: "",
number: "",
password: "",
type: "",
phone: "",
autoLogin: true,
});
} catch (error) {
console.error(error);
}
// 自定义事件监听
phoneElement.addEventListener("phone-message", (message) => {
console.log("message", message.detail || message);
});
// 使用组件的 API
phoneElement.handleCall("10010")
</script>
</body>
</html>
在前端框架中使用
Vue3 单文件组件。
<script setup>
import { ref, onMounted } from 'vue';
import { register } from 'yimi-webrtc-phone';
const phoneBarRef = ref(null);
const state = ref(0);
const size = ref('large');
const colors = ref({...})
const handle = () => {
// 组件 api 使用
phoneBarRef.value.handleCall('10000');
}
onMounted(() => {
// 调用注册自定义组件的方法
register();
// 自定义事件监听
phoneBarRef.value.addEventListener('phone-message', (e) => {
......
});
});
</script>
<template>
<yimi-webrtc-phone ref="phoneBarRef"></yimi-webrtc-phone>
</template>React 单文件组件。
import { register } from 'yimi-webrtc-phone';
import { useRef, useEffect } from 'react';
const App = () => {
// dom 节点的 ref
const phoneRef = useRef(null);
// 标记是否调用了 register(), 避免重复注册报错
const initialized = useRef(false);
const configed = useRef(false);
useEffect(() => {
if (!initialized.current) {
// 注册为 Web Component
register();
initialized.current = true;
}
return () => {};
}, []);
useEffect(() => {
if (configed.current) {
console.log('Called initconfig()!')
return;
}
if (phoneRef.current) {
// 注册事件监听
phoneRef.current.addEventListener('phone-message', (message) => {
const { type, data } = message.detail[0];
switch (type) {...}
});
// 初始化配置项
try {
phoneRef.current.initConfig({...});
configed.current = true;
} catch (error) {
console.error('error:', error);
}
}
}, [phoneRef.current]);
return (
<div className="content">
<yimi-webrtc-phone ref={phoneRef} colors={JSON.Stringify({...})}></yimi-webrtc-phone>
</div>
);
};
export default App;自定义样式 UI 样式
属性
该组件支持传入两个属性。
| 属性 | 值 | 是否必须 | 描述 | | ------- | --------------------------------- | -------- | ------------------------------------------------------------ | | size | 'small' | 'default' | 'large' | 否 | 用于定于 UI 的大小,默认值为 default | | colors | JSON object | 否 | 用于定义各种状态的背景色 | | buttons | 'array' | 否 | 用于自定义 UI 展示的按钮,该配置如果不传入则默认展示全部按钮。如果不想要如下可定义的六个按钮,可传入空数组。 |
// ES Module
<yimi-webrtc-phone id="phone" size="small" colors='{...}' buttons="[...]"></yimi-webrtc-phone>
// IIFE
// 设置 UI 的大小
phoneElement.size = "large"; // 'small' | 'default' | 'large'
// 设置 UI 各种状态的背景色
// 传入的 colors 对象仅支持如下 key,value 支持 css 支持的所有颜色格式及颜色关键字。
// 将 colors 属性传入 phoneElement 时,需要将其转换为 JSON 字符串
phoneElement.colors = {
offline: 'black', // 离线,默认为 #909399
free: 'green', // 空闲,默认为 #31BB79
ringing: 'pink', // 振铃,默认为 #E6A23C
talking: 'blue', // 通话,默认为 #EC693D
busy: 'yellow', // 忙线,默认为 #ED9D17
after_call: 'oragin', // 话后处理,默认为 #EC693D
};
// 设置自定义的按钮
phoneElement.buttons = [
'login', // 展示签入按钮,点击签入按钮后会出现签入表单
'logout', // 展示签出按钮
'callout', // 展示呼出按钮,点击后会出现号码输入框
'status', // 暂时状态切换的下拉框
'mute', // 展示静音和取消静音按钮
'endAfterCall' // 展示结束话后处理按钮
]
// Vue3 单文件组件
<yimi-webrtc-phone ref="phoneBarRef" .size="size" .colors="JSON.Stringify(colors)" .buttons="buttons"></yimi-webrtc-phone>自定义 UI
该 SDK 的 UI 定义使用 web component 的具名插槽() 实现。可自定义的 UI 组件如下:
| Slot Name | Description | Props |
| -------------- | ---------------- | ----- |
| login | 签入按钮 | - |
| logout | 签出按钮 | - |
| call | 呼叫按钮 | - |
| hangup | 挂断按钮 | - |
| mute | 静音按钮 | - |
| cancel-mute | 取消静音按钮 | - |
| answer | 接听按钮 | - |
| endAfterCall | 结束话后处理按钮 | - |
| status | 状态选择器 | - |
| phoneForm | 呼叫表单 | - |
| loginForm | 登录表单 | - |
Vue3 示例代码,搭配 UI 框架 element-plus 。
<script setup>
...
import { register } from './webrtc-phone';
const config = ref({...});
// 如果你使用了 phoneForm slot 自定义输入号码的表单
// 你的处理 handleCall() 的逻辑
const handleCall = (flag) => {
if (!flag) {
phoneBarRef.value.handleCall(); // 不传入可以用于关闭表单 phoneForm,可用于处理点击取消
} else {
phoneBarRef.value.handleCall(number.value); // 调用呼出
}
}
// 如果你使用了 loginForm slot 自定义登录的表单
// 你的处理 initConfig() 的逻辑
const handleSubmit = (flag) => {
if (!flag) {
phoneBarRef.value.initConfig(); // 不传入可以用于关闭表单 phoneForm,可用于处理点击取消
} else {
phoneBarRef.value.initConfig(config.value); // 调用初始化坐席的方法
}
}
// 如果你使用了 status slot 自定义状态切换
const changeStatus = (code) => {
try {
phoneBarRef.value.changeSeatStatus(code); // 仅支持传入 1 或 2
} catch (error) {
log('error', error);
}
}
onMounted(() => {
// 注册名称为 yimi-webrtc-phone 的 Web Compnent
register();
// 监听自定义事件
phoneBarRef.value.addEventListener('phone-message', (e) => {
// log('phone-message', e);
const { type, data } = e.detail[0];
log('type', type, 'data', data);
if (type === 'status') {
state.value = data;
}
});
try {
// 如果 onMounted 生命周期中调用 initConfig() 可以实现自动登录
// phoneBarRef.value.initConfig(config.value);
} catch (error) {
console.log('error', error);
}
});
</script>
<template>
<yimi-webrtc-phone ref="phoneBarRef" .size="size" .colors="JSON.stringify(colors)">
// 签入按钮
<span slot="login">
<el-tooltip content="签入">
<el-button type="primary">签入</el-button>
</el-tooltip>
</span>
// 签出按钮
<span slot="logout"></span>
// 呼出按钮
<span slot="call"></span>
// 挂断按钮
<span slot="hangup"></span>
// 静音按钮
<span slot="mute"></span>
// 取消静音按钮
<span slot="cancel-mute"></span>
// 接听按钮
<span slot="answer" class="blink"></span>
// 结束话后处理按钮
<span slot="endAfterCall"></span>
// 切换坐席状态
<span slot="status"></span>
// 输入号码表单
<el-form slot="phoneForm" class="phone-form">
<el-form-item label="电话号码">
<el-input v-model="number" placeholder="请输入号码"></el-input>
</el-form-item>
... others form item
</el-form>
// 登录表单
<el-form slot="loginForm" class="phone-form" :model="config" style="width: 400px;" label-width="100px"
label-position="right">
<!-- 服务器地址 -->
<el-form-item label="服务器地址" prop="server" required>
<el-input v-model="config.server" placeholder="请输入服务器地址"></el-input>
</el-form-item>
<!-- 总机号 officeNumber -->
<!-- 工号 number-->
<!-- 密码 password-->
<!-- 类型 type-->
<!--预设签入状态 preStatus-->
<!-- 电话 phone-->
...
</el-form>
</yimi-webrtc-phone>
</template>
<style>
...
</style>
React 代码示例,搭配 UI 框加 Ant-Design。
import { register } from 'yimi-webrtc-phone';
import { useRef, useEffect, useState } from 'react';
import { Button, Select, Form, Input, Popover } from 'antd';
import {......} from '@ant-design/icons';
import './App.css';
const App = () => {
// dom 节点的 ref
const phoneRef = useRef(null);
// 标记是否调用了 register();
// 避免重复注册报错
const initialized = useRef(false);
const configed = useRef(false);
const colors = useRef({...});
const [isVoip, setIsVoip] = useState(true);
const config = useRef({...});
// 你的调用切换坐席状态的方法
const handleChange = (value) => {
console.log(`selected ${value}`);
if (phoneRef.current) {
phoneRef.current.changeSeatStatus(value); // 仅支持传入 1 或 2
}
};
// 你的输入号码表单的方法
const onFinish = (values) => {
console.log('Success:', values);
if (phoneRef.current) {
phoneRef.current.handleCall(values.number);
}
};
// 你的登录表单的方法
const onFormFinish = (values) => {
console.log('Success:', values);
if (phoneRef.current) {
phoneRef.current.initConfig(values);
}
};
const handleCall = () => {
if (phoneRef.current) {
phoneRef.current.handleCall(); // 关闭输入号码表单,可用于处理点击取消
}
};
const handleSubmit = (values) => {
if (phoneRef.current) {
phoneRef.current.initConfig(); // 关闭登录表单,可用于处理点击取消
}
};
const onValuesChange = (changedValues, allValues) => {
if (changedValues.type) {
setIsVoip(changedValues.type === 2);
console.log('isVoip:', isVoip);
}
};
useEffect(() => {
if (!initialized.current) {
// 注册为 Web Component
register();
initialized.current = true;
}
return () => {};
}, []);
useEffect(() => {
if (configed.current) {
console.log('Called initconfig()!');
return;
}
if (phoneRef.current) {
// 注册事件监听
phoneRef.current.addEventListener('phone-message', (message) => {
const { type, data } = message.detail[0];
switch (type) {......}
});
// 初始化配置项
try {
// phoneRef.current.initConfig(config.current); // 此处调用可实现自动登录
} catch (error) {
console.error('error:', error);
}
}
}, [phoneRef.current]);
return (
<div className="content">
<yimi-webrtc-phone
ref={phoneRef}
size="large"
colors={JSON.stringify(colors.current)}
>
<span slot="login">
// 签入按钮
<Popover content="login">
<Button size="large" color="#4096ff" shape="square" type="primary">
login
</Button>
</Popover>
</span>
// 签出按钮
<span slot="logout">...</span>
// 呼出按钮
<span slot="call">...</span>
// 挂断按钮
<span slot="hangup">...</span>
// 静音按钮
<span slot="mute">...</span>
// 取消静音按钮
<span slot="cancel-mute">...</span>
// 接听按钮
<span slot="answer">...</span>
// 结束话后处理按钮
<span slot="endAfterCall">...</span>
// 状态切换选择框
<span slot="status">...</span>
// 输入号码表单
<Form
slot="phoneForm"
// other props
>
<Form.Item
label="Phone Number"
name="number"
rules={[
{
required: true,
message: 'Please input phone number!',
},
]}
>
<Input placeholder="Input phone number" />
</Form.Item>
... others Form.Item
</Form>
// 登录表单
<Form
slot="loginForm"
// other props
>
<Form.Item
label="Server"
name="server"
rules={[
{
required: true,
message: 'Please input server!',
},
]}
>
<Input placeholder="Input server" />
</Form.Item>
...... others Form.Item
</Form>
</yimi-webrtc-phone>
</div>
);
};
export default App;原生 HTML 中使用可参照以上代码示例。
自定义事件
自定义事件用于抛出 SDK 内部的状态和当前的通话信息。
该自定义事件的回调函数回收到一个 event,向外抛出的数据存储在 event.detail[0] 中。
// 自定义事件监听
phoneElement.addEventListener("phone-message", (message) => {
console.log("message", message.detail[0]);
const { type, data } = message.detail[0];
if (type === "status") {
console.log("坐席状态变化", data);
} else {
console.log("通话信息变化", data);
}
......
});事件类型
| 字段 | 描述 | 取值 | | ---- | ------------------ | ------------------------------------------------------------ | | type | 标记抛出的事件类型 | status | register | newPBXCall | cancelPBXCall | calloutResponse | answeredPBXCall | endPBXCall | kickedOffLine | | data | 随事件的数据体 | number | object |
类型描述
| 类型 | 描述 | 值类型 | | -------------------- | ------------------------------------------------------------ | ------ | | status | 当前坐席的状态,对应关系见下表 | number | | register | 签入操作结果的推送,200 为成功 | number | | newPBXCall | 有电话呼入,可调用 handleAnswer() 进行接听,也可以在 UI 操作接听 | object | | cancelPBXCall | 来电取消,即在坐席未接的时候对方已挂断 | object | | calloutResponse | 呼出响应,呼出可通过 UI 操作,也可以通过 handleCall() 接口呼出 | object | | answeredPBXCall | 通话建立 | object | | endPBXCall | 通话结束 | object | | kickedOffLine | 坐席在其他地方登录被踢下线,或者是在后台强制签出该坐席 | object | | preSetStatusResponse | 用于获取 preSetBusy 和 handleCancelPreSetBusy 接口操作的结果,参数中有 reason 字段,200 标记操作成功 | object |
状态映射
| Code | Status | | ---- | -------- | | 0 | 离线 | | 1 | 空闲 | | 2 | 忙碌 | | 3 | 振铃中 | | 4 | 通话中 | | 6 | 话后处理 |
通话信息
| 字段 | 描述 | | ----------- | ------------------------ | | name | 当前通话对象的名称 | | number | 当前通话对象的号码 | | attribution | 当前通话对象号码的归属地 |
更多字段信息可以参照 jssip-emicnet 的文档。
API
initConfig()
该接口用于初始化 SDK 及登录坐席。
如果使用了 loginForm 插槽,可用于关闭表单。
| Parameter | Type | Require | Description |
| --------- | -------- | ------- | -------------------------------------------------------- |
| parameter | object | NO | 初始化的相关参数,如果用于登录坐席,参数必须且正确传递。 |
// 以 vue3 为例
const config = ref({
server: '', // 服务器地址
officeNumber: '', // 总机号
number: '', // 分机号(坐席号)
password: '', // 坐席密码
type: '2', // 登录类型 2-voip 4-回拨话机 5-sip话机
preStatus: '2', // 登录之后的状态,默认是空闲,可以传入 1,2。1-空闲 2-忙碌,忙碌状态不能被呼入,可以呼出。
phone: '', // 回拨话机号或者 sip 话机号,当 type !== 2 时,该参数必传
// autoLogin: true, // 是否需要自动登录,如果为 true,则会自动登录,登陆失败 UI 会显示签入按钮,可手动操作,相应错误信息会显示在 UI 上
})
try {
phoneBarRef.value.initConfig(config.value);
} catch (error) {
log('error', error);
}
// 用于关闭表单
phoneBarRef.value.initConfig();在版本 1.1.0 中,移除 initConfig() 的 autoLogin 字段,实现自动登录,可在使用该 web component 的页面中初始化调用 initConfig(),并传入正确的参数即可。例如在 Vue3 的 onMounted 中调用。
handleCall()
该接口用于拨打电话。
如果使用了 phoneForm 插槽,可用于关闭表单。
| Parameter | Type | Require | Description |
| --------- | ------------------- | ------------------------------------------------- | ------------------------------------------------- |
| parameter | string object | NO | 如果用于呼出,参数必须且正确传递。 |
phoneBarRef.value.handleCall('10010');
// or
phoneBarRef.value.handleCall({
callee: '10010', // Required: Phone number to call, 该参数必传。
[orther_params]: '', // orther_params 一般为客户需要透传的参数,传入后,可在通话相关推送的 userData 中获取
...
})
// 用于关闭表单
phoneBarRef.value.handleCall();handleLogout()
该接口用于签出坐席。
| Parameter | Type | Require | Description | | --------- | ---- | ------- | ----------- | | -- | -- | -- | -- |
// Basic usage
phoneBarRef.value.handleLogout();
// With event listener for status change
phoneBarRef.value.addEventListener('phone-message', (event) => {
const { type, data } = event.detail[0];
if (type === 'status' && data === 0) {
console.log('Successfully logged out');
}
});
phoneBarRef.value.handleLogout();handleAnswer()
该接口用于接听电话。
仅在有呼入的情况可用,即 newPBXCall 事件被触发时。
| Parameter | Type | Require | Description | | --------- | ---- | ------- | ----------- | | -- | -- | -- | -- |
// Basic usage
phoneBarRef.value.handleAnswer();
// With Event Listener
phoneBarRef.value.addEventListener('phone-message', (event) => {
const { type, data } = event.detail[0];
// Listen for incoming calls
if (type === 'newPBXCall') {
phoneBarRef.value.handleAnswer();
}
// Verify call is connected
if (type === 'answeredPBXCall') {
console.log('Call connected');
}
});handleMute()
用于静音通话的坐席侧。该接口为异步接口,有返回值代表操作成功,返回值 true 为静音状态,返回值 false 为未静音状态。
该接口仅可在易米产品的 v8 版本中使用。仅在通话状态可用,即 status === 4 时。
| Parameter | Type | Require | Description | | --------- | ---- | ------- | ----------- | | -- | -- | -- | -- |
try {
const isMuted = await phoneBarRef.value.handleMute();
console.log('Call is now', isMuted ? 'muted' : 'unmuted');
} catch (error) {
console.error('Mute operation failed:', error.message);
}handleEndAfterCall()
用于结束话后处理状态。
仅在化后处理状态可用,即 status === 6 时。
| Parameter | Type | Require | Description | | --------- | ---- | ------- | ----------- | | -- | -- | -- | -- |
// Basic usage
phoneBarRef.value.handleEndAfterCall();
// With Event Listener
phoneBarRef.value.addEventListener('phone-message', (event) => {
const { type, data } = event.detail[0];
// Monitor status changes
if (type === 'status') {
if (data === 6) { // AFTER_CALL state
// Auto end after-call work after 30 seconds
setTimeout(() => {
phoneBarRef.value.handleEndAfterCall();
}, 30000);
}
else if (data === 1) { // FREE state
console.log('After-call work completed');
}
}
});handleHangup()
该接口用于挂断和拒接电话。
仅在通话中或者有呼入的情况下可用。即 newPBXCall 被触发或者 status === 4 时。
| Parameter | Type | Require | Description | | --------- | ---- | ------- | ----------- | | -- | -- | -- | -- |
// Basic usage
phoneBarRef.value.handleHangup();
// With Event Listener
phoneBarRef.value.addEventListener('phone-message', (event) => {
const { type, data } = event.detail[0];
// Monitor call states
if (type === 'endPBXCall') {
console.log('Call ended');
}
// Verify status change
if (type === 'status' && data === 1) {
console.log('Agent returned to FREE state');
}
});
phoneBarRef.value.handleHangup();getSeatStatus()
用于获取当前坐席的状态。
| Parameter | Type | Require | Description | | --------- | ---- | ------- | ----------- | | -- | -- | -- | -- |
// Basic usage,status 值对应的状态参照上文列出的映射
const status = phoneBarRef.value.getSeatStatus();changeSeatStatus()
用于切换坐席的状态。仅在坐席为空闲/忙碌/话后处理的状态下可切换。
| Parameter | Type | Require | Description | | --------- | ---------------- | ------- | ----------------------------------- | | parameter | number | string | yes | 可选值为 1 和 2,1 是空闲,2 是忙碌 |
// Basic usage
phoneBarRef.value.changeSeatStatus(1); // 切换为空闲
phoneBarRef.value.changeSeatStatus(2); // 切换为忙碌handlePreSetBusy
用于预设通话结束后的坐席状态为忙碌。仅当坐席在通话中/振铃中可使用。
| Parameter | Type | Require | Description | | --------- | ------ | ------- | ------------------ | | ccNumber | string | yes | 当前通话的唯一标识 |
handleCancelPreSetBusy
用于取消预设通话结束后的坐席状态为忙碌。仅当坐席在通话中/振铃中可使用。是 handlePreSetBusy 的逆操作。
| Parameter | Type | Require | Description | | --------- | ------ | ------- | ------------------ | | ccNumber | string | yes | 当前通话的唯一标识 |
exportLogs()
该接口用于导出写入 LocalStorage 的操作日志,导出格式为 txt。
| Parameter | Type | Require | Description | | --------- | ---- | ------- | ----------- | | -- | -- | -- | -- |
phoneBarRef.value.exportLogs();
// 数据格式
[
{
"timestamp": "2024-03-21T10:30:45.123Z",
"level": "info|warn|error",
"message": "log message content"
}
]通话结束原因说明
| Code | Value | | ---- | ---------------- | | 1 | 企业非工作时间 | | 2 | 呼入白名单限制 | | 3 | 呼入黑名单 | | 4 | 菜单放弃 | | 5 | 技能组放弃 | | 6 | 排队放弃 | | 7 | 振铃放弃 | | 8 | 技能组非工作时间 | | 9 | 无空闲坐席 | | 10 | 坐席离线 | | 11 | 坐席忙碌 | | 12 | 回呼坐席失败 | | 13 | 坐席未接 | | 14 | 坐席流转 | | 15 | 技能组无人接听 | | 16 | 呼叫外线失败 | | 17 | 外线未接 | | 18 | 呼叫客户失败 | | 19 | 客户未接 | | 20 | 呼出放弃 | | 21 | 呼出白名单限制 | | 22 | 呼出黑名单 | | 23 | 呼出防骚扰限制 | | 24 | 呼出时间限制 | | 25 | 客户挂断 | | 26 | 坐席挂断 | | 27 | 外线挂断 | | 28 | 异常中断 | | 29 | 班长强拆 | | 30 | 班长拦截 | | 31 | 转接挂断 | | 32 | 菜单结束 | | 33 | 坐席停用 | | 34 | 总机号码停用 | | 35 | 转接放弃 | | 36 | 系统挂断 | | 37 | 总机号码删除 | | 38 | 排队超时 | | 39 | 技能组不存在 | | 40 | 坐席不存在 | | 41 | 转IVR流程失败 | | 42 | 坐席取消 | | 43 | 取消转接 | | 44 | 转接失败 | | 45 | 技能组呼叫失败 | | 46 | 流转放弃 | | 47 | 呼叫放弃 | | 48 | 异常结束 |
