1
0
forked from dyf/dyf-vue-ui
Files
dyf-vue-ui/src/views/controlCenter/100J/index.vue

1628 lines
57 KiB
Vue
Raw Normal View History

2026-03-10 18:03:33 +08:00
<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">
2026-03-18 16:12:29 +08:00
<el-col :lg="9" :xs="24">
2026-03-10 18:03:33 +08:00
<div class="content-card">
<h4 class="section-title">报警模式</h4>
<div class="light-mode">
<!-- 使用v-for循环渲染灯光模式卡片 -->
2026-03-19 11:42:26 +08:00
<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 }">
2026-03-17 18:39:40 +08:00
<img :src="mode.active ? mode.activeIcon : mode.icon" :alt="mode.name"
class="mode-icon" />
<div class="mode-name">{{ mode.name }}</div>
</div>
2026-03-10 18:03:33 +08:00
<el-switch v-model="mode.switchStatusVioice" />
</div>
</div>
</div>
</el-col>
2026-03-18 16:12:29 +08:00
<el-col :lg="9" :xs="24">
2026-03-17 18:39:40 +08:00
<div class="content-card1">
<h4 class="section-title">警示灯爆闪</h4>
<div class="light-mode">
<!-- 使用v-for循环渲染灯光模式卡片 -->
2026-03-19 11:42:26 +08:00
<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 }">
2026-03-17 18:39:40 +08:00
<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>
2026-03-19 11:42:26 +08:00
<div class="content-card2" style="margin-top: 8px;">
2026-03-17 18:39:40 +08:00
<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"
2026-03-18 16:12:29 +08:00
:value="item.id" ite>
2026-03-17 18:39:40 +08:00
</el-option>
</el-select>
</div>
2026-03-18 16:12:29 +08:00
<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>
2026-03-17 18:39:40 +08:00
</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>
2026-03-19 11:42:26 +08:00
<Microphone />
2026-03-17 18:39:40 +08:00
</el-icon>
<span>上传语音</span>
</div>
<div class="voice-btn-item" @click="handleTextToVoice">
<el-icon>
2026-03-19 11:42:26 +08:00
<Microphone />
2026-03-17 18:39:40 +08:00
</el-icon>
<span>文字转语音</span>
</div>
<div class="voice-btn-item" @click="handleAllVoice">
<el-icon>
2026-03-19 11:42:26 +08:00
<Microphone />
2026-03-17 18:39:40 +08:00
</el-icon>
<span>所有语音</span>
</div>
</div>
</div>
</div>
</div>
</el-col>
2026-03-18 16:12:29 +08:00
<el-col :lg="6" :xs="24">
2026-03-17 18:39:40 +08:00
<div class="brightness-alarm alarm-btn-wrapper">
2026-03-10 18:03:33 +08:00
<el-button type="danger" class="alarm-btn" @click="forceAlarm" :loading="forceAlarmLoading"
2026-03-18 16:12:29 +08:00
:loading-text="forceAlarmLoading ? '报警中...' : '强制报警'"> {{ deviceDetail.voiceStrobeAlarm ===
1 ? '报警中' : '强制报警' }}</el-button>
<el-button type="default" class="alarm-btn cancel" @click="showClose">解除报警</el-button>
2026-03-10 18:03:33 +08:00
</div>
<div class="content-card_gps">
<h4 class="section-title">位置信息</h4>
<div class="location-info">
<div class="location-item">
2026-03-17 18:39:40 +08:00
<span class="location-icon">📍</span>
2026-03-19 11:42:26 +08:00
<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>
2026-03-10 18:03:33 +08:00
</div>
2026-03-19 11:42:26 +08:00
<div class="location-item1">
<div>地址</div>
2026-03-10 18:03:33 +08:00
<el-button link type="primary" class="view-btn"
@click="lookMap(deviceDetail)">查看</el-button>
</div>
2026-03-19 11:42:26 +08:00
<div class="lacatin_gps">{{ deviceDetail.address || "未获取到地址" }}</div>
2026-03-10 18:03:33 +08:00
</div>
</div>
2026-03-19 11:42:26 +08:00
<div class="content-card_gps" style="margin-top: 20px;">
2026-03-17 18:39:40 +08:00
<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>
2026-03-19 11:42:26 +08:00
<el-button type="primary" link class="save-btn" v-loading="lightModesLoading"
2026-03-17 18:39:40 +08:00
@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>
2026-03-19 11:42:26 +08:00
<el-button type="primary" link class="save-btn" @click="saveBtnVolume">保存
2026-03-17 18:39:40 +08:00
</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>
2026-03-19 11:42:26 +08:00
<el-button type="primary" link class="save-btn" @click="saveBtnstrobe">保存
2026-03-17 18:39:40 +08:00
</el-button>
</div>
</div>
</div>
</div>
2026-03-10 18:03:33 +08:00
</el-col>
</el-row>
2026-03-17 18:39:40 +08:00
</div>
2026-03-10 18:03:33 +08:00
2026-03-17 18:39:40 +08:00
<!-- 录制语音弹窗对齐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">
2026-03-18 16:12:29 +08:00
<span v-for="i in 30" :key="i" class="wave-bar"
:style="{ height: `${Math.random() * 40 + 10}px` }"></span>
2026-03-17 18:39:40 +08:00
</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 }">
2026-03-18 16:12:29 +08:00
<el-icon>
<Microphone />
</el-icon>
2026-03-17 18:39:40 +08:00
</div>
<div class="record-time">{{ formatTime(recordDuration) }}</div>
<div class="record-play-btn">
<el-button v-if="isRecording" type="danger" circle @click="stopRecordVoice">
2026-03-18 16:12:29 +08:00
<el-icon>
<!-- <Pause /> -->
</el-icon>
2026-03-17 18:39:40 +08:00
</el-button>
<el-button v-else type="primary" circle @click="startRecordVoice">
2026-03-18 16:12:29 +08:00
<el-icon>
<VideoPlay />
</el-icon>
2026-03-17 18:39:40 +08:00
</el-button>
</div>
</div>
<!-- 底部操作 -->
<div class="record-footer">
<el-button link type="primary" @click="resetRecord" :disabled="isRecording">重新录制</el-button>
2026-03-18 16:12:29 +08:00
<el-button type="primary" class="record-confirm" @click="saveRecordVoice"
:disabled="!recordedBlob">完成</el-button>
2026-03-17 18:39:40 +08:00
</div>
</div>
</el-dialog>
<!-- 上传语音弹窗 -->
<el-dialog title="上传语音" v-model="uploadVoiceDialog" width="480px" class="voice-dialog" :show-close="true">
<div class="upload-content">
2026-03-18 16:12:29 +08:00
<div class="upload-area" :class="{ dragOver: isDragOver }" @dragover.prevent="isDragOver = true"
@dragleave.prevent="isDragOver = false" @drop.prevent="handleDrop" @click="triggerFileInput">
2026-03-17 18:39:40 +08:00
<div class="upload-icon">
2026-03-18 16:12:29 +08:00
<el-icon>
<Document />
</el-icon>
2026-03-17 18:39:40 +08:00
</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>
2026-03-18 16:12:29 +08:00
<input ref="uploadFileInput" type="file" accept="audio/*" style="display: none"
@change="handleFileUpload" />
2026-03-17 18:39:40 +08:00
</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">
2026-03-18 16:12:29 +08:00
<el-input v-model="textToVoiceForm.content" type="textarea" rows="4" placeholder="请输入要转换的文字内容"
class="tts-textarea"></el-input>
2026-03-17 18:39:40 +08:00
<div class="tts-actions">
2026-03-18 16:12:29 +08:00
<!-- <el-button link type="primary" @click="handleUploadText">上传文本</el-button> -->
2026-03-17 18:39:40 +08:00
<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">
2026-03-18 16:12:29 +08:00
<span v-for="i in 30" :key="i" class="wave-bar"
:style="{ height: `${Math.random() * 40 + 10}px` }"></span>
2026-03-10 18:03:33 +08:00
</div>
</div>
2026-03-17 18:39:40 +08:00
<div class="audio-player">
2026-03-18 16:12:29 +08:00
<el-slider v-model="ttsCurrentTime" :max="ttsDuration" :show-tooltip="false"
style="flex: 1;"></el-slider>
2026-03-17 18:39:40 +08:00
<div class="time-info">
<span>{{ formatTime(ttsCurrentTime) }}</span>
<span>{{ formatTime(ttsDuration) }}</span>
2026-03-10 18:03:33 +08:00
</div>
2026-03-17 18:39:40 +08:00
</div>
<div class="player-controls">
<el-button circle @click="toggleTtsPlay">
2026-03-18 16:12:29 +08:00
<el-icon>
<VideoPlay v-if="!ttsPlaying" />
<!-- <Pause v-else /> -->
</el-icon>
2026-03-17 18:39:40 +08:00
</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">
2026-03-18 16:12:29 +08:00
<el-icon>
<Document />
</el-icon>
2026-03-17 18:39:40 +08:00
<span class="voice-name">{{ item.fileNameExt }}</span>
2026-03-18 16:12:29 +08:00
<span>{{ item.duration }}</span>
2026-03-10 18:03:33 +08:00
</div>
2026-03-18 16:12:29 +08:00
<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 ? '使用中' : '使用' }}
2026-03-17 18:39:40 +08:00
</el-button>
2026-03-10 18:03:33 +08:00
</div>
</div>
2026-03-17 18:39:40 +08:00
</div>
</div>
</el-dialog>
<!-- 充电提示框 -->
2026-03-10 18:03:33 +08:00
<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>
2026-03-17 18:39:40 +08:00
2026-03-10 18:03:33 +08:00
<script setup name="DeviceControl" lang="ts">
2026-03-17 18:39:40 +08:00
import { useRoute, useRouter } from 'vue-router';
2026-03-10 18:03:33 +08:00
import { useMqtt } from '@/utils/mqtt';
2026-03-17 18:39:40 +08:00
import api from '@/api/controlCenter/controlPanel/100J';
2026-03-10 18:03:33 +08:00
import { DeviceDetail, LightMode } from '@/api/controlCenter/controlPanel/types';
2026-03-17 18:39:40 +08:00
// 路由和实例
const route = useRoute();
2026-03-10 18:03:33 +08:00
const router = useRouter();
2026-03-17 18:39:40 +08:00
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 导入图片资源
2026-03-19 11:42:26 +08:00
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';
2026-03-17 18:39:40 +08:00
// 基础状态
const forceAlarmLoading = ref(false);
const lightModesLoading = ref(false);
const centerDialogVisible = ref(false);
const currentVoiceId = ref(''); // 当前选中的语音ID
const voiceList = ref<any[]>([]); // 语音列表
// ====================== 录制语音 ======================
2026-03-19 11:42:26 +08:00
import Recorder from 'recorder-core';
// 引入 MP3 编码器(核心:生成标准 MP3
import 'recorder-core/src/engine/mp3';
import 'recorder-core/src/engine/mp3-engine';
2026-03-17 18:39:40 +08:00
const recordVoiceDialog = ref(false);
const isRecording = ref(false);
const recordDuration = ref(0);
2026-03-19 11:42:26 +08:00
const recorderIns = ref<any>(null); // Recorder 实例
2026-03-17 18:39:40 +08:00
let recordTimer: NodeJS.Timeout | null = null;
2026-03-19 11:42:26 +08:00
const recordedBlob = ref()
2026-03-17 18:39:40 +08:00
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}`;
};
2026-03-19 11:42:26 +08:00
// ------------- 开始录制语音上传 -------------
2026-03-17 18:39:40 +08:00
const startRecordVoice = async () => {
try {
2026-03-19 11:42:26 +08:00
// 1. 重置状态
resetRecord();
recorderIns.value = Recorder({
type: 'mp3', // 直接录制为 MP3
sampleRate: 44100, // 标准采样率
bitRate: 128, // 128kbps微信兼容
onProcess: (buffers: any, power: number, bufferDuration: number, bufferSampleRate: number) => {
2026-03-17 18:39:40 +08:00
}
2026-03-19 11:42:26 +08:00
});
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();
});
2026-03-17 18:39:40 +08:00
} catch (err) {
2026-03-19 11:42:26 +08:00
proxy?.$modal.msgError('录制异常,请重试');
resetRecord();
2026-03-17 18:39:40 +08:00
}
};
2026-03-19 11:42:26 +08:00
// ------------- 停止录制 -------------
2026-03-17 18:39:40 +08:00
const stopRecordVoice = () => {
2026-03-19 11:42:26 +08:00
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();
2026-03-17 18:39:40 +08:00
}
};
2026-03-19 11:42:26 +08:00
// ------------- 重置录制状态 -------------
2026-03-17 18:39:40 +08:00
const resetRecord = () => {
isRecording.value = false;
recordDuration.value = 0;
recordedBlob.value = null;
if (recordTimer) {
clearInterval(recordTimer);
recordTimer = null;
}
2026-03-19 11:42:26 +08:00
// 释放 Recorder 资源
if (recorderIns.value) {
recorderIns.value.close();
recorderIns.value = null;
}
2026-03-17 18:39:40 +08:00
};
2026-03-19 11:42:26 +08:00
// ------------- 保存/上传标准 MP3 文件 -------------
2026-03-17 18:39:40 +08:00
const saveRecordVoice = () => {
2026-03-19 11:42:26 +08:00
// 1. 校验 MP3 Blob 是否有效
if (!recordedBlob.value || recordedBlob.value.size === 0) {
proxy?.$modal.msgWarning('暂无有效录制的 MP3 文件');
2026-03-17 18:39:40 +08:00
return;
}
2026-03-19 11:42:26 +08:00
// 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) {
}
2026-03-17 18:39:40 +08:00
};
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();
2026-03-18 16:12:29 +08:00
formData.append('file', uploadFile.value);
2026-03-17 18:39:40 +08:00
formData.append('deviceId', route.params.deviceId as string);
2026-03-18 16:12:29 +08:00
proxy?.$modal.loading('上传中...');
2026-03-17 18:39:40 +08:00
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,
2026-03-18 16:12:29 +08:00
text: textToVoiceForm.value.content
2026-03-17 18:39:40 +08:00
});
if (res.code === 200) {
2026-03-18 16:12:29 +08:00
ttsResultUrl.value = res.data;
ttsDuration.value = res.data;
2026-03-17 18:39:40 +08:00
proxy?.$modal.msgSuccess('文字转语音成功');
2026-03-18 16:12:29 +08:00
textToVoiceDialog.value = false
2026-03-17 18:39:40 +08:00
} else {
2026-03-18 16:12:29 +08:00
proxy?.$modal.msgError(res.msg);
2026-03-17 18:39:40 +08:00
}
} 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) {
2026-03-18 16:12:29 +08:00
// ttsAudio.pause();
2026-03-17 18:39:40 +08:00
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;
};
2026-03-18 16:12:29 +08:00
// 上传语音
2026-03-17 18:39:40 +08:00
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;
2026-03-18 16:12:29 +08:00
// 播放弹框
const toggleVoicePlay = async (voiceId: string) => {
// 如果当前点击的是正在播放的语音,直接暂停
2026-03-17 18:39:40 +08:00
if (playingVoiceId.value === voiceId && voicePlaying.value) {
voiceAudio?.pause();
voicePlaying.value = false;
return;
}
2026-03-18 16:12:29 +08:00
// 如果有其他语音正在播放,先暂停
if (voicePlaying.value && voiceAudio) {
voiceAudio.pause();
2026-03-17 18:39:40 +08:00
voicePlaying.value = false;
2026-03-18 16:12:29 +08:00
}
// 播放当前选中的语音
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 = '';
2026-03-17 18:39:40 +08:00
voicePlaying.value = false;
}
};
2026-03-18 16:12:29 +08:00
// 删除语音
const deleteVoiceById = async (fileId: string) => {
2026-03-17 18:39:40 +08:00
try {
await ElMessageBox.confirm('确定要删除该语音?', '提示');
2026-03-18 16:12:29 +08:00
const res = await api.deviceDeleteAudioFile({ deviceId: route.params.deviceId, fileId });
2026-03-17 18:39:40 +08:00
if (res.code === 200) {
2026-03-18 16:12:29 +08:00
proxy?.$modal.msgSuccess(res.msg);
2026-03-17 18:39:40 +08:00
queryAudioFileInfo();
} else {
2026-03-18 16:12:29 +08:00
proxy?.$modal.msgError(res.msg);
2026-03-17 18:39:40 +08:00
}
2026-03-18 16:12:29 +08:00
} catch (err) { }
2026-03-17 18:39:40 +08:00
};
2026-03-18 16:12:29 +08:00
// 重命名
const renameVoice = async (item: any) => {
2026-03-17 18:39:40 +08:00
const { value } = await ElMessageBox.prompt('请输入新名称', '重命名', { inputPlaceholder: '请输入新名称' });
if (value) {
2026-03-19 11:42:26 +08:00
const res = await api.videRenameAudioFile({ deviceId: route.params.deviceId, fileId: item.fileId, fileName: value });
2026-03-17 18:39:40 +08:00
if (res.code === 200) {
2026-03-18 16:12:29 +08:00
proxy?.$modal.msgSuccess(res.msg);
2026-03-17 18:39:40 +08:00
queryAudioFileInfo();
} else {
2026-03-18 16:12:29 +08:00
proxy?.$modal.msgError(res.msg);
2026-03-17 18:39:40 +08:00
}
}
};
2026-03-18 16:12:29 +08:00
// 使用
2026-03-17 18:39:40 +08:00
const useVoice = async (voiceId: string) => {
2026-03-18 16:12:29 +08:00
const res = await api.deviceUpdateVoice({ id: voiceId });
2026-03-17 18:39:40 +08:00
if (res.code === 200) {
2026-03-18 16:12:29 +08:00
proxy?.$modal.msgSuccess(res.msg);
2026-03-17 18:39:40 +08:00
currentVoiceId.value = voiceId;
2026-03-18 16:12:29 +08:00
await queryAudioFileInfo()
2026-03-17 18:39:40 +08:00
} else {
proxy?.$modal.msgError('设置失败');
}
};
const handleAllVoice = () => {
allVoiceDialog.value = true;
queryAudioFileInfo();
};
// ====================== 原有功能 ======================
2026-03-10 18:03:33 +08:00
const sta_VoiceType = ref([
2026-03-17 18:39:40 +08:00
{ id: '0', name: '公安', icon: sg, activeIcon: sgAc, switchStatusVioice: true, active: true },
2026-03-18 16:12:29 +08:00
{ id: '1', name: '消防', icon: sg, activeIcon: sgAc, switchStatusVioice: false, active: false },
2026-03-17 18:39:40 +08:00
{ 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 },
]);
2026-03-10 18:03:33 +08:00
const lightModes = ref<LightMode[]>([
2026-03-17 18:39:40 +08:00
{ 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 },
2026-03-10 18:03:33 +08:00
]);
2026-03-17 18:39:40 +08:00
2026-03-10 18:03:33 +08:00
const deviceDetail = ref<DeviceDetail & { typeName: string }>({
2026-03-18 16:12:29 +08:00
lightBrightness: '20',
deviceName: '',
deviceImei: '',
2026-03-17 18:39:40 +08:00
onlineStatus: 1,
batteryPercentage: 80,
2026-03-18 16:12:29 +08:00
batteryRemainingTime: '',
longitude: '',
latitude: '',
address: '',
2026-03-10 18:03:33 +08:00
sendMsg: '',
chargeState: '0',
typeName: '',
alarmStatus: 0,
2026-03-18 16:12:29 +08:00
strobeFrequency: '10',
2026-03-17 18:39:40 +08:00
volume: '50',
2026-03-18 16:12:29 +08:00
voiceStrobeAlarm: 0,
voiceBroadcast: 0
2026-03-10 18:03:33 +08:00
});
2026-03-17 18:39:40 +08:00
2026-03-10 18:03:33 +08:00
const isUpdatingStatus = ref(false);
2026-03-17 18:39:40 +08:00
const isSyncingStatus = ref(false);
const queryAudioFileInfo = () => {
api.queryAudioFileList({ deviceId: route.params.deviceId }).then(res => {
2026-03-18 16:12:29 +08:00
if (res.code == 200) {
voiceList.value = res.data || [];
const activeVoice = voiceList.value.find(item => item.useStatus === 1);
if (activeVoice) {
currentVoiceId.value = activeVoice.id;
}
}
2026-03-17 18:39:40 +08:00
}).catch(err => proxy?.$modal.msgError('获取语音列表失败'));
};
2026-03-18 16:12:29 +08:00
// 报警模式
2026-03-10 18:03:33 +08:00
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);
2026-03-18 16:12:29 +08:00
if (!targetMode) return;
//切换选中状态
const isCurrentlyActive = targetMode.active;
2026-03-10 18:03:33 +08:00
sta_VoiceType.value.forEach(mode => {
2026-03-18 16:12:29 +08:00
mode.active = false;
mode.switchStatusVioice = false;
2026-03-10 18:03:33 +08:00
});
2026-03-18 16:12:29 +08:00
// 如果当前是未选中状态,则选中;如果是选中状态,则取消
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();
}
}
2026-03-10 18:03:33 +08:00
};
2026-03-17 18:39:40 +08:00
2026-03-18 16:12:29 +08:00
// 警示灯爆闪模
2026-03-10 18:03:33 +08:00
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;
2026-03-18 16:12:29 +08:00
const isCurrentlyActive = targetMode.active;
const enable = isCurrentlyActive ? 0 : 1; // 选中则关闭,未选中则开启
//关闭时传 enable:0开启时传 enable:1
const res = await api.strobeMode({
deviceId,
mode: targetMode.instructValue,
enable
});
2026-03-10 18:03:33 +08:00
if (res.code === 200) {
ElMessage.closeAll();
proxy?.$modal.msgSuccess(res.msg);
2026-03-18 16:12:29 +08:00
if (enable === 0) {
lightModes.value.forEach(mode => {
mode.active = false;
mode.switchStatus = false;
});
} else {
setActiveLightMode(modeId);
}
2026-03-10 18:03:33 +08:00
} else {
proxy?.$modal.msgError(res.msg);
const prevActiveMode = lightModes.value.find(m => m.active);
2026-03-17 18:39:40 +08:00
if (prevActiveMode) setActiveLightMode(prevActiveMode.id);
2026-03-10 18:03:33 +08:00
}
} catch (error) {
const prevActiveMode = lightModes.value.find(m => m.active);
2026-03-17 18:39:40 +08:00
if (prevActiveMode) setActiveLightMode(prevActiveMode.id);
2026-03-18 16:12:29 +08:00
} finally {
isUpdatingStatus.value = false;
}
2026-03-10 18:03:33 +08:00
};
2026-03-18 16:12:29 +08:00
// 全局性的方法
2026-03-10 18:03:33 +08:00
const setActiveLightMode = (targetModeId: string) => {
isSyncingStatus.value = true;
lightModes.value.forEach(mode => {
2026-03-17 18:39:40 +08:00
mode.active = mode.id === targetModeId;
mode.switchStatus = mode.active;
2026-03-10 18:03:33 +08:00
});
isSyncingStatus.value = false;
};
2026-03-17 18:39:40 +08:00
2026-03-18 16:12:29 +08:00
// 列表详情
2026-03-10 18:03:33 +08:00
const getList = async () => {
try {
const deviceId = route.params.deviceId;
if (!deviceId) return;
const res = await api.deviceDeatil(deviceId as string);
deviceDetail.value = res.data;
2026-03-18 16:12:29 +08:00
// 警示灯模式初始化
2026-03-10 18:03:33 +08:00
const mainLightMode = String(res.data.strobeMode);
2026-03-17 18:39:40 +08:00
const matchedMode = lightModes.value.find(mode => mode.instructValue === mainLightMode);
if (matchedMode) setActiveLightMode(matchedMode.id);
2026-03-18 16:12:29 +08:00
// 报警模式初始化
2026-03-17 18:39:40 +08:00
const alarmMode = String(res.data.alarmMode);
const voiceStrobeAlarm = String(res.data.voiceStrobeAlarm);
2026-03-10 18:03:33 +08:00
sta_VoiceType.value.forEach(mode => {
mode.active = false;
mode.switchStatusVioice = false;
});
2026-03-18 16:12:29 +08:00
//如果接口返回无选中,默认选中第一个
let hasActiveMode = false;
2026-03-10 18:03:33 +08:00
if (voiceStrobeAlarm === '1') {
2026-03-17 18:39:40 +08:00
const matchedVoiceMode = sta_VoiceType.value.find(mode => mode.id === alarmMode);
2026-03-10 18:03:33 +08:00
if (matchedVoiceMode) {
matchedVoiceMode.active = true;
matchedVoiceMode.switchStatusVioice = true;
2026-03-18 16:12:29 +08:00
hasActiveMode = true;
2026-03-10 18:03:33 +08:00
}
}
2026-03-18 16:12:29 +08:00
// 无选中时默认选中第一个
if (!hasActiveMode) {
sta_VoiceType.value[0].active = true;
sta_VoiceType.value[0].switchStatusVioice = true;
}
} catch (error) { console.log('获取设备详情失败:', error); }
2026-03-10 18:03:33 +08:00
};
2026-03-18 16:12:29 +08:00
// 灯光亮度
2026-03-10 18:03:33 +08:00
const saveBtnlight = () => {
2026-03-17 18:39:40 +08:00
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);
};
2026-03-10 18:03:33 +08:00
2026-03-18 16:12:29 +08:00
// 爆闪频率
2026-03-10 18:03:33 +08:00
const saveBtnstrobe = () => {
2026-03-17 18:39:40 +08:00
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('保存爆闪频率失败'));
};
2026-03-18 16:12:29 +08:00
// 音 量
2026-03-10 18:03:33 +08:00
const saveBtnVolume = () => {
2026-03-17 18:39:40 +08:00
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('保存音量失败'));
};
2026-03-10 18:03:33 +08:00
2026-03-18 16:12:29 +08:00
// 解除报警
2026-03-10 18:03:33 +08:00
const showClose = async () => {
try {
await proxy?.$modal.confirm('确定要对该设备解除报警?', '提示');
2026-03-18 16:12:29 +08:00
// 获取当前选中的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);
2026-03-17 18:39:40 +08:00
if (res.code === 200) {
2026-03-18 16:12:29 +08:00
deviceDetail.value.voiceStrobeAlarm = 0;
proxy?.$modal.msgSuccess(res.msg);
2026-03-19 11:42:26 +08:00
//await getList();
2026-03-17 18:39:40 +08:00
}
2026-03-18 16:12:29 +08:00
} catch (error: any) {
proxy?.$modal.msgError(error.msg);
}
2026-03-17 18:39:40 +08:00
};
2026-03-10 18:03:33 +08:00
2026-03-18 16:12:29 +08:00
// 强制报警
2026-03-10 18:03:33 +08:00
const forceAlarm = async () => {
try {
await proxy?.$modal.confirm('确定要对该设备开启强制报警?', '提示');
2026-03-17 18:39:40 +08:00
forceAlarmLoading.value = true;
2026-03-18 16:12:29 +08:00
// 获取当前选中的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);
2026-03-17 18:39:40 +08:00
if (res.code === 200) {
2026-03-18 16:12:29 +08:00
deviceDetail.value.voiceStrobeAlarm = 1;
proxy?.$modal.msgSuccess(res.msg);
2026-03-19 11:42:26 +08:00
// await getList();
2026-03-10 18:03:33 +08:00
}
2026-03-18 16:12:29 +08:00
} catch (error: any) {
proxy?.$modal.msgError(error.msg);
} finally {
forceAlarmLoading.value = false;
}
2026-03-17 18:39:40 +08:00
};
2026-03-18 16:12:29 +08:00
// 查看跳转到地图设备
2026-03-10 18:03:33 +08:00
const lookMap = (row: any) => {
2026-03-17 18:39:40 +08:00
router.push({ path: '/controlCenter/controlPanel', query: { view: 'map', deviceId: row.deviceId } });
};
2026-03-18 16:12:29 +08:00
// 播放如果,在强制报警中,点击播放的时候描会报警语音,如果没有报警中,就是的单纯的播放语音这个逻辑
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) {
2026-03-19 11:42:26 +08:00
// proxy?.$modal.msgError(err.msg);
2026-03-18 16:12:29 +08:00
}
}
// 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) {
2026-03-19 11:42:26 +08:00
// proxy?.$modal.msgError(err.msg);
2026-03-18 16:12:29 +08:00
}
2026-03-17 18:39:40 +08:00
}
};
2026-03-10 18:03:33 +08:00
onMounted(async () => {
await getList();
2026-03-17 18:39:40 +08:00
queryAudioFileInfo();
2026-03-10 18:03:33 +08:00
});
2026-03-17 18:39:40 +08:00
2026-03-10 18:03:33 +08:00
onUnmounted(() => {
2026-03-17 18:39:40 +08:00
disconnect();
if (recordTimer) clearInterval(recordTimer);
if (mediaRecorder.value && isRecording.value) mediaRecorder.value.stop();
if (audioStream) audioStream.getTracks().forEach(track => track.stop());
2026-03-18 16:12:29 +08:00
// if (ttsAudio) ttsAudio.pause();
2026-03-17 18:39:40 +08:00
if (voiceAudio) voiceAudio.pause();
2026-03-10 18:03:33 +08:00
});
</script>
2026-03-17 18:39:40 +08:00
2026-03-10 18:03:33 +08:00
<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 {
2026-03-18 16:12:29 +08:00
.online {
color: #00ff00;
}
.offline {
color: rgb(224, 52, 52);
}
2026-03-10 18:03:33 +08:00
}
}
.content-wrapper {
2026-03-18 16:12:29 +08:00
.content-row {
margin-bottom: 20px;
}
2026-03-10 18:03:33 +08:00
.content-card {
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1);
background: white;
2026-03-19 11:42:26 +08:00
padding: 1px 20px;
2026-03-10 18:03:33 +08:00
border: 1px solid #ebeef5;
2026-03-19 11:42:26 +08:00
min-height: 700px;
2026-03-10 18:03:33 +08:00
}
2026-03-17 18:39:40 +08:00
.content-card1 {
2026-03-10 18:03:33 +08:00
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1);
background: white;
2026-03-19 11:42:26 +08:00
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;
2026-03-10 18:03:33 +08:00
border: 1px solid #ebeef5;
2026-03-19 11:42:26 +08:00
height: 250px;
2026-03-17 18:39:40 +08:00
}
2026-03-10 18:03:33 +08:00
2026-03-17 18:39:40 +08:00
.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;
2026-03-18 16:12:29 +08:00
min-height: 240px;
2026-03-10 18:03:33 +08:00
}
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
color: #303133;
}
.light-mode {
2026-03-17 18:39:40 +08:00
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
2026-03-10 18:03:33 +08:00
}
.lacatin_gps {
height: 70px;
2026-03-19 11:42:26 +08:00
background: #F7F8FC;
border-radius: 4px 4px 4px 4px;
2026-03-17 18:39:40 +08:00
width: 100%;
2026-03-10 18:03:33 +08:00
padding: 10px;
2026-03-17 18:39:40 +08:00
word-break: break-all;
2026-03-19 11:42:26 +08:00
margin-top: 15px;
2026-03-17 18:39:40 +08:00
}
.mode_2 {
padding: 15px;
border: 1px solid #dcdfe6;
border-radius: 8px;
width: 105px;
text-align: center;
2026-03-19 11:42:26 +08:00
}
.active {
border-radius: 4px 4px 4px 4px;
border: 1px solid #027CFB;
background: rgba(2, 124, 251, 0.06);
2026-03-10 18:03:33 +08:00
}
2026-03-19 11:42:26 +08:00
2026-03-10 18:03:33 +08:00
.mode-card {
display: flex;
flex-direction: column;
align-items: center;
border-radius: 8px;
2026-03-17 18:39:40 +08:00
width: 125px;
2026-03-10 18:03:33 +08:00
cursor: pointer;
transition: all 0.3s ease;
2026-03-17 18:39:40 +08:00
margin-bottom: 20px;
2026-03-10 18:03:33 +08:00
&.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;
}
2026-03-17 18:39:40 +08:00
}
2026-03-10 18:03:33 +08:00
2026-03-17 18:39:40 +08:00
.alarm-btn-wrapper {
display: flex;
gap: 10px;
margin-bottom: 10px;
2026-03-18 16:12:29 +08:00
height: 100px;
2026-03-10 18:03:33 +08:00
}
.brightness-alarm {
align-items: center;
margin-bottom: 10px;
.brightness-control {
display: flex;
align-items: center;
2026-03-17 18:39:40 +08:00
gap: 8px;
2026-03-10 18:03:33 +08:00
border-radius: 39px;
background: rgba(255, 255, 255, 1);
width: 100%;
2026-03-17 18:39:40 +08:00
padding: 8px 15px;
margin-top: 10px;
2026-03-10 18:03:33 +08:00
.brightness-label {
font-size: 14px;
color: #606266;
2026-03-17 18:39:40 +08:00
min-width: 75px;
2026-03-10 18:03:33 +08:00
}
.brightness-value {
font-size: 14px;
color: #409eff;
font-weight: 500;
}
.save-btn {
padding: 6px 20px;
border-radius: 29px;
2026-03-19 11:42:26 +08:00
// background: rgba(2, 124, 251, 1);
2026-03-17 18:39:40 +08:00
border: none;
2026-03-10 18:03:33 +08:00
}
.inputTFT {
2026-03-17 18:39:40 +08:00
flex: 1;
min-width: 80px;
2026-03-10 18:03:33 +08:00
height: 40px;
}
}
.alarm-btn {
background-color: rgba(224, 52, 52, 1);
border-color: rgba(224, 52, 52, 1);
2026-03-18 16:12:29 +08:00
padding: 20px 20px;
border-radius: 30px;
2026-03-17 18:39:40 +08:00
&.cancel {
2026-03-18 16:12:29 +08:00
background-color: rgba(224, 52, 52, 0.08);
border-color: rgba(224, 52, 52, 0.08);
color: rgba(224, 52, 52, 1);
2026-03-17 18:39:40 +08:00
}
2026-03-10 18:03:33 +08:00
}
}
.location-info {
2026-03-19 11:42:26 +08:00
.location-item1 {
display: flex;
justify-content: space-between;
color: #38404F;
}
2026-03-10 18:03:33 +08:00
.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;
}
}
}
2026-03-17 18:39:40 +08:00
.voice-play-section {
.current-voice {
2026-03-10 18:03:33 +08:00
display: flex;
align-items: center;
2026-03-17 18:39:40 +08:00
gap: 10px;
margin-bottom: 20px;
2026-03-10 18:03:33 +08:00
2026-03-17 18:39:40 +08:00
.voice-label {
2026-03-10 18:03:33 +08:00
font-size: 14px;
color: #606266;
2026-03-17 18:39:40 +08:00
min-width: 80px;
2026-03-10 18:03:33 +08:00
}
2026-03-17 18:39:40 +08:00
.voice-select {
2026-03-10 18:03:33 +08:00
flex: 1;
2026-03-17 18:39:40 +08:00
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 4px;
cursor: pointer;
}
.play-btn {
padding: 8px 20px;
border-radius: 20px;
2026-03-10 18:03:33 +08:00
}
}
2026-03-17 18:39:40 +08:00
.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;
2026-03-19 11:42:26 +08:00
// border: 1px solid #dcdfe6;
2026-03-17 18:39:40 +08:00
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #409eff;
background-color: rgba(64, 158, 255, 0.05);
}
.el-icon {
2026-03-19 11:42:26 +08:00
font-size: 24px;
2026-03-17 18:39:40 +08:00
margin-bottom: 5px;
2026-03-19 11:42:26 +08:00
color: #027CFB;
border: 1px solid rgba(2, 124, 251, 0.2);
border-radius: 6px 6px 6px 6px;
width: 37px;
height: 37px;
2026-03-17 18:39:40 +08:00
}
span {
2026-03-19 11:42:26 +08:00
font-size: 14px;
color: #027CFB;
margin-top: 5px;
2026-03-17 18:39:40 +08:00
}
}
}
2026-03-10 18:03:33 +08:00
}
}
2026-03-17 18:39:40 +08:00
}
2026-03-10 18:03:33 +08:00
2026-03-17 18:39:40 +08:00
// 弹窗通用样式
.voice-dialog {
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
2026-03-18 16:12:29 +08:00
2026-03-17 18:39:40 +08:00
:deep(.el-dialog__header) {
padding: 16px 20px;
border-bottom: 1px solid #ebeef5;
margin: 0;
}
2026-03-18 16:12:29 +08:00
2026-03-17 18:39:40 +08:00
:deep(.el-dialog__headerbtn) {
top: 16px;
right: 20px;
}
2026-03-18 16:12:29 +08:00
2026-03-17 18:39:40 +08:00
:deep(.el-dialog__body) {
padding: 20px;
}
2026-03-18 16:12:29 +08:00
2026-03-17 18:39:40 +08:00
:deep(.el-dialog__footer) {
padding: 16px 20px;
border-top: 1px solid #ebeef5;
}
}
2026-03-10 18:03:33 +08:00
2026-03-17 18:39:40 +08:00
// 录制语音弹窗样式
.record-content {
text-align: center;
2026-03-10 18:03:33 +08:00
2026-03-17 18:39:40 +08:00
.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%;
2026-03-10 18:03:33 +08:00
2026-03-17 18:39:40 +08:00
.wave-bar {
width: 3px;
background: #409eff;
border-radius: 2px;
transition: height 0.2s;
}
}
2026-03-10 18:03:33 +08:00
2026-03-17 18:39:40 +08:00
.waveform-placeholder {
width: 100%;
height: 2px;
background: #dcdfe6;
2026-03-10 18:03:33 +08:00
}
}
2026-03-17 18:39:40 +08:00
.record-control {
2026-03-10 18:03:33 +08:00
display: flex;
2026-03-17 18:39:40 +08:00
flex-direction: column;
2026-03-10 18:03:33 +08:00
align-items: center;
2026-03-17 18:39:40 +08:00
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;
}
2026-03-10 18:03:33 +08:00
}
2026-03-17 18:39:40 +08:00
.record-footer {
2026-03-10 18:03:33 +08:00
display: flex;
2026-03-17 18:39:40 +08:00
justify-content: space-between;
2026-03-10 18:03:33 +08:00
align-items: center;
2026-03-17 18:39:40 +08:00
.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;
2026-03-10 18:03:33 +08:00
}
2026-03-17 18:39:40 +08:00
.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;
}
2026-03-10 18:03:33 +08:00
}
}
}
2026-03-17 18:39:40 +08:00
// 文字转语音弹窗样式
.tts-content {
.tts-textarea {
margin-bottom: 15px;
2026-03-10 18:03:33 +08:00
}
2026-03-17 18:39:40 +08:00
.tts-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
2026-03-10 18:03:33 +08:00
}
2026-03-17 18:39:40 +08:00
.tts-preview {
.waveform-container {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
2026-03-10 18:03:33 +08:00
2026-03-17 18:39:40 +08:00
.waveform {
display: flex;
gap: 3px;
align-items: center;
height: 100%;
.wave-bar {
width: 3px;
background: #409eff;
border-radius: 2px;
}
}
}
2026-03-10 18:03:33 +08:00
2026-03-17 18:39:40 +08:00
.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;
}
2026-03-10 18:03:33 +08:00
2026-03-17 18:39:40 +08:00
.preview-actions {
display: flex;
justify-content: flex-end;
gap: 20px;
}
2026-03-10 18:03:33 +08:00
}
}
2026-03-17 18:39:40 +08:00
// 所有语音弹窗样式
.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;
}
}
}
}
2026-03-10 18:03:33 +08:00
}
.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;
}
2026-03-17 18:39:40 +08:00
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(64, 158, 255, 0.7);
}
2026-03-18 16:12:29 +08:00
2026-03-17 18:39:40 +08:00
70% {
transform: scale(1.05);
box-shadow: 0 0 0 10px rgba(64, 158, 255, 0);
}
2026-03-18 16:12:29 +08:00
2026-03-17 18:39:40 +08:00
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(64, 158, 255, 0);
}
}
2026-03-18 16:12:29 +08:00
.active {
color: #3333;
}
2026-03-17 18:39:40 +08:00
</style>