pc端100j功能完成
This commit is contained in:
@ -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
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
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
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
BIN
src/assets/images/rg1Ac.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/images/组 62.png
Normal file
BIN
src/assets/images/组 62.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 992 B |
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user