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" :class="deviceDetail.alarmStatus == 1 ? '' : 'displayNone'">
|
|
|
|
|
|
<el-col :lg="24" :xs="24">
|
|
|
|
|
|
<div class="staticRwo" :class="deviceDetail.alarmStatus == 1 ? '' : 'displayNone'"
|
|
|
|
|
|
@click="showClose()">
|
|
|
|
|
|
设备强制报警中!
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
<!-- 第一行:灯光模式 + 灯光亮度、强制报警、位置信息 -->
|
|
|
|
|
|
<el-row :gutter="20" class="content-row">
|
2026-03-17 18:39:40 +08:00
|
|
|
|
<el-col :lg="8" :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循环渲染灯光模式卡片 -->
|
|
|
|
|
|
<div class="mode-card" :class="{ 'active': mode.active }"
|
|
|
|
|
|
@click.stop="handleVoiceType(mode.id)" v-for="mode in sta_VoiceType" :key="mode.id">
|
2026-03-17 18:39:40 +08:00
|
|
|
|
<div class="mode_2">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<el-col :lg="8" :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循环渲染灯光模式卡片 -->
|
|
|
|
|
|
<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">
|
|
|
|
|
|
<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-card1" 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">
|
|
|
|
|
|
</el-option>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-button type="primary" class="play-btn" @click="playCurrentVoice">播放</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>
|
|
|
|
|
|
<Upload />
|
|
|
|
|
|
</el-icon>
|
|
|
|
|
|
<span>上传语音</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="voice-btn-item" @click="handleTextToVoice">
|
|
|
|
|
|
<el-icon>
|
|
|
|
|
|
<Document />
|
|
|
|
|
|
</el-icon>
|
|
|
|
|
|
<span>文字转语音</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="voice-btn-item" @click="handleAllVoice">
|
|
|
|
|
|
<el-icon>
|
|
|
|
|
|
<List />
|
|
|
|
|
|
</el-icon>
|
|
|
|
|
|
<span>所有语音</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :lg="8" :xs="24">
|
|
|
|
|
|
<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-17 18:39:40 +08:00
|
|
|
|
v-if="deviceDetail.voiceStrobeAlarm === 0 || deviceDetail.voiceStrobeAlarm === null"
|
2026-03-10 18:03:33 +08:00
|
|
|
|
:loading-text="forceAlarmLoading ? '报警中...' : '强制报警'"> {{
|
|
|
|
|
|
forceAlarmLoading ? '报警中' : '强制报警' }}</el-button>
|
2026-03-17 18:39:40 +08:00
|
|
|
|
<el-button type="default" class="alarm-btn cancel" @click="showClose"
|
|
|
|
|
|
v-if="deviceDetail.voiceStrobeAlarm === 1">解除</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-10 18:03:33 +08:00
|
|
|
|
<span>经纬度 {{ deviceDetail && deviceDetail.longitude ?
|
|
|
|
|
|
Number(deviceDetail.longitude).toFixed(4) : '无' }}
|
|
|
|
|
|
{{ deviceDetail && deviceDetail.latitude ? Number(deviceDetail.latitude).toFixed(4)
|
2026-03-17 18:39:40 +08:00
|
|
|
|
: '无' }} </span>
|
2026-03-10 18:03:33 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="location-item">
|
|
|
|
|
|
<div>地址 <span class="lacatin_gps">{{ deviceDetail.address || "未获取到地址" }}</span></div>
|
|
|
|
|
|
<el-button link type="primary" class="view-btn"
|
|
|
|
|
|
@click="lookMap(deviceDetail)">查看</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-17 18:39:40 +08:00
|
|
|
|
<div class="content-card_gps" style="margin-top: 10px;">
|
|
|
|
|
|
<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" 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" 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" class="save-btn" @click="saveBtnstrobe">保存
|
|
|
|
|
|
</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">
|
|
|
|
|
|
<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>
|
2026-03-10 18:03:33 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-17 18:39:40 +08:00
|
|
|
|
<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>
|
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">
|
|
|
|
|
|
<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>
|
2026-03-10 18:03:33 +08:00
|
|
|
|
</div>
|
2026-03-17 18:39:40 +08:00
|
|
|
|
<div class="voice-player" v-if="playingVoiceId === item.id">
|
|
|
|
|
|
<el-slider v-model="voiceCurrentTime" :max="voiceDuration" :show-tooltip="false" style="flex: 1;"></el-slider>
|
|
|
|
|
|
<div class="time-info">
|
|
|
|
|
|
<span>{{ formatTime(voiceCurrentTime) }}</span>
|
|
|
|
|
|
<span>{{ formatTime(voiceDuration) }}</span>
|
2026-03-10 18:03:33 +08:00
|
|
|
|
</div>
|
2026-03-17 18:39:40 +08:00
|
|
|
|
<el-button circle @click="toggleVoicePlay(item.id)">
|
|
|
|
|
|
<el-icon><VideoPlay v-if="!voicePlaying" /><Pause v-else /></el-icon>
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="voice-actions" v-else>
|
|
|
|
|
|
<el-button link type="primary" @click="playVoiceById(item.id)">播放</el-button>
|
|
|
|
|
|
<el-button link type="danger" @click="deleteVoiceById(item.fileId )">删除</el-button>
|
|
|
|
|
|
<el-button link type="primary" @click="renameVoice(item.fileId)">重命名</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="useVoice(item.id)">使用</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';
|
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
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
|
|
|
|
import { getDeviceStatus } from '@/utils/function';
|
|
|
|
|
|
|
|
|
|
|
|
// 路由和实例
|
|
|
|
|
|
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-10 18:03:33 +08:00
|
|
|
|
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';
|
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[]>([]); // 语音列表
|
|
|
|
|
|
|
|
|
|
|
|
// MQTT相关
|
2026-03-10 18:03:33 +08:00
|
|
|
|
const {
|
|
|
|
|
|
connected,
|
|
|
|
|
|
connect,
|
|
|
|
|
|
subscribe,
|
|
|
|
|
|
onConnect,
|
|
|
|
|
|
onError,
|
|
|
|
|
|
onMessage,
|
|
|
|
|
|
disconnect
|
|
|
|
|
|
} = useMqtt();
|
2026-03-17 18:39:40 +08:00
|
|
|
|
|
|
|
|
|
|
// ====================== 录制语音 ======================
|
|
|
|
|
|
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);
|
|
|
|
|
|
let recordTimer: NodeJS.Timeout | null = null;
|
|
|
|
|
|
let audioStream: MediaStream | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
|
|
|
|
proxy?.$modal.msgError('当前浏览器不支持麦克风录制,请使用Chrome/Edge/Firefox');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
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();
|
|
|
|
|
|
isRecording.value = true;
|
|
|
|
|
|
recordDuration.value = 0;
|
|
|
|
|
|
recordTimer = setInterval(() => recordDuration.value++, 1000);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
proxy?.$modal.msgError('麦克风权限申请失败');
|
|
|
|
|
|
if (audioStream) {
|
|
|
|
|
|
audioStream.getTracks().forEach(track => track.stop());
|
|
|
|
|
|
audioStream = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const stopRecordVoice = () => {
|
|
|
|
|
|
if (!mediaRecorder.value || !isRecording.value) return;
|
|
|
|
|
|
mediaRecorder.value.stop();
|
|
|
|
|
|
isRecording.value = false;
|
|
|
|
|
|
if (recordTimer) {
|
|
|
|
|
|
clearInterval(recordTimer);
|
|
|
|
|
|
recordTimer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const resetRecord = () => {
|
|
|
|
|
|
isRecording.value = false;
|
|
|
|
|
|
recordDuration.value = 0;
|
|
|
|
|
|
recordedBlob.value = null;
|
|
|
|
|
|
audioChunks.value = [];
|
|
|
|
|
|
if (recordTimer) {
|
|
|
|
|
|
clearInterval(recordTimer);
|
|
|
|
|
|
recordTimer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const saveRecordVoice = () => {
|
|
|
|
|
|
if (!recordedBlob.value) {
|
|
|
|
|
|
proxy?.$modal.msgWarning('暂无录制的语音文件');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
|
formData.append('voiceFile', recordedBlob.value, `record_${new Date().getTime()}.mp3`);
|
|
|
|
|
|
formData.append('deviceId', route.params.deviceId as string);
|
|
|
|
|
|
proxy?.$modal.loading('保存中...', { lock: true });
|
|
|
|
|
|
api.uploadAudioToOss(formData).then(res => {
|
|
|
|
|
|
proxy?.$modal.closeLoading();
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
proxy?.$modal.msgSuccess('语音录制并保存成功');
|
|
|
|
|
|
recordVoiceDialog.value = false;
|
|
|
|
|
|
queryAudioFileInfo();
|
|
|
|
|
|
resetRecord();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
proxy?.$modal.msgError('保存语音失败:' + res.msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
|
proxy?.$modal.closeLoading();
|
|
|
|
|
|
proxy?.$modal.msgError('保存语音失败');
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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('voiceFile', uploadFile.value);
|
|
|
|
|
|
formData.append('deviceId', route.params.deviceId as string);
|
|
|
|
|
|
proxy?.$modal.loading('上传中...', { lock: true });
|
|
|
|
|
|
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 handleUploadText = () => {
|
|
|
|
|
|
proxy?.$modal.msgInfo('上传文本功能开发中');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
content: textToVoiceForm.value.content
|
|
|
|
|
|
});
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
ttsResultUrl.value = res.data.url;
|
|
|
|
|
|
ttsDuration.value = res.data.duration || 145;
|
|
|
|
|
|
proxy?.$modal.msgSuccess('文字转语音成功');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
proxy?.$modal.msgError('转换失败:' + res.msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
proxy?.$modal.msgError('转换失败');
|
|
|
|
|
|
} 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);
|
|
|
|
|
|
const voiceCurrentTime = ref(0);
|
|
|
|
|
|
const voiceDuration = ref(0);
|
|
|
|
|
|
let voiceAudio: HTMLAudioElement | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
const playVoiceById = async (voiceId: string) => {
|
|
|
|
|
|
if (playingVoiceId.value === voiceId && voicePlaying.value) {
|
|
|
|
|
|
voiceAudio?.pause();
|
|
|
|
|
|
voicePlaying.value = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
playingVoiceId.value = voiceId;
|
|
|
|
|
|
const voice = voiceList.value.find(v => v.id === voiceId);
|
|
|
|
|
|
if (!voice) return;
|
|
|
|
|
|
if (voiceAudio) voiceAudio.pause();
|
|
|
|
|
|
voiceAudio = new Audio(voice.url);
|
|
|
|
|
|
voiceDuration.value = voice.duration || 145;
|
|
|
|
|
|
voiceAudio.ontimeupdate = () => voiceCurrentTime.value = Math.floor(voiceAudio!.currentTime);
|
|
|
|
|
|
voiceAudio.onended = () => {
|
|
|
|
|
|
voicePlaying.value = false;
|
|
|
|
|
|
voiceCurrentTime.value = 0;
|
|
|
|
|
|
};
|
|
|
|
|
|
await voiceAudio.play();
|
|
|
|
|
|
voicePlaying.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const toggleVoicePlay = (voiceId: string) => {
|
|
|
|
|
|
if (voicePlaying.value) {
|
|
|
|
|
|
voiceAudio?.pause();
|
|
|
|
|
|
voicePlaying.value = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
playVoiceById(voiceId);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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('删除成功');
|
|
|
|
|
|
queryAudioFileInfo();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
proxy?.$modal.msgError('删除失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const renameVoice = async (fileId: string) => {
|
|
|
|
|
|
const { value } = await ElMessageBox.prompt('请输入新名称', '重命名', { inputPlaceholder: '请输入新名称' });
|
|
|
|
|
|
if (value) {
|
|
|
|
|
|
const res = await api.videRenameAudioFile({ deviceId: route.params.deviceId, fileId, fileName: value });
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
proxy?.$modal.msgSuccess('重命名成功');
|
|
|
|
|
|
queryAudioFileInfo();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
proxy?.$modal.msgError('重命名失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const useVoice = async (voiceId: string) => {
|
|
|
|
|
|
const res = await api.deviceUpdateVoice({id: voiceId });
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
proxy?.$modal.msgSuccess('已设置为当前语音');
|
|
|
|
|
|
currentVoiceId.value = voiceId;
|
|
|
|
|
|
} 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 },
|
|
|
|
|
|
{ id: 'fire', 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 },
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
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-17 18:39:40 +08:00
|
|
|
|
lightBrightness: '50',
|
|
|
|
|
|
deviceName: 'HBY100',
|
|
|
|
|
|
deviceImei: 'HBY100J',
|
|
|
|
|
|
onlineStatus: 1,
|
|
|
|
|
|
batteryPercentage: 80,
|
|
|
|
|
|
batteryRemainingTime: '1小时55分钟',
|
|
|
|
|
|
longitude: '114.1167',
|
|
|
|
|
|
latitude: '30.4744',
|
|
|
|
|
|
address: '湖北省武汉市洪山区光谷大国际企业中心',
|
2026-03-10 18:03:33 +08:00
|
|
|
|
sendMsg: '',
|
|
|
|
|
|
chargeState: '0',
|
|
|
|
|
|
typeName: '',
|
|
|
|
|
|
alarmStatus: 0,
|
2026-03-17 18:39:40 +08:00
|
|
|
|
strobeFrequency: '50',
|
|
|
|
|
|
volume: '50',
|
|
|
|
|
|
voiceStrobeAlarm: 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 => {
|
|
|
|
|
|
voiceList.value = res.data || [];
|
|
|
|
|
|
}).catch(err => proxy?.$modal.msgError('获取语音列表失败'));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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-17 18:39:40 +08:00
|
|
|
|
if (!targetMode || targetMode.active) return;
|
2026-03-10 18:03:33 +08:00
|
|
|
|
sta_VoiceType.value.forEach(mode => {
|
|
|
|
|
|
mode.active = mode.id === targetId;
|
2026-03-17 18:39:40 +08:00
|
|
|
|
mode.switchStatusVioice = mode.active;
|
2026-03-10 18:03:33 +08:00
|
|
|
|
});
|
2026-03-17 18:39:40 +08:00
|
|
|
|
const params = { deviceIds: [deviceId], voiceStrobeAlarm: 1, mode: targetId };
|
2026-03-10 18:03:33 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const res = await api.SosSetting(params);
|
2026-03-17 18:39:40 +08:00
|
|
|
|
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-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-17 18:39:40 +08:00
|
|
|
|
const res = await api.lightModeSettings({ deviceId, instructValue: targetMode.instructValue, deviceImei: deviceDetail.value.deviceImei, typeName: deviceDetail.value.typeName });
|
2026-03-10 18:03:33 +08:00
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
ElMessage.closeAll();
|
|
|
|
|
|
proxy?.$modal.msgSuccess(res.msg);
|
|
|
|
|
|
setActiveLightMode(modeId);
|
|
|
|
|
|
} 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);
|
|
|
|
|
|
} finally { isUpdatingStatus.value = 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 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-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;
|
|
|
|
|
|
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);
|
|
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
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-17 18:39:40 +08:00
|
|
|
|
} catch (error) { console.error('获取设备详情失败:', error); }
|
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
|
|
|
|
|
|
|
|
|
|
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-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
|
|
|
|
|
|
|
|
|
|
const showClose = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await proxy?.$modal.confirm('确定要对该设备解除报警?', '提示');
|
2026-03-17 18:39:40 +08:00
|
|
|
|
const data = { deviceIds: [route.params.deviceId], typeName: deviceDetail.value.typeName, deviceImeiList: [deviceDetail.value.deviceImei], instructValue: '0' };
|
|
|
|
|
|
const res = await api.forceAlarm(data);
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
proxy?.$modal.msgSuccess('解除报警成功');
|
|
|
|
|
|
await getList();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) { if (error !== 'cancel') proxy?.$modal.msgError('解除报警失败'); }
|
|
|
|
|
|
};
|
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;
|
|
|
|
|
|
const data = { deviceIds: [route.params.deviceId], typeName: deviceDetail.value.typeName, deviceImeiList: [deviceDetail.value.deviceImei], instructValue: '1' };
|
|
|
|
|
|
const res = await api.forceAlarm(data);
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
proxy?.$modal.msgSuccess('强制报警已开启');
|
|
|
|
|
|
await getList();
|
2026-03-10 18:03:33 +08:00
|
|
|
|
}
|
2026-03-17 18:39:40 +08:00
|
|
|
|
} catch (error: any) { if (error !== 'cancel') proxy?.$modal.msgError('强制报警失败'); } finally { forceAlarmLoading.value = false; }
|
|
|
|
|
|
};
|
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-10 18:03:33 +08:00
|
|
|
|
|
2026-03-17 18:39:40 +08:00
|
|
|
|
const playCurrentVoice = () => {
|
|
|
|
|
|
if (!currentVoiceId.value) {
|
|
|
|
|
|
proxy?.$modal.msgWarning('请先选择语音');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
api.playVoice({ deviceId: route.params.deviceId, voiceId: currentVoiceId.value }).then(res => {
|
|
|
|
|
|
if (res.code === 200) proxy?.$modal.msgSuccess('开始播放语音');
|
|
|
|
|
|
else proxy?.$modal.msgError('播放语音失败:' + res.msg);
|
|
|
|
|
|
}).catch(err => proxy?.$modal.msgError('播放语音失败'));
|
|
|
|
|
|
};
|
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());
|
|
|
|
|
|
if (ttsAudio) ttsAudio.pause();
|
|
|
|
|
|
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-17 18:39:40 +08:00
|
|
|
|
.online { color: #00ff00; }
|
|
|
|
|
|
.offline { color: rgb(224, 52, 52); }
|
2026-03-10 18:03:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-wrapper {
|
2026-03-17 18:39:40 +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-17 18:39:40 +08:00
|
|
|
|
padding: 20px;
|
2026-03-10 18:03:33 +08:00
|
|
|
|
border: 1px solid #ebeef5;
|
2026-03-17 18:39:40 +08:00
|
|
|
|
min-height:730px;
|
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-17 18:39:40 +08:00
|
|
|
|
padding: 20px;
|
2026-03-10 18:03:33 +08:00
|
|
|
|
border: 1px solid #ebeef5;
|
2026-03-17 18:39:40 +08:00
|
|
|
|
min-height: 250px;
|
|
|
|
|
|
}
|
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-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;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: rgba(247, 248, 252, 1);
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mode_2 {
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
border: 1px solid #dcdfe6;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
width: 105px;
|
|
|
|
|
|
text-align: center;
|
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-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;
|
|
|
|
|
|
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);
|
|
|
|
|
|
padding: 8px 20px;
|
|
|
|
|
|
border-radius: 27px;
|
2026-03-17 18:39:40 +08:00
|
|
|
|
|
|
|
|
|
|
&.cancel {
|
|
|
|
|
|
background-color: #f0f0f0;
|
|
|
|
|
|
border-color: #dcdfe6;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
}
|
2026-03-10 18:03:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.location-info {
|
|
|
|
|
|
.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;
|
|
|
|
|
|
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: 20px;
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
: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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|