Vue3 錄音與播放套件 js-audio-recorder

安装js-audio-recorder

1
npm install js-audio-recorder --save

獲取數據

1
2
3
4
5
// 獲取錄音數據 buffer
recorder.getRecordAnalyseData()

// 獲取播放數據 buffer
recorder.getPlayAnalyseData()

錄音方法

1
2
3
4
5
6
7
8
9
10
11
// 錄音
recorder.start()

// 暫停
recorder.pause()

// 繼續
recorder.resume()

// 結束
recorder.stop()

播放

1
2
3
4
5
6
7
8
9
10
11
// 播放
recorder.play()

// 暫停
recorder.pausePlay()

// 繼續
recorder.resumePlay()

// 結束
recorder.stopPlay()

下载方法

1
2
3
4
5
// 下载 pcm 文件
recorder.downloadPCM()

// 下载 wam 文件
recorder.downloadWAV()

銷毀

1
2
// 銷毀
recorder.destroy()

參數配置

1
2
3
4
5
6
7
8
9
10
11
 let query = {
// 取樣位數,支援 8 或 16,預設是 16
sampleBits: 16,
// 取樣率,支援11025、16000、22050、24000、44100、48000,依瀏覽器預設值
sampleRate: 48000,
// 聲道,支援1或2,預設是1
numChannels: 1,
// 是否記錄邊,預設是false
compiling: false
}
new Recorder(query)

常用属性 attrs duration

1
2
// 時長
recorder.duration

文件大小 fileSize

1
recorder.fileSize
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
<template>
<div class="BaseRecorder">
<div class="BaseRecorder-record">
<i @click="startRecorder()" title="開始錄音" class="fa-solid fa-microphone"></i>
<i @click="resumeRecorder()" title="继续錄音" class="fa-solid fa-microphone-lines"></i>
<i @click="pauseRecorder()" title="暂停錄音" class="fa-solid fa-microphone-lines-slash"></i>
<i @click="stopRecorder()" title="停止錄音" class="fa-solid fa-microphone-slash"></i>
</div>
<div class="BaseRecorder-play">
<i @click="playRecorder()" class="fa-solid fa-volume-high"></i>
<i @click="pausePlayRecorder()" class="fa-solid fa-volume-xmark"></i>

<button @click="resumePlayRecorder()">恢复錄音播放</button>
<button @click="stopPlayRecorder()">停止錄音播放</button>
</div>
<div class="BaseRecorder-download">
<i @click="downPCM()" title="下载PCM" class="fa-solid fa-cloud-arrow-down"></i>
<i @click="downWAV()" title="下载WAV" class="fa-solid fa-cloud-arrow-down"></i>

</div>
<div class="BaseRecorder-destroy">
<button @click="destroyRecorder()">销毁錄音</button>

</div>
<div class="BaseRecorder-wave">
<canvas ref="record"></canvas>
<canvas ref="play"></canvas>
</div>
</div>

</template>

<script setup lang="ts">
import Recorder from 'js-audio-recorder'
import { ref, computed,onMounted} from 'vue'
const recorder = ref<any>(null)
//波浪图-录音
const drawRecordId = ref<any>(null)
//波浪图-播放
const drawPlayId = ref<any>(null)
const init = () => {
let query = {
// 取樣位數,支援 8 或 16,預設是 16
sampleBits: 16,
// 取樣率,支援11025、16000、22050、24000、44100、48000,依瀏覽器預設值
sampleRate: 48000,
// 聲道,支援1或2,預設是1
numChannels: 1,
// 是否記錄邊,預設是false
compiling: false
}
recorder.value = new Recorder(query)
// console.log('recorder.value',recorder.value)
}

//開始錄音
const startRecorder =() =>{
recorder.value.start()
.then(
() => {
drawRecord()
},
error => {
// 出错了
console.log(`${error.name} : ${error.message}`)
}
)
}
//ref="record" =>this.$refs.record
const record = ref<any>()
//繪製記錄
const drawRecord = () => {
//**方法通知瀏覽器我們想要產生動畫,並且要求瀏覽器在下次重繪畫面前呼叫特定函數更新動畫。這個方法接受一個引數作為下次重繪前調用的回呼函數。
let query = {
canvas: record.value,
dataArray: recorder.value.getRecordAnalyseData(),
bgcolor: 'rgb(50 65 150)',
lineWidth: 2,
lineColor: 'rgb(255, 255, 255)'
}
drawRecordId.value = requestAnimationFrame(drawRecord)
drawWave(query)
console.log('query', query)
console.log('drawRecordId.value', drawRecordId.value)
}
//畫波
const drawWave = ({
canvas,
dataArray,
bgcolor = 'rgb(200, 200, 200)',
lineWidth = 2,
lineColor = 'rgb(0, 0, 0)'
}) => {
if (!canvas) return
const ctx = record.value.getContext('2d')
const bufferLength = dataArray.length
// 一個點佔多少位置,每個bufferLength個點要控制
const sliceWidth = canvas.width / bufferLength
// 受傷點的x軸位
let x = 0

// 填充背景色
ctx.fillStyle = bgcolor
ctx.fillRect(0, 0, canvas.width, canvas.height)

// 設定,設計顏色
ctx.lineWidth = lineWidth
ctx.strokeStyle = lineColor

ctx.beginPath()

for (let i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 128
const y = (v * canvas.height) / 2

if (i === 0) {
// 第一个点
ctx.moveTo(x, y)
} else {
// 剩余的点
ctx.lineTo(x, y)
}
// 依次平移,绘制所有点
x += sliceWidth
}

// 最后一个点
ctx.lineTo(canvas.width, canvas.height / 2)
ctx.stroke()
}
// 继续錄音
const resumeRecorder = () => {
recorder.value.resume();
}
//暂停錄音
const pauseRecorder = () => {
recorder.value.pause();
//cancelAnimationFrame:是一種JavaScript實作動畫的技巧,它解決了傳統計時器(setTimeout和setInterval)在處理動畫時的一些問題,特別是與瀏覽器重繪
drawRecordId.value && cancelAnimationFrame(drawRecordId.value)
drawRecordId.value = null
}
// 停止錄音
const stopRecorder = () => {
recorder.value.stop();
drawRecordId.value && cancelAnimationFrame(drawRecordId.value)
drawRecordId.value = null
}
//录音播放
const playRecorder = () => {
recorder.value.play()
drawPlay()
}
/**
* 绘制波浪图-播放
* */
const play = ref<any>(null)
//
const drawPlay = () => {
drawPlayId.value = requestAnimationFrame(drawPlay)
drawWave({
canvas: play,
dataArray: recorder.value.getPlayAnalyseData()
})
console.log('recorder.value.getPlayAnalyseData()', recorder.value.getPlayAnalyseData())
}
//暂停录音播放
const pausePlayRecorder = () => {
recorder.value.pausePlay()
}
//恢复录音播放
const resumePlayRecorder = () => {
recorder.value.resumePlay()
drawPlay() // 绘制波浪图
}
//
const stopPlayRecorder = () => {
recorder.value.stopPlay();

}

const downPCM = () => {
recorder.value.downloadPCM('新文件');
console.log(recorder.value.downloadPCM('新文件'))
}
const downWAV = () => {
recorder.value.downloadWAV('新文件');
console.log(recorder.value.downloadWAV('新文件'))
}
const destroyRecorder = () => {
recorder.value.destroy().then(function () {
drawRecordId.value && cancelAnimationFrame(drawRecordId.value)
drawRecordId.value = null

drawPlayId.value && cancelAnimationFrame(drawPlayId.value)
drawPlayId.value = null
recorder.value = null
})
}


onMounted(() => {
init();
})
</script>

參考
前端工程師
API串接常見問題 - CORS - 概念篇 (1)
API串接常見問題 - CORS - 概念篇 (2)

使用 MediaStream 錄製 API

MediaStream 錄製 API

About Time

new Date():現在時間(字符)

1
new Date();

console.log(Wed Jan 04 2023 15:14:33 GMT+0800 (台北標準時間))

1
2
3
4
5
6
7
8
9
10
const dateObject = new Date() 
const year = new Date().getFullYear() //年
const month = new Date().getMonth() //月+1
const day = new Date().getDay() //星期幾
const date = new Date().getDate() //日期
const hour = new Date().getHours() //0-24 小時
const minute = new Date().getMinutes() //0-59 分鐘
const second = new Date().getSeconds() //0-59 秒
const ms = new Date().getMilliseconds() //0-999
const timestamp = new Date().getTime(); //時間搓

TimeStamp 時間搓

1
Date.now();

console.log(‘TimeStamp’,1672816660300)

UTC(Universal Time Coordinated : 世界統一時間、世界標準時間

GMT(格林威治標準時間),格林威治標準時間(也稱為格林威治時間)

Unix 时间戳:這是基於 UTC 1970.01.01 00:00:00 到現在的總秒數,所以這個總秒數全世界都是這樣的,也就是說 Unix 的時間戳和時區無關

timezone時區

1
2
3
const timezoneByMins = new Date().getTimezoneOffset() //-480
//除以60後正負相轉才是時區
const timezone = -(new Date().getTimezoneOffset()/60) //-8

台灣時間 UTC +8

1
2
3
4
5
const date = new Date()

console.log(date.toLocaleString()) //2023/01/04 下午4:33:41
console.log(date.toLocaleDateString()) //2023/01/04
console.log(date.toLocaleTimeString()) //下午4:33:41

日期字串格式

這個 ISO 8601 Extended Format 格式大概長這樣:

1
YYYY-MM-DDTHH:mm:ss.sssZ

表單type 取得的 time 為 ‘10:00’這種格式如何改為時間搓

邏輯:獲得現在時間 =>切割時間與分鐘 => 設定時間與分鐘 =>獲得的時間字符getTime()

1
2
3
4
5
6
7
8
9
10
11
12
let now = new Date();
//表單type 取得的 time 為 '10:00'
const hourMinute = '10:00';
//split() 方法可以用來根據你指定的分隔符號,將字串切割成一個字串陣列。
var [hour, minute] = hourMinute.split(':');
now.setHours(hour) ;//setHours()方法依據本地時間來設定日期物件中的小時
now.setMinutes(minute);//setMinutes()方法依據本地時間來設定日期物件中的分鐘
now.setSeconds(0);
now.setMilliseconds(0);

console.log('現在時間',now) // Sat Apr 20 2024 10:00:00 GMT+0800 (台北標準時間);
console.log('時間搓',now.getTime()) // now.getTime()

控制時間與製作計時器的函式

setTimeout:延遲了某段時間後,才去執行的指定的程式碼,只會執行一次就結束

1
2
3
4
5
6
7
//一秒後執行
setTimeout(
()=>{
alert("Hello!");
}
,1000)

時鐘setInterval:設定時間的間隔

//無限次的設置定時器執行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<div class="today">{{ today }}</div>
<div class="time">{{ time }}</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
//moment時間套件
import moment from 'moment';

const today = ref<any>(moment(new Date()).format("yyyy MM DD"));
const time = ref<any>();
onMounted(()=>{
setInterval(()=>{
time.value = new Date().getHours() + ':' + new Date().getMinutes() + ':' + new Date().getSeconds();
},1000)
})
</script>

clearInterval():取消setInterval()設的定時器

Vue3 動態 Web title

Vue 動態 Title

  • Pinia全局綁定title 與 切換時改變title
  • 綁定body 設定屬性 setAttribute class 改變body class
  • 監控 watchEffect 改變
  • 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
    import { defineStore } from 'pinia';

    export const usePageStore = defineStore('page', () => {

    const setPageClass = (pageClass:string) => {
    document.querySelector('body')?.setAttribute('class', pageClass);
    }
    const getPageClass=()=>{
    let pageClass:any | string =localStorage.getItem('pageClass');
    switch (pageClass) {
    case pageClass:
    document.querySelector('body')?.setAttribute('class', pageClass);
    break
    }
    }
    const changeTitleTag = (title: string) => {
    const titleTag = document.querySelector('title');

    if (titleTag != null) {
    titleTag.innerText = '採購系統'+' | '+title;
    }
    }

    return{
    setPageClass,getPageClass,changeTitleTag
    }
    })

    引入頁面
    首頁頁面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <template>
    <div>{{ title }}</div>
    </template>

    <script setup lang="ts">
    import { ref, onMounted,watchEffect } from 'vue'
    import { usePageStore } from '../stores/titleTag'

    const storePageTag = usePageStore()
    const {getPageClass,changeTitleTag,setPageClass} = storePageTag
    const title = ref<string>('首頁')
    const pageClass = ref<string>('homePage')
    watchEffect(() => {
    getPageClass();
    setPageClass(pageClass.value)
    })
    onMounted(() => {
    changeTitleTag(title.value);
    });
    </script>

    登入頁面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <template>
    <div>{{ title }}</div>
    </template>

    <script setup lang="ts">
    import { ref, onMounted,watchEffect } from 'vue'
    import { usePageStore } from '../stores/titleTag'
    const storePageTag = usePageStore()
    const {getPageClass,changeTitleTag,setPageClass} = storePageTag
    const title = ref<string>('請先登入')
    const pageClass = ref<string>('loginPage')
    watchEffect(() => {
    getPageClass();
    setPageClass(pageClass.value)
    })
    onMounted(() => {
    changeTitleTag(title.value);

    });
    </script>

    Node express 權限管理

    權限管理

    資料表設計
    • 使用者資料表: `users` 使用者,上面綁角色role_id,每個使用者都有一種角色
    • 角色資料表: `roles` 角色,用來設定權限用
    • 模組資料表: `module_permissions` 模組權限,用來綁定底下功能權限
    • 功能資料表: `func_permissions` 功能權限,用來綁底層的權限
    • 權限資料表: `permissions` 用來綁func_permissions與permission_types的表
    • 權限型別資料表: `permission_types` 權限分類,可自行定義分類,如:查看、新增、編輯、刪除…等
    使用者 => (一對多) 角色 => (一對多) 模組 => (一對多) 功能 => (一對多) 權限 => (多對一) 權限型別

    根據使用者角色獲得頁面檢視權限

    1
    2
    3
    4
    SELECT users.id, users.uId, users.userName, users.userEmail, users.userPassword, users.userPhone, users.roleId, users.token ,modulePermissions.modulePermissionId,modulePermissions.modulePermissionName,modulePermissions.pageLink 
    FROM `users`
    INNER JOIN `modulePermissions`
    ON users.uId=modulePermissions.roleId WHERE users.userName =?

    bcryptjs密碼加密與驗證

    安裝bcryptjs bcryptjs
    密碼加密,此套件的加密是不可逆的,所以沒有辦法從加密後的結果回推原始密碼,相對安全性提高非常多

    1
    2
    npm install bcryptjs --save

    加密後的 bcrypt 分為四個部分:

    • Bcrypt
      該字串為 UTF-8 編碼,並且包含一個終止符
    • Round
      (回合數)每增加一次就加倍雜湊次數,預設10次
    • Salt
      (加鹽)128 bits 22個字元
    • Hash
      (雜湊)138 bits 31個字元

    Node express MySql 註冊與登入

    註冊

    因爲註冊使用到bcryptjs密碼加密功能,
    bcrypt加密語法

    1
    2
    3
    4
    5
    6
    //引入bcryptjs密碼加密
    const bcrypt = require('bcryptjs')

    const saltRounds = 10;
    const hashPassword = bcrypt.hashSync(密碼, saltRounds)

    models.users.js

    1
    INSERT INTO `users` (column1, column2, column3...) SET (value1, value2, value3...)

    token

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //bcryptjs密碼加密
    const bcrypt = require('bcryptjs')
    //載入 jsonwebtoken
    const jwt = require('jsonwebtoken')
    // 建立使用者
    var User = function(newUser){
    this.uId = newUser.uId;
    this.userName = newUser.userName;
    this.userEmail = newUser.userEmail;
    this.userPassword = bcrypt.hashSync(newUser.userPassword, 3);//bcrypt.hashSync(newUser.userPassword, 12)
    this.userPhone= newUser.userPhone;
    this.roleId= newUser.roleId;
    this.token = jwt.sign({_id: newUser.uId ,username: newUser.userName,email: newUser.userEmail}, 'RESTFULAPI',{expiresIn: "24h"}).toString();//jwt
    this.createdAt= newUser.createdAt;
    this.updatedAt= newUser.updatedAt;
    this.deletedAt= newUser.deletedAt;
    }
    User.create = function (newUser, callback) {
    db.query('INSERT INTO users SET ?', newUser, callback);
    };
    module.exports = User;

    登入

    尋找帳號與密碼驗證 => 內連結的模組資料表的資料內容 (取出modulePermissionId與modulePermissionName 並且新增到menu )
    使用資料庫語法:

    1
    2
    3
    4

    SELECT 顯示欄位 FROM `資料表1` INNER JOIN `資料表2` ON 資料表1uId=資料表2roleId WHERE 資料表1.欄位名 ='';
    //例如
    SELECT users.id, users.uId, users.userName, users.userEmail, users.userPassword, users.userPhone, users.roleId, users.token ,modulePermissions.modulePermissionId,modulePermissions.modulePermissionName FROM `users` INNER JOIN `modulePermissions` ON users.uId=modulePermissions.roleId WHERE users.userName =?;

    登入時必須帳號登入時驗證密碼(bcrypt.compareSync(密碼, 密碼加密))

    bcrypt驗證語法

    1
    bcrypt.compareSync(密碼, 密碼加密)

    models.users.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    'use strict';

    var db = require('../config/database');
    //bcryptjs密碼加密
    const bcrypt = require('bcryptjs')
    //載入 jsonwebtoken
    const jwt = require('jsonwebtoken')

    User.userName = (userName, result) =>
    {
    let sql = 'SELECT users.id, users.uId, users.userName, users.userEmail, users.userPassword, users.userPhone, users.roleId, users.token ,modulePermissions.modulePermissionId,modulePermissions.modulePermissionName FROM `users` INNER JOIN `modulePermissions` ON users.uId=modulePermissions.roleId WHERE users.userName =?';
    db.query(sql, userName, (err, row, fields) =>
    {
    if (err) result(err, null,);
    result(null, row);
    });
    };

    controllers.users.js

    • bcrypt.hashSync(密碼, SaltRound)
    • bcrypt.compareSync(密碼,加密密碼)
    加密後的 bcrypt 分為四個部分:
    • Bcrypt
      該字串為 UTF-8 編碼,並且包含一個終止符
    • Round
      (回合數)每增加一次就加倍雜湊次數,預設10次
    • Salt
      (加鹽)128 bits 22個字元
    • Hash
      (雜湊)138 bits 31個字元
    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
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    const User = require('../models/users');

    exports.login = async (req, res, next) =>
    {
    /*
    #swagger.summary='登入',
    #swagger.description = '' */

    /* #swagger.parameters['body'] = {
    in: 'body',
    description: '',
    required: true,
    schema: { $ref: "#/definitions/Login" }
    } */

    /* #swagger.security = [{
    "apiKeyAuth": []
    }] */

    const userName = req.body.userName;
    const userPassword = req.body.userPassword;

    try {
    //檢查是否有userName 帳號
    User.userName(userName, (error, item) =>
    {
    if (item.length != 0 && bcrypt.compareSync(userPassword, item[0].userPassword) ) {
    // console.log('登入item', item)
    console.log('登入驗證密碼', bcrypt.compareSync(userPassword, item[0].userPassword))
    let menu = [];
    // forEach
    item.forEach(el => {
    let query = {
    modulePermissionId: el.modulePermissionId,
    modulePermissionName: el.modulePermissionName,
    }
    menu.push(query)
    });
    let loginValue = {
    userName: item[0].userName,
    token: item[0].token,
    menu: menu,
    }
    return res.status(200).json(loginValue);
    } else {
    return res.status(400).send('沒有userName')
    }
    })
    }
    catch (error) {
    return res.status(500).json({message: error.message})
    }
    }
    routes/users.js
    1
    2
    3
    4
    5
    var express = require('express');
    var router = express.Router();
    const userController = require('../controllers/users');

    router.post('/login', userController.login);

    MySql 目錄

    AUTO INCREMENT 欄位會自動遞增資料行的值

    AUTO_INCREMENT 欄位會自動遞增資料行的值

    AUTO INCREMENT 欄位會自動遞增資料行的值,因為每次新增資料時欄位值都會自動遞增也就是說 AUTO INCREMENT 欄位值會是唯一的,該欄位用途就像是一個識別碼或流水號,而 AUTO INCREMENT 常與 Primary Key 一起搭配使用。
    將 users 資料表中的 id 欄位 AUTO INCREMENT 欄位會自動遞增資料行的值,

    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
    CREATE TABLE `users` ( 
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    uId INT NOT NULL UNIQUE,
    userName VARCHAR(30) NOT NULL,
    userEmail VARCHAR(50) NOT NULL,
    userPassword VARCHAR(30) NOT NULL,
    userPhone VARCHAR(10) NOT NULL,
    roleId INT NOT NULL,
    createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    deletedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
    );
    CREATE TABLE `users` (
    id INT NOT NULL AUTO_INCREMENT,
    uId INT NOT NULL,
    userName VARCHAR(50) NOT NULL,
    userEmail VARCHAR(30)NOT NULL,
    userPassword VARCHAR(30) NOT NULL,
    userPhone VARCHAR(10) NOT NULL,
    roleId INT NOT NULL,
    createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    deletedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ,
    PRIMARY KEY (id),
    UNIQUE (uId)
    );
    參考:AUTO INCREMENT 欄位 (SQL AUTO INCREMENT column)