diff --git a/package.json b/package.json index ca5fd88..46f5773 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,13 @@ "image-conversion": "2.1.1", "js-cookie": "3.0.5", "jsencrypt": "3.3.2", + "lamejs": "^1.2.1", "mitt": "^3.0.1", "nprogress": "0.2.0", "paho-mqtt": "^1.1.0", "pinia": "3.0.2", "qrcode-vue3": "^1.7.1", + "recorder-core": "^1.3.25011100", "screenfull": "6.0.2", "vue": "3.5.13", "vue-cropper": "1.1.1", @@ -72,7 +74,7 @@ "sass": "1.87.0", "terser": "^5.43.1", "typescript": "~5.8.3", - "unocss": "^66.0.0", + "unocss": "^0.58.0", "unplugin-auto-import": "19.1.2", "unplugin-icons": "22.1.0", "unplugin-vue-components": "28.5.0", diff --git a/src/assets/images/hb.png b/src/assets/images/hb.png new file mode 100644 index 0000000..e619cde Binary files /dev/null and b/src/assets/images/hb.png differ diff --git a/src/assets/images/hbAc.png b/src/assets/images/hbAc.png new file mode 100644 index 0000000..84e9e34 Binary files /dev/null and b/src/assets/images/hbAc.png differ diff --git a/src/assets/images/rg1.png b/src/assets/images/rg1.png new file mode 100644 index 0000000..0d82789 Binary files /dev/null and b/src/assets/images/rg1.png differ diff --git a/src/assets/images/rg1Ac.png b/src/assets/images/rg1Ac.png new file mode 100644 index 0000000..448f71d Binary files /dev/null and b/src/assets/images/rg1Ac.png differ diff --git a/src/assets/images/组 62.png b/src/assets/images/组 62.png new file mode 100644 index 0000000..56ce98f Binary files /dev/null and b/src/assets/images/组 62.png differ diff --git a/src/views/controlCenter/100J/index.vue b/src/views/controlCenter/100J/index.vue index 05358fd..ae44c01 100644 --- a/src/views/controlCenter/100J/index.vue +++ b/src/views/controlCenter/100J/index.vue @@ -22,9 +22,9 @@

报警模式

-
-
+
+
{{ mode.name }}
@@ -40,9 +40,9 @@

警示灯爆闪

-
-
+
+
{{ mode.name }}
@@ -52,7 +52,7 @@
-
+

语音播报

@@ -81,19 +81,19 @@
- + 上传语音
- + 文字转语音
- + 所有语音
@@ -114,19 +114,24 @@
📍 - 经纬度 {{ deviceDetail && deviceDetail.longitude ? - Number(deviceDetail.longitude).toFixed(4) : '无' }} - {{ deviceDetail && deviceDetail.latitude ? Number(deviceDetail.latitude).toFixed(4) - : '无' }} +
经纬度 +
{{ deviceDetail && deviceDetail.longitude ? + Number(deviceDetail.longitude).toFixed(4) : '0.00' }} + {{ deviceDetail && deviceDetail.latitude ? + Number(deviceDetail.latitude).toFixed(4) + : '0.00' }}
+
+
-
-
地址 {{ deviceDetail.address || "未获取到地址" }}
+
+
地址
查看
+
{{ deviceDetail.address || "未获取到地址" }}
-
+

调节

@@ -135,7 +140,7 @@
%
- 保存
@@ -145,7 +150,7 @@
%
- 保存 + 保存
@@ -155,7 +160,7 @@
%
- 保存 + 保存
@@ -322,22 +327,16 @@ import { useRoute, useRouter } from 'vue-router'; import { useMqtt } from '@/utils/mqtt'; import api from '@/api/controlCenter/controlPanel/100J'; import { DeviceDetail, LightMode } from '@/api/controlCenter/controlPanel/types'; -import { getDeviceStatus } from '@/utils/function'; // 路由和实例 const route = useRoute(); const router = useRouter(); const { proxy } = getCurrentInstance() as ComponentInternalInstance; -import request from '@/utils/request'; // 导入图片资源 -import closeDefault from '@/assets/images/close.png'; -import closeActive from '@/assets/images/close_HL.png'; -import rb from '@/assets/images/rb.png'; -import rbAc from '@/assets/images/rbAc.png'; -import sg from '@/assets/images/sg.png'; -import sgAc from '@/assets/images/sgAc.png'; -import { object } from 'vue-types'; - +import rb from '@/assets/images/rg1.png'; +import rbAc from '@/assets/images/rg1Ac.png'; +import sg from '@/assets/images/hb.png'; +import sgAc from '@/assets/images/hbAc.png'; // 基础状态 const forceAlarmLoading = ref(false); const lightModesLoading = ref(false); @@ -345,26 +344,17 @@ const centerDialogVisible = ref(false); const currentVoiceId = ref(''); // 当前选中的语音ID const voiceList = ref([]); // 语音列表 -// MQTT相关 -const { - connected, - connect, - subscribe, - onConnect, - onError, - onMessage, - disconnect -} = useMqtt(); - // ====================== 录制语音 ====================== +import Recorder from 'recorder-core'; +// 引入 MP3 编码器(核心:生成标准 MP3) +import 'recorder-core/src/engine/mp3'; +import 'recorder-core/src/engine/mp3-engine'; const recordVoiceDialog = ref(false); const isRecording = ref(false); const recordDuration = ref(0); -const recordedBlob = ref(null); -const audioChunks = ref([]); -const mediaRecorder = ref(null); +const recorderIns = ref(null); // Recorder 实例 let recordTimer: NodeJS.Timeout | null = null; -let audioStream: MediaStream | null = null; +const recordedBlob = ref() const formatTime = (seconds: number) => { const min = String(Math.floor(seconds / 60)).padStart(2, '0'); @@ -372,80 +362,118 @@ const formatTime = (seconds: number) => { return `${min}:${sec}`; }; +// ------------- 开始录制语音上传 ------------- const startRecordVoice = async () => { try { - if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { - proxy?.$modal.msgError('当前浏览器不支持麦克风录制,请使用Chrome/Edge/Firefox'); - return; - } - audioStream = await navigator.mediaDevices.getUserMedia({ audio: true }); - mediaRecorder.value = new MediaRecorder(audioStream); - audioChunks.value = []; - mediaRecorder.value.ondataavailable = (e) => audioChunks.value.push(e.data); - mediaRecorder.value.onstop = () => { - recordedBlob.value = new Blob(audioChunks.value, { type: 'audio/mp3' }); - if (audioStream) { - audioStream.getTracks().forEach(track => track.stop()); - audioStream = null; + // 1. 重置状态 + resetRecord(); + recorderIns.value = Recorder({ + type: 'mp3', // 直接录制为 MP3 + sampleRate: 44100, // 标准采样率 + bitRate: 128, // 128kbps(微信兼容) + onProcess: (buffers: any, power: number, bufferDuration: number, bufferSampleRate: number) => { } - }; - mediaRecorder.value.start(); - isRecording.value = true; - recordDuration.value = 0; - recordTimer = setInterval(() => recordDuration.value++, 1000); + }); + recorderIns.value.open(() => { + // 权限申请成功,开始录制 + recorderIns.value.start(); + isRecording.value = true; + recordDuration.value = 0; + recordTimer = setInterval(() => recordDuration.value++, 1000); + // proxy?.$modal.msgSuccess('开始录制语音...'); + }, (err: any) => { + // 权限申请失败 + proxy?.$modal.msgError(`麦克风权限申请失败`); + resetRecord(); + }); } catch (err) { - proxy?.$modal.msgError('麦克风权限申请失败'); - if (audioStream) { - audioStream.getTracks().forEach(track => track.stop()); - audioStream = null; - } + proxy?.$modal.msgError('录制异常,请重试'); + resetRecord(); } }; +// ------------- 停止录制 ------------- const stopRecordVoice = () => { - if (!mediaRecorder.value || !isRecording.value) return; - mediaRecorder.value.stop(); - isRecording.value = false; - if (recordTimer) { - clearInterval(recordTimer); - recordTimer = null; + if (!recorderIns.value || !isRecording.value) { + proxy?.$modal.msgWarning('未在录制状态'); + return; + } + + try { + // 停止录制并获取 MP3 Blob + recorderIns.value.stop((blob: Blob, duration: number) => { + // blob 是标准 MP3 格式(微信100%兼容) + recordedBlob.value = blob; + console.log('标准 MP3 生成成功:', blob); + + isRecording.value = false; + if (recordTimer) clearInterval(recordTimer); + proxy?.$modal.msgSuccess(`录制完成,时长:${Math.round(duration)}秒`); + + // 释放资源 + recorderIns.value.close(); + }, (err: any) => { + // proxy?.$modal.msgError(`停止录制失败:${err.msg}`); + resetRecord(); + }); + } catch (err) { + proxy?.$modal.msgError('停止录制异常'); + resetRecord(); } }; +// ------------- 重置录制状态 ------------- const resetRecord = () => { isRecording.value = false; recordDuration.value = 0; recordedBlob.value = null; - audioChunks.value = []; if (recordTimer) { clearInterval(recordTimer); recordTimer = null; } + // 释放 Recorder 资源 + if (recorderIns.value) { + recorderIns.value.close(); + recorderIns.value = null; + } }; - +// ------------- 保存/上传标准 MP3 文件 ------------- const saveRecordVoice = () => { - if (!recordedBlob.value) { - proxy?.$modal.msgWarning('暂无录制的语音文件'); + // 1. 校验 MP3 Blob 是否有效 + if (!recordedBlob.value || recordedBlob.value.size === 0) { + proxy?.$modal.msgWarning('暂无有效录制的 MP3 文件'); return; } - const formData = new FormData(); - formData.append('file', recordedBlob.value, `record_${new Date().getTime()}.mp3`); - formData.append('deviceId', route.params.deviceId as string); - proxy?.$modal.loading('保存中...'); - api.uploadAudioToOss(formData).then(res => { - proxy?.$modal.closeLoading(); - if (res.code === 200) { - proxy?.$modal.msgSuccess('保存成功'); - recordVoiceDialog.value = false; - queryAudioFileInfo(); - resetRecord(); - } else { - proxy?.$modal.msgError('保存语音失败:' + res.msg); - } - }).catch(err => { - proxy?.$modal.closeLoading(); - proxy?.$modal.msgError('保存语音失败'); - }); + // 2. 校验 deviceId + const deviceId = route?.params?.deviceId; + if (!deviceId || typeof deviceId !== 'string') { + return; + } + try { + // 3. 构建 FormData(上传标准 MP3) + const formData = new FormData(); + formData.append('deviceId', deviceId); + const fileName = `${new Date().getTime()}.mp3`; + formData.append('file', recordedBlob.value, fileName); + // 4. 上传接口 + proxy?.$modal.loading('保存中...'); + api.uploadAudioToOss(formData).then(res => { + proxy?.$modal.closeLoading(); + if (res.code === 200) { + proxy?.$modal.msgSuccess('保存成功'); + recordVoiceDialog.value = false; + queryAudioFileInfo(); + resetRecord(); + } else { + proxy?.$modal.msgError('保存语音失败'); + } + }).catch(err => { + proxy?.$modal.closeLoading(); + proxy?.$modal.msgError('保存语音失败'); + }); + } catch (err) { + + } }; const handleRecordVoice = () => { @@ -638,7 +666,7 @@ const deleteVoiceById = async (fileId: string) => { const renameVoice = async (item: any) => { const { value } = await ElMessageBox.prompt('请输入新名称', '重命名', { inputPlaceholder: '请输入新名称' }); if (value) { - const res = await api.videRenameAudioFile({ deviceId: route.params.deviceId, fileId:item.fileId, fileName: value }); + const res = await api.videRenameAudioFile({ deviceId: route.params.deviceId, fileId: item.fileId, fileName: value }); if (res.code === 200) { proxy?.$modal.msgSuccess(res.msg); queryAudioFileInfo(); @@ -884,7 +912,7 @@ const showClose = async () => { if (res.code === 200) { deviceDetail.value.voiceStrobeAlarm = 0; proxy?.$modal.msgSuccess(res.msg); - await getList(); + //await getList(); } } catch (error: any) { proxy?.$modal.msgError(error.msg); @@ -908,7 +936,7 @@ const forceAlarm = async () => { deviceDetail.value.voiceStrobeAlarm = 1; proxy?.$modal.msgSuccess(res.msg); - await getList(); + // await getList(); } } catch (error: any) { proxy?.$modal.msgError(error.msg); @@ -939,7 +967,7 @@ const playCurrentVoice = async () => { proxy?.$modal.msgError(res.msg); } } catch (err: any) { - proxy?.$modal.msgError(err.msg); + // proxy?.$modal.msgError(err.msg); } } // 2. 非报警中场景:单纯播放/暂停语音 @@ -958,7 +986,7 @@ const playCurrentVoice = async () => { proxy?.$modal.msgError(res.msg); } } catch (err: any) { - proxy?.$modal.msgError(err.msg); + // proxy?.$modal.msgError(err.msg); } } }; @@ -1018,18 +1046,27 @@ onUnmounted(() => { border-radius: 8px; box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1); background: white; - padding: 20px; + padding: 1px 20px; border: 1px solid #ebeef5; - min-height: 730px; + min-height: 700px; } .content-card1 { border-radius: 8px; box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1); background: white; - padding: 20px; + padding: 1px 20px; border: 1px solid #ebeef5; - min-height: 250px; + height: 440px; + } + + .content-card2 { + border-radius: 8px; + box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1); + background: white; + padding: 1px 20px; + border: 1px solid #ebeef5; + height: 250px; } .content-card_gps { @@ -1056,11 +1093,12 @@ onUnmounted(() => { .lacatin_gps { height: 70px; - border-radius: 4px; - background: rgba(247, 248, 252, 1); + background: #F7F8FC; + border-radius: 4px 4px 4px 4px; width: 100%; padding: 10px; word-break: break-all; + margin-top: 15px; } .mode_2 { @@ -1069,8 +1107,17 @@ onUnmounted(() => { border-radius: 8px; width: 105px; text-align: center; + + } + .active { + border-radius: 4px 4px 4px 4px; + border: 1px solid #027CFB; + background: rgba(2, 124, 251, 0.06); + } + + .mode-card { display: flex; flex-direction: column; @@ -1142,7 +1189,7 @@ onUnmounted(() => { .save-btn { padding: 6px 20px; border-radius: 29px; - background: rgba(2, 124, 251, 1); + // background: rgba(2, 124, 251, 1); border: none; } @@ -1168,6 +1215,12 @@ onUnmounted(() => { } .location-info { + .location-item1 { + display: flex; + justify-content: space-between; + color: #38404F; + } + .location-item { display: flex; align-items: center; @@ -1235,7 +1288,7 @@ onUnmounted(() => { flex-direction: column; align-items: center; padding: 10px; - border: 1px solid #dcdfe6; + // border: 1px solid #dcdfe6; border-radius: 8px; cursor: pointer; transition: all 0.3s; @@ -1246,14 +1299,19 @@ onUnmounted(() => { } .el-icon { - font-size: 20px; + font-size: 24px; margin-bottom: 5px; - color: #409eff; + color: #027CFB; + border: 1px solid rgba(2, 124, 251, 0.2); + border-radius: 6px 6px 6px 6px; + width: 37px; + height: 37px; } span { - font-size: 12px; - color: #606266; + font-size: 14px; + color: #027CFB; + margin-top: 5px; } } }