pc端100j功能完成

This commit is contained in:
fengerli
2026-03-19 11:42:26 +08:00
parent 8584cc78b2
commit fee33a68c6
7 changed files with 174 additions and 114 deletions

View File

@ -35,11 +35,13 @@
"image-conversion": "2.1.1", "image-conversion": "2.1.1",
"js-cookie": "3.0.5", "js-cookie": "3.0.5",
"jsencrypt": "3.3.2", "jsencrypt": "3.3.2",
"lamejs": "^1.2.1",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"paho-mqtt": "^1.1.0", "paho-mqtt": "^1.1.0",
"pinia": "3.0.2", "pinia": "3.0.2",
"qrcode-vue3": "^1.7.1", "qrcode-vue3": "^1.7.1",
"recorder-core": "^1.3.25011100",
"screenfull": "6.0.2", "screenfull": "6.0.2",
"vue": "3.5.13", "vue": "3.5.13",
"vue-cropper": "1.1.1", "vue-cropper": "1.1.1",
@ -72,7 +74,7 @@
"sass": "1.87.0", "sass": "1.87.0",
"terser": "^5.43.1", "terser": "^5.43.1",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"unocss": "^66.0.0", "unocss": "^0.58.0",
"unplugin-auto-import": "19.1.2", "unplugin-auto-import": "19.1.2",
"unplugin-icons": "22.1.0", "unplugin-icons": "22.1.0",
"unplugin-vue-components": "28.5.0", "unplugin-vue-components": "28.5.0",

BIN
src/assets/images/hb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/images/hbAc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/images/rg1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

BIN
src/assets/images/rg1Ac.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

View File

@ -22,9 +22,9 @@
<h4 class="section-title">报警模式</h4> <h4 class="section-title">报警模式</h4>
<div class="light-mode"> <div class="light-mode">
<!-- 使用v-for循环渲染灯光模式卡片 --> <!-- 使用v-for循环渲染灯光模式卡片 -->
<div class="mode-card" :class="{ 'active': mode.active }" <div class="mode-card" @click.stop="handleVoiceType(mode.id)" v-for="mode in sta_VoiceType"
@click.stop="handleVoiceType(mode.id)" v-for="mode in sta_VoiceType" :key="mode.id"> :key="mode.id">
<div class="mode_2"> <div class="mode_2" :class="{ 'active': mode.active }">
<img :src="mode.active ? mode.activeIcon : mode.icon" :alt="mode.name" <img :src="mode.active ? mode.activeIcon : mode.icon" :alt="mode.name"
class="mode-icon" /> class="mode-icon" />
<div class="mode-name">{{ mode.name }}</div> <div class="mode-name">{{ mode.name }}</div>
@ -40,9 +40,9 @@
<h4 class="section-title">警示灯爆闪</h4> <h4 class="section-title">警示灯爆闪</h4>
<div class="light-mode"> <div class="light-mode">
<!-- 使用v-for循环渲染灯光模式卡片 --> <!-- 使用v-for循环渲染灯光模式卡片 -->
<div class="mode-card" :class="{ 'active': mode.active }" <div class="mode-card" @click.stop="handleModeClick(mode.id)" v-for="mode in lightModes"
@click.stop="handleModeClick(mode.id)" v-for="mode in lightModes" :key="mode.id"> :key="mode.id">
<div class="mode_2"> <div class="mode_2" :class="{ 'active': mode.active }">
<img :src="mode.active ? mode.activeIcon : mode.icon" :alt="mode.name" <img :src="mode.active ? mode.activeIcon : mode.icon" :alt="mode.name"
class="mode-icon" /> class="mode-icon" />
<div class="mode-name">{{ mode.name }}</div> <div class="mode-name">{{ mode.name }}</div>
@ -52,7 +52,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="content-card1" style="margin-top: 8px;"> <div class="content-card2" style="margin-top: 8px;">
<h4 class="section-title">语音播报</h4> <h4 class="section-title">语音播报</h4>
<div class="voice-play-section"> <div class="voice-play-section">
<div class="current-voice"> <div class="current-voice">
@ -81,19 +81,19 @@
</div> </div>
<div class="voice-btn-item" @click="handleUploadVoice"> <div class="voice-btn-item" @click="handleUploadVoice">
<el-icon> <el-icon>
<Upload /> <Microphone />
</el-icon> </el-icon>
<span>上传语音</span> <span>上传语音</span>
</div> </div>
<div class="voice-btn-item" @click="handleTextToVoice"> <div class="voice-btn-item" @click="handleTextToVoice">
<el-icon> <el-icon>
<Document /> <Microphone />
</el-icon> </el-icon>
<span>文字转语音</span> <span>文字转语音</span>
</div> </div>
<div class="voice-btn-item" @click="handleAllVoice"> <div class="voice-btn-item" @click="handleAllVoice">
<el-icon> <el-icon>
<List /> <Microphone />
</el-icon> </el-icon>
<span>所有语音</span> <span>所有语音</span>
</div> </div>
@ -114,19 +114,24 @@
<div class="location-info"> <div class="location-info">
<div class="location-item"> <div class="location-item">
<span class="location-icon">📍</span> <span class="location-icon">📍</span>
<span>经纬度 {{ deviceDetail && deviceDetail.longitude ? <div style="font-weight: 600; font-size: 14px;">经纬度
Number(deviceDetail.longitude).toFixed(4) : '无' }} <div style="margin-top: 10px;">{{ deviceDetail && deviceDetail.longitude ?
{{ deviceDetail && deviceDetail.latitude ? Number(deviceDetail.latitude).toFixed(4) Number(deviceDetail.longitude).toFixed(4) : '0.00' }}
: '无' }} </span> {{ deviceDetail && deviceDetail.latitude ?
Number(deviceDetail.latitude).toFixed(4)
: '0.00' }} </div>
</div>
</div> </div>
<div class="location-item"> <div class="location-item1">
<div>地址 <span class="lacatin_gps">{{ deviceDetail.address || "未获取到地址" }}</span></div> <div>地址</div>
<el-button link type="primary" class="view-btn" <el-button link type="primary" class="view-btn"
@click="lookMap(deviceDetail)">查看</el-button> @click="lookMap(deviceDetail)">查看</el-button>
</div> </div>
<div class="lacatin_gps">{{ deviceDetail.address || "未获取到地址" }}</div>
</div> </div>
</div> </div>
<div class="content-card_gps" style="margin-top: 10px;"> <div class="content-card_gps" style="margin-top: 20px;">
<div> <div>
<h4 class="section-title">调节</h4> <h4 class="section-title">调节</h4>
<div class="brightness-alarm"> <div class="brightness-alarm">
@ -135,7 +140,7 @@
<el-input class="inputTFT" v-model="deviceDetail.lightBrightness" :min="10" <el-input class="inputTFT" v-model="deviceDetail.lightBrightness" :min="10"
:max="100" :step="1" /> :max="100" :step="1" />
<div class="brightness-value">%</div> <div class="brightness-value">%</div>
<el-button type="primary" class="save-btn" v-loading="lightModesLoading" <el-button type="primary" link class="save-btn" v-loading="lightModesLoading"
@click="saveBtnlight">保存 </el-button> @click="saveBtnlight">保存 </el-button>
</div> </div>
</div> </div>
@ -145,7 +150,7 @@
<el-input class="inputTFT" v-model="deviceDetail.volume" :min="10" :max="100" <el-input class="inputTFT" v-model="deviceDetail.volume" :min="10" :max="100"
:step="1" /> :step="1" />
<div class="brightness-value">%</div> <div class="brightness-value">%</div>
<el-button type="primary" class="save-btn" @click="saveBtnVolume">保存 <el-button type="primary" link class="save-btn" @click="saveBtnVolume">保存
</el-button> </el-button>
</div> </div>
</div> </div>
@ -155,7 +160,7 @@
<el-input class="inputTFT" v-model="deviceDetail.strobeFrequency" :min="0.5" <el-input class="inputTFT" v-model="deviceDetail.strobeFrequency" :min="0.5"
:max="10" :step="1" /> :max="10" :step="1" />
<div class="brightness-value">%</div> <div class="brightness-value">%</div>
<el-button type="primary" class="save-btn" @click="saveBtnstrobe">保存 <el-button type="primary" link class="save-btn" @click="saveBtnstrobe">保存
</el-button> </el-button>
</div> </div>
</div> </div>
@ -322,22 +327,16 @@ import { useRoute, useRouter } from 'vue-router';
import { useMqtt } from '@/utils/mqtt'; import { useMqtt } from '@/utils/mqtt';
import api from '@/api/controlCenter/controlPanel/100J'; import api from '@/api/controlCenter/controlPanel/100J';
import { DeviceDetail, LightMode } from '@/api/controlCenter/controlPanel/types'; import { DeviceDetail, LightMode } from '@/api/controlCenter/controlPanel/types';
import { getDeviceStatus } from '@/utils/function';
// 路由和实例 // 路由和实例
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import request from '@/utils/request';
// 导入图片资源 // 导入图片资源
import closeDefault from '@/assets/images/close.png'; import rb from '@/assets/images/rg1.png';
import closeActive from '@/assets/images/close_HL.png'; import rbAc from '@/assets/images/rg1Ac.png';
import rb from '@/assets/images/rb.png'; import sg from '@/assets/images/hb.png';
import rbAc from '@/assets/images/rbAc.png'; import sgAc from '@/assets/images/hbAc.png';
import sg from '@/assets/images/sg.png';
import sgAc from '@/assets/images/sgAc.png';
import { object } from 'vue-types';
// 基础状态 // 基础状态
const forceAlarmLoading = ref(false); const forceAlarmLoading = ref(false);
const lightModesLoading = ref(false); const lightModesLoading = ref(false);
@ -345,26 +344,17 @@ const centerDialogVisible = ref(false);
const currentVoiceId = ref(''); // 当前选中的语音ID const currentVoiceId = ref(''); // 当前选中的语音ID
const voiceList = ref<any[]>([]); // 语音列表 const voiceList = ref<any[]>([]); // 语音列表
// 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 recordVoiceDialog = ref(false);
const isRecording = ref(false); const isRecording = ref(false);
const recordDuration = ref(0); const recordDuration = ref(0);
const recordedBlob = ref<Blob | null>(null); const recorderIns = ref<any>(null); // Recorder 实例
const audioChunks = ref<BlobPart[]>([]);
const mediaRecorder = ref<MediaRecorder | null>(null);
let recordTimer: NodeJS.Timeout | null = null; let recordTimer: NodeJS.Timeout | null = null;
let audioStream: MediaStream | null = null; const recordedBlob = ref()
const formatTime = (seconds: number) => { const formatTime = (seconds: number) => {
const min = String(Math.floor(seconds / 60)).padStart(2, '0'); const min = String(Math.floor(seconds / 60)).padStart(2, '0');
@ -372,80 +362,118 @@ const formatTime = (seconds: number) => {
return `${min}:${sec}`; return `${min}:${sec}`;
}; };
// ------------- 开始录制语音上传 -------------
const startRecordVoice = async () => { const startRecordVoice = async () => {
try { try {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { // 1. 重置状态
proxy?.$modal.msgError('当前浏览器不支持麦克风录制请使用Chrome/Edge/Firefox'); resetRecord();
return; recorderIns.value = Recorder({
} type: 'mp3', // 直接录制为 MP3
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true }); sampleRate: 44100, // 标准采样率
mediaRecorder.value = new MediaRecorder(audioStream); bitRate: 128, // 128kbps微信兼容
audioChunks.value = []; onProcess: (buffers: any, power: number, bufferDuration: number, bufferSampleRate: number) => {
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;
} }
}; });
mediaRecorder.value.start(); recorderIns.value.open(() => {
isRecording.value = true; // 权限申请成功,开始录制
recordDuration.value = 0; recorderIns.value.start();
recordTimer = setInterval(() => recordDuration.value++, 1000); isRecording.value = true;
recordDuration.value = 0;
recordTimer = setInterval(() => recordDuration.value++, 1000);
// proxy?.$modal.msgSuccess('开始录制语音...');
}, (err: any) => {
// 权限申请失败
proxy?.$modal.msgError(`麦克风权限申请失败`);
resetRecord();
});
} catch (err) { } catch (err) {
proxy?.$modal.msgError('麦克风权限申请失败'); proxy?.$modal.msgError('录制异常,请重试');
if (audioStream) { resetRecord();
audioStream.getTracks().forEach(track => track.stop());
audioStream = null;
}
} }
}; };
// ------------- 停止录制 -------------
const stopRecordVoice = () => { const stopRecordVoice = () => {
if (!mediaRecorder.value || !isRecording.value) return; if (!recorderIns.value || !isRecording.value) {
mediaRecorder.value.stop(); proxy?.$modal.msgWarning('未在录制状态');
isRecording.value = false; return;
if (recordTimer) { }
clearInterval(recordTimer);
recordTimer = null; 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 = () => { const resetRecord = () => {
isRecording.value = false; isRecording.value = false;
recordDuration.value = 0; recordDuration.value = 0;
recordedBlob.value = null; recordedBlob.value = null;
audioChunks.value = [];
if (recordTimer) { if (recordTimer) {
clearInterval(recordTimer); clearInterval(recordTimer);
recordTimer = null; recordTimer = null;
} }
// 释放 Recorder 资源
if (recorderIns.value) {
recorderIns.value.close();
recorderIns.value = null;
}
}; };
// ------------- 保存/上传标准 MP3 文件 -------------
const saveRecordVoice = () => { const saveRecordVoice = () => {
if (!recordedBlob.value) { // 1. 校验 MP3 Blob 是否有效
proxy?.$modal.msgWarning('暂无录制的语音文件'); if (!recordedBlob.value || recordedBlob.value.size === 0) {
proxy?.$modal.msgWarning('暂无有效录制的 MP3 文件');
return; return;
} }
const formData = new FormData(); // 2. 校验 deviceId
formData.append('file', recordedBlob.value, `record_${new Date().getTime()}.mp3`); const deviceId = route?.params?.deviceId;
formData.append('deviceId', route.params.deviceId as string); if (!deviceId || typeof deviceId !== 'string') {
proxy?.$modal.loading('保存中...'); return;
api.uploadAudioToOss(formData).then(res => { }
proxy?.$modal.closeLoading(); try {
if (res.code === 200) { // 3. 构建 FormData上传标准 MP3
proxy?.$modal.msgSuccess('保存成功'); const formData = new FormData();
recordVoiceDialog.value = false; formData.append('deviceId', deviceId);
queryAudioFileInfo(); const fileName = `${new Date().getTime()}.mp3`;
resetRecord(); formData.append('file', recordedBlob.value, fileName);
} else { // 4. 上传接口
proxy?.$modal.msgError('保存语音失败:' + res.msg); proxy?.$modal.loading('保存中...');
} api.uploadAudioToOss(formData).then(res => {
}).catch(err => { proxy?.$modal.closeLoading();
proxy?.$modal.closeLoading(); if (res.code === 200) {
proxy?.$modal.msgError('保存语音失败'); proxy?.$modal.msgSuccess('保存成功');
}); recordVoiceDialog.value = false;
queryAudioFileInfo();
resetRecord();
} else {
proxy?.$modal.msgError('保存语音失败');
}
}).catch(err => {
proxy?.$modal.closeLoading();
proxy?.$modal.msgError('保存语音失败');
});
} catch (err) {
}
}; };
const handleRecordVoice = () => { const handleRecordVoice = () => {
@ -638,7 +666,7 @@ const deleteVoiceById = async (fileId: string) => {
const renameVoice = async (item: any) => { const renameVoice = async (item: any) => {
const { value } = await ElMessageBox.prompt('请输入新名称', '重命名', { inputPlaceholder: '请输入新名称' }); const { value } = await ElMessageBox.prompt('请输入新名称', '重命名', { inputPlaceholder: '请输入新名称' });
if (value) { 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) { if (res.code === 200) {
proxy?.$modal.msgSuccess(res.msg); proxy?.$modal.msgSuccess(res.msg);
queryAudioFileInfo(); queryAudioFileInfo();
@ -884,7 +912,7 @@ const showClose = async () => {
if (res.code === 200) { if (res.code === 200) {
deviceDetail.value.voiceStrobeAlarm = 0; deviceDetail.value.voiceStrobeAlarm = 0;
proxy?.$modal.msgSuccess(res.msg); proxy?.$modal.msgSuccess(res.msg);
await getList(); //await getList();
} }
} catch (error: any) { } catch (error: any) {
proxy?.$modal.msgError(error.msg); proxy?.$modal.msgError(error.msg);
@ -908,7 +936,7 @@ const forceAlarm = async () => {
deviceDetail.value.voiceStrobeAlarm = 1; deviceDetail.value.voiceStrobeAlarm = 1;
proxy?.$modal.msgSuccess(res.msg); proxy?.$modal.msgSuccess(res.msg);
await getList(); // await getList();
} }
} catch (error: any) { } catch (error: any) {
proxy?.$modal.msgError(error.msg); proxy?.$modal.msgError(error.msg);
@ -939,7 +967,7 @@ const playCurrentVoice = async () => {
proxy?.$modal.msgError(res.msg); proxy?.$modal.msgError(res.msg);
} }
} catch (err: any) { } catch (err: any) {
proxy?.$modal.msgError(err.msg); // proxy?.$modal.msgError(err.msg);
} }
} }
// 2. 非报警中场景:单纯播放/暂停语音 // 2. 非报警中场景:单纯播放/暂停语音
@ -958,7 +986,7 @@ const playCurrentVoice = async () => {
proxy?.$modal.msgError(res.msg); proxy?.$modal.msgError(res.msg);
} }
} catch (err: any) { } catch (err: any) {
proxy?.$modal.msgError(err.msg); // proxy?.$modal.msgError(err.msg);
} }
} }
}; };
@ -1018,18 +1046,27 @@ onUnmounted(() => {
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1); box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1);
background: white; background: white;
padding: 20px; padding: 1px 20px;
border: 1px solid #ebeef5; border: 1px solid #ebeef5;
min-height: 730px; min-height: 700px;
} }
.content-card1 { .content-card1 {
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1); box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1);
background: white; background: white;
padding: 20px; padding: 1px 20px;
border: 1px solid #ebeef5; 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 { .content-card_gps {
@ -1056,11 +1093,12 @@ onUnmounted(() => {
.lacatin_gps { .lacatin_gps {
height: 70px; height: 70px;
border-radius: 4px; background: #F7F8FC;
background: rgba(247, 248, 252, 1); border-radius: 4px 4px 4px 4px;
width: 100%; width: 100%;
padding: 10px; padding: 10px;
word-break: break-all; word-break: break-all;
margin-top: 15px;
} }
.mode_2 { .mode_2 {
@ -1069,8 +1107,17 @@ onUnmounted(() => {
border-radius: 8px; border-radius: 8px;
width: 105px; width: 105px;
text-align: center; text-align: center;
} }
.active {
border-radius: 4px 4px 4px 4px;
border: 1px solid #027CFB;
background: rgba(2, 124, 251, 0.06);
}
.mode-card { .mode-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1142,7 +1189,7 @@ onUnmounted(() => {
.save-btn { .save-btn {
padding: 6px 20px; padding: 6px 20px;
border-radius: 29px; border-radius: 29px;
background: rgba(2, 124, 251, 1); // background: rgba(2, 124, 251, 1);
border: none; border: none;
} }
@ -1168,6 +1215,12 @@ onUnmounted(() => {
} }
.location-info { .location-info {
.location-item1 {
display: flex;
justify-content: space-between;
color: #38404F;
}
.location-item { .location-item {
display: flex; display: flex;
align-items: center; align-items: center;
@ -1235,7 +1288,7 @@ onUnmounted(() => {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 10px; padding: 10px;
border: 1px solid #dcdfe6; // border: 1px solid #dcdfe6;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
@ -1246,14 +1299,19 @@ onUnmounted(() => {
} }
.el-icon { .el-icon {
font-size: 20px; font-size: 24px;
margin-bottom: 5px; 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 { span {
font-size: 12px; font-size: 14px;
color: #606266; color: #027CFB;
margin-top: 5px;
} }
} }
} }