├── .gitignore ├── README.md ├── miniprogram ├── api │ └── index.ts ├── app.json ├── app.less ├── app.ts ├── components │ ├── no-data │ │ ├── no-data.json │ │ ├── no-data.less │ │ ├── no-data.ts │ │ └── no-data.wxml │ └── svg-icon │ │ ├── svg-icon.json │ │ ├── svg-icon.less │ │ ├── svg-icon.ts │ │ └── svg-icon.wxml ├── consts │ └── index.ts ├── img │ ├── delete.svg │ ├── figure.svg │ ├── logo.svg │ ├── logout.svg │ ├── microphone.svg │ ├── play.svg │ ├── search.svg │ ├── show │ │ ├── 01.PNG │ │ ├── 02.PNG │ │ ├── 03.PNG │ │ ├── 04.PNG │ │ ├── 05.PNG │ │ └── 06.PNG │ ├── suspend.svg │ └── tabBar │ │ ├── microphone-selected.png │ │ ├── microphone.png │ │ ├── my-selected.png │ │ └── my.png ├── mock │ └── data.ts ├── pages │ ├── index │ │ ├── call │ │ │ ├── call.json │ │ │ ├── call.less │ │ │ ├── call.ts │ │ │ ├── call.wxml │ │ │ ├── call.wxss │ │ │ └── scene │ │ │ │ ├── record │ │ │ │ ├── record.json │ │ │ │ ├── record.less │ │ │ │ ├── record.ts │ │ │ │ └── record.wxml │ │ │ │ ├── scene.json │ │ │ │ ├── scene.less │ │ │ │ ├── scene.ts │ │ │ │ ├── scene.wxml │ │ │ │ └── scene.wxss │ │ ├── index.json │ │ ├── index.less │ │ ├── index.ts │ │ └── index.wxml │ ├── login │ │ ├── login.json │ │ ├── login.less │ │ ├── login.ts │ │ └── login.wxml │ └── my │ │ ├── my.json │ │ ├── my.less │ │ ├── my.ts │ │ └── my.wxml ├── sitemap.json └── utils │ ├── env.ts │ ├── request.ts │ └── util.ts ├── package.json ├── project.config.json ├── project.private.config.json ├── tsconfig.json └── typings ├── index.d.ts └── types ├── index.d.ts └── wx ├── index.d.ts ├── lib.wx.api.d.ts ├── lib.wx.app.d.ts ├── lib.wx.behavior.d.ts ├── lib.wx.cloud.d.ts ├── lib.wx.component.d.ts ├── lib.wx.event.d.ts └── lib.wx.page.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | yarn.lock 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | # typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | 64 | .DS_Store 65 | 66 | .vscode 67 | 68 | run/ 69 | 70 | dist/ 71 | 72 | package-lock.json 73 | 74 | nei.* 75 | api-mocks 76 | compilation-stats.json 77 | .history 78 | .idea 79 | *.js 80 | !miniprogram/other/**/*.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 录音播放小程序 2 | 比较完整的小程序项目,提供了小程序录音、播放、上传等功能实现的参考。 3 | 4 | 主要使用以下 api 5 | 1. 全局唯一的录音管理器:[RecorderManager](https://developers.weixin.qq.com/miniprogram/dev/api/media/recorder/RecorderManager.html) 6 | 1. [wx.createInnerAudioContext](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/wx.createInnerAudioContext.html): 创建内部 audio 上下文 [InnerAudioContext](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/InnerAudioContext.html) 对象 7 | 8 | ## 使用步骤 9 | 1. `npm i` // 如未有依赖,可忽略 10 | 1. 工具 -> 构建 npm 11 | 12 | ## 约束 13 | 1. TS 声明文件放置 typings/ 中,不需要 import、export 14 | 1. git 过滤 ts 编译后的 js 文件,避免使用,非要使用统一放置 miniprogram/other/ 目录下 15 | 1. 不需要生成 .wxss 文件,改成 .less 文件,已开启编译 16 | 17 | ## 注意事项 18 | 19 | ```javascript 20 | /** 21 | * 小程序陈年老 bug: 22 | * 提交审核 envVersion === 'develop', 23 | * 根据这个字段区分接口域名, 24 | * 导致审核时无法访问内网的域名 25 | * 审核被驳回 26 | * 只能每次提交审核的时候手动改下代码 27 | * 测试的时候在改回来。。。 28 | */ 29 | 30 | // env.ts 31 | const { miniProgram: { envVersion } } = wx.getAccountInfoSync(); 32 | ``` 33 | 34 | ## 常见问题 35 | 1. 如果发现编译失效,可手动尝试 `npm run tsc`,已开启自动编译 ts、less 36 | 1. 如无法预览、调试,关闭当前项目,点击导入右侧管理,删除当前项目,重新加载 37 | 38 | PS. 最新开发工具,已支持自动生成 ts、less,忽略 js、wxss 39 | 40 | ## 部分展示 41 |
42 | 首页 43 | 录音 44 | 录音中 45 | 播放 46 | 播放中 47 | 登陆 48 |
49 | -------------------------------------------------------------------------------- /miniprogram/api/index.ts: -------------------------------------------------------------------------------- 1 | import request from '../utils/request'; 2 | import mock from '../mock/data'; 3 | 4 | /** 5 | * 发送手机验证码接口 6 | * @param data 7 | * @param loading 8 | */ 9 | export function reqSmsCode(data: { mobile: string }, loading = true) { 10 | return request({ 11 | url: '/api/reqSmsCode', 12 | data, 13 | mockData: { 14 | 15 | } 16 | }, loading); 17 | } 18 | 19 | /** 20 | * 校验手机验证码接口(登录) 21 | * @param data 22 | * @param loading 23 | */ 24 | export function verifySmsCode(data: { mobile: string; verifyCode: string }, loading = true) { 25 | return request({ 26 | url: '/api/verifySmsCode', 27 | data, 28 | mockData: { 29 | name: '张三', 30 | phone: 12345678901 31 | } 32 | }, loading); 33 | } 34 | 35 | /** 36 | * 录音师的话术列表接口 37 | * @param data 38 | */ 39 | export function operateList(data: { 40 | page: number; 41 | pageSize: number; 42 | status?: number; // 0-未完成;1-已完成 43 | botName: string; 44 | }, loading = true) { 45 | return request({ 46 | url: '/api/operate/list', 47 | data, 48 | mockData: mock.operateList, 49 | }, loading); 50 | } 51 | 52 | /** 53 | * 录音师的话术录音详情 54 | * @param data 55 | */ 56 | export function operateBotDetail(data: { 57 | botId: number; 58 | accountKey: string; 59 | }, loading = true) { 60 | return request({ 61 | url: '/api/operate/botDetail', 62 | data, 63 | mockData: mock.operateBotDetail 64 | }, loading); 65 | } 66 | 67 | export function getRecordingSceneDetail(data: { sceneId: number; accountKey: string }) { 68 | return request({ 69 | url: '/api/operate/sceneDetail', 70 | data, 71 | mockData: mock.sceneDetail 72 | }, true) 73 | } 74 | 75 | /** 76 | * 录音师的快速开始上下条切换 77 | * @param data 78 | */ 79 | export function lastOrNext(data: { 80 | curId?: number; // 当前的录音id 81 | accountKey: string; // 账号 82 | botId: number; // 话术id 83 | type?: 'LAST' | 'NEXT'; // LAST/NEXT 84 | sceneId?: number; // 画布id 85 | qaId?: number; // 问答知识id 86 | curNum?: number; // 当前第几条 87 | isQuickStart?: boolean; // 是否快速开始 88 | }, loading = true) { 89 | return request({ 90 | url: '/api/operate/lastOrNext', 91 | data, 92 | mockData: { 93 | ...mock.sceneDetail.recordingDetails.find(it => it.recordingId === Number(data.curId)) || mock.sceneDetail.recordingDetails[1], 94 | } 95 | }, loading); 96 | } 97 | 98 | /** 99 | * 上传获取 nos token 等信息 100 | * @param data 101 | */ 102 | export function getNosToken(data: { fileName: string }) { 103 | return request<{ 104 | bucket: number; 105 | expireSeconds: string; 106 | objectName: number; 107 | token: string; 108 | }>({ 109 | url: '/api/nos/token', 110 | data, 111 | mockData: { 112 | bucket: 1, 113 | expireSeconds: 'string', 114 | objectName: 1, 115 | token: 'string', 116 | } 117 | }); 118 | } -------------------------------------------------------------------------------- /miniprogram/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/index/index", 4 | "pages/login/login", 5 | "pages/my/my", 6 | "pages/index/call/call", 7 | "pages/index/call/scene/scene", 8 | "pages/index/call/scene/record/record" 9 | ], 10 | "window": { 11 | "backgroundColor": "#F5F7FA", 12 | "backgroundTextStyle": "light", 13 | "navigationBarBackgroundColor": "#F5F7FA", 14 | "navigationBarTitleText": "", 15 | "navigationBarTextStyle": "black" 16 | }, 17 | "tabBar": { 18 | "custom": false, 19 | "borderStyle": "white", 20 | "color": "#BFBFBF", 21 | "selectedColor": "#337EFF", 22 | "list": [ 23 | { 24 | "pagePath": "pages/index/index", 25 | "text": "话术录音", 26 | "iconPath": "./img/tabBar/microphone.png", 27 | "selectedIconPath": "./img/tabBar/microphone-selected.png" 28 | }, 29 | { 30 | "pagePath": "pages/my/my", 31 | "text": "我的", 32 | "iconPath": "./img/tabBar/my.png", 33 | "selectedIconPath": "./img/tabBar/my-selected.png" 34 | } 35 | ] 36 | }, 37 | "lazyCodeLoading": "requiredComponents", 38 | "style": "v2", 39 | "sitemapLocation": "sitemap.json" 40 | } -------------------------------------------------------------------------------- /miniprogram/app.less: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | page { 3 | background-color: #F5F7FA; 4 | color: #222222; 5 | height: 100%; 6 | font-size: 28rpx; 7 | /* background: linear-gradient(157.39deg, #BBDAFF -1.1%, rgba(245, 247, 250, 0) 70.2%), linear-gradient(231.59deg, #F9FFDA 5.17%, rgba(245, 247, 250, 0) 62.34%); */ 8 | /* background: url('//res.qiyukf.net/ys/17acb1fbb8562113ac211358dc904d49') right top / 100% no-repeat #F5F7FA; */ 9 | } 10 | 11 | view { 12 | box-sizing: border-box; 13 | } 14 | 15 | .bottom-fix { 16 | padding-bottom: calc(env(safe-area-inset-bottom) / 2); 17 | } 18 | 19 | button.btn { 20 | display: block; 21 | width: 100%; 22 | background: #337EFF; 23 | border-radius: 100rpx; 24 | color: #FFFFFF; 25 | padding: 0; 26 | font-size: 32rpx; 27 | height: 92rpx; 28 | line-height: 92rpx; 29 | margin: 0; 30 | } 31 | 32 | button.btn.btn-block { 33 | width: 100%; 34 | } 35 | 36 | .fw, 37 | .fw-n { 38 | font-weight: normal; 39 | } 40 | 41 | .fw-b { 42 | font-weight: bold; 43 | } 44 | 45 | .fw-br { 46 | font-weight: bolder; 47 | } 48 | 49 | .fw-lr { 50 | font-weight: lighter; 51 | } 52 | 53 | .fw-1 { 54 | font-weight: 100; 55 | } 56 | 57 | .fw-2 { 58 | font-weight: 200; 59 | } 60 | 61 | .fw-3 { 62 | font-weight: 300; 63 | } 64 | 65 | .fw-4 { 66 | font-weight: 400; 67 | } 68 | 69 | .fw-5 { 70 | font-weight: 500; 71 | } 72 | 73 | .fw-6 { 74 | font-weight: 600; 75 | } 76 | 77 | .fw-7 { 78 | font-weight: 700; 79 | } 80 | 81 | .fw-8 { 82 | font-weight: 800; 83 | } 84 | 85 | .fs-12 { 86 | font-size: 12rpx; 87 | } 88 | 89 | .fs-14 { 90 | font-size: 14rpx; 91 | } 92 | 93 | .fs-16 { 94 | font-size: 16rpx; 95 | } 96 | 97 | .fs-18 { 98 | font-size: 18rpx; 99 | } 100 | 101 | .fs-20 { 102 | font-size: 20rpx; 103 | } 104 | 105 | .fs-24 { 106 | font-size: 24rpx; 107 | } 108 | 109 | .fs-28 { 110 | font-size: 28rpx; 111 | } 112 | 113 | .fs-32 { 114 | font-size: 32rpx; 115 | } 116 | 117 | .fs-36 { 118 | font-size: 32rpx; 119 | } 120 | 121 | .va-t { 122 | vertical-align: top; 123 | } 124 | 125 | .va-m { 126 | vertical-align: middle; 127 | } 128 | 129 | .va-bl { 130 | vertical-align: baseline; 131 | } 132 | 133 | .va-b { 134 | vertical-align: bottom; 135 | } 136 | 137 | .ta-l { 138 | text-align: left; 139 | } 140 | 141 | .ta-r { 142 | text-align: right; 143 | } 144 | 145 | .ta-c { 146 | text-align: center; 147 | } 148 | 149 | .ta-j { 150 | text-align: justify; 151 | } 152 | 153 | .lh-1 { 154 | line-height: 1; 155 | } 156 | 157 | .h-full { 158 | height: 100%; 159 | } 160 | 161 | .whs-n { 162 | white-space: normal; 163 | } 164 | 165 | .whs-p { 166 | white-space: pre; 167 | } 168 | 169 | .whs-nw { 170 | white-space: nowrap; 171 | } 172 | 173 | .wb-bw { 174 | word-break: break-word; 175 | } 176 | 177 | .wb-ba { 178 | word-break: break-all; 179 | } 180 | 181 | .wb-ka { 182 | word-break: keep-all; 183 | } 184 | 185 | .m-0 { 186 | margin: 0; 187 | } 188 | 189 | .p-0 { 190 | padding: 0; 191 | } 192 | 193 | .p-8 { 194 | padding: 8rpx; 195 | } 196 | 197 | .p-16 { 198 | padding: 16rpx; 199 | } 200 | 201 | .p-24 { 202 | padding: 24rpx; 203 | } 204 | 205 | .p-32 { 206 | padding: 32rpx; 207 | } 208 | 209 | .p-40 { 210 | padding: 40rpx; 211 | } 212 | 213 | .mt-0 { 214 | margin-top: 0; 215 | } 216 | /* 217 | xs 218 | sm 219 | md 220 | lg 221 | */ 222 | .mt-8 { 223 | margin-top: 8rpx; 224 | } 225 | 226 | .mt-16 { 227 | margin-top: 16rpx; 228 | } 229 | 230 | .mt-32 { 231 | margin-top: 32rpx; 232 | } 233 | 234 | .mt-40 { 235 | margin-top: 40rpx; 236 | } 237 | 238 | .mr-0 { 239 | margin-right: 0; 240 | } 241 | 242 | .mr-4 { 243 | margin-right: 4rpx; 244 | } 245 | 246 | .mr-8 { 247 | margin-right: 8rpx; 248 | } 249 | 250 | .mr-16 { 251 | margin-right: 16rpx; 252 | } 253 | 254 | .mr-32 { 255 | margin-right: 32rpx; 256 | } 257 | 258 | .mr-40 { 259 | margin-right: 40rpx; 260 | } 261 | 262 | .mb-0 { 263 | margin-bottom: 0; 264 | } 265 | 266 | .mb-8 { 267 | margin-bottom: 8rpx; 268 | } 269 | 270 | .mb-16 { 271 | margin-bottom: 16rpx; 272 | } 273 | 274 | .mb-32 { 275 | margin-bottom: 32rpx; 276 | } 277 | 278 | .mb-40 { 279 | margin-bottom: 40rpx; 280 | } 281 | 282 | .ml-0 { 283 | margin-left: 0; 284 | } 285 | 286 | .ml-4 { 287 | margin-left: 4rpx; 288 | } 289 | 290 | .ml-8 { 291 | margin-left: 8rpx; 292 | } 293 | 294 | .ml-16 { 295 | margin-left: 16rpx; 296 | } 297 | 298 | .ml-32 { 299 | margin-left: 32rpx; 300 | } 301 | 302 | .ml-40 { 303 | margin-left: 40rpx; 304 | } 305 | 306 | .pt-0 { 307 | padding-top: 0; 308 | } 309 | 310 | .pt-8 { 311 | padding-top: 8rpx; 312 | } 313 | 314 | .pt-16 { 315 | padding-top: 16rpx; 316 | } 317 | 318 | .pt-24 { 319 | padding-top: 24rpx; 320 | } 321 | 322 | .pt-32 { 323 | padding-top: 32rpx; 324 | } 325 | 326 | .pt-40 { 327 | padding-top: 40rpx; 328 | } 329 | 330 | .pr-0 { 331 | padding-right: 0; 332 | } 333 | 334 | .pr-8 { 335 | padding-right: 8rpx; 336 | } 337 | 338 | .pr-16 { 339 | padding-right: 16rpx; 340 | } 341 | 342 | .pr-24 { 343 | padding-right: 24rpx; 344 | } 345 | 346 | .pr-32 { 347 | padding-right: 32rpx; 348 | } 349 | 350 | .pr-40 { 351 | padding-right: 40rpx; 352 | } 353 | 354 | .pb-0 { 355 | padding-bottom: 0; 356 | } 357 | 358 | .pb-8 { 359 | padding-bottom: 8rpx; 360 | } 361 | 362 | .pb-16 { 363 | padding-bottom: 16rpx; 364 | } 365 | 366 | .pb-24 { 367 | padding-bottom: 24rpx; 368 | } 369 | 370 | .pb-32 { 371 | padding-bottom: 32rpx; 372 | } 373 | 374 | .pb-40 { 375 | padding-bottom: 40rpx; 376 | } 377 | 378 | .pl-0 { 379 | padding-left: 0; 380 | } 381 | 382 | .pl-8 { 383 | padding-left: 8rpx; 384 | } 385 | 386 | .pl-16 { 387 | padding-left: 16rpx; 388 | } 389 | 390 | .pl-24 { 391 | padding-left: 24rpx; 392 | } 393 | 394 | .pl-32 { 395 | padding-left: 32rpx; 396 | } 397 | 398 | .pl-40 { 399 | padding-left: 40rpx; 400 | } 401 | 402 | .m-a { 403 | margin: auto; 404 | } 405 | 406 | .mt-a { 407 | margin-top: auto; 408 | } 409 | 410 | .mr-a { 411 | margin-right: auto; 412 | } 413 | 414 | .mb-a { 415 | margin-bottom: auto; 416 | } 417 | 418 | .ml-a { 419 | margin-left: auto; 420 | } 421 | 422 | .mtb-a { 423 | margin-top: auto; 424 | margin-bottom: auto; 425 | } 426 | 427 | .mlr-a { 428 | margin-left: auto; 429 | margin-right: auto; 430 | } 431 | 432 | .pos, 433 | .pos-r { 434 | position: relative; 435 | } 436 | 437 | .pos-s { 438 | position: static; 439 | } 440 | 441 | .pos-a { 442 | position: absolute; 443 | } 444 | 445 | .pos-f { 446 | position: fixed; 447 | } 448 | 449 | .trbl { 450 | top: 0; 451 | right: 0; 452 | bottom: 0; 453 | left: 0; 454 | } 455 | 456 | .t { 457 | top: 0; 458 | } 459 | 460 | .r { 461 | right: 0; 462 | } 463 | 464 | .b { 465 | bottom: 0; 466 | } 467 | 468 | .l { 469 | left: 0; 470 | } 471 | 472 | .t-a { 473 | top: auto; 474 | } 475 | 476 | .r-a { 477 | right: auto; 478 | } 479 | 480 | .b-a { 481 | bottom: auto; 482 | } 483 | 484 | .l-a { 485 | left: auto; 486 | } 487 | 488 | .fl, 489 | .fl-st { 490 | float: left; 491 | } 492 | 493 | .fr, 494 | .fl-nd { 495 | float: right; 496 | } 497 | 498 | .fl-n { 499 | float: none; 500 | } 501 | 502 | .cl-b { 503 | clear: both; 504 | } 505 | 506 | .cl-st { 507 | clear: left; 508 | } 509 | 510 | .cl-nd { 511 | clear: right; 512 | } 513 | 514 | /* .clearfix { 515 | *zoom: 1; 516 | } */ 517 | 518 | .clearfix::after { 519 | content: ' '; 520 | display: block; 521 | height: 0; 522 | clear: both; 523 | visibility: hidden; 524 | } 525 | 526 | .d-b { 527 | display: block; 528 | } 529 | 530 | .d-n { 531 | display: none; 532 | } 533 | 534 | .d-i { 535 | display: inline; 536 | } 537 | 538 | .d-ib { 539 | display: inline-block; 540 | } 541 | 542 | .d-flex { 543 | display: flex; 544 | } 545 | 546 | .flex-sa { 547 | justify-content: space-around; 548 | } 549 | 550 | .flex-sb { 551 | justify-content: space-between; 552 | } 553 | 554 | .flex-aic { 555 | align-items: center; 556 | } 557 | 558 | .flex-center { 559 | justify-content: center; 560 | align-items: center; 561 | } 562 | 563 | .flex-1 { 564 | flex: 1; 565 | } 566 | 567 | .flex-dc { 568 | flex-direction: column; 569 | } 570 | 571 | .flex-dr { 572 | flex-direction: row; 573 | } 574 | 575 | .v-h { 576 | visibility: hidden; 577 | } 578 | 579 | .v-v { 580 | visibility: visible; 581 | } 582 | 583 | .ov-h { 584 | overflow: hidden; 585 | } 586 | 587 | .ov-v { 588 | overflow: visible; 589 | } 590 | 591 | .ov-s { 592 | overflow: scroll; 593 | } 594 | 595 | .ov-a { 596 | overflow: auto; 597 | } 598 | 599 | .ovx-h { 600 | overflow-x: hidden; 601 | } 602 | 603 | .ovx-v { 604 | overflow-x: visible; 605 | } 606 | 607 | .ovx-s { 608 | overflow-x: scroll; 609 | } 610 | 611 | .ovx-a { 612 | overflow-x: auto; 613 | } 614 | 615 | .ovy-h { 616 | overflow-y: hidden; 617 | } 618 | 619 | .ovy-v { 620 | overflow-y: visible; 621 | } 622 | 623 | .ovy-s { 624 | overflow-y: scroll; 625 | } 626 | 627 | .ovy-a { 628 | overflow-y: auto; 629 | } 630 | 631 | .bg-n { 632 | background: none; 633 | } 634 | 635 | .bgc-w { 636 | background-color: white; 637 | } 638 | 639 | .bgc-b { 640 | background-color: black; 641 | } 642 | 643 | .bgc-t { 644 | background-color: transparent; 645 | } 646 | 647 | .bgc-pr { 648 | background-color: #337eff; 649 | } 650 | 651 | .bgc-333 { 652 | background-color: #333333; 653 | } 654 | 655 | .bgc-prlt { 656 | background-color: mix(#337eff, white, 25%); 657 | } 658 | 659 | .bgc-333lt { 660 | background-color: mix(#333333, white, 25%); 661 | } 662 | 663 | .bgc-gray { 664 | background-color: #F5F5F7; 665 | } 666 | 667 | .bgc-green { 668 | background-color: #00BF80; 669 | } 670 | 671 | .bgc-red { 672 | background-color: #F24957; 673 | } 674 | 675 | 676 | .c-w { 677 | color: white; 678 | } 679 | 680 | .c-b { 681 | color: black; 682 | } 683 | 684 | .c-222 { 685 | color: #222222; 686 | } 687 | 688 | .c-333 { 689 | color: #333333; 690 | } 691 | 692 | .c-666 { 693 | color: #666666; 694 | } 695 | 696 | .c-999 { 697 | color: #999999; 698 | } 699 | 700 | .c-gray { 701 | color: #BFBFBF; 702 | } 703 | 704 | .c-pr { 705 | color: #337EFF; 706 | } 707 | 708 | .c-green { 709 | color: #00BF80; 710 | } 711 | 712 | .c-red { 713 | color: #F24957; 714 | } 715 | 716 | .op-0 { 717 | opacity: 0; 718 | } 719 | 720 | .op-2 { 721 | opacity: 0.2; 722 | } 723 | 724 | .op-4 { 725 | opacity: 0.4; 726 | } 727 | 728 | .op-6 { 729 | opacity: 0.6; 730 | } 731 | 732 | .op-8 { 733 | opacity: 0.8; 734 | } 735 | 736 | .op-10 { 737 | opacity: 1; 738 | } 739 | 740 | 741 | .ol-n { 742 | outline: none; 743 | } 744 | 745 | 746 | .bd-n { 747 | border: none; 748 | } 749 | 750 | .bdcl-h { 751 | border-radius: 50%; 752 | } 753 | 754 | .bdcl-c { 755 | border-collapse: collapse; 756 | } 757 | 758 | .bdrs-0 { 759 | border-radius: 0; 760 | } 761 | 762 | .bdrs-2 { 763 | border-radius: 2rpx; 764 | } 765 | 766 | .bdrs-4 { 767 | border-radius: 4rpx; 768 | } 769 | 770 | .bdrs-6 { 771 | border-radius: 6rpx; 772 | } 773 | 774 | .bdrs-8 { 775 | border-radius: 8rpx; 776 | } 777 | 778 | .bdrs-16 { 779 | border-radius: 16rpx; 780 | } 781 | 782 | .bdrs-32 { 783 | border-radius: 32rpx; 784 | } 785 | 786 | .lis-n { 787 | list-style: none; 788 | } 789 | 790 | .cur-p { 791 | cursor: pointer; 792 | } 793 | 794 | .cur-na { 795 | cursor: not-allowed; 796 | } 797 | 798 | .cur-m { 799 | cursor: move; 800 | } 801 | 802 | .w-full { 803 | width: 100%; 804 | } 805 | 806 | .w-10 { 807 | width: 10rpx; 808 | } 809 | 810 | .w-20 { 811 | width: 20rpx; 812 | } 813 | 814 | .w-30 { 815 | width: 30rpx; 816 | } 817 | 818 | .w-40 { 819 | width: 40rpx; 820 | } 821 | 822 | .w-50 { 823 | width: 50rpx; 824 | } 825 | 826 | .w-60 { 827 | width: 60rpx; 828 | } 829 | 830 | .w-70 { 831 | width: 70rpx; 832 | } 833 | 834 | .w-80 { 835 | width: 80rpx; 836 | } 837 | 838 | .w-100 { 839 | width: 100rpx; 840 | } 841 | 842 | /* height */ 843 | .h-8 { 844 | height: 8rpx; 845 | } 846 | 847 | .h-10 { 848 | height: 10rpx; 849 | } 850 | 851 | .h-20 { 852 | height: 20rpx; 853 | } 854 | 855 | .h-30 { 856 | height: 30rpx; 857 | } 858 | 859 | .h-40 { 860 | height: 40rpx; 861 | } 862 | 863 | .h-50 { 864 | height: 50rpx; 865 | } 866 | 867 | .h-60 { 868 | height: 60rpx; 869 | } 870 | 871 | .h-70 { 872 | height: 70rpx; 873 | } 874 | 875 | .h-80 { 876 | height: 80rpx; 877 | } 878 | 879 | .h-90 { 880 | height: 90rpx; 881 | } 882 | 883 | .h-100 { 884 | height: 100rpx; 885 | } 886 | 887 | .h-full { 888 | height: 100%; 889 | } 890 | 891 | .elipsis { 892 | overflow: hidden; 893 | text-overflow: ellipsis; 894 | white-space: nowrap; 895 | } -------------------------------------------------------------------------------- /miniprogram/app.ts: -------------------------------------------------------------------------------- 1 | // app.ts 2 | import { envConfig } from './utils/env'; 3 | 4 | App({ 5 | globalData: { 6 | envConfig 7 | }, 8 | onLaunch() { 9 | wx.getSystemInfo({ 10 | success: e => { 11 | this.globalData.StatusBarHeight = e.statusBarHeight; 12 | const capsule = wx.getMenuButtonBoundingClientRect(); 13 | if (capsule) { 14 | this.globalData.CustomRect = capsule; 15 | this.globalData.CustomBarHeight = capsule.bottom + capsule.top - e.statusBarHeight; 16 | } else { 17 | this.globalData.CustomBarHeight = e.statusBarHeight + 50; 18 | } 19 | } 20 | }); 21 | wx.setInnerAudioOption({ 22 | // 即使是在静音模式下,也能播放声音 23 | obeyMuteSwitch: false, 24 | }); 25 | }, 26 | }) -------------------------------------------------------------------------------- /miniprogram/components/no-data/no-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /miniprogram/components/no-data/no-data.less: -------------------------------------------------------------------------------- 1 | /* components/no-data/no-data.wxss */ -------------------------------------------------------------------------------- /miniprogram/components/no-data/no-data.ts: -------------------------------------------------------------------------------- 1 | // components/no-data/no-data.ts 2 | Component({ 3 | options: { 4 | styleIsolation: 'apply-shared', 5 | }, 6 | /** 7 | * 组件的属性列表 8 | */ 9 | properties: { 10 | text: { 11 | type: String, 12 | value: '暂无数据' 13 | }, 14 | visible: { 15 | type: Boolean, 16 | value: false, 17 | } 18 | }, 19 | 20 | /** 21 | * 组件的初始数据 22 | */ 23 | data: { 24 | 25 | }, 26 | 27 | /** 28 | * 组件的方法列表 29 | */ 30 | methods: { 31 | 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /miniprogram/components/no-data/no-data.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{text}} 6 | 7 | -------------------------------------------------------------------------------- /miniprogram/components/svg-icon/svg-icon.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /miniprogram/components/svg-icon/svg-icon.less: -------------------------------------------------------------------------------- 1 | .svg-icon { 2 | vertical-align: middle; 3 | display: inline-block; 4 | mask-repeat: no-repeat; 5 | -webkit-mask-repeat: no-repeat; 6 | -moz-mask-repeat: no-repeat; 7 | mask-size: cover; 8 | -webkit-mask-size: cover; 9 | -moz-mask-size: cover; 10 | height: 48rpx; 11 | width: 48rpx; 12 | } -------------------------------------------------------------------------------- /miniprogram/components/svg-icon/svg-icon.ts: -------------------------------------------------------------------------------- 1 | Component({ 2 | options: { 3 | styleIsolation: 'apply-shared', 4 | }, 5 | 6 | properties: { 7 | src: String, 8 | extClass: String, 9 | extStyle: String, 10 | color: String, 11 | size: Number, 12 | va: String, 13 | }, 14 | 15 | observers: { 16 | color: function (color) { 17 | this.setData({ 18 | backgroundColor: color ? `background-color: ${color};` : '', 19 | }); 20 | }, 21 | size: function (size) { 22 | size = size || 48; 23 | this.setData({ 24 | sizeStyle: size ? `height: ${size}rpx; width: ${size}rpx;` : '', 25 | }); 26 | }, 27 | va: function(va) { 28 | this.setData({ 29 | verticalAlign: va ? `vertical-align: ${va};` : '', 30 | }); 31 | } 32 | }, 33 | 34 | data: { 35 | 36 | }, 37 | 38 | lifetimes: { 39 | ready() { 40 | const url = this.data.src; 41 | wx.getFileSystemManager().readFile({ 42 | filePath: `${url}`, 43 | encoding: 'base64', 44 | success: res => { 45 | const base64 = 'data:image/svg+xml;base64,' + res.data; 46 | const maskImage = `mask-image:url(${base64}); -webkit-mask-image:url(${base64}); -moz-mask-image:url(${base64})`; 47 | this.setData({ 48 | maskImage, 49 | }) 50 | }, 51 | fail: res => { 52 | console.log(`${url} load fail, res = `, res) 53 | } 54 | }) 55 | } 56 | }, 57 | 58 | methods: { 59 | 60 | } 61 | }) -------------------------------------------------------------------------------- /miniprogram/components/svg-icon/svg-icon.wxml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /miniprogram/consts/index.ts: -------------------------------------------------------------------------------- 1 | // wx Storage 统一配置处 2 | const { miniProgram: { envVersion } } = wx.getAccountInfoSync(); 3 | 4 | export const StorageMap = { 5 | UserInfo: `${envVersion}_userInfo`, 6 | Cookie: `${envVersion}_cookie`, 7 | }; 8 | 9 | // 录音状态 10 | export const RecordingStatusMap = { 11 | '0': '未完成', 12 | '1': '已完成' 13 | } 14 | 15 | export const ScriptRecordingStatusMap = { 16 | '0': { 17 | text: '未录音', 18 | className: '' 19 | }, 20 | '1': { 21 | text: '已录音', 22 | className: 'success' 23 | } 24 | } -------------------------------------------------------------------------------- /miniprogram/img/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /miniprogram/img/figure.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /miniprogram/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /miniprogram/img/logout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /miniprogram/img/microphone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /miniprogram/img/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /miniprogram/img/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /miniprogram/img/show/01.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Godiswill/mp-recorder-player/b6586f68c97d4fde7a654c5d7bbe9f36ae5fad09/miniprogram/img/show/01.PNG -------------------------------------------------------------------------------- /miniprogram/img/show/02.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Godiswill/mp-recorder-player/b6586f68c97d4fde7a654c5d7bbe9f36ae5fad09/miniprogram/img/show/02.PNG -------------------------------------------------------------------------------- /miniprogram/img/show/03.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Godiswill/mp-recorder-player/b6586f68c97d4fde7a654c5d7bbe9f36ae5fad09/miniprogram/img/show/03.PNG -------------------------------------------------------------------------------- /miniprogram/img/show/04.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Godiswill/mp-recorder-player/b6586f68c97d4fde7a654c5d7bbe9f36ae5fad09/miniprogram/img/show/04.PNG -------------------------------------------------------------------------------- /miniprogram/img/show/05.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Godiswill/mp-recorder-player/b6586f68c97d4fde7a654c5d7bbe9f36ae5fad09/miniprogram/img/show/05.PNG -------------------------------------------------------------------------------- /miniprogram/img/show/06.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Godiswill/mp-recorder-player/b6586f68c97d4fde7a654c5d7bbe9f36ae5fad09/miniprogram/img/show/06.PNG -------------------------------------------------------------------------------- /miniprogram/img/suspend.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /miniprogram/img/tabBar/microphone-selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Godiswill/mp-recorder-player/b6586f68c97d4fde7a654c5d7bbe9f36ae5fad09/miniprogram/img/tabBar/microphone-selected.png -------------------------------------------------------------------------------- /miniprogram/img/tabBar/microphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Godiswill/mp-recorder-player/b6586f68c97d4fde7a654c5d7bbe9f36ae5fad09/miniprogram/img/tabBar/microphone.png -------------------------------------------------------------------------------- /miniprogram/img/tabBar/my-selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Godiswill/mp-recorder-player/b6586f68c97d4fde7a654c5d7bbe9f36ae5fad09/miniprogram/img/tabBar/my-selected.png -------------------------------------------------------------------------------- /miniprogram/img/tabBar/my.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Godiswill/mp-recorder-player/b6586f68c97d4fde7a654c5d7bbe9f36ae5fad09/miniprogram/img/tabBar/my.png -------------------------------------------------------------------------------- /miniprogram/mock/data.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | operateList: { 3 | "list": [ 4 | { 5 | "botId": 1113, 6 | "botName": "下马饮君酒", 7 | "accountKey": "ac5427ebde8e4586bdc6e6b5bea8bf2e", 8 | "percent": 0, 9 | "updateTime": 1655982300000 10 | }, 11 | { 12 | "botId": 1147, 13 | "botName": "问君何所之", 14 | "accountKey": "ff9c6080eda345f4b227631cfb6735d2", 15 | "percent": 20, 16 | "updateTime": 1655959946000 17 | }, 18 | { 19 | "botId": 3145, 20 | "botName": "君言不得意", 21 | "accountKey": "ff9c6080eda345f4b227631cfb6735d2", 22 | "percent": 45, 23 | "updateTime": 1655959924000 24 | }, 25 | { 26 | "botId": 2146, 27 | "botName": "归卧南山陲", 28 | "accountKey": "ff9c6080eda345f4b227631cfb6735d2", 29 | "percent": 77, 30 | "updateTime": 1655959889000 31 | }, 32 | { 33 | "botId": 1165, 34 | "botName": "但去莫复问", 35 | "accountKey": "operationBackground", 36 | "percent": 100, 37 | "updateTime": 1655959874000 38 | }, 39 | { 40 | "botId": 2149, 41 | "botName": "白云无尽时", 42 | "accountKey": "e957dab6d3314955b60655fa9baef990", 43 | "percent": 100, 44 | "updateTime": 1655955303000 45 | } 46 | ], 47 | "total": 6, 48 | "page": 1, 49 | "pageSize": 30 50 | }, 51 | operateBotDetail: { 52 | "mainProcessDetails": [ 53 | { 54 | "sceneName": "开场白", 55 | "sceneId": 1191, 56 | "total": 6, 57 | "recordedTotal": 6 58 | }, 59 | { 60 | "sceneName": "挽留流程", 61 | "sceneId": 2193, 62 | "total": 2, 63 | "recordedTotal": 2 64 | }, 65 | { 66 | "sceneName": "添加链路", 67 | "sceneId": 2194, 68 | "total": 13, 69 | "recordedTotal": 11 70 | } 71 | ], 72 | "total": 22, 73 | "recordedTotal": 20, 74 | "mainProcessTotal": 22, 75 | "recordedMainProcessTotal": 20, 76 | "botId": 1113, 77 | "botName": "电商话术" 78 | }, 79 | sceneDetail: { 80 | "sceneName": "挽留流程", 81 | "sceneId": 2193, 82 | "recordingDetails": [ 83 | { 84 | "recordingId": 12198, 85 | "callScript": "(可惜,挽回的语气)啊,这样啊,这次主要是我们对老玩家的专属回馈福利活动,只有接到电话的玩家才可以领取的,不需要支付任何额外费用,另外,添加专属客服微信还可以咨询领取其他热门游戏的VIP免费福利,名额有限,您可以先通过了解一下的嘛(请求的语气)", 86 | "url": "https://urchin.nosdn.127.net/wecall/700564EB-1111-475C-81A7-148083E40B86.wav", 87 | "status": 1, 88 | "accountKey": "ac5427ebde8e4586bdc6e6b5bea8bf2e", 89 | "duration": 19, 90 | "type": "MAIN_PROCESS", 91 | "extraId": "47780", 92 | "botId": 1113, 93 | "sceneId": 2193, 94 | total: 2, 95 | curNum: 1 96 | }, 97 | { 98 | "recordingId": 12199, 99 | "callScript": "那没关系哈,稍后这边给您发一条福利短信,您感兴趣的话也可以点开短信链接,添加我们的微信领取礼包,这边就不打扰您了,祝您生活愉快,再见!", 100 | "url": "", 101 | "status": 0, 102 | "accountKey": "ac5427ebde8e4586bdc6e6b5bea8bf2e", 103 | "duration": 13, 104 | "type": "MAIN_PROCESS", 105 | "extraId": "47781", 106 | "botId": 1113, 107 | "sceneId": 2193, 108 | total: 2, 109 | curNum: 2 110 | } 111 | ], 112 | "total": 2, 113 | "recordedTotal": 1 114 | } 115 | } -------------------------------------------------------------------------------- /miniprogram/pages/index/call/call.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "", 3 | "navigationBarBackgroundColor": "#ffffff", 4 | "usingComponents": { 5 | "svg-icon": "../../../components/svg-icon/svg-icon" 6 | } 7 | } -------------------------------------------------------------------------------- /miniprogram/pages/index/call/call.less: -------------------------------------------------------------------------------- 1 | .call { 2 | & > view { 3 | padding-left: 24rpx; 4 | padding-right: 24rpx; 5 | padding-bottom: 24rpx; 6 | } 7 | .call-header { 8 | height: 70rpx; 9 | border-bottom: 1rpx solid #E6E7EB; 10 | } 11 | .call-panel-item { 12 | height: 92rpx; 13 | } 14 | .call-panel-item + .call-panel-item { 15 | border-top: 1rpx solid #e6e7eb; 16 | } 17 | } -------------------------------------------------------------------------------- /miniprogram/pages/index/call/call.ts: -------------------------------------------------------------------------------- 1 | import { operateBotDetail } from "../../../api/index"; 2 | 3 | Page({ 4 | 5 | /** 6 | * 页面的初始数据 7 | */ 8 | data: { 9 | botId: 0, 10 | accountKey: '', 11 | detail: undefined, 12 | title: '', 13 | }, 14 | 15 | bindGotoScene(e: WechatMiniprogram.BaseEvent) { 16 | const { type, item } = e.currentTarget.dataset as IScenePageParams; 17 | let sceneId: number = 0; 18 | if (isSceneData(item)) { 19 | sceneId = item.sceneId 20 | } 21 | wx.navigateTo({ 22 | url: `./scene/scene?sceneId=${sceneId || ''}&accountKey=${this.data.accountKey}&type=${type}&botId=${this.data.botId}` 23 | }) 24 | }, 25 | 26 | bindClearAll() { 27 | wx.showModal({ 28 | title: '清空全部', 29 | content: '是否要清空全部录音', 30 | success: (res) => { 31 | if (res.confirm) { 32 | 33 | } 34 | } 35 | }); 36 | }, 37 | 38 | /** 39 | * 生命周期函数--监听页面加载 40 | */ 41 | onLoad(query: ICallDetailQuery) { 42 | this.setData(query) 43 | wx.setNavigationBarTitle({ 44 | title: query.title 45 | }); 46 | }, 47 | 48 | /** 49 | * 生命周期函数--监听页面显示 50 | */ 51 | onShow() { 52 | this.fetchDetail(); 53 | }, 54 | 55 | fetchDetail() { 56 | operateBotDetail({ 57 | botId: this.data.botId, 58 | accountKey: this.data.accountKey, 59 | }).then(res => { 60 | this.setData({ 61 | detail: res 62 | }) 63 | }) 64 | }, 65 | 66 | quickStart() { 67 | const { botId, accountKey } = this.data; 68 | if (!botId) return; 69 | wx.navigateTo({ 70 | url: `./scene/record/record?botId=${botId}&accountKey=${accountKey}`, 71 | }); 72 | }, 73 | 74 | }) 75 | 76 | function isSceneData(item: any): item is TMainProcessDetail { 77 | return !!item.sceneName; 78 | } -------------------------------------------------------------------------------- /miniprogram/pages/index/call/call.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{detail.recordedTotal}}已录 / {{detail.total}}全部 4 | 5 | 清空全部 6 | 7 | 8 | 9 | 10 | 11 | 主话术流程({{detail.recordedMainProcessTotal}}/{{detail.mainProcessTotal}}) 12 | 13 | 14 | {{item.sceneName}} 15 | {{item.recordedTotal !== item.total ? '待录音': '已录音'}} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /miniprogram/pages/index/call/call.wxss: -------------------------------------------------------------------------------- 1 | .call > view { 2 | padding-left: 24rpx; 3 | padding-right: 24rpx; 4 | } 5 | .call .call-header { 6 | height: 70rpx; 7 | border-bottom: 1rpx solid #E6E7EB; 8 | } 9 | .call .call-panel-item { 10 | height: 92rpx; 11 | } 12 | .call .call-panel-item + .call-panel-item { 13 | border-top: 1rpx solid #e6e7eb; 14 | } 15 | -------------------------------------------------------------------------------- /miniprogram/pages/index/call/scene/record/record.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "录音", 3 | "usingComponents": { 4 | "svg-icon": "../../../../../components/svg-icon/svg-icon" 5 | } 6 | } -------------------------------------------------------------------------------- /miniprogram/pages/index/call/scene/record/record.less: -------------------------------------------------------------------------------- 1 | .record { 2 | .record-header { 3 | height: 92rpx; 4 | z-index: 1; 5 | } 6 | } -------------------------------------------------------------------------------- /miniprogram/pages/index/call/scene/record/record.ts: -------------------------------------------------------------------------------- 1 | import { formatClock, uploadFile, showErrMsg } from '../../../../../utils/util'; 2 | import { lastOrNext } from '../../../../../api/index'; 3 | 4 | const recordOptions = { 5 | duration: 10 * 60 * 1000, 6 | sampleRate: 8000, 7 | numberOfChannels: 1, 8 | format: 'wav', 9 | }; 10 | 11 | const msgMap: Record = { 12 | 'operateRecorder:fail auth deny': '请右上角设置中授权麦克风', 13 | // 'operateRecorder:fail is recording or paused': '您操作过快', 14 | // 'operateRecorder:fail recorder not start': '您操作过快', 15 | }; 16 | 17 | let timer: number; 18 | let clockCurTime: number; 19 | const recorderManager: WechatMiniprogram.RecorderManager = wx.getRecorderManager(); 20 | const innerAudioContext: WechatMiniprogram.InnerAudioContext = wx.createInnerAudioContext(); 21 | let gap = 0; 22 | let isError = false; 23 | 24 | function disabledClick() { 25 | const now = Date.now(); 26 | if (now - gap < 800) { 27 | return true; 28 | } 29 | gap = now; 30 | return false; 31 | } 32 | 33 | const initRData = { // 初始录音数据 34 | isRecording: false, 35 | clock: '00:00:00', 36 | } 37 | 38 | const recordingData = { // 录音中数据 39 | isRecording: true, 40 | } 41 | 42 | const initPlayData = { 43 | isPlaying: false, 44 | currentTime: '00:00', 45 | playPercent: 0, 46 | } 47 | 48 | const playingData = { 49 | isPlaying: true, 50 | } 51 | 52 | 53 | Page({ 54 | 55 | /** 56 | * 页面的初始数据 57 | */ 58 | data: { 59 | audioShapingSwitch: true, 60 | duration: '00:00', 61 | ...initPlayData, 62 | ...initRData, 63 | }, 64 | 65 | noEffectStopRecorder() { 66 | if (this.data.isRecording) { 67 | isError = true; 68 | recorderManager.stop(); 69 | } 70 | }, 71 | 72 | switchChange() { 73 | this.setData({ 74 | audioShapingSwitch: !this.data.audioShapingSwitch, 75 | }); 76 | }, 77 | 78 | async getDetail(type?: TGetDetailType) { 79 | const { detail, isFirst, isLast } = this.data; 80 | if (type === 'LAST' && isFirst || type === 'NEXT' && isLast) return; 81 | const res = await lastOrNext({ 82 | ...this.data.query, 83 | curId: detail?.recordingId || this.data.query?.recordingId, 84 | type, 85 | curNum: detail?.curNum, 86 | }); 87 | if (!res?.recordingId) return; 88 | if (innerAudioContext.currentTime) { 89 | innerAudioContext.stop(); 90 | } 91 | if (res.url && res.url !== detail?.url) { 92 | innerAudioContext.src = res.url; 93 | } 94 | this.setData({ 95 | ...initPlayData, 96 | ...initRData, 97 | isFirst: res.curNum === 1, 98 | isLast: res.curNum === res.total, 99 | detail: { 100 | ...res, 101 | percent: Math.ceil(100 * res.curNum / res.total || 0), 102 | }, 103 | duration: formatClock(res.duration * 1000, true), 104 | bottomText: this.data.query.recordingId ? `${res.curNum}/${res.total}全部` : `${res.total}待录音`, 105 | }); 106 | }, 107 | 108 | bindDeleteRecord() { 109 | wx.showModal({ 110 | title: '删除', 111 | content: '确定需要删除?', 112 | success: async (res) => { 113 | if (res.confirm) { 114 | await this.getDetail('CUR'); 115 | this.setData({ 116 | detail: { 117 | ...this.data.detail, 118 | url: '', 119 | }, 120 | }); 121 | } 122 | } 123 | }); 124 | }, 125 | 126 | /** 127 | * 生命周期函数--监听页面加载 128 | */ 129 | onLoad(query: TRecordUrlQuery) { 130 | console.log('页面查询字符:', query) 131 | this.setData({ 132 | query, 133 | }); 134 | wx.setKeepScreenOn({ 135 | keepScreenOn: true, 136 | }); 137 | }, 138 | 139 | async bindLastOrNext(e?: WechatMiniprogram.BaseEvent) { 140 | const { type } = e?.currentTarget.dataset || {}; 141 | await this.getDetail(type as TGetDetailType); 142 | }, 143 | 144 | startClock(init = true) { 145 | if (init) { 146 | clockCurTime = Date.now(); 147 | } 148 | if (timer) clearInterval(timer); // 防止内存泄漏 149 | timer = setInterval(() => { 150 | const now = Date.now(); 151 | const gap = now - clockCurTime; 152 | this.setData({ 153 | clock: formatClock(gap), 154 | }); 155 | }, 1000); 156 | }, 157 | 158 | stopClock() { 159 | clearInterval(timer); 160 | }, 161 | 162 | bindStartPlay() { 163 | if (disabledClick()) return; 164 | innerAudioContext.play(); 165 | }, 166 | 167 | bindStopPlay() { 168 | if (disabledClick()) return; 169 | innerAudioContext.stop(); 170 | }, 171 | 172 | bindStartRecord() { 173 | if (disabledClick()) return; 174 | recorderManager.start(recordOptions as WechatMiniprogram.RecorderManagerStartOption); 175 | }, 176 | 177 | bindStopRecord() { 178 | if (disabledClick()) return; 179 | // 停止录音 180 | recorderManager.stop(); 181 | }, 182 | 183 | initRecorder() { 184 | // 监听录音错误事件 185 | recorderManager.onError((err) => { 186 | this.noEffectStopRecorder(); 187 | 188 | showErrMsg(msgMap[err.errMsg] || err.errMsg || '小程序错误'); 189 | console.log('recorderManager.onError', err); 190 | }); 191 | // 监听已录制完指定帧大小的文件事件。如果设置了 frameSize,则会回调此事件。 192 | // recorderManager.onFrameRecorded(({ frameBuffer, isLastFrame }) => { 193 | // console.log('frameBuffer.byteLength: ', frameBuffer.byteLength) 194 | // console.log('isLastFrame: ', isLastFrame); 195 | // }); 196 | /** 197 | * 监听录音因为受到系统占用而被中断开始事件。 198 | * 以下场景会触发此事件:微信语音聊天、微信视频聊天。 199 | * 此事件触发后,录音会被暂停。 200 | * pause 事件在此事件后触发 201 | */ 202 | recorderManager.onInterruptionBegin(() => { 203 | console.log('监听录音因为受到系统占用而被中断开始事件。'); 204 | }); 205 | /** 206 | * 监听录音中断结束事件。 207 | * 在收到 interruptionBegin 事件之后, 208 | * 小程序内所有录音会暂停,收到此事件之后才可再次录音成功。 209 | */ 210 | recorderManager.onInterruptionEnd(() => { 211 | console.log('监听录音中断结束事件。'); 212 | }); 213 | // 监听录音暂停事件 214 | recorderManager.onPause(() => { 215 | console.log('recorder pause'); 216 | // 立马停止,重新开始,没有恢复机制 217 | this.noEffectStopRecorder(); 218 | }); 219 | // 监听录音继续事件 220 | recorderManager.onResume(() => { 221 | console.log('recorder onResume') 222 | }); 223 | // 开始录音 224 | recorderManager.onStart(() => { 225 | 226 | console.log('recorder start'); 227 | this.startClock(); 228 | this.setData({ 229 | ...recordingData, 230 | }); 231 | }); 232 | 233 | // 停止录音 234 | recorderManager.onStop(async (res) => { 235 | console.log('recorder stop', res) 236 | // 停止后立即更新状态,以免异常 237 | this.stopClock(); 238 | this.setData({ 239 | ...initRData, 240 | }); 241 | if (isError) { 242 | isError = false; 243 | return; 244 | } 245 | const { tempFilePath, duration } = res; 246 | console.log('tempFilePath', tempFilePath); 247 | const url = await uploadFile({ filePath: tempFilePath }); 248 | 249 | // 快速开始时,获取的都是未录音,会冲掉当前上传试听,这里手动设置一下 250 | if (innerAudioContext.currentTime) { 251 | innerAudioContext.stop(); 252 | } 253 | innerAudioContext.src = url; 254 | this.setData({ 255 | ...initPlayData, 256 | ...initRData, 257 | detail: { 258 | ...this.data.detail, 259 | url, 260 | duration: Math.ceil(duration / 1000), 261 | }, 262 | duration: formatClock(duration, true), 263 | }); 264 | // await this.getDetail('CUR'); 265 | }); 266 | }, 267 | 268 | initAudioPlayer() { 269 | // 监听音频进入可以播放状态的事件。但不保证后面可以流畅播放 270 | innerAudioContext.onCanplay(() => { 271 | console.log('监听音频进入可以播放状态的事件'); 272 | 273 | }); 274 | // 监听音频自然播放至结束的事件 275 | innerAudioContext.onEnded(() => { 276 | console.log('监听音频自然播放至结束的事件'); 277 | this.setData({ 278 | ...initPlayData 279 | }); 280 | }); 281 | 282 | // 监听音频播放错误事件 283 | innerAudioContext.onError((res) => { 284 | /** 285 | * 10001 系统错误 286 | * 10002 网络错误 287 | * 10003 文件错误 288 | * 10004 格式错误 289 | * -1 未知错误 290 | */ 291 | console.log(res.errCode, res.errMsg); 292 | this.setData({ 293 | ...initPlayData 294 | }); 295 | }); 296 | 297 | // 监听音频暂停事件 298 | innerAudioContext.onPause(() => { 299 | console.log('监听音频暂停事件'); 300 | this.setData({ 301 | ...initPlayData 302 | }); 303 | }); 304 | 305 | // 监听音频播放事件 306 | innerAudioContext.onPlay(() => { 307 | console.log('开始播放'); 308 | this.setData({ 309 | ...playingData, 310 | }); 311 | }); 312 | 313 | // 监听音频完成跳转操作的事件 314 | innerAudioContext.onSeeked(() => { 315 | console.log('监听音频完成跳转操作的事件'); 316 | 317 | }); 318 | 319 | // 监听音频进行跳转操作的事件 320 | innerAudioContext.onSeeking(() => { 321 | console.log('监听音频进行跳转操作的事件'); 322 | 323 | }); 324 | 325 | // 监听音频停止事件 326 | innerAudioContext.onStop(() => { 327 | console.log('监听音频停止事件'); 328 | this.setData({ 329 | ...initPlayData, 330 | }); 331 | }); 332 | 333 | // 监听音频播放进度更新事件 334 | innerAudioContext.onTimeUpdate(() => { 335 | console.log('监听音频播放进度更新事件'); 336 | 337 | let playPercent = 0; 338 | const duration = this.data.detail.duration || innerAudioContext.duration; 339 | try { 340 | playPercent = Math.ceil(((innerAudioContext.currentTime * 1000) / (duration * 1000)) * 100) || 0; 341 | } catch (e) { 342 | playPercent = 0; 343 | } 344 | playPercent = playPercent && playPercent > 100 ? 100 : playPercent; 345 | const currentTime = formatClock(innerAudioContext.currentTime * 1000, true); 346 | console.log('当前播放时间:', currentTime); 347 | console.log('微信暴露时间:', innerAudioContext.duration); 348 | console.log('后端返回时间:', duration); 349 | console.log('当前播放进度:', playPercent); 350 | this.setData({ 351 | currentTime, 352 | playPercent, 353 | }); 354 | }); 355 | 356 | // 监听音频加载中事件。当音频因为数据不足,需要停下来加载时会触发 357 | innerAudioContext.onWaiting(() => { 358 | console.log('监听音频加载中事件'); 359 | 360 | // const duration = innerAudioContext.duration; 361 | // if (!duration) return; 362 | // console.log('设置 duration:', duration); 363 | // this.setData({ 364 | // duration: formatClock(duration * 1000, true), 365 | // }); 366 | }); 367 | 368 | 369 | }, 370 | 371 | /** 372 | * 生命周期函数--监听页面初次渲染完成 373 | */ 374 | onReady() { 375 | this.initRecorder(); 376 | this.initAudioPlayer(); 377 | }, 378 | 379 | /** 380 | * 生命周期函数--监听页面显示 381 | */ 382 | onShow() { 383 | this.getDetail('CUR'); 384 | }, 385 | 386 | /** 387 | * 生命周期函数--监听页面卸载 388 | */ 389 | onUnload() { 390 | console.log('切换页面停止录音或播放'); 391 | innerAudioContext.stop(); 392 | 393 | this.noEffectStopRecorder(); 394 | }, 395 | }) -------------------------------------------------------------------------------- /miniprogram/pages/index/call/scene/record/record.wxml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | {{detail.showName}} 12 | {{detail.status ? '已录音' : '待录音'}} 13 | 14 | 15 | 16 | 17 | 18 | {{detail.callScript}} 19 | 20 | 21 | 剪切开头结尾的空白录音 22 | 23 | 24 | 25 | 26 | 27 | 28 | {{currentTime}} 29 | 30 | {{duration}} 31 | 32 | 33 | 34 | 35 | 36 |