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",
"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",

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>
<div class="light-mode">
<!-- 使用v-for循环渲染灯光模式卡片 -->
<div class="mode-card" :class="{ 'active': mode.active }"
@click.stop="handleVoiceType(mode.id)" v-for="mode in sta_VoiceType" :key="mode.id">
<div class="mode_2">
<div class="mode-card" @click.stop="handleVoiceType(mode.id)" v-for="mode in sta_VoiceType"
:key="mode.id">
<div class="mode_2" :class="{ 'active': mode.active }">
<img :src="mode.active ? mode.activeIcon : mode.icon" :alt="mode.name"
class="mode-icon" />
<div class="mode-name">{{ mode.name }}</div>
@ -40,9 +40,9 @@
<h4 class="section-title">警示灯爆闪</h4>
<div class="light-mode">
<!-- 使用v-for循环渲染灯光模式卡片 -->
<div class="mode-card" :class="{ 'active': mode.active }"
@click.stop="handleModeClick(mode.id)" v-for="mode in lightModes" :key="mode.id">
<div class="mode_2">
<div class="mode-card" @click.stop="handleModeClick(mode.id)" v-for="mode in lightModes"
:key="mode.id">
<div class="mode_2" :class="{ 'active': mode.active }">
<img :src="mode.active ? mode.activeIcon : mode.icon" :alt="mode.name"
class="mode-icon" />
<div class="mode-name">{{ mode.name }}</div>
@ -52,7 +52,7 @@
</div>
</div>
</div>
<div class="content-card1" style="margin-top: 8px;">
<div class="content-card2" style="margin-top: 8px;">
<h4 class="section-title">语音播报</h4>
<div class="voice-play-section">
<div class="current-voice">
@ -81,19 +81,19 @@
</div>
<div class="voice-btn-item" @click="handleUploadVoice">
<el-icon>
<Upload />
<Microphone />
</el-icon>
<span>上传语音</span>
</div>
<div class="voice-btn-item" @click="handleTextToVoice">
<el-icon>
<Document />
<Microphone />
</el-icon>
<span>文字转语音</span>
</div>
<div class="voice-btn-item" @click="handleAllVoice">
<el-icon>
<List />
<Microphone />
</el-icon>
<span>所有语音</span>
</div>
@ -114,19 +114,24 @@
<div class="location-info">
<div class="location-item">
<span class="location-icon">📍</span>
<span>经纬度 {{ deviceDetail && deviceDetail.longitude ?
Number(deviceDetail.longitude).toFixed(4) : '无' }}
{{ deviceDetail && deviceDetail.latitude ? Number(deviceDetail.latitude).toFixed(4)
: '无' }} </span>
<div style="font-weight: 600; font-size: 14px;">经纬度
<div style="margin-top: 10px;">{{ deviceDetail && deviceDetail.longitude ?
Number(deviceDetail.longitude).toFixed(4) : '0.00' }}
{{ deviceDetail && deviceDetail.latitude ?
Number(deviceDetail.latitude).toFixed(4)
: '0.00' }} </div>
</div>
<div class="location-item">
<div>地址 <span class="lacatin_gps">{{ deviceDetail.address || "未获取到地址" }}</span></div>
</div>
<div class="location-item1">
<div>地址</div>
<el-button link type="primary" class="view-btn"
@click="lookMap(deviceDetail)">查看</el-button>
</div>
<div class="lacatin_gps">{{ deviceDetail.address || "未获取到地址" }}</div>
</div>
</div>
<div class="content-card_gps" style="margin-top: 10px;">
<div class="content-card_gps" style="margin-top: 20px;">
<div>
<h4 class="section-title">调节</h4>
<div class="brightness-alarm">
@ -135,7 +140,7 @@
<el-input class="inputTFT" v-model="deviceDetail.lightBrightness" :min="10"
:max="100" :step="1" />
<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>
</div>
</div>
@ -145,7 +150,7 @@
<el-input class="inputTFT" v-model="deviceDetail.volume" :min="10" :max="100"
:step="1" />
<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>
</div>
</div>
@ -155,7 +160,7 @@
<el-input class="inputTFT" v-model="deviceDetail.strobeFrequency" :min="0.5"
:max="10" :step="1" />
<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>
</div>
</div>
@ -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<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 isRecording = ref(false);
const recordDuration = ref(0);
const recordedBlob = ref<Blob | null>(null);
const audioChunks = ref<BlobPart[]>([]);
const mediaRecorder = ref<MediaRecorder | null>(null);
const recorderIns = ref<any>(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,65 +362,100 @@ const formatTime = (seconds: number) => {
return `${min}:${sec}`;
};
// ------------- 开始录制语音上传 -------------
const startRecordVoice = async () => {
try {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
proxy?.$modal.msgError('当前浏览器不支持麦克风录制请使用Chrome/Edge/Firefox');
return;
// 1. 重置状态
resetRecord();
recorderIns.value = Recorder({
type: 'mp3', // 直接录制为 MP3
sampleRate: 44100, // 标准采样率
bitRate: 128, // 128kbps微信兼容
onProcess: (buffers: any, power: number, bufferDuration: number, bufferSampleRate: number) => {
}
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;
}
};
mediaRecorder.value.start();
});
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();
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);
recordTimer = null;
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;
}
// 2. 校验 deviceId
const deviceId = route?.params?.deviceId;
if (!deviceId || typeof deviceId !== 'string') {
return;
}
try {
// 3. 构建 FormData上传标准 MP3
const formData = new FormData();
formData.append('file', recordedBlob.value, `record_${new Date().getTime()}.mp3`);
formData.append('deviceId', route.params.deviceId as string);
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();
@ -440,12 +465,15 @@ const saveRecordVoice = () => {
queryAudioFileInfo();
resetRecord();
} else {
proxy?.$modal.msgError('保存语音失败' + res.msg);
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;
}
}
}