1
0
forked from dyf/dyf-vue-ui
Files
dyf-vue-ui/src/views/controlCenter/100J/index.vue
2026-03-19 11:42:26 +08:00

1628 lines
57 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="device-page p-2">
<!-- 头部信息栏 -->
<div class="header-bar">
<div>设备名称{{ deviceDetail.deviceName }}</div>
<div>设备型号{{ deviceDetail.deviceImei }}</div>
<div class="device-status">设备状态
<span :class="{ online: deviceDetail.onlineStatus === 1, offline: deviceDetail?.onlineStatus === 0 }">
{{ deviceDetail.onlineStatus === 1 ? '在线' : '离线' }}
</span>
</div>
<div>电量{{ deviceDetail.batteryPercentage || 0 }}%</div>
<div>续航{{ deviceDetail.batteryRemainingTime || "0" }} 分钟</div>
</div>
<!-- 主体内容区域 -->
<div class="content-wrapper">
<!-- 第一行灯光模式 + 灯光亮度强制报警位置信息 -->
<el-row :gutter="20" class="content-row">
<el-col :lg="9" :xs="24">
<div class="content-card">
<h4 class="section-title">报警模式</h4>
<div class="light-mode">
<!-- 使用v-for循环渲染灯光模式卡片 -->
<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>
</div>
<el-switch v-model="mode.switchStatusVioice" />
</div>
</div>
</div>
</el-col>
<el-col :lg="9" :xs="24">
<div class="content-card1">
<h4 class="section-title">警示灯爆闪</h4>
<div class="light-mode">
<!-- 使用v-for循环渲染灯光模式卡片 -->
<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>
</div>
<el-switch v-model="mode.switchStatus" />
</div>
</div>
</div>
<div class="content-card2" style="margin-top: 8px;">
<h4 class="section-title">语音播报</h4>
<div class="voice-play-section">
<div class="current-voice">
<span class="voice-label">当前语音</span>
<div class="voice-select">
<el-select v-model="currentVoiceId" placeholder="请选择语音" style="width: 100%;">
<el-option v-for="item in voiceList" :key="item.id" :label="item.fileNameExt"
:value="item.id" ite>
</el-option>
</el-select>
</div>
<el-button type="primary" class="play-btn" @click="playCurrentVoice(1)"
v-if="deviceDetail.voiceBroadcast !== 1">
播放</el-button>
<el-button v-else type="info" class="play-btn" @click="playCurrentVoice(0)"> 暂停
</el-button>
</div>
<div class="voice-manage-section">
<span class="voice-label">语音管理</span>
<div class="voice-manage-btns">
<div class="voice-btn-item" @click="handleRecordVoice">
<el-icon>
<Microphone />
</el-icon>
<span>录制语音</span>
</div>
<div class="voice-btn-item" @click="handleUploadVoice">
<el-icon>
<Microphone />
</el-icon>
<span>上传语音</span>
</div>
<div class="voice-btn-item" @click="handleTextToVoice">
<el-icon>
<Microphone />
</el-icon>
<span>文字转语音</span>
</div>
<div class="voice-btn-item" @click="handleAllVoice">
<el-icon>
<Microphone />
</el-icon>
<span>所有语音</span>
</div>
</div>
</div>
</div>
</div>
</el-col>
<el-col :lg="6" :xs="24">
<div class="brightness-alarm alarm-btn-wrapper">
<el-button type="danger" class="alarm-btn" @click="forceAlarm" :loading="forceAlarmLoading"
:loading-text="forceAlarmLoading ? '报警中...' : '强制报警'"> {{ deviceDetail.voiceStrobeAlarm ===
1 ? '报警中' : '强制报警' }}</el-button>
<el-button type="default" class="alarm-btn cancel" @click="showClose">解除报警</el-button>
</div>
<div class="content-card_gps">
<h4 class="section-title">位置信息</h4>
<div class="location-info">
<div class="location-item">
<span class="location-icon">📍</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>
<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: 20px;">
<div>
<h4 class="section-title">调节</h4>
<div class="brightness-alarm">
<div class="brightness-control">
<div class="brightness-label">灯光亮度</div>
<el-input class="inputTFT" v-model="deviceDetail.lightBrightness" :min="10"
:max="100" :step="1" />
<div class="brightness-value">%</div>
<el-button type="primary" link class="save-btn" v-loading="lightModesLoading"
@click="saveBtnlight">保存 </el-button>
</div>
</div>
<div class="brightness-alarm">
<div class="brightness-control">
<div class="brightness-label"> </div>
<el-input class="inputTFT" v-model="deviceDetail.volume" :min="10" :max="100"
:step="1" />
<div class="brightness-value">%</div>
<el-button type="primary" link class="save-btn" @click="saveBtnVolume">保存
</el-button>
</div>
</div>
<div class="brightness-alarm">
<div class="brightness-control">
<div class="brightness-label">爆闪频率</div>
<el-input class="inputTFT" v-model="deviceDetail.strobeFrequency" :min="0.5"
:max="10" :step="1" />
<div class="brightness-value">%</div>
<el-button type="primary" link class="save-btn" @click="saveBtnstrobe">保存
</el-button>
</div>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
<!-- 录制语音弹窗对齐UI -->
<el-dialog title="录制语音" v-model="recordVoiceDialog" width="480px" class="voice-dialog" :show-close="true">
<div class="record-content">
<!-- 波形图 -->
<div class="waveform-container">
<div class="waveform" v-if="isRecording">
<span v-for="i in 30" :key="i" class="wave-bar"
:style="{ height: `${Math.random() * 40 + 10}px` }"></span>
</div>
<div class="waveform-placeholder" v-else>
<span class="wave-placeholder"></span>
</div>
</div>
<!-- 录制控制区 -->
<div class="record-control">
<div class="mic-icon" :class="{ recording: isRecording }">
<el-icon>
<Microphone />
</el-icon>
</div>
<div class="record-time">{{ formatTime(recordDuration) }}</div>
<div class="record-play-btn">
<el-button v-if="isRecording" type="danger" circle @click="stopRecordVoice">
<el-icon>
<!-- <Pause /> -->
</el-icon>
</el-button>
<el-button v-else type="primary" circle @click="startRecordVoice">
<el-icon>
<VideoPlay />
</el-icon>
</el-button>
</div>
</div>
<!-- 底部操作 -->
<div class="record-footer">
<el-button link type="primary" @click="resetRecord" :disabled="isRecording">重新录制</el-button>
<el-button type="primary" class="record-confirm" @click="saveRecordVoice"
:disabled="!recordedBlob">完成</el-button>
</div>
</div>
</el-dialog>
<!-- 上传语音弹窗 -->
<el-dialog title="上传语音" v-model="uploadVoiceDialog" width="480px" class="voice-dialog" :show-close="true">
<div class="upload-content">
<div class="upload-area" :class="{ dragOver: isDragOver }" @dragover.prevent="isDragOver = true"
@dragleave.prevent="isDragOver = false" @drop.prevent="handleDrop" @click="triggerFileInput">
<div class="upload-icon">
<el-icon>
<Document />
</el-icon>
</div>
<el-button type="primary" class="select-file-btn" @click="triggerFileInput">选择文件</el-button>
<div class="upload-tip">将文件拖拽至此区域</div>
<div class="upload-file-info" v-if="uploadFile">
<span class="file-name">{{ uploadFile.name }}</span>
<span class="file-size">{{ (uploadFile.size / 1024).toFixed(2) }} KB</span>
</div>
</div>
<input ref="uploadFileInput" type="file" accept="audio/*" style="display: none"
@change="handleFileUpload" />
</div>
<template #footer>
<el-button @click="uploadVoiceDialog = false">取消</el-button>
<el-button type="primary" @click="confirmUploadVoice" :disabled="!uploadFile">确认</el-button>
</template>
</el-dialog>
<!-- 文字转语音弹窗 -->
<el-dialog title="文字转语音" v-model="textToVoiceDialog" width="520px" class="voice-dialog" :show-close="true">
<div class="tts-content">
<el-input v-model="textToVoiceForm.content" type="textarea" rows="4" placeholder="请输入要转换的文字内容"
class="tts-textarea"></el-input>
<div class="tts-actions">
<!-- <el-button link type="primary" @click="handleUploadText">上传文本</el-button> -->
<el-button type="primary" @click="convertTextToVoice" :loading="ttsLoading">开始转换</el-button>
</div>
<!-- 转换后预览 -->
<div class="tts-preview" v-if="ttsResultUrl">
<div class="waveform-container">
<div class="waveform">
<span v-for="i in 30" :key="i" class="wave-bar"
:style="{ height: `${Math.random() * 40 + 10}px` }"></span>
</div>
</div>
<div class="audio-player">
<el-slider v-model="ttsCurrentTime" :max="ttsDuration" :show-tooltip="false"
style="flex: 1;"></el-slider>
<div class="time-info">
<span>{{ formatTime(ttsCurrentTime) }}</span>
<span>{{ formatTime(ttsDuration) }}</span>
</div>
</div>
<div class="player-controls">
<el-button circle @click="toggleTtsPlay">
<el-icon>
<VideoPlay v-if="!ttsPlaying" />
<!-- <Pause v-else /> -->
</el-icon>
</el-button>
</div>
<div class="preview-actions">
<el-button link type="primary" @click="saveTtsResult">保存</el-button>
<el-button type="primary" @click="useTtsResult">使用</el-button>
</div>
</div>
</div>
</el-dialog>
<!-- 所有语音列表弹窗 -->
<el-dialog title="所有语音" v-model="allVoiceDialog" width="560px" class="voice-dialog" :show-close="true">
<div class="all-voice-content">
<div class="voice-list">
<div class="voice-item" v-for="item in voiceList" :key="item.id">
<div class="voice-info">
<el-icon>
<Document />
</el-icon>
<span class="voice-name">{{ item.fileNameExt }}</span>
<span>{{ item.duration }}</span>
</div>
<div class="voice-actions">
<el-button link type="primary" @click="toggleVoicePlay(item.id)">
{{ playingVoiceId === item.id && voicePlaying ? '暂停' : '播放' }}
</el-button>
<el-button link type="danger" @click="deleteVoiceById(item.fileId)">删除</el-button>
<el-button link type="primary" @click="renameVoice(item)">重命名</el-button>
<el-button link type="primary"
:class="{ 'active': item.useStatus, 'btn-default': !item.useStatus }"
:disabled="item.useStatus == 1" @click="useVoice(item.id)">
{{ item.useStatus == 1 ? '使用中' : '使用' }}
</el-button>
</div>
</div>
</div>
</div>
</el-dialog>
<!-- 充电提示框 -->
<el-dialog title="充电提示" v-model="centerDialogVisible" width="15%">
<div style="display: flex; align-items: center;">
<h3 style="color: rgba(224, 52, 52, 1)">设备电量低于20%</h3>
</div>
<div>请及时充电</div>
<span slot="footer" class="dialog-footer" style="text-align: right;display: block;">
<el-button type="primary" @click="centerDialogVisible = false"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script setup name="DeviceControl" lang="ts">
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';
// 路由和实例
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 导入图片资源
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);
const centerDialogVisible = ref(false);
const currentVoiceId = ref(''); // 当前选中的语音ID
const voiceList = ref<any[]>([]); // 语音列表
// ====================== 录制语音 ======================
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 recorderIns = ref<any>(null); // Recorder 实例
let recordTimer: NodeJS.Timeout | null = null;
const recordedBlob = ref()
const formatTime = (seconds: number) => {
const min = String(Math.floor(seconds / 60)).padStart(2, '0');
const sec = String(seconds % 60).padStart(2, '0');
return `${min}:${sec}`;
};
// ------------- 开始录制语音上传 -------------
const startRecordVoice = async () => {
try {
// 1. 重置状态
resetRecord();
recorderIns.value = Recorder({
type: 'mp3', // 直接录制为 MP3
sampleRate: 44100, // 标准采样率
bitRate: 128, // 128kbps微信兼容
onProcess: (buffers: any, power: number, bufferDuration: number, bufferSampleRate: number) => {
}
});
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('录制异常,请重试');
resetRecord();
}
};
// ------------- 停止录制 -------------
const stopRecordVoice = () => {
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;
if (recordTimer) {
clearInterval(recordTimer);
recordTimer = null;
}
// 释放 Recorder 资源
if (recorderIns.value) {
recorderIns.value.close();
recorderIns.value = null;
}
};
// ------------- 保存/上传标准 MP3 文件 -------------
const saveRecordVoice = () => {
// 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('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 = () => {
recordVoiceDialog.value = true;
resetRecord();
};
// ====================== 上传语音 ======================
const uploadVoiceDialog = ref(false);
const isDragOver = ref(false);
const uploadFile = ref<File | null>(null);
const uploadFileInput = ref<HTMLInputElement | null>(null);
const triggerFileInput = () => uploadFileInput.value?.click();
const handleDrop = (e: DragEvent) => {
isDragOver.value = false;
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const file = files[0];
if (file.type.startsWith('audio/')) uploadFile.value = file;
else proxy?.$modal.msgError('请上传音频文件');
}
};
const handleFileUpload = (e: Event) => {
const target = e.target as HTMLInputElement;
const files = target.files;
if (files && files.length > 0) {
const file = files[0];
if (file.type.startsWith('audio/')) uploadFile.value = file;
else proxy?.$modal.msgError('请上传音频文件');
}
target.value = '';
};
const confirmUploadVoice = () => {
if (!uploadFile.value) return;
const formData = new FormData();
formData.append('file', uploadFile.value);
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('语音上传成功');
uploadVoiceDialog.value = false;
uploadFile.value = null;
queryAudioFileInfo();
} else {
proxy?.$modal.msgError('上传失败:' + res.msg);
}
}).catch(err => {
proxy?.$modal.closeLoading();
proxy?.$modal.msgError('上传失败');
});
};
const handleUploadVoice = () => {
uploadVoiceDialog.value = true;
uploadFile.value = null;
};
// ====================== 文字转语音 ======================
const textToVoiceDialog = ref(false);
const textToVoiceForm = ref({ content: '' });
const ttsLoading = ref(false);
const ttsResultUrl = ref('');
const ttsPlaying = ref(false);
const ttsCurrentTime = ref(0);
const ttsDuration = ref(0);
let ttsAudio: HTMLAudioElement | null = null;
const convertTextToVoice = async () => {
if (!textToVoiceForm.value.content.trim()) {
proxy?.$modal.msgWarning('请输入要转换的文字内容');
return;
}
ttsLoading.value = true;
try {
const res = await api.videTtsToOss({
deviceId: route.params.deviceId,
text: textToVoiceForm.value.content
});
if (res.code === 200) {
ttsResultUrl.value = res.data;
ttsDuration.value = res.data;
proxy?.$modal.msgSuccess('文字转语音成功');
textToVoiceDialog.value = false
} else {
proxy?.$modal.msgError(res.msg);
}
} catch (err) {
} finally {
ttsLoading.value = false;
}
};
const toggleTtsPlay = () => {
if (!ttsResultUrl.value) return;
if (!ttsAudio) {
ttsAudio = new Audio(ttsResultUrl.value);
ttsAudio.ontimeupdate = () => ttsCurrentTime.value = Math.floor(ttsAudio!.currentTime);
ttsAudio.onended = () => {
ttsPlaying.value = false;
ttsCurrentTime.value = 0;
};
}
if (ttsPlaying.value) {
// ttsAudio.pause();
ttsPlaying.value = false;
} else {
ttsAudio.play();
ttsPlaying.value = true;
}
};
const saveTtsResult = () => {
proxy?.$modal.msgSuccess('语音已保存');
queryAudioFileInfo();
textToVoiceDialog.value = false;
};
const useTtsResult = () => {
proxy?.$modal.msgSuccess('语音已设置为当前使用');
textToVoiceDialog.value = false;
};
// 上传语音
const handleTextToVoice = () => {
textToVoiceDialog.value = true;
textToVoiceForm.value = { content: '' };
ttsResultUrl.value = '';
ttsPlaying.value = false;
ttsCurrentTime.value = 0;
};
// ====================== 所有语音 ======================
const allVoiceDialog = ref(false);
const playingVoiceId = ref('');
const voicePlaying = ref(false);
let voiceAudio: HTMLAudioElement | null = null;
// 播放弹框
const toggleVoicePlay = async (voiceId: string) => {
// 如果当前点击的是正在播放的语音,直接暂停
if (playingVoiceId.value === voiceId && voicePlaying.value) {
voiceAudio?.pause();
voicePlaying.value = false;
return;
}
// 如果有其他语音正在播放,先暂停
if (voicePlaying.value && voiceAudio) {
voiceAudio.pause();
voicePlaying.value = false;
}
// 播放当前选中的语音
const voice = voiceList.value.find(v => v.id === voiceId);
if (!voice || !voice.fileUrl) {
proxy?.$modal.msgWarning('语音文件不存在');
return;
}
try {
playingVoiceId.value = voiceId;
voiceAudio = new Audio(voice.fileUrl);
// 仅保留播放结束后的状态重置
voiceAudio.onended = () => {
voicePlaying.value = false;
playingVoiceId.value = '';
};
await voiceAudio.play();
voicePlaying.value = true;
} catch (err: any) {
proxy?.$modal.msgError(err.msg);
playingVoiceId.value = '';
voicePlaying.value = false;
}
};
// 删除语音
const deleteVoiceById = async (fileId: string) => {
try {
await ElMessageBox.confirm('确定要删除该语音?', '提示');
const res = await api.deviceDeleteAudioFile({ deviceId: route.params.deviceId, fileId });
if (res.code === 200) {
proxy?.$modal.msgSuccess(res.msg);
queryAudioFileInfo();
} else {
proxy?.$modal.msgError(res.msg);
}
} catch (err) { }
};
// 重命名
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 });
if (res.code === 200) {
proxy?.$modal.msgSuccess(res.msg);
queryAudioFileInfo();
} else {
proxy?.$modal.msgError(res.msg);
}
}
};
// 使用
const useVoice = async (voiceId: string) => {
const res = await api.deviceUpdateVoice({ id: voiceId });
if (res.code === 200) {
proxy?.$modal.msgSuccess(res.msg);
currentVoiceId.value = voiceId;
await queryAudioFileInfo()
} else {
proxy?.$modal.msgError('设置失败');
}
};
const handleAllVoice = () => {
allVoiceDialog.value = true;
queryAudioFileInfo();
};
// ====================== 原有功能 ======================
const sta_VoiceType = ref([
{ id: '0', name: '公安', icon: sg, activeIcon: sgAc, switchStatusVioice: true, active: true },
{ id: '1', name: '消防', icon: sg, activeIcon: sgAc, switchStatusVioice: false, active: false },
{ id: '2', name: '应急', icon: sg, activeIcon: sgAc, switchStatusVioice: false, active: false },
{ id: '3', name: '交警', icon: sg, activeIcon: sgAc, switchStatusVioice: false, active: false },
{ id: '4', name: '市政', icon: sg, activeIcon: sgAc, switchStatusVioice: false, active: false },
{ id: '5', name: '铁路', icon: sg, activeIcon: sgAc, switchStatusVioice: false, active: false },
{ id: '6', name: '医疗', icon: sg, activeIcon: sgAc, switchStatusVioice: false, active: false },
{ id: '7', name: '部队', icon: sg, activeIcon: sgAc, switchStatusVioice: false, active: false },
{ id: '8', name: '水利', icon: sg, activeIcon: sgAc, switchStatusVioice: false, active: false },
]);
const lightModes = ref<LightMode[]>([
{ id: 'redBlueAlternate', name: '红蓝交替', icon: rb, activeIcon: rbAc, switchStatus: true, instructValue: '6', active: true },
{ id: 'redFlash', name: '红色', icon: rb, activeIcon: rbAc, switchStatus: false, instructValue: '0', active: false },
{ id: 'yellowFlash', name: '黄色', icon: rb, activeIcon: rbAc, switchStatus: false, instructValue: '2', active: false },
{ id: 'redClockwise', name: '红色顺时针', icon: rb, activeIcon: rbAc, switchStatus: false, instructValue: '3', active: false },
{ id: 'yellowClockwise', name: '黄色顺时针', icon: rb, activeIcon: rbAc, switchStatus: false, instructValue: '4', active: false },
{ id: 'redBlueClockwise', name: '红蓝顺时针', icon: rb, activeIcon: rbAc, switchStatus: false, instructValue: '5', active: false },
]);
const deviceDetail = ref<DeviceDetail & { typeName: string }>({
lightBrightness: '20',
deviceName: '',
deviceImei: '',
onlineStatus: 1,
batteryPercentage: 80,
batteryRemainingTime: '',
longitude: '',
latitude: '',
address: '',
sendMsg: '',
chargeState: '0',
typeName: '',
alarmStatus: 0,
strobeFrequency: '10',
volume: '50',
voiceStrobeAlarm: 0,
voiceBroadcast: 0
});
const isUpdatingStatus = ref(false);
const isSyncingStatus = ref(false);
const queryAudioFileInfo = () => {
api.queryAudioFileList({ deviceId: route.params.deviceId }).then(res => {
if (res.code == 200) {
voiceList.value = res.data || [];
const activeVoice = voiceList.value.find(item => item.useStatus === 1);
if (activeVoice) {
currentVoiceId.value = activeVoice.id;
}
}
}).catch(err => proxy?.$modal.msgError('获取语音列表失败'));
};
// 报警模式
const handleVoiceType = async (targetId: string) => {
const deviceId = route.params.deviceId as string;
if (!deviceId) return;
const targetMode = sta_VoiceType.value.find(mode => mode.id === targetId);
if (!targetMode) return;
//切换选中状态
const isCurrentlyActive = targetMode.active;
sta_VoiceType.value.forEach(mode => {
mode.active = false;
mode.switchStatusVioice = false;
});
// 如果当前是未选中状态,则选中;如果是选中状态,则取消
if (!isCurrentlyActive) {
targetMode.active = true;
targetMode.switchStatusVioice = true;
}
// 只有在报警中时,才发送请求
if (deviceDetail.value.voiceStrobeAlarm == 1) {
const params = {
deviceIds: [deviceId],
voiceStrobeAlarm: 1,
mode: isCurrentlyActive ? '' : targetId
};
try {
const res = await api.SosSetting(params);
if (res.code === 200) proxy?.$modal.msgSuccess(res.msg);
} catch (error) {
await getList();
}
}
};
// 警示灯爆闪模
const handleModeClick = async (modeId: string) => {
if (isUpdatingStatus.value || isSyncingStatus.value) return;
try {
const deviceId = route.params.deviceId as string;
if (!deviceId) return;
const targetMode = lightModes.value.find(m => m.id === modeId);
if (!targetMode || !targetMode.instructValue) return;
isUpdatingStatus.value = true;
const isCurrentlyActive = targetMode.active;
const enable = isCurrentlyActive ? 0 : 1; // 选中则关闭,未选中则开启
//关闭时传 enable:0开启时传 enable:1
const res = await api.strobeMode({
deviceId,
mode: targetMode.instructValue,
enable
});
if (res.code === 200) {
ElMessage.closeAll();
proxy?.$modal.msgSuccess(res.msg);
if (enable === 0) {
lightModes.value.forEach(mode => {
mode.active = false;
mode.switchStatus = false;
});
} else {
setActiveLightMode(modeId);
}
} else {
proxy?.$modal.msgError(res.msg);
const prevActiveMode = lightModes.value.find(m => m.active);
if (prevActiveMode) setActiveLightMode(prevActiveMode.id);
}
} catch (error) {
const prevActiveMode = lightModes.value.find(m => m.active);
if (prevActiveMode) setActiveLightMode(prevActiveMode.id);
} finally {
isUpdatingStatus.value = false;
}
};
// 全局性的方法
const setActiveLightMode = (targetModeId: string) => {
isSyncingStatus.value = true;
lightModes.value.forEach(mode => {
mode.active = mode.id === targetModeId;
mode.switchStatus = mode.active;
});
isSyncingStatus.value = false;
};
// 列表详情
const getList = async () => {
try {
const deviceId = route.params.deviceId;
if (!deviceId) return;
const res = await api.deviceDeatil(deviceId as string);
deviceDetail.value = res.data;
// 警示灯模式初始化
const mainLightMode = String(res.data.strobeMode);
const matchedMode = lightModes.value.find(mode => mode.instructValue === mainLightMode);
if (matchedMode) setActiveLightMode(matchedMode.id);
// 报警模式初始化
const alarmMode = String(res.data.alarmMode);
const voiceStrobeAlarm = String(res.data.voiceStrobeAlarm);
sta_VoiceType.value.forEach(mode => {
mode.active = false;
mode.switchStatusVioice = false;
});
//如果接口返回无选中,默认选中第一个
let hasActiveMode = false;
if (voiceStrobeAlarm === '1') {
const matchedVoiceMode = sta_VoiceType.value.find(mode => mode.id === alarmMode);
if (matchedVoiceMode) {
matchedVoiceMode.active = true;
matchedVoiceMode.switchStatusVioice = true;
hasActiveMode = true;
}
}
// 无选中时默认选中第一个
if (!hasActiveMode) {
sta_VoiceType.value[0].active = true;
sta_VoiceType.value[0].switchStatusVioice = true;
}
} catch (error) { console.log('获取设备详情失败:', error); }
};
// 灯光亮度
const saveBtnlight = () => {
lightModesLoading.value = true;
const data = { deviceId: route.params.deviceId, brightness: deviceDetail.value.lightBrightness };
api.lightModeSettings(data).then(res => {
if (res.code === 200) proxy?.$modal.msgSuccess(res.msg);
else proxy?.$modal.msgError(res.msg);
lightModesLoading.value = false;
}).catch(() => lightModesLoading.value = false);
};
// 爆闪频率
const saveBtnstrobe = () => {
const data = { deviceId: route.params.deviceId, frequency: deviceDetail.value.strobeFrequency };
api.staticPowerSetting(data).then(res => {
if (res.code === 200) proxy?.$modal.msgSuccess(res.msg);
}).catch(err => proxy?.$modal.msgError('保存爆闪频率失败'));
};
// 音 量
const saveBtnVolume = () => {
const data = { deviceId: route.params.deviceId, volume: deviceDetail.value.volume };
api.settingUpdateVolume(data).then(res => {
if (res.code === 200) proxy?.$modal.msgSuccess(res.msg);
}).catch(err => proxy?.$modal.msgError('保存音量失败'));
};
// 解除报警
const showClose = async () => {
try {
await proxy?.$modal.confirm('确定要对该设备解除报警?', '提示');
// 获取当前选中的mode
const currentMode = sta_VoiceType.value.find(mode => mode.active)?.id || '0';
const data = {
deviceIds: [route.params.deviceId],
voiceStrobeAlarm: 0,
mode: currentMode
};
const res = await api.SosSetting(data);
if (res.code === 200) {
deviceDetail.value.voiceStrobeAlarm = 0;
proxy?.$modal.msgSuccess(res.msg);
//await getList();
}
} catch (error: any) {
proxy?.$modal.msgError(error.msg);
}
};
// 强制报警
const forceAlarm = async () => {
try {
await proxy?.$modal.confirm('确定要对该设备开启强制报警?', '提示');
forceAlarmLoading.value = true;
// 获取当前选中的mode
const currentMode = sta_VoiceType.value.find(mode => mode.active)?.id || '0';
const data = {
deviceIds: [route.params.deviceId],
voiceStrobeAlarm: 1,
mode: currentMode
};
const res = await api.SosSetting(data);
if (res.code === 200) {
deviceDetail.value.voiceStrobeAlarm = 1;
proxy?.$modal.msgSuccess(res.msg);
// await getList();
}
} catch (error: any) {
proxy?.$modal.msgError(error.msg);
} finally {
forceAlarmLoading.value = false;
}
};
// 查看跳转到地图设备
const lookMap = (row: any) => {
router.push({ path: '/controlCenter/controlPanel', query: { view: 'map', deviceId: row.deviceId } });
};
// 播放如果,在强制报警中,点击播放的时候描会报警语音,如果没有报警中,就是的单纯的播放语音这个逻辑
const playCurrentVoice = async () => {
// 1. 报警中场景:播放/切换报警语音
if (deviceDetail.value.voiceStrobeAlarm === 1) {
try {
const currentMode = sta_VoiceType.value.find(mode => mode.active)?.id || '0';
const data = {
deviceIds: [route.params.deviceId],
voiceStrobeAlarm: 1,
mode: currentMode
};
const res = await api.SosSetting(data);
if (res.code === 200) {
proxy?.$modal.msgSuccess(res.msg);
await getList();
} else {
proxy?.$modal.msgError(res.msg);
}
} catch (err: any) {
// proxy?.$modal.msgError(err.msg);
}
}
// 2. 非报警中场景:单纯播放/暂停语音
else {
const targetStatus = deviceDetail.value.voiceBroadcast === 1 ? 0 : 1;
try {
const res = await api.deviceVoiceBroadcast({
deviceId: route.params.deviceId,
voiceBroadcast: targetStatus
});
if (res.code === 200) {
deviceDetail.value.voiceBroadcast = targetStatus;
proxy?.$modal.msgSuccess(res.msg);
await getList();
} else {
proxy?.$modal.msgError(res.msg);
}
} catch (err: any) {
// proxy?.$modal.msgError(err.msg);
}
}
};
onMounted(async () => {
await getList();
queryAudioFileInfo();
});
onUnmounted(() => {
disconnect();
if (recordTimer) clearInterval(recordTimer);
if (mediaRecorder.value && isRecording.value) mediaRecorder.value.stop();
if (audioStream) audioStream.getTracks().forEach(track => track.stop());
// if (ttsAudio) ttsAudio.pause();
if (voiceAudio) voiceAudio.pause();
});
</script>
<style lang="scss" scoped>
.p-2 {
background: rgba(247, 248, 252, 1);
min-height: 100vh;
box-sizing: border-box;
padding: 15px;
}
.device-page {
.header-bar {
border-radius: 8px;
background: linear-gradient(135deg, #3400e7, #009bff);
color: white;
padding: 20px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
.device-status {
.online {
color: #00ff00;
}
.offline {
color: rgb(224, 52, 52);
}
}
}
.content-wrapper {
.content-row {
margin-bottom: 20px;
}
.content-card {
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1);
background: white;
padding: 1px 20px;
border: 1px solid #ebeef5;
min-height: 700px;
}
.content-card1 {
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1);
background: white;
padding: 1px 20px;
border: 1px solid #ebeef5;
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 {
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1);
background: white;
padding: 20px;
border: 1px solid #ebeef5;
min-height: 240px;
}
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
color: #303133;
}
.light-mode {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.lacatin_gps {
height: 70px;
background: #F7F8FC;
border-radius: 4px 4px 4px 4px;
width: 100%;
padding: 10px;
word-break: break-all;
margin-top: 15px;
}
.mode_2 {
padding: 15px;
border: 1px solid #dcdfe6;
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;
align-items: center;
border-radius: 8px;
width: 125px;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 20px;
&.active {
border-color: #409eff;
background-color: rgba(64, 158, 255, 0.05);
}
.mode-icon {
width: 48px;
margin-bottom: 10px;
transition: all 0.3s ease;
object-fit: scale-down;
height: 50px;
}
.mode-name {
font-size: 14px;
margin-bottom: 12px;
color: #606266;
}
.el-switch {
--el-switch-on-color: #409eff;
--el-switch-off-color: #dcdfe6;
}
}
.alarm-btn-wrapper {
display: flex;
gap: 10px;
margin-bottom: 10px;
height: 100px;
}
.brightness-alarm {
align-items: center;
margin-bottom: 10px;
.brightness-control {
display: flex;
align-items: center;
gap: 8px;
border-radius: 39px;
background: rgba(255, 255, 255, 1);
width: 100%;
padding: 8px 15px;
margin-top: 10px;
.brightness-label {
font-size: 14px;
color: #606266;
min-width: 75px;
}
.brightness-value {
font-size: 14px;
color: #409eff;
font-weight: 500;
}
.save-btn {
padding: 6px 20px;
border-radius: 29px;
// background: rgba(2, 124, 251, 1);
border: none;
}
.inputTFT {
flex: 1;
min-width: 80px;
height: 40px;
}
}
.alarm-btn {
background-color: rgba(224, 52, 52, 1);
border-color: rgba(224, 52, 52, 1);
padding: 20px 20px;
border-radius: 30px;
&.cancel {
background-color: rgba(224, 52, 52, 0.08);
border-color: rgba(224, 52, 52, 0.08);
color: rgba(224, 52, 52, 1);
}
}
}
.location-info {
.location-item1 {
display: flex;
justify-content: space-between;
color: #38404F;
}
.location-item {
display: flex;
align-items: center;
margin-bottom: 15px;
font-size: 14px;
color: #606266;
.location-icon {
margin-right: 8px;
font-weight: bold;
color: #409eff;
}
.view-btn {
margin: 0 8px;
padding: 0;
font-size: 13px;
}
}
}
.voice-play-section {
.current-voice {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
.voice-label {
font-size: 14px;
color: #606266;
min-width: 80px;
}
.voice-select {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 4px;
cursor: pointer;
}
.play-btn {
padding: 8px 20px;
border-radius: 20px;
}
}
.voice-manage-section {
.voice-label {
font-size: 14px;
color: #606266;
display: block;
margin-bottom: 15px;
}
.voice-manage-btns {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
.voice-btn-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
// border: 1px solid #dcdfe6;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #409eff;
background-color: rgba(64, 158, 255, 0.05);
}
.el-icon {
font-size: 24px;
margin-bottom: 5px;
color: #027CFB;
border: 1px solid rgba(2, 124, 251, 0.2);
border-radius: 6px 6px 6px 6px;
width: 37px;
height: 37px;
}
span {
font-size: 14px;
color: #027CFB;
margin-top: 5px;
}
}
}
}
}
}
// 弹窗通用样式
.voice-dialog {
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
:deep(.el-dialog__header) {
padding: 16px 20px;
border-bottom: 1px solid #ebeef5;
margin: 0;
}
:deep(.el-dialog__headerbtn) {
top: 16px;
right: 20px;
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__footer) {
padding: 16px 20px;
border-top: 1px solid #ebeef5;
}
}
// 录制语音弹窗样式
.record-content {
text-align: center;
.waveform-container {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
.waveform {
display: flex;
gap: 3px;
align-items: center;
height: 100%;
.wave-bar {
width: 3px;
background: #409eff;
border-radius: 2px;
transition: height 0.2s;
}
}
.waveform-placeholder {
width: 100%;
height: 2px;
background: #dcdfe6;
}
}
.record-control {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-bottom: 20px;
.mic-icon {
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(64, 158, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: #409eff;
transition: all 0.3s;
&.recording {
animation: pulse 1s infinite;
}
}
.record-time {
font-size: 18px;
font-weight: 500;
color: #303133;
}
}
.record-footer {
display: flex;
justify-content: space-between;
align-items: center;
.record-confirm {
padding: 8px 30px;
border-radius: 20px;
}
}
}
// 上传语音弹窗样式
.upload-content {
.upload-area {
border: 2px dashed #dcdfe6;
border-radius: 8px;
padding: 40px 20px;
text-align: center;
transition: all 0.3s;
cursor: pointer;
&.dragOver {
border-color: #409eff;
background: rgba(64, 158, 255, 0.05);
}
.upload-icon {
font-size: 48px;
color: #409eff;
margin-bottom: 15px;
}
.select-file-btn {
margin-bottom: 10px;
}
.upload-tip {
font-size: 12px;
color: #909399;
}
.upload-file-info {
margin-top: 15px;
padding: 10px;
background: #f5f7fa;
border-radius: 4px;
text-align: left;
.file-name {
font-weight: 500;
color: #303133;
}
.file-size {
font-size: 12px;
color: #909399;
margin-left: 10px;
}
}
}
}
// 文字转语音弹窗样式
.tts-content {
.tts-textarea {
margin-bottom: 15px;
}
.tts-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.tts-preview {
.waveform-container {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
.waveform {
display: flex;
gap: 3px;
align-items: center;
height: 100%;
.wave-bar {
width: 3px;
background: #409eff;
border-radius: 2px;
}
}
}
.audio-player {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
.time-info {
display: flex;
gap: 10px;
font-size: 12px;
color: #909399;
}
}
.player-controls {
display: flex;
justify-content: center;
margin-bottom: 15px;
}
.preview-actions {
display: flex;
justify-content: flex-end;
gap: 20px;
}
}
}
// 所有语音弹窗样式
.all-voice-content {
.voice-list {
max-height: 400px;
overflow-y: auto;
.voice-item {
padding: 15px 0;
border-bottom: 1px solid #ebeef5;
&:last-child {
border-bottom: none;
}
.voice-info {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
.voice-name {
font-weight: 500;
color: #303133;
}
}
.voice-player {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
.time-info {
display: flex;
gap: 10px;
font-size: 12px;
color: #909399;
}
}
.voice-actions {
display: flex;
justify-content: flex-end;
gap: 15px;
}
}
}
}
}
.staticRwo {
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1);
background: white;
border: 1px solid #ebeef5;
height: auto;
line-height: 36px;
box-sizing: border-box;
text-indent: 15px;
color: #ff0000;
font-weight: bold;
font-size: 17px;
margin-bottom: 5px;
}
.displayNone {
display: none !important;
}
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(64, 158, 255, 0.7);
}
70% {
transform: scale(1.05);
box-shadow: 0 0 0 10px rgba(64, 158, 255, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(64, 158, 255, 0);
}
}
.active {
color: #3333;
}
</style>