Xazn-vue/src/views/iot/device/voicecard.vue

1672 lines
68 KiB
Vue
Raw Normal View History

2025-05-30 16:23:45 +08:00
<template>
<div class="running-status">
<el-row :gutter="20">
<el-col :xs="24" :sm="24" :md="24" :lg="16" :xl="14" class="status-col">
<!-- 设备模式和OTA升级部分 -->
<el-row :gutter="20" class="mode-section">
<!-- 设备模式 -->
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-card class="mode-card" shadow="hover">
<div class="mode-header">
<i class="el-icon-menu"></i>
<span class="mode-title">{{ $t('device.running-status.866086-0') }}</span>
</div>
<div class="mode-content">
<span class="title" :style="{ color: statusColor.background }">{{ title }}</span>
2025-06-06 06:26:26 +08:00
<el-button type="text" @click="printThingsModels" style="margin-left: 10px">
<i class="el-icon-printer"></i> 打印物模型
</el-button>
2025-05-30 16:23:45 +08:00
</div>
</el-card>
</el-col>
<!-- 设备升级 -->
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-card class="mode-card" shadow="hover">
<div class="mode-header">
<svg-icon icon-class="ota" />
<span class="mode-title">{{ $t('device.running-status.866086-1') }}</span>
</div>
<div class="mode-content">
<el-button type="primary" size="mini" :plain="true" @click="viewVersion()">
{{ $t('device.running-status.866086-44') }}
</el-button>
</div>
</el-card>
</el-col>
</el-row>
<!-- 声卡基础设置 -->
<el-card class="settings-card" shadow="hover">
<div slot="header" class="settings-header">
<span class="settings-title">基础设置</span>
</div>
2025-06-06 06:26:26 +08:00
<el-form :model="basicSettings" label-width="100px" style="margin-top: 20px;">
<el-form-item label="音频开关">
<el-switch v-model="basicSettings.audioEnabled" active-color="#13ce66"
inactive-color="#ff4949" @change="handleAudioSwitchChange">
</el-switch>
</el-form-item>
2025-05-30 16:23:45 +08:00
<el-form-item label="音量设置">
<el-slider v-model="basicSettings.volume" :min="0" :max="100" :format-tooltip="formatVolume"
2025-06-06 06:26:26 +08:00
@change="handleVolumeChange" style="width: 80%" :disabled="!basicSettings.audioEnabled">
2025-05-30 16:23:45 +08:00
</el-slider>
</el-form-item>
</el-form>
</el-card>
<!-- 音频列表 -->
<el-card class="audio-list-card" shadow="hover">
<div slot="header" class="audio-list-header">
<span class="audio-list-title">音频列表</span>
2025-06-06 06:26:26 +08:00
<el-button type="primary" size="mini" icon="el-icon-plus" @click="showAddAudioDialog">
添加音频
</el-button>
2025-05-30 16:23:45 +08:00
</div>
<el-table :data="audioList" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }"
border>
<el-table-column prop="id" label="序号" width="80" align="center">
</el-table-column>
<el-table-column prop="name" label="音频名称" min-width="150">
</el-table-column>
2025-06-06 06:26:26 +08:00
<el-table-column label="操作" width="120" align="center">
<template slot-scope="scope">
<el-button type="text" icon="el-icon-delete" @click="handleDeleteAudio(scope.row)">
</el-button>
</template>
2025-05-30 16:23:45 +08:00
</el-table-column>
</el-table>
</el-card>
<!-- 默认列表 -->
<el-card class="default-list-card" shadow="hover">
<div slot="header" class="default-list-header">
2025-06-06 06:26:26 +08:00
<span class="default-list-title">播放列表</span>
<el-button type="primary" size="mini" icon="el-icon-plus" @click="showAddPlaylistDialog">
添加音频
</el-button>
2025-05-30 16:23:45 +08:00
</div>
<el-table :data="defaultList" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }"
border>
<el-table-column prop="id" label="序号" width="80" align="center">
</el-table-column>
<el-table-column prop="name" label="音频名称" min-width="150">
</el-table-column>
2025-06-06 06:26:26 +08:00
<el-table-column prop="playTime" label="播放时间" width="120" align="center">
2025-05-30 16:23:45 +08:00
</el-table-column>
2025-06-06 06:26:26 +08:00
<el-table-column prop="weekdays" label="重复" width="200" align="center">
</el-table-column>
<el-table-column label="雷达" width="100" align="center">
2025-05-30 16:23:45 +08:00
<template slot-scope="scope">
2025-06-06 06:26:26 +08:00
<el-tag :type="scope.row.radarEnabled ? 'success' : 'info'">
{{ scope.row.radarEnabled ? '开启' : '关闭' }}
2025-05-30 16:23:45 +08:00
</el-tag>
</template>
</el-table-column>
2025-06-06 06:26:26 +08:00
<el-table-column label="状态" width="120" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" :active-value="'启用'" :inactive-value="'禁用'"
@change="handleStatusChange(scope.row)">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template slot-scope="scope">
<el-button type="text" icon="el-icon-delete" @click="handleDeletePlaylist(scope.row)">
</el-button>
</template>
</el-table-column>
2025-05-30 16:23:45 +08:00
</el-table>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
<!-- 远程喊话控制面板 -->
<el-card class="voice-control-card" shadow="hover">
<div slot="header" class="voice-control-header">
<span class="voice-control-title">远程喊话</span>
</div>
<div class="voice-control-content">
<div class="recorder-status">
<div class="status-indicator" :class="{ 'recording': isRecording }">
<i class="el-icon-microphone"></i>
</div>
<span class="status-text">{{ recordingStatus }}</span>
</div>
<div class="timer-display" v-if="isRecording">
{{ formatTime(recordingTime) }}
</div>
<div class="control-buttons">
<el-button type="primary" icon="el-icon-video-play" circle @click="startRecording"
:disabled="isRecording">
</el-button>
<el-button type="danger" icon="el-icon-video-pause" circle @click="stopRecording"
:disabled="!isRecording">
</el-button>
<el-button type="success" icon="el-icon-upload2" circle @click="uploadRecording"
:disabled="!hasRecording || isRecording">
</el-button>
</div>
<!-- 录音预览 -->
<div class="recording-preview" v-if="hasRecording">
<div class="preview-title">录音预览</div>
<div class="audio-player">
<audio ref="audioPlayer" :src="audioUrl" controls></audio>
<div class="preview-controls">
<el-button type="text" icon="el-icon-refresh" @click="reRecord"
:disabled="isRecording">
重新录制
</el-button>
</div>
</div>
</div>
<div class="recording-list" v-if="recordings.length > 0">
<div class="list-title">最近录音</div>
<el-scrollbar style="height: 200px">
<div v-for="(recording, index) in recordings" :key="index" class="recording-item">
<span class="recording-name">{{ recording.name }}</span>
<span class="recording-time">{{ recording.time }}</span>
<el-button type="text" icon="el-icon-delete" @click="deleteRecording(index)">
</el-button>
</div>
</el-scrollbar>
</div>
</div>
</el-card>
<!-- 设备监测图表-->
<el-row :gutter="20" v-if="deviceInfo.chartList.length > 0">
<el-col :xs="24" :sm="12" :md="12" :lg="24" :xl="12" v-for="(item, index) in deviceInfo.chartList"
:key="index">
<el-card shadow="hover" style="border-radius: 8px; margin-bottom: 20px">
<div ref="map" style="height: 230px; width: 185px; margin: 0 auto; margin-bottom: 15px">
</div>
</el-card>
</el-col>
</el-row>
</el-col>
</el-row>
<!-- 固件版本查看对话框 -->
<el-dialog :title="$t('device.running-status.866086-10')" :visible.sync="openVersion" width="550px"
append-to-body>
<el-form ref="firmwareForm" label-width="100px" :model="firmwareParams" :inline="true" :rules="rules">
<el-form-item :label="$t('device.running-status.866086-38')" prop="firmwareType">
<el-select v-model="deviceInfo.firmwareType" :placeholder="$t('firmware.index.222541-51')"
@change="handleVersionInputChange" style="width: 350px" disabled>
<el-option v-for="item in firmwareTypeList" :key="item.value" :label="item.label"
:value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('device.running-status.866086-39')" prop="">
<el-input :placeholder="$t('device.running-status.866086-40')" v-model="deviceInfo.firmwareVersion"
style="width: 350px" disabled>
<template slot="prepend">Version</template>
</el-input>
</el-form-item>
</el-form>
<div slot="footer">
<el-tooltip effect="dark" :content="$t('device.running-status.866086-41')" placement="top-start">
<el-button type="primary" @click="getLatestFirmware" :disabled="device.status !== 3">{{
$t('device.running-status.866086-42') }}</el-button>
</el-tooltip>
<el-button @click="cancel1">{{ $t('cancel') }}</el-button>
</div>
</el-dialog>
<!-- 添加或修改产品固件对话框 -->
<el-dialog :title="$t('device.running-status.866086-10')" :visible.sync="openFirmware" width="600px"
append-to-body>
<div v-if="firmware == null" style="text-align: center; font-size: 16px">
<i class="el-icon-success" style="color: #67c23a"></i>
{{ $t('device.running-status.866086-11') }}
</div>
<el-descriptions :column="1" border size="large"
v-if="firmware != null && deviceInfo.firmwareVersion < firmware.version"
:labelStyle="{ width: '150px', 'font-weight': 'bold' }">
<template slot="title">
<el-link icon="el-icon-success" type="success" :underline="false">{{
$t('device.running-status.866086-12') }}</el-link>
</template>
<el-descriptions-item :label="$t('device.running-status.866086-13')">{{ firmware.firmwareName
}}</el-descriptions-item>
<el-descriptions-item :label="$t('device.device-edit.148398-4')">{{ firmware.productName
}}</el-descriptions-item>
<el-descriptions-item :label="$t('device.device-edit.148398-12')">Version {{ firmware.version
}}</el-descriptions-item>
<el-descriptions-item :label="$t('device.running-status.866086-16')">
<el-link :href="getDownloadUrl(firmware.filePath)" :underline="false" type="primary">{{
getDownloadUrl(firmware.filePath) }}</el-link>
</el-descriptions-item>
<el-descriptions-item :label="$t('device.running-status.866086-17')">{{ firmware.remark
}}</el-descriptions-item>
</el-descriptions>
<div slot="footer" class="dialog-footer">
<el-button type="success" @click="otaUpgrade"
v-if="firmware != null && deviceInfo.firmwareVersion < firmware.version">{{
$t('device.running-status.866086-18') }}</el-button>
<el-button @click="cancel">{{ $t('cancel') }}</el-button>
</div>
</el-dialog>
2025-06-06 06:26:26 +08:00
<!-- 添加音频对话框 -->
<el-dialog title="添加音频" :visible.sync="addAudioDialogVisible" width="600px" append-to-body>
<el-form :model="newAudio" :rules="audioRules" ref="audioForm" label-width="120px">
<el-form-item label="备注" prop="remark">
<el-input v-model="newAudio.remark" placeholder="请输入备注"></el-input>
</el-form-item>
<el-form-item label="主持人声音" prop="per">
<el-select v-model="newAudio.per" placeholder="请选择主持人声音" style="width: 100%">
<el-option label="小美" :value="0"></el-option>
<el-option label="小宇" :value="1"></el-option>
<el-option label="逍遥" :value="3"></el-option>
<el-option label="丫丫" :value="4"></el-option>
</el-select>
</el-form-item>
<el-form-item label="合成语速" prop="spd">
<el-slider v-model="newAudio.spd" :min="0" :max="15" :step="1" :format-tooltip="formatSpeed">
</el-slider>
</el-form-item>
<el-form-item label="合成音调" prop="pit">
<el-slider v-model="newAudio.pit" :min="0" :max="15" :step="1" :format-tooltip="formatPitch">
</el-slider>
</el-form-item>
<el-form-item label="合成音量" prop="vol">
<el-slider v-model="newAudio.vol" :min="0" :max="15" :step="1" :format-tooltip="formatVolume">
</el-slider>
</el-form-item>
<el-form-item label="合成文本" prop="tex_utf8">
<el-input type="textarea" :rows="4" v-model="newAudio.tex_utf8" placeholder="请输入需要合成的文本">
</el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="addAudioDialogVisible = false"> </el-button>
<el-button type="primary" @click="submitAudioForm"> </el-button>
</div>
</el-dialog>
<!-- 添加播放列表对话框 -->
<el-dialog title="添加播放列表" :visible.sync="addPlaylistDialogVisible" width="600px" append-to-body>
<el-form :model="newPlaylist" :rules="playlistRules" ref="playlistForm" label-width="120px">
<el-form-item label="音频选择" prop="audioId">
<el-select v-model="newPlaylist.audioId" placeholder="请选择音频" style="width: 100%">
<el-option v-for="item in audioList" :key="item.id" :label="item.name" :value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="播放时间" prop="playTime">
<div class="time-range">
<el-time-picker v-model="newPlaylist.playTimeStart" format="HH:mm" placeholder="开始时间"
style="width: 45%">
</el-time-picker>
<span class="time-separator"></span>
<el-time-picker v-model="newPlaylist.playTimeEnd" format="HH:mm" placeholder="结束时间"
style="width: 45%">
</el-time-picker>
</div>
</el-form-item>
<el-form-item label="星期重复" prop="weekdays">
<el-checkbox-group v-model="newPlaylist.weekdays">
<el-checkbox label="1">周一</el-checkbox>
<el-checkbox label="2">周二</el-checkbox>
<el-checkbox label="3">周三</el-checkbox>
<el-checkbox label="4">周四</el-checkbox>
<el-checkbox label="5">周五</el-checkbox>
<el-checkbox label="6">周六</el-checkbox>
<el-checkbox label="0">周日</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="雷达开关" prop="radarEnabled">
<el-switch v-model="newPlaylist.radarEnabled" active-color="#13ce66" inactive-color="#ff4949">
</el-switch>
</el-form-item>
<el-collapse-transition>
<div v-show="newPlaylist.radarEnabled" class="radar-settings">
<el-form-item label="速度范围" prop="radarSpeed">
<div class="speed-range">
<el-input-number v-model="newPlaylist.radarSpeedMin" :min="0"
:max="newPlaylist.radarSpeedMax" :step="1" placeholder="最小速度">
</el-input-number>
<span class="speed-separator">-</span>
<el-input-number v-model="newPlaylist.radarSpeedMax" :min="newPlaylist.radarSpeedMin"
:max="200" :step="1" placeholder="最大速度">
</el-input-number>
<span class="speed-unit">km/h</span>
</div>
</el-form-item>
</div>
</el-collapse-transition>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="addPlaylistDialogVisible = false"> </el-button>
<el-button type="primary" @click="submitPlaylistForm"> </el-button>
</div>
</el-dialog>
2025-05-30 16:23:45 +08:00
</div>
</template>
<script>
import { getLatestFirmware } from '@/api/iot/firmware';
import { serviceInvoke, serviceInvokeReply } from '@/api/iot/runstatus';
import { getOrderControl } from '@/api/iot/control';
export default {
name: 'running-status',
props: {
device: {
type: Object,
default: null,
},
},
watch: {
device: {
handler(newVal) {
if (newVal && newVal.deviceId != 0) {
this.deviceInfo = newVal;
this.updateDeviceStatus(this.deviceInfo);
this.$nextTick(function () {
this.MonitorChart();
});
2025-06-06 06:26:26 +08:00
console.log("物模型", JSON.stringify(this.deviceInfo.thingsModels));
2025-05-30 16:23:45 +08:00
if (this.deviceInfo.thingsModels && this.deviceInfo.thingsModels.length > 0) {
this.deviceInfo.thingsModels = this.device.thingsModels.sort((a, b) => b.order - a.order);
}
if (this.deviceInfo.chartList && this.deviceInfo.chartList.length > 0) {
this.deviceInfo.chartList = this.deviceInfo.chartList.sort((a, b) => b.order - a.order);
}
}
},
},
},
data() {
return {
title: '设备控制',
shadowUnEnable: false,
statusColor: {
background: '#67C23A',
color: '#fff',
maxWidth: '200px',
},
firmware: {},
openFirmware: false,
loading: true,
deviceInfo: {
deviceId: 0,
serialNumber: '',
productId: '',
productName: '',
status: 0,
isShadow: 0,
rssi: 0,
firmwareVersion: '',
wirelessVersion: '',
firmwareType: 1,
protocolCode: '',
thingsModels: [],
chartList: [],
},
firmwareParams: {
firmwareType: '',
versionInput: '',
},
monitorChart: [
{
chart: {},
data: {
id: '',
name: '',
value: '',
},
},
],
openVersion: false,
firmwareTypeList: [
{
label: this.$t('firmware.index.222541-52'),
value: 1,
},
{
label: 'HTTP',
value: 2,
},
],
rules: {
firmwareType: [
{
required: true,
message: this.$t('device.running-status.866086-43'),
trigger: 'blur',
},
],
},
// 声卡相关数据
basicSettings: {
2025-06-06 06:26:26 +08:00
volume: 50,
audioEnabled: true
2025-05-30 16:23:45 +08:00
},
audioList: [
2025-06-06 06:26:26 +08:00
{ id: 1, name: '音频1' },
{ id: 2, name: '音频2' },
{ id: 3, name: '音频3' }
2025-05-30 16:23:45 +08:00
],
defaultList: [
2025-06-06 06:26:26 +08:00
{
id: 1,
name: '默认音频1',
playTime: '08:00',
weekdays: '周一, 周三, 周五',
radarEnabled: true,
status: '启用'
},
{
id: 2,
name: '默认音频2',
playTime: '12:30',
weekdays: '周二, 周四',
radarEnabled: false,
status: '启用'
},
{
id: 3,
name: '默认音频3',
playTime: '18:00',
weekdays: '周六, 周日',
radarEnabled: true,
status: '禁用'
}
2025-05-30 16:23:45 +08:00
],
// 录音相关数据
isRecording: false,
recordingTime: 0,
recordingStatus: '准备就绪',
hasRecording: false,
mediaRecorder: null,
audioChunks: [],
recordings: [],
timer: null,
audioUrl: null,
2025-06-06 06:26:26 +08:00
// 音频列表相关数据
addAudioDialogVisible: false,
newAudio: {
remark: '',
per: 0,
spd: 5,
pit: 5,
vol: 5,
tex_utf8: '',
filename: ''
},
audioRules: {
remark: [
{ required: true, message: '请输入备注', trigger: 'blur' }
],
per: [
{ required: true, message: '请选择主持人声音', trigger: 'change' }
],
spd: [
{ required: true, message: '请设置合成语速', trigger: 'change' }
],
pit: [
{ required: true, message: '请设置合成音调', trigger: 'change' }
],
vol: [
{ required: true, message: '请设置合成音量', trigger: 'change' }
],
tex_utf8: [
{ required: true, message: '请输入合成文本', trigger: 'blur' }
]
},
// 播放列表相关数据
addPlaylistDialogVisible: false,
newPlaylist: {
name: '',
type: '用户',
status: '启用',
audioId: '',
playTimeStart: null,
playTimeEnd: null,
weekdays: [],
radarEnabled: false,
radarSpeedMin: 0,
radarSpeedMax: 120
},
playlistRules: {
audioId: [
{ required: true, message: '请选择音频', trigger: 'change' }
],
playTime: [
{
validator: (rule, value, callback) => {
if (!this.newPlaylist.playTimeStart || !this.newPlaylist.playTimeEnd) {
callback(new Error('请选择播放时间段'));
} else if (this.newPlaylist.playTimeStart >= this.newPlaylist.playTimeEnd) {
callback(new Error('开始时间必须小于结束时间'));
} else {
callback();
}
},
trigger: 'change'
}
],
weekdays: [
{ required: true, message: '请选择重复日期', trigger: 'change' }
],
radarSpeed: [
{
validator: (rule, value, callback) => {
if (this.newPlaylist.radarEnabled) {
if (!this.newPlaylist.radarSpeedMin || !this.newPlaylist.radarSpeedMax) {
callback(new Error('请设置速度范围'));
} else if (this.newPlaylist.radarSpeedMin >= this.newPlaylist.radarSpeedMax) {
callback(new Error('最小速度必须小于最大速度'));
} else {
callback();
}
} else {
callback();
}
},
trigger: 'change'
}
]
},
2025-05-30 16:23:45 +08:00
};
},
mounted() {
if (this.device && this.device.deviceId) {
this.handleDeviceChange(this.device);
this.initDataStatus();
this.initData();
}
},
methods: {
2025-06-06 06:26:26 +08:00
//发送指令
async mqttPublish(device, model) {
const command = {};
command[model.id] = model.shadow;
const params = {
deviceId: device.deviceId,
modelId: model.modelId,
};
const response = await getOrderControl(params);
if (response.code != 200) {
this.$message({
type: 'warning',
message: response.msg,
});
return;
}
const data = {
serialNumber: device.serialNumber,
productId: device.productId,
remoteCommand: command,
identifier: model.id,
modelName: model.name,
isShadow: device.status != 3,
type: model.type,
};
//设备在线状态判断
if (this.device.status !== 3 && this.device.isShadow !== 1) {
if (this.device.status === 1) {
title = this.$t('device.device-variable.930930-0');
} else if (this.device.status === 2) {
title = this.$t('device.device-variable.930930-1');
} else {
title = this.$t('device.device-variable.930930-2');
}
this.$message({
type: 'warning',
message: title,
});
return;
}
if ((this.deviceInfo.protocolCode === 'MODBUS-TCP' || this.deviceInfo.protocolCode === 'MODBUS-RTU') && this.device.status === 3) {
await serviceInvokeReply(data).then((response) => {
if (response.code === 200) {
this.$message({
type: 'success',
message: this.$t('device.running-status.866086-25'),
});
} else {
this.$message.error(response.msg);
}
});
} else {
await serviceInvoke(data).then((response) => {
if (response.code === 200) {
this.$message({
type: 'success',
message: this.$t('device.running-status.866086-25'),
});
} else {
this.$message.error(response.msg);
}
});
}
},
2025-05-30 16:23:45 +08:00
// 保留原有的设备状态相关方法
handleDeviceChange(device) {
if (device && device.deviceId != 0) {
const { firmwareVersion, wirelessVersion, firmwareType, ...res } = device;
const data = {
version: firmwareType === 1 ? firmwareVersion : wirelessVersion,
firmwareType,
...res,
};
this.deviceInfo = data;
this.updateDeviceStatus(this.deviceInfo);
this.$nextTick(() => {
this.MonitorChart();
});
if (this.deviceInfo.thingsModels && this.deviceInfo.thingsModels.length > 0) {
this.deviceInfo.thingsModels = this.deviceInfo.thingsModels.sort((a, b) => b.order - a.order);
2025-06-06 06:26:26 +08:00
this.updateBasicSettings(); // 更新基础设置
this.printThingsModels();
2025-05-30 16:23:45 +08:00
}
if (this.deviceInfo.chartList && this.deviceInfo.chartList.length > 0) {
this.deviceInfo.chartList = this.deviceInfo.chartList.sort((a, b) => b.order - a.order);
}
}
},
2025-06-06 06:26:26 +08:00
// 更新基础设置
updateBasicSettings() {
if (!this.deviceInfo.thingsModels) return;
// 更新音频开关状态
const playEnModel = this.deviceInfo.thingsModels.find(model => model.id === 'play_en');
if (playEnModel) {
this.basicSettings.audioEnabled = playEnModel.shadow === '1';
}
// 更新音量设置
const volumeModel = this.deviceInfo.thingsModels.find(model => model.id === 'volume');
if (volumeModel) {
this.basicSettings.volume = parseInt(volumeModel.shadow) || 50;
}
// 更新音频列表
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model.id === 'mp3_list');
if (mp3ListModel && mp3ListModel.shadow) {
try {
// 解析 JSON 字符串
const jsonStr = mp3ListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
// 获取 mp3_list 数组
if (data.sound_card && data.sound_card.mp3_list) {
// 更新音频列表
this.audioList = data.sound_card.mp3_list.map((item, index) => {
// 从 "1_def" 格式中提取名称
const name = item.split('_')[1] || item;
return {
id: index + 1,
name: name
};
});
}
} catch (error) {
console.error('解析音频列表失败:', error);
}
}
},
printThingsModels() {
console.log('当前物模型数据:', JSON.stringify(this.deviceInfo.thingsModels, null, 2));
this.$message({
message: '物模型数据已打印到控制台',
type: 'success'
});
},
2025-05-30 16:23:45 +08:00
// 声卡特有方法
formatVolume(val) {
return val + '%';
},
handleVolumeChange(val) {
2025-06-06 06:26:26 +08:00
const volumeModel = this.deviceInfo.thingsModels.find(model => model.id === 'volume');
if (volumeModel) {
volumeModel.shadow = val.toString();
this.mqttPublish(this.deviceInfo, volumeModel);
}
2025-05-30 16:23:45 +08:00
},
// 保留其他必要的方法
initData() {
this.$busEvent.$on('updateData', (params) => {
this.updateParam(params);
});
},
2025-06-06 06:26:26 +08:00
// 处理设备上报的数据更新
// updateParam(params) {
// console.log(1111111111)
// if (!params || !this.deviceInfo.thingsModels) return;
// const { serialNumber, productId, data } = params;
// if (data && this.deviceInfo.serialNumber === serialNumber) {
// // 更新物模型数据
// this.deviceInfo.thingsModels.forEach(model => {
// if (data[model.id] !== undefined) {
// model.shadow = data[model.id];
// }
// });
// // 更新基础设置
// this.updateBasicSettings();
// }
// },
//更新参数值
updateParam(params) {
let { serialNumber, productId, data } = params;
let isComplete = false;
data = data.message;
if (data) {
for (let j = 0; j < data.length; j++) {
for (let k = 0; k < this.deviceInfo.thingsModels.length && !isComplete; k++) {
if (this.deviceInfo.thingsModels[k].id == data[j].id) {
const variable = this.deviceInfo.thingsModels[k];
// 普通类型(小数/整数/字符串/布尔/枚举)
if (this.deviceInfo.thingsModels[k].datatype.type == 'decimal' || this.deviceInfo.thingsModels[k].datatype.type == 'integer') {
variable.shadow = Number(data[j].value);
} else {
variable.shadow = data[j].value;
}
}
if (this.deviceInfo.thingsModels[k].datatype.type == 'object') {
// 对象类型
for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.params.length; n++) {
if (this.deviceInfo.thingsModels[k].datatype.params[n].id == data[j].id) {
this.deviceInfo.thingsModels[k].datatype.params[n].shadow = data[j].value;
isComplete = true;
break;
}
}
} else if (this.deviceInfo.thingsModels[k].datatype.type == 'array') {
// 数组类型
if (this.deviceInfo.thingsModels[k].datatype.arrayType == 'object') {
// 1.对象类型数组,id为数组中一个元素,例如array_01_gateway_temperature
if (String(data[j].id).indexOf('array_') == 0) {
for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayParams.length; n++) {
for (let m = 0; m < this.deviceInfo.thingsModels[k].datatype.arrayParams[n].length; m++) {
if (this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].id == data[j].id) {
this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].shadow = data[j].value;
isComplete = true;
break;
}
}
if (isComplete) {
break;
}
}
} else {
// 2.对象类型数组例如gateway_temperature,消息ID添加前缀后匹配
for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayParams.length; n++) {
for (let m = 0; m < this.deviceInfo.thingsModels[k].datatype.arrayParams[n].length; m++) {
let index = n > 9 ? String(n) : '0' + k;
let prefix = 'array_' + index + '_';
if (this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].id == prefix + data[j].id) {
this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].shadow = data[j].value;
}
}
}
}
} else {
// 整数、小数和字符串类型数组
for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayModel.length; n++) {
if (this.deviceInfo.thingsModels[k].datatype.arrayModel[n].id == data[j].id) {
this.deviceInfo.thingsModels[k].datatype.arrayModel[n].shadow = data[j].value;
break;
}
}
}
}
}
// 图表数据
for (let k = 0; k < this.deviceInfo.chartList.length; k++) {
if (this.deviceInfo.chartList[k].id.indexOf('array_') == 0) {
// 数组类型匹配,例如array_00_gateway_temperature
if (this.deviceInfo.chartList[k].id == data[j].id) {
this.deviceInfo.chartList[k].shadow = data[j].value;
// 更新图表
for (let m = 0; m < this.monitorChart.length; m++) {
if (data[j].id == this.monitorChart[m].data.id) {
let data = [
{
value: this.deviceInfo.chartList[k].shadow,
name: this.monitorChart[m].data.name,
},
];
this.monitorChart[m].chart.setOption({
series: [
{
data: data,
},
],
});
break;
}
}
}
} else {
// 普通类型匹配
if (this.deviceInfo.chartList[k].id == data[j].id) {
this.deviceInfo.chartList[k].shadow = data[j].value;
// 更新图表
for (let m = 0; m < this.monitorChart.length; m++) {
if (data[j].id == this.monitorChart[m].data.id) {
let data = [
{
value: this.deviceInfo.chartList[k].shadow,
name: this.monitorChart[m].data.name,
},
];
this.monitorChart[m].chart.setOption({
series: [
{
data: data,
},
],
});
break;
}
}
}
}
if (isComplete) {
break;
}
}
}
}
this.updateBasicSettings();
},
2025-05-30 16:23:45 +08:00
initDataStatus() {
this.$busEvent.$on('updateStatus', (status) => {
this.updateStatus(status);
});
},
updateStatus(status) {
let { serialNumber, productId, data } = status;
if (data) {
if (this.deviceInfo.serialNumber == serialNumber) {
this.deviceInfo.status = data.status;
this.deviceInfo.isShadow = data.isShadow;
this.deviceInfo.rssi = data.rssi;
this.updateDeviceStatus(this.deviceInfo);
}
}
},
updateDeviceStatus(device) {
if (device.status == 3) {
this.statusColor.background = '#12d09f';
this.title = this.$t('device.running-status.866086-26');
this.shadowUnEnable = false;
} else {
if (device.isShadow == 1) {
this.statusColor.background = '#486FF2';
this.title = this.$t('device.running-status.866086-27');
this.shadowUnEnable = false;
} else {
this.statusColor.background = '#909399';
this.title = this.$t('device.running-status.866086-28');
this.shadowUnEnable = true;
}
}
this.$emit('statusEvent', this.deviceInfo.status);
},
// 保留固件更新相关方法
viewVersion() {
this.openVersion = true;
this.firmwareParams.firmwareType = 1;
this.firmwareParams.versionInput = '';
this.handleVersionInputChange();
},
handleVersionInputChange() {
if (this.firmwareParams.firmwareType == 1) {
this.firmwareParams.versionInput = 'Version' + this.device.firmwareVersion;
} else {
this.firmwareParams.versionInput = 'Version' + this.device.wirelessVersion;
}
},
cancel1() {
this.openVersion = false;
},
getLatestFirmware() {
const { deviceId, firmwareType } = this.deviceInfo;
getLatestFirmware(deviceId, firmwareType).then((response) => {
if (response.code === 200) {
this.firmware = response.data;
this.openFirmware = true;
}
});
},
cancel() {
this.openFirmware = false;
},
getDownloadUrl(path) {
return window.location.origin + process.env.VUE_APP_BASE_API + path;
},
MonitorChart() {
for (let i = 0; i < this.deviceInfo.chartList.length; i++) {
this.monitorChart[i] = {
chart: this.$echarts.init(this.$refs.map[i]),
data: {
id: this.deviceInfo.chartList[i].id,
name: this.deviceInfo.chartList[i].name,
value: this.deviceInfo.chartList[i].shadow ? this.deviceInfo.chartList[i].shadow : this.deviceInfo.chartList[i].datatype.min,
},
};
var option;
option = {
tooltip: {
formatter: ' {b} <br/> {c}' + this.deviceInfo.chartList[i].datatype.unit,
},
series: [
{
name: this.deviceInfo.chartList[i].datatype.type,
type: 'gauge',
min: this.deviceInfo.chartList[i].datatype.min,
max: this.deviceInfo.chartList[i].datatype.max,
colorBy: 'data',
splitNumber: 10,
radius: '100%',
splitLine: {
distance: 4,
},
axisLabel: {
fontSize: 10,
distance: 10,
},
axisTick: {
distance: 4,
},
axisLine: {
lineStyle: {
width: 8,
color: [
[0.2, '#409EFF'],
[0.8, '#12d09f'],
[1, '#F56C6C'],
],
opacity: 0.3,
},
},
pointer: {
icon: 'triangle',
length: '60%',
width: 7,
},
progress: {
show: true,
width: 8,
},
detail: {
valueAnimation: true,
formatter: '{value}' + ' ' + this.deviceInfo.chartList[i].datatype.unit,
offsetCenter: [0, '80%'],
fontSize: 20,
},
data: [
{
value: this.deviceInfo.chartList[i].shadow ? this.deviceInfo.chartList[i].shadow : this.deviceInfo.chartList[i].datatype.min,
name: this.deviceInfo.chartList[i].name,
},
],
title: {
offsetCenter: [0, '115%'],
fontSize: 16,
},
},
],
};
option && this.monitorChart[i].chart.setOption(option);
}
},
// 录音相关方法
async startRecording() {
if (this.isRecording) return;
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.mediaRecorder = new MediaRecorder(stream);
this.audioChunks = [];
this.mediaRecorder.ondataavailable = (event) => {
this.audioChunks.push(event.data);
};
this.mediaRecorder.onstop = () => {
const audioBlob = new Blob(this.audioChunks, { type: 'audio/mp3' });
this.hasRecording = true;
this.recordingStatus = '录音完成';
// 创建音频预览URL
this.audioUrl = URL.createObjectURL(audioBlob);
};
// 设置数据收集间隔为100ms
this.mediaRecorder.start(100);
this.isRecording = true;
this.recordingStatus = '正在录音...';
this.recordingTime = 0;
// 开始计时
this.timer = setInterval(() => {
this.recordingTime++;
}, 1000);
} catch (error) {
this.$message.error('无法访问麦克风');
console.error('录音错误:', error);
}
},
stopRecording() {
if (this.mediaRecorder && this.isRecording) {
this.mediaRecorder.stop();
this.isRecording = false;
clearInterval(this.timer);
// 停止所有音轨
this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
},
formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
},
reRecord() {
// 释放之前的音频URL
if (this.audioUrl) {
URL.revokeObjectURL(this.audioUrl);
this.audioUrl = null;
}
this.hasRecording = false;
this.audioChunks = [];
this.recordingStatus = '准备就绪';
this.recordingTime = 0; // 重置录音时长
},
async uploadRecording() {
if (!this.hasRecording) return;
try {
const audioBlob = new Blob(this.audioChunks, { type: 'audio/mp3' });
const formData = new FormData();
formData.append('file', audioBlob, `recording_${Date.now()}.mp3`);
// TODO: 替换为实际的上传API
// const response = await uploadFile(formData);
this.$message.success('上传成功');
this.recordings.unshift({
name: `录音_${this.formatTime(this.recordingTime)}`,
time: new Date().toLocaleString()
});
// 重置录音状态
this.reRecord();
} catch (error) {
this.$message.error('上传失败');
console.error('上传错误:', error);
}
},
deleteRecording(index) {
this.recordings.splice(index, 1);
2025-06-06 06:26:26 +08:00
},
handleAudioSwitchChange(val) {
const playEnModel = this.deviceInfo.thingsModels.find(model => model.id === 'play_en');
if (playEnModel) {
playEnModel.shadow = val ? '1' : '0';
this.mqttPublish(this.deviceInfo, playEnModel);
}
},
showAddAudioDialog() {
this.addAudioDialogVisible = true;
this.newAudio = {
remark: '',
per: 0,
spd: 5,
pit: 5,
vol: 5,
tex_utf8: '',
filename: ''
};
},
// 获取未使用的最小ID
getNextAvailableId() {
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model.id === 'mp3_list');
if (mp3ListModel) {
try {
const jsonStr = mp3ListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
if (data.sound_card && data.sound_card.mp3_list) {
// 获取所有已使用的ID
const usedIds = data.sound_card.mp3_list.map(item => {
const id = parseInt(item.split('_')[0]);
return isNaN(id) ? 0 : id;
});
// 找到最小的未使用ID
let nextId = 1;
while (usedIds.includes(nextId)) {
nextId++;
}
return nextId;
}
} catch (error) {
console.error('解析mp3_list失败:', error);
}
}
return 1; // 如果出错返回1
},
submitAudioForm() {
this.$refs.audioForm.validate((valid) => {
if (valid) {
// 找到mp3_list物模型
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model.id === 'mp3_list');
if (mp3ListModel) {
try {
// 获取下一个可用的ID
const nextId = this.getNextAvailableId();
// 生成文件名
const filename = `${nextId}_${this.newAudio.remark}`;
// 构建TTS对象
const ttsData = {
JSON_id: 1,
sound_card: {
TTS: {
per: this.newAudio.per,
spd: this.newAudio.spd,
pit: this.newAudio.pit,
vol: this.newAudio.vol,
tex_utf8: this.newAudio.tex_utf8,
filename: filename
}
}
};
// 更新物模型shadow值
mp3ListModel.shadow = 'JSON=' + JSON.stringify(ttsData);
// 发送更新
this.mqttPublish(this.deviceInfo, mp3ListModel);
this.addAudioDialogVisible = false;
this.$message.success('添加成功');
} catch (error) {
console.error('添加音频失败:', error);
this.$message.error('添加失败');
}
}
}
});
},
handleDeleteAudio(row) {
this.$confirm('确认删除该音频吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 找到mp3_list物模型
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model.id === 'mp3_list');
if (mp3ListModel) {
try {
// 解析当前JSON
const jsonStr = mp3ListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
// 从音频名称中提取完整ID
const audioId = row.name;
// 从mp3_list中移除对应音频
if (data.sound_card && data.sound_card.mp3_list) {
data.sound_card.mp3_list = data.sound_card.mp3_list.filter(item => !item.endsWith(audioId));
// 更新物模型shadow值
const newShadow = 'JSON=' + JSON.stringify(data);
mp3ListModel.shadow = newShadow;
// 发送更新
this.mqttPublish(this.deviceInfo, mp3ListModel).then(() => {
this.$message.success('删除成功');
}).catch(error => {
console.error('发送删除命令失败:', error);
this.$message.error('删除失败');
});
}
} catch (error) {
console.error('解析或更新mp3_list失败:', error);
this.$message.error('删除失败');
}
}
}).catch(() => { });
},
showAddPlaylistDialog() {
this.addPlaylistDialogVisible = true;
this.newPlaylist = {
name: '',
type: '用户',
status: '启用',
audioId: '',
playTimeStart: null,
playTimeEnd: null,
weekdays: [],
radarEnabled: false,
radarSpeedMin: 0,
radarSpeedMax: 120
};
},
submitPlaylistForm() {
this.$refs.playlistForm.validate((valid) => {
if (valid) {
const selectedAudio = this.audioList.find(audio => audio.id === this.newPlaylist.audioId);
this.defaultList.push({
id: this.defaultList.length + 1,
name: selectedAudio ? selectedAudio.name : '',
type: this.newPlaylist.type,
status: this.newPlaylist.status,
playTime: `${this.formatTime(this.newPlaylist.playTimeStart)} - ${this.formatTime(this.newPlaylist.playTimeEnd)}`,
weekdays: this.formatWeekdays(this.newPlaylist.weekdays),
radarEnabled: this.newPlaylist.radarEnabled,
radarSpeed: this.newPlaylist.radarEnabled ?
`${this.newPlaylist.radarSpeedMin}-${this.newPlaylist.radarSpeedMax}km/h` : ''
});
this.addPlaylistDialogVisible = false;
this.$message.success('添加成功');
}
});
},
formatTime(time) {
if (!time) return '';
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
},
formatWeekdays(weekdays) {
const weekMap = {
'0': '周日',
'1': '周一',
'2': '周二',
'3': '周三',
'4': '周四',
'5': '周五',
'6': '周六'
};
return weekdays.map(day => weekMap[day]).join(', ');
},
handleStatusChange(row) {
// TODO: 调用相应的API更新播放列表状态
console.log('状态变更:', row);
},
handleDeletePlaylist(row) {
this.$confirm('确认删除该音频吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const index = this.defaultList.findIndex(item => item.id === row.id);
if (index > -1) {
this.defaultList.splice(index, 1);
}
this.$message.success('删除成功');
}).catch(() => { });
},
formatSpeed(val) {
return val;
},
formatPitch(val) {
return val;
},
formatVolume(val) {
return val;
},
2025-05-30 16:23:45 +08:00
},
};
</script>
<style lang="scss" scoped>
.running-status {
padding: 20px;
.status-col {
.title {
line-height: 28px;
font-size: 16px;
}
}
.mode-section {
margin-bottom: 30px;
}
.mode-card {
margin-bottom: 20px;
transition: all 0.3s;
padding: 20px;
&:hover {
transform: translateY(-5px);
}
.mode-header {
display: flex;
align-items: center;
margin-bottom: 15px;
i,
.svg-icon {
font-size: 20px;
margin-right: 8px;
color: #409EFF;
}
.mode-title {
font-size: 16px;
font-weight: bold;
color: #303133;
}
}
.mode-content {
text-align: center;
padding: 10px 0;
.title {
font-size: 16px;
font-weight: bold;
}
}
}
.settings-card,
.audio-list-card,
.default-list-card {
margin-bottom: 20px;
transition: all 0.3s;
&:hover {
transform: translateY(-5px);
}
.settings-header,
.audio-list-header,
.default-list-header {
display: flex;
justify-content: space-between;
align-items: center;
.settings-title,
.audio-list-title,
.default-list-title {
font-size: 16px;
font-weight: bold;
color: #303133;
}
}
}
.el-table {
margin-top: 15px;
}
.el-slider {
margin-top: 10px;
}
}
.voice-control-card {
margin-bottom: 20px;
transition: all 0.3s;
&:hover {
transform: translateY(-5px);
}
.voice-control-header {
display: flex;
justify-content: space-between;
align-items: center;
.voice-control-title {
font-size: 16px;
font-weight: bold;
color: #303133;
}
}
.voice-control-content {
padding: 20px;
text-align: center;
.recorder-status {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
.status-indicator {
width: 60px;
height: 60px;
border-radius: 50%;
background: #f4f4f5;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
transition: all 0.3s;
i {
font-size: 30px;
color: #909399;
}
&.recording {
background: #fef0f0;
animation: pulse 1.5s infinite;
i {
color: #f56c6c;
}
}
}
.status-text {
font-size: 14px;
color: #606266;
}
}
.timer-display {
font-size: 24px;
font-weight: bold;
color: #303133;
margin-bottom: 20px;
}
.control-buttons {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 20px;
.el-button {
width: 50px;
height: 50px;
font-size: 20px;
}
}
.recording-preview {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
.preview-title {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
text-align: left;
}
.audio-player {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
audio {
width: 100%;
height: 40px;
}
.preview-controls {
display: flex;
justify-content: center;
margin-top: 5px;
.el-button {
padding: 8px 15px;
i {
margin-right: 5px;
}
}
}
}
}
.recording-list {
text-align: left;
border-top: 1px solid #ebeef5;
padding-top: 15px;
.list-title {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
}
.recording-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #ebeef5;
.recording-name {
flex: 1;
font-size: 14px;
color: #303133;
}
.recording-time {
font-size: 12px;
color: #909399;
margin-right: 10px;
}
}
}
}
}
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(245, 108, 108, 0.4);
}
70% {
transform: scale(1.1);
box-shadow: 0 0 0 10px rgba(245, 108, 108, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(245, 108, 108, 0);
}
}
2025-06-06 06:26:26 +08:00
.radar-settings {
margin-top: -20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
.speed-range {
display: flex;
align-items: center;
gap: 10px;
.el-input-number {
width: 120px;
}
.speed-separator {
color: #606266;
font-size: 16px;
}
.speed-unit {
color: #606266;
margin-left: 5px;
}
}
}
.time-range {
display: flex;
align-items: center;
gap: 10px;
.time-separator {
color: #606266;
font-size: 14px;
}
}
2025-05-30 16:23:45 +08:00
</style>