Vue外呼控制台:依赖预编译、UEditor集成与录音回放
2026/9/14 15:15:13 网站建设 项目流程

简介:一套基于Vue框架的机器人电话营销前端源码,可直接运行并支持二次开发,面向需要搭建电话营销系统的前端工程师,涉及话术配置、客户资料管理、外呼记录看板等核心功能界面。压缩包内共九百三十个文件,以Vue单文件组件、JavaScript逻辑文件和样式资源为主,包括三百八十八个js、三百三十三个vue、四十三个scss、一十八个css等类型,另有图片、Markdown文档和配置文件,整体大小约十四点四九兆字节,目录结构清晰,便于分层查阅与复用。目前已有二百二十七人学习,项目内置完整npm工作流,通过依赖安装、公共库预编译、本地开发和生产打包即可稳定运行,可帮助熟练度中等的Vue开发者深入理解电话营销业务前端的架构组织、组件通信和工程化配置思路,适合作为团队项目启动模板或个人学习参考。

1. 外呼机器人控制台,为什么要用 Vue 单独维护一套前端

机器人电话营销(业内一般叫外呼机器人)项目的前端,面对的不是 C 端用户,而是运营、坐席和管理员。他们要创建外呼任务、配置话术、盯实时话单、回听录音、处理未接通名单,一个后台页面同时承担状态机、富文本编辑和大量表格渲染。这种项目功能密度高、改版节奏快,前后端拆开,由 Vue 单独维护一套前端工程,几乎是必然选择。

这个源码包是一个完整的 Vue 2 业务前端:路由里有任务创建、话术模板、通话记录等模块,静态资源下带了一套 UEditor 富文本编辑器的皮肤文件(editor.css、dialog.css、editor_ie7.css 这一批),构建脚本里还有 npm run dll 的依赖预编译步骤。适合两类人:一是想找一个能跑起来的真实业务工程、看完整目录结构的前端新人;二是做外呼、CRM、客服工单类系统,想参考任务状态、录音回放、模板编辑这些模块怎么落地的团队。

我拆完这套代码的感受是,业务页面反而不是重点,构建层的 DLL 预编译、UEditor 在 Vue 组件里的生命周期管理、以及录音 m3u8 分片的播放处理,这三处才是真正有复用价值的东西,后面按这三个点展开。

2. npm run dll 依赖预编译:webpack DllPlugin 在外呼项目里的实际作用

2.1 外呼项目为什么需要依赖预编译

外呼控制台的第三方依赖很典型:vue、vue-router、vuex、axios,再加上 element-ui 组件库和 echarts 报表。问题在于这些库几乎每次开发启动都要被 webpack 重新解析、转译、压缩,表格页面越加越多,dev server 冷启动能拖到一两分钟,改一行配置再重启又是几十秒。npm run dll 的思路,是把"基本不动的第三方代码"预先编译成一份 vendor.dll.js,同时生成 vendor-manifest.json 记录每个模块的 id 和文件位置;之后的 dev 和 build 通过 DllReferencePlugin 直接引用这份产物,webpack 不再进入这些库的源码去解析,启动时间能明显降下来。

这个方案是 webpack 3/4 时代针对大工程的主流做法,新项目现在普遍改用持久化缓存,但源码包里保留了这套配置,说明工程就是那个时期搭起来的。项目里还保留了 .babelrc 这种以配置文件为中心的 Babel 写法,同期的构建脚本还是 webpack.[env].conf.js 分文件组织,这些旧约定和 dll 是配套的,照这个结构维护成本最低。

2.2 三份关键配置:dll 专用配置、引用配置和入口 HTML

先看 package.json 里的脚本,注意 dll 必须在 dev 和 build 之前执行:

{ "scripts": { "dll": "webpack --config build/webpack.dll.conf.js", "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "build": "node build/build.js", "build:report": "npm run build --report" } }

dll 专用的 webpack 配置是这样:

// build/webpack.dll.conf.js const path = require('path') const webpack = require('webpack') module.exports = { entry: { vendor: ['vue', 'vue-router', 'vuex', 'axios', 'element-ui', 'echarts'] }, output: { path: path.resolve(__dirname, '../static/js'), filename: '[name].dll.js', // 生成 vendor.dll.js library: '[name]_library' // 全局变量名,供 DllReferencePlugin 对照 }, plugins: [ new webpack.DllPlugin({ path: path.resolve(__dirname, 'vendor-manifest.json'), name: '[name]_library', context: __dirname }) ] }

entry 里列的库就是"预编译边界",新增第三方库时要把库名补进来再重跑 dll。output.library 和 DllPlugin 的 name 必须一致,否则 manifest 和实际产物对不上;context 也要固定,因为后续引用 manifest 时是按 context 去解析模块 id 的。

主构建配置里引用这份 manifest:

// build/webpack.base.conf.js const webpack = require('webpack') module.exports = { // ...其余配置 plugins: [ new webpack.DllReferencePlugin({ context: path.resolve(__dirname, '..'), manifest: require('./vendor-manifest.json') }) ] }

最后是入口 HTML,vendor.dll.js 必须用普通 script 标签先于业务 bundle 加载:

<!-- index.html --> <script src="/static/js/vendor.dll.js"></script> <script src="/dist/js/app.js"></script>

vendor.dll.js 是独立于 webpack 运行时的产物,不再经过后续构建,所以不能走打包入口,只能这样直接引用。顺序反了会出现页面白屏,控制台报 vendor_library 未定义。

2.3 用了 dll 之后的对比和最容易踩的坑

场景未用 DLL 时的表现用 DLL 后的表现
dev 冷启动第三方库全部重编译,60 秒以上第三方库跳过,10~20 秒进页面
升级 element-ui 版本改版本号重启即可必须重跑 npm run dll,否则 manifest 过期
新增一个第三方库直接 import 就能用要加进 dll entry 再重跑 dll

这里有个高频报错:升级依赖后忘记重跑 dll,页面打开后控制台报 Cannot read property 'xxx' of undefined,十有八九是 manifest 里的模块 id 和 vendor.dll.js 不一致。处理办法是删掉 vendor-manifest.json 和 static/js/vendor.dll.js,重新 npm install 后再执行 npm run dll。另外 CI 构建机上如果没执行过 dll 这步,产物会缺 vendor 声明,直接报 ReferenceError,所以流水线里要把 dll 作为一个独立 stage 前置。npm install 阶段如果遇到 node-sass 这类原生模块编译失败,和 dll 没有关系,是 Node 版本和依赖版本不匹配,先解决环境再看 dll 报错。

3. UEditor 话术模板:项目里那批 editor*.css 资源怎么挂进 Vue

3.1 从文件清单反推出一套完整的 UEditor

看项目文件列表,editor.css、dialog.css、editor_ie7.css、editor_iequirks.css、editor_gecko.css、dialog_ie8.css 这一批,正是 UEditor 的皮肤样式和不同内核的兼容补丁。外呼系统里 UEditor 不是装饰:话术配置是运营改动最频繁的环节,要加粗关键引导句、插入图片说明,还要把名单字段(客户姓名、产品名、日期)以变量形式放进话术,普通 textarea 完全扛不住。

这些 CSS 的对应关系如下:

文件作用
editor.css编辑器主体样式:工具栏、编辑区、下拉菜单
dialog.css插入图片、链接等通用对话框的样式
editor_ie.css / editor_ie7.css / editor_ie8.cssIE 各版本的基础与 hack 兼容样式
editor_iequirks.cssIE 怪异模式下的兼容样式
editor_gecko.cssGecko 内核(Firefox)的兼容样式
dialog_ie7.css / dialog_ie8.css对话框在旧 IE 下的定位修正
animate.min.css弹层展开收起的动画

出现这么多 IE 兼容文件,不代表项目要支持老 IE,而是 ueditor.all.js 加载时会按 navigator.userAgent 判断内核,动态注入对应样式表。如果手动删掉 editor_ie7.css,在老内核环境会出现工具栏错位,保留不用的文件反而是安全的做法。

3.2 在 Vue 组件里管理 UEditor 的生命周期

常见做法是封装一个 TplEditor 组件,所有话术编辑页面复用它。不要想当然地用 v-model 去绑 UEditor 的 DOM,它的内容变化由编辑器内部事件驱动,正确姿势是监听 contentChange 再向外抛输入事件:

// src/components/TplEditor.vue <template> <div ref="editor" class="tpl-editor"></div> </template> <script> export default { name: 'TplEditor', props: { value: { type: String, default: '' }, height: { type: Number, default: 320 } }, mounted () { // UE 是 ueditor.all.js 挂到 window 的全局对象,先做存在性检查 if (!window.UE) { console.error('UEditor 未加载,请确认 static/ueditor 已引入') return } this.editor = UE.getEditor(this.$refs.editor, { initialFrameHeight: this.height, UEDITOR_HOME_URL: '/static/ueditor/', toolbars: [[ 'bold', 'italic', 'underline', 'fontsize', 'forecolor', '|', 'insertimage', 'link', '|', 'inserttpl' ]] }) // ready 事件里回填父组件传入的内容 this.editor.addListener('ready', () => { this.editor.setContent(this.value || '') }) // 内容变化时同步给 v-model this.editor.addListener('contentChange', () => { this.$emit('input', this.editor.getContent()) }) }, beforeDestroy () { // 不销毁会残留编辑器实例的定时器和全局事件 this.editor && this.editor.destroy() } } </script>

UE.getEditor 的第二个参数是配置对象,这里最关键的是 UEDITOR_HOME_URL,它决定语言包、皮肤图片、插件资源的相对路径,写错的话 dev 环境正常,打包部署后图标全裂。toolbars 二维数组里每一项是一组工具按钮,'|' 是分隔符。业务上把话术编辑的工具栏收敛成加粗、颜色、图片、链接这几项,比默认的完整工具栏更符合坐席使用习惯。

3.3 做"插入话术变量"的扩展按钮

3.3.1 用 registerUI 注册业务按钮

外呼话术必须支持变量占位,坐席在编辑器里点一个按钮就能插入${customerName}这样的标记:

// src/utils/ueditor-ext.js UE.registerUI('inserttpl', function (editor) { const btn = new UE.ui.Button({ name: 'inserttpl', title: '插入话术变量', onclick () { // inserthtml 是 UE 内置命令,把占位符当 HTML 片段插入光标处 editor.execCommand('inserthtml', '${customerName}') } }) return btn })

registerUI 注册的按钮会出现在所有 UEditor 实例的 toolbars 里,前提是工具名一致。execCommand 的 inserthtml 命令会在当前光标位置插入内容,比手动拼 selection 对象可靠得多。占位符保存到后端后,外呼引擎把名单字段替换成真实值再送给语音合成,前端只负责占位符的展示和校验。

3.3.2 保存前校验变量格式

话术保存时常见的问题是运营手改导致${缺了右括号,这类脏数据到外呼引擎才暴露,代价很高。可以在提交前加一道校验:

function validateTpl (html) { const tagRe = /\$\{\s*([\w.]+)\s*\}/g const broken = [] let m while ((m = tagRe.exec(html))) { // 这里只需要确认占位符格式完整 if (!m[1]) broken.push(m[0]) } if (broken.length) { throw new Error('存在不完整的变量占位符') } }

正则匹配${...}结构,m[1] 拿不到值时说明占位符不完整,直接拦截保存请求。这一步虽然只有十几行,但在多话术、多线路的外呼项目里能挡掉相当一部分线上误呼问题。

4. 呼叫任务与通话录音:vue-router 组织方式和状态映射

4.1 路由层级:任务模块用嵌套路由,record 页面独立

外呼任务的页面结构是"任务列表 → 创建任务 → 任务详情",详情页要复用任务模块的布局和权限判断,所以用嵌套路由,子页面通过 props 传 id:

// src/router/index.js import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export default new Router({ // 用 history 模式要求部署端配 try_files 回退,否则刷新就 404 mode: 'history', routes: [ { path: '/', redirect: '/task/list' }, { path: '/task', component: () => import('@/views/task/TaskLayout.vue'), children: [ { path: 'list', name: 'TaskList', component: () => import('@/views/task/TaskList.vue') }, { path: 'create', name: 'TaskCreate', component: () => import('@/views/task/TaskCreate.vue') }, { path: 'detail/:id', name: 'TaskDetail', component: () => import('@/views/task/TaskDetail.vue'), props: true } ] }, { path: '/record', name: 'RecordList', component: () => import('@/views/record/RecordList.vue') } ] })

组件全部用动态 import,用户从列表页点进详情页才加载详情代码块。props: true 把路由 params 里的 id 直接映射成组件的 props,组件内部不要再从 this.$route 里拿参数,方便单测和复用。外呼系统的菜单权限一般按路由 meta 控制,实际操作里会在路由表上挂 meta: { permission: 'task:view' },由全局守卫在跳转前校验,record 页面独立成一级路由也是因为它要单独控制录音调听权限。

4.2 任务状态机:后端一个数字,前端一张映射表

外呼任务是典型状态机:新建、执行中、暂停、完成、失败。后端一般返回数字状态码,前端不要把它散落在各个组件里判断,集中维护一张映射:

状态码含义前端标签样式数据层动作
0待执行灰色标签到点自动触发外呼
1呼叫中蓝色标签,带 loading列表页 5 秒轮询话单
2已接通绿色标签可回听录音、转人工
3未接通橙色标签进入重呼策略排期
4已完成默认标签查看意图识别结果
5已暂停黄色标签等待手动恢复

状态迁移的合法性放在后端校验,前端只做展示和可操作性控制。例如已暂停的任务只显示"恢复"和"复制"按钮,不显示"编辑",用一张 actionMap 控制:

// src/views/task/status.js export const STATUS = { 0: { label: '待执行', type: 'info' }, 1: { label: '呼叫中', type: 'primary', loading: true }, 2: { label: '已接通', type: 'success' }, 3: { label: '未接通', type: 'warning' }, 4: { label: '已完成', type: 'default' }, 5: { label: '已暂停', type: 'warning' } } export const ACTIONS = { 0: ['edit', 'start'], 5: ['start', 'copy'], 4: ['copy', 'export'] }

ACTIONS 表决定按钮渲染,新增状态只改这一处,不用翻列表页和详情页两套模板。如果后端调整了状态码,status.js 要和主要页面同一个迭代周期上线,否则列表页会渲染出空的 label,用户看到空白标签会直接走工单。

4.3 通话录音播放:mp3 直链和 m3u8 分片两条路径

外呼录音在系统里有两种存储形态:短录音给 mp3 直链,长录音或需要转写的场景会走 HLS 分片,也就是 m3u8。浏览器原生 audio 播不了 m3u8,常见做法是引入 hls.js:

// src/views/record/RecordList.vue <template> <audio ref="player" controls class="record-player"></audio> </template> <script> import Hls from 'hls.js' export default { name: 'RecordList', beforeDestroy () { // 手动销毁 hls 实例,避免切页后音频资源不释放 if (this.hls) this.hls.destroy() }, methods: { playRecord (url) { const audio = this.$refs.player if (!url) return if (url.indexOf('.m3u8') > -1 && Hls.isSupported()) { this.hls && this.hls.destroy() this.hls = new Hls() this.hls.loadSource(url) this.hls.attachMedia(audio) this.hls.on(Hls.Events.ERROR, (e, data) => { // 分片加载失败时先尝试恢复,恢复不了再报错 if (data.fatal) { this.hls.recoverMediaError() } }) } else { audio.src = url } audio.play().catch(() => { // 浏览器自动播放策略会拒绝 play(),提示用户手动点击 console.warn('录音播放被浏览器拦截,请手动点击播放') }) } } } </script>

每次播放前先销毁上一个 hls 实例,否则连续切换多条录音时,旧分片的请求会持续堆积,网络面板里全是 pending 的 ts 请求。audio.play() 在移动端经常被自动播放策略拦截,catch 里给一条提示比直接报错体验好。外呼录音还有一个注意点:如果录音文件需要鉴权,m3u8 里的分片地址要带签名参数,后端生成播放地址时就要把过期时间算好,前端只消费处理过的 URL,不要在响应拦截器里给 ts 请求追加 token,会打爆签名服务。

5. 打包产物体检:bundle 分析和 UEditor 样式失真的修复顺序

npm install npm run dll npm run build --report

build 结束后会自动打开 webpack-bundle-analyzer 的报告页,这里主要看三块:vendor.dll.js 里各库的占比、业务代码里有没有把 UEditor 或 echarts 重复打包、以及各路由 chunk 的体积是否可接受。如果报告里出现两份 echarts,八成是某个页面直接 import 了 echarts 而不是走统一封装,把高频组件收敛到一个公共入口是外呼报表类页面的常规操作。

5.1 顺手裁剪 moment 语言包

外呼后台的时间处理基本都会引 moment,默认带上全量语言包,里面七成用不到。在 webpack 配置里加一条 IgnorePlugin:

// build/webpack.base.conf.js new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)

加完后 moment 默认只在需要的地方手动引入中文包。这个改动同时作用于 vendor.dll.js 和业务 bundle,改完记得重新执行 npm run dll,再对比 report 里的体积变化,是我处理这类老工程最先做的一步。

5.2 UEditor 打包后布局异常的标准修复流程

vue 打包后布局异常,在外呼这类带富文本的项目里非常常见,症状是 dev 正常、生产环境工具栏图标消失、弹层错位。根因是 ueditor.config.js 里 UEDITOR_HOME_URL 的默认值在打包后被 publicPath 干扰,CSS 里相对路径引用的图片资源全部 404。处理顺序我一般是这样:

  1. 把 ueditor 整套目录放在 static 下(static 会被整体拷贝到 dist),保证 ueditor.all.js、lang、themes 的目录结构完整。
  2. 在 ueditor.config.js 里把 window.UEDITOR_HOME_URL 改为部署后的绝对路径,比如 /static/ueditor/。
  3. 部署到子目录时,publicPath 和 UEDITOR_HOME_URL 要同步改,只改一个必然出问题。
  4. 完成后在浏览器 Network 面板过滤 editor.css 和 themes 下的图片,逐一确认 200。

验证手段是一句命令:

grep -r "static/ueditor" dist/

只要构建产物里能搜到 static/ueditor 相关引用,且文件实际存在于 dist/static/ueditor 下,样式问题基本就解决了;搜不到则说明资源被 webpack 过滤掉了,需要回头检查 static 目录的拷贝配置。如果 dist 里能搜到引用,但 Network 面板还是 404,多半是 Nginx 的 location 规则把静态目录的请求也回退到了 index.html,检查 try_files 里有没有给 static 后缀单独的匹配段。按这个顺序排查下来,UEditor 的样式问题不需要翻源码就能定位。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询