2025-06-06 06:23:49 +08:00

1329 lines
37 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<template>
<view class="voice-control">
<!-- 设备状态卡片 -->
<view class="card">
<view class="status-titletop">{{ title }}</view>
<view style="padding:20rpx;">
<u--form labelPosition="left" labelWidth="100"
:labelStyle="{ marginRight: '16px', lineHeight: '32px', width: '50px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', color: '#000000' }">
<view class="version-wrap">
<u-form-item :label="$tt('status.deviceVersion')">
<u-row>
<u-col span="8">
<u--text :text="'Version ' + device.firmwareVersion"></u--text>
</u-col>
</u-row>
</u-form-item>
</view>
</u--form>
</view>
</view>
<button @click="print()">测试打印</button>
<!-- 基础信息 -->
<view class="card basic-info">
<view class="section-title">基础信息</view>
<view class="info-content">
<!-- 音量控制 -->
<view class="volume-slider">
<view class="volume-icon">
<image src="https://iot-xcwl.cn/doc/photo/voice.svg" mode="aspectFit" class="volume-svg">
</image>
</view>
<view class="slider-container">
<u-slider v-model="volume" :min="0" :max="100" :step="1" @change="volumeChange"
:disabled="device.status !== 3" height="4" activeColor="#2979ff" blockSize="18"
:showValue="false">
</u-slider>
<view class="volume-marks">
<text>0</text>
<text>50</text>
<text>100</text>
</view>
</view>
<view class="volume-value">{{ volume }}%</view>
</view>
<!-- 音频开关 -->
<view class="audio-switch">
<text>音频开关</text>
<u-switch v-model="audioEnabled" @change="audioSwitchChange" :disabled="device.status !== 3"
size="22"></u-switch>
</view>
</view>
</view>
<!-- 音频列表 -->
<view class="card audio-list">
<view class="section-title">
<text>音频列表</text>
<image src="https://iot-xcwl.cn/doc/photo/add.svg" mode="aspectFit" class="add-icon"
@click="showAddAudioModal"></image>
</view>
<view class="list-container">
<view class="empty-tip" v-if="audioList.length === 0">
<u-icon name="music" size="50" color="#c0c4cc"></u-icon>
<text>暂无音频文件</text>
</view>
<view class="audio-item" v-for="(item, index) in audioList" :key="index">
<view class="audio-info">
<u-icon name="play-right" size="18" color="#2979ff"></u-icon>
<text class="audio-name">{{ item.name }}</text>
</view>
<view class="audio-actions">
<text class="audio-duration">{{ item.duration }}</text>
<u-icon name="trash" size="18" color="#ff4d4f" @click="deleteAudio(index)"></u-icon>
</view>
</view>
</view>
</view>
<!-- 默认音频列表 -->
<view class="card default-list">
<view class="section-title">
<text>默认列表</text>
<image src="https://iot-xcwl.cn/doc/photo/add.svg" mode="aspectFit" class="add-icon"
@click="showAddDefaultModal"></image>
</view>
<view class="list-container">
<view class="empty-tip" v-if="defaultList.length === 0">
<u-icon name="star" size="50" color="#c0c4cc"></u-icon>
<text>暂无默认音频</text>
</view>
<view class="audio-item" v-for="(item, index) in defaultList" :key="index">
<view class="audio-info">
<u-icon name="star-fill" size="18" color="#ff9900"></u-icon>
<text class="audio-name">{{ item.name }}</text>
</view>
<view class="audio-actions">
<text class="audio-duration">{{ item.duration }}</text>
<u-icon name="trash" size="18" color="#ff4d4f" @click="deleteDefault(index)"></u-icon>
</view>
</view>
</view>
</view>
<!-- 远程喊话 -->
<view class="card remote-talk">
<view class="section-title">远程喊话</view>
<view class="talk-container">
<view class="talk-tip">按住按钮开始录音松开结束录音</view>
<view class="talk-button" :class="{ recording: isRecording }" @touchstart="startRecording"
@touchend="stopRecording" @touchcancel="cancelRecording">
<u-icon :name="isRecording ? 'mic-filled' : 'mic'" size="28"
:color="isRecording ? '#ff4d4f' : '#2979ff'"></u-icon>
<text>{{ isRecording ? '录音中...' : '按住说话' }}</text>
</view>
</view>
</view>
<!-- 添加音频弹窗 -->
<u-popup :show="showAddAudio" mode="center" @close="showAddAudio = false" round="8">
<view class="add-audio-modal">
<view class="modal-title">添加音频</view>
<view class="modal-content">
<u-form :model="newAudio" ref="audioForm">
<u-form-item label="备注名" prop="name" borderBottom>
<u-input v-model="newAudio.name" placeholder="请输入备注名"></u-input>
</u-form-item>
<u-form-item label="主持人" prop="per" borderBottom>
<u-radio-group v-model="newAudio.per">
<u-radio label="小美" name="0"></u-radio>
<u-radio label="小宇" name="1"></u-radio>
<u-radio label="逍遥" name="3"></u-radio>
<u-radio label="丫丫" name="4"></u-radio>
</u-radio-group>
</u-form-item>
<u-form-item label="合成语速" prop="spd" borderBottom>
<view class="slider-with-value">
<u-slider v-model="newAudio.spd" :min="0" :max="15" :step="1" :showValue="true" class="custom-slider"></u-slider>
</view>
</u-form-item>
<u-form-item label="合成音调" prop="pit" borderBottom>
<view class="slider-with-value">
<u-slider v-model="newAudio.pit" :min="0" :max="15" :step="1" :showValue="true" class="custom-slider"></u-slider>
</view>
</u-form-item>
<u-form-item label="合成音量" prop="vol" borderBottom>
<view class="slider-with-value">
<u-slider v-model="newAudio.vol" :min="0" :max="15" :step="1" :showValue="true" class="custom-slider"></u-slider>
</view>
</u-form-item>
<u-form-item label="合成文本" prop="text" borderBottom>
<u-input v-model="newAudio.text" type="textarea" placeholder="请输入合成文本" height="100"></u-input>
</u-form-item>
</u-form>
</view>
<view class="modal-footer">
<u-button @click="showAddAudio = false" plain size="small">取消</u-button>
<u-button type="primary" @click="confirmAddAudio" size="small">确定</u-button>
</view>
</view>
</u-popup>
<!-- 添加默认音频弹窗 -->
<u-popup :show="showAddDefault" mode="center" @close="showAddDefault = false" round="8">
<view class="add-audio-modal">
<view class="modal-title">添加默认音频</view>
<view class="modal-content">
<u-form :model="newDefault" ref="defaultForm">
<u-form-item label="开始时间" prop="startTime" borderBottom>
<picker mode="time" :value="newDefault.startTime" start="00:00" end="23:59"
@change="startTimeChange">
<view class="picker-value">{{ newDefault.startTime || '请选择开始时间' }}</view>
</picker>
</u-form-item>
<u-form-item label="结束时间" prop="endTime" borderBottom>
<picker mode="time" :value="newDefault.endTime" start="00:00" end="23:59"
@change="endTimeChange">
<view class="picker-value">{{ newDefault.endTime || '请选择结束时间' }}</view>
</picker>
</u-form-item>
<u-form-item label="重复" prop="repeat" borderBottom>
<view class="week-picker">
<view class="week-item" v-for="(day, index) in weekDays" :key="index"
:class="{ active: newDefault.repeatDays.includes(index) }"
@click="toggleWeekDay(index)">
{{ day }}
</view>
</view>
</u-form-item>
<u-form-item label="雷达开关" prop="radarEnabled" borderBottom>
<u-switch v-model="newDefault.radarEnabled" @change="radarSwitchChange"></u-switch>
</u-form-item>
<template v-if="newDefault.radarEnabled">
<u-form-item label="速度范围" prop="speedRange" borderBottom>
<view class="speed-range">
<u-input v-model="newDefault.minSpeed" type="number" placeholder="最小速度"></u-input>
<text class="separator">-</text>
<u-input v-model="newDefault.maxSpeed" type="number" placeholder="最大速度"></u-input>
<text class="unit">km/h</text>
</view>
</u-form-item>
</template>
<u-form-item label="选择音频" prop="audioFile" borderBottom>
<picker :range="audioList" range-key="name" :value="audioIndex" @change="audioChange">
<view class="picker-value">{{ selectedAudioName || '请选择音频' }}</view>
</picker>
</u-form-item>
</u-form>
</view>
<view class="modal-footer">
<u-button @click="showAddDefault = false" plain size="small">取消</u-button>
<u-button type="primary" @click="confirmAddDefault" size="small">确定</u-button>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import {
serviceInvoke
} from '@/apis/modules/runtime.js';
export default {
name: 'VoiceControl',
props: {
device: {
type: Object,
required: true
}
},
watch: {
// 兼容小程序
device: function(newVal, oldVal) {
if (newVal.deviceName !== '') {
this.deviceInfo = newVal;
if (this.deviceInfo.deviceType != 3) {
//分-属性和操作
this.operateList = this.deviceInfo.thingsModels && this.deviceInfo.thingsModels.filter((
item) =>
item.isReadonly == '0');
this.attributeList = this.deviceInfo.thingsModels && this.deviceInfo.thingsModels.filter((
item) =>
item.isReadonly == '1');
//排序
this.attributeList = this.attributeList.sort((a, b) => b.order - a.order);
this.operateList = this.operateList.sort((a, b) => b.order - a.order);
}
this.updateDeviceStatus(this.deviceInfo);
this.updateBasicSettings();
console.log("wumoxing", JSON.stringify(this.deviceInfo.thingsModels))
}
}
},
data() {
return {
title: '设备离线',
volume: 50,
audioEnabled: true,
isRecording: false,
recorderManager: null,
tempFilePath: '',
showAddAudio: false,
showAddDefault: false,
showStartTimePicker: false,
showEndTimePicker: false,
showAudioPicker: false,
weekDays: ['日', '一', '二', '三', '四', '五', '六'],
audioIndex: -1,
newAudio: {
name: '',
per: '0',
spd: '5',
pit: '5',
vol: '5',
text: '',
file: null
},
deviceInfo: {
chartList: [],
},
newDefault: {
startTime: '',
endTime: '',
repeatDays: [],
radarEnabled: false,
minSpeed: '',
maxSpeed: '',
audioFile: null
},
audioFiles: [],
audioList: [{
name: '欢迎语.mp3',
duration: '00:15'
},
{
name: '提示音.mp3',
duration: '00:05'
},
{
name: '背景音乐.mp3',
duration: '03:45'
}
],
defaultList: [{
name: '默认欢迎语.mp3',
duration: '00:10'
},
{
name: '默认提示音.mp3',
duration: '00:03'
}
]
};
},
created() {
if (this.device !== null && Object.keys(this.device).length !== 0) {
this.deviceInfo = this.device;
this.updateDeviceStatus(this.deviceInfo);
// this.initChart();
// console.log("deviceinfo", JSON.stringify(this.deviceInfo))
};
this.mqttCallback();
this.recorderManager = uni.getRecorderManager();
this.initRecorder();
this.updateBasicSettings();
// this.print();
},
methods: {
print() {
console.log("测试打印", JSON.stringify(this.deviceInfo.thingsModels))
},
checkOnline(callback, ...args) {
if (this.device.status !== 3) {
uni.showToast({
title: '设备离线,无法操作',
icon: 'none'
});
return false;
}
if (typeof callback === 'function') {
return callback.apply(this, args);
}
return true;
},
async volumeChange(value) {
if (!this.checkOnline()) return;
try {
const volumeModel = this.device.thingsModels.find(item => item.id === 'volume');
if (volumeModel) {
volumeModel.shadow = value.toString();
await this.mqttPublish(this.device, volumeModel);
}
} catch (error) {
console.error('调整音量失败:', error);
uni.showToast({
title: '操作失败: ' + error.message,
icon: 'none'
});
}
},
/* Mqtt回调处理 */
mqttCallback() {
this.$mqttTool.client.on('message', (topic, message, buffer) => {
let topics = topic.split('/');
let productId = topics[1];
let deviceNum = topics[2];
message = JSON.parse(message.toString());
if (topics[3] == 'status') {
console.log('接收到【设备状态-运行】主题:', topic);
console.log('接收到【设备状态-运行】内容:', message);
// 更新列表中设备的状态
if (this.deviceInfo.serialNumber == deviceNum) {
this.deviceInfo.status = message.status;
this.deviceInfo.isShadow = message.isShadow;
this.deviceInfo.rssi = message.rssi;
this.updateDeviceStatus(this.deviceInfo);
this.updateBasicSettings();
console.log("wumoxing", JSON.stringify(this.deviceInfo.thingsModels))
}
}
//兼容设备回复
if (topics[4] == 'reply') {
uni.showToast({
icon: 'none',
title: message,
})
}
if (topics[3] == 'property' || topics[3] == 'function' || topic.endsWith(
'ws/service')) {
console.log('接收到【物模型】主题:', topic);
console.log('接收到【物模型】内容:', message);
// 更新列表中设备的属性
if (this.deviceInfo.serialNumber == deviceNum) {
for (let j = 0; j < message.message.length; j++) {
let isComplete = false;
// 设备状态
for (let k = 0; k < this.deviceInfo.thingsModels.length && !
isComplete; k++) {
if (this.deviceInfo.thingsModels[k].id == message.message[j].id) {
// 普通类型
this.deviceInfo.thingsModels[k].shadow = message.message[j].value;
if (message.message[j].id === 'mp3_list') {
console.log('收到mp3_list更新:', message.message[j].value);
}
isComplete = true;
break;
} else 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 == message.message[j]
.id) {
this.deviceInfo.thingsModels[k].datatype.params[n]
.shadow =
message.message[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(message.message[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 == message.message[j].id) {
this.deviceInfo.thingsModels[k]
.datatype.arrayParams[n]
[m].shadow = message.message[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 + message.message[j].id) {
this.deviceInfo.thingsModels[k]
.datatype.arrayParams[n]
[m].shadow = message.message[j].value;
isComplete = true;
}
}
if (isComplete) {
break;
}
}
}
} else {
// 整数、小数和字符串类型数组
for (let n = 0; n < this.deviceInfo.thingsModels[k]
.datatype.arrayModel
.length; n++) {
if (this.deviceInfo.thingsModels[k].datatype
.arrayModel[n].id ==
message.message[j].id) {
this.deviceInfo.thingsModels[k].datatype
.arrayModel[n].shadow =
message.message[j].value;
isComplete = true;
break;
}
}
}
}
};
// 监测数据
for (let k = 0; k < this.deviceInfo.chartList.length && !
isComplete; k++) {
if (this.deviceInfo.chartList[k].id.indexOf("array_") == 0) {
// 数组类型匹配,例如array_00_gateway_temperature
if (this.deviceInfo.chartList[k].id == message.message[j].id) {
// let shadows = message[j].value.split(",");
this.deviceInfo.chartList[k].shadow = message.message[j].value;
// 更新图表
for (let m = 0; m < this.monitorChart.length; m++) {
if (this.deviceInfo.chartList[k].id == this
.monitorChart[m].id) {
// uchart中data取值范围0-1需要最小数+监测值,然后除于区间值
let value = (Number(message.message[j].value) + Math
.abs(this
.deviceInfo.chartList[k].datatype
.min)) / (Math.abs(
this.deviceInfo.chartList[k]
.datatype.min) + Math
.abs(this.deviceInfo.chartList[k]
.datatype.max));
this.monitorChart[m].data.series[0].data =
value;
this.monitorChart[m].opts.title.name = message.message[
j].value + ' ' +
this.deviceInfo.chartList[k].datatype.unit;
break;
}
}
isComplete = true;
break;
}
} else {
// 普通类型匹配
if (this.deviceInfo.chartList[k].id == message.message[j].id) {
this.deviceInfo.chartList[k].shadow = message.message[j].value;
// 更新图表
for (let m = 0; m < this.monitorChart.length; m++) {
if (this.deviceInfo.chartList[k].id == this
.monitorChart[m].id) {
// uchart中data取值范围0-1需要最小数+监测值,然后除于区间值
let value = (Number(message.message[j].value) + Math
.abs(this
.deviceInfo.chartList[k].datatype
.min)) / (Math.abs(
this.deviceInfo.chartList[k]
.datatype.min) + Math
.abs(this.deviceInfo.chartList[k]
.datatype.max));
this.monitorChart[m].data.series[0].data =
value;
this.monitorChart[m].opts.title.name = message.message[
j].value + ' ' +
this.deviceInfo.chartList[k].datatype.unit;
break;
}
}
isComplete = true;
break;
}
}
if (isComplete) {
break;
}
};
}
}
this.updateBasicSettings();
}
});
},
async mqttPublish(device, model) {
const command = {};
command[model.id] = model.shadow;
const data = {
serialNumber: device.serialNumber,
productId: device.productId,
remoteCommand: command,
identifier: model.id,
modelName: model.name,
isShadow: device.status != 3,
type: model.type
};
serviceInvoke(data).then(response => {
if (response.code === 200) {
uni.showToast({
icon: 'none',
title: this.$tt('status.service')
});
}
});
},
updateDeviceStatus(device) {
if (device.status === 3) {
this.title = this.$tt('status.online');
} else {
this.title = device.isShadow === 1 ? this.$tt('status.shadow') : this.$tt('status.deviceOffline');
}
},
initRecorder() {
this.recorderManager.onStart(() => console.log('录音开始'));
this.recorderManager.onStop(res => {
console.log('录音结束', res);
this.tempFilePath = res.tempFilePath;
this.uploadAudio(res.tempFilePath);
});
this.recorderManager.onError(res => {
console.error('录音错误', res);
uni.showToast({
title: '录音失败',
icon: 'none'
});
});
},
startRecording() {
if (!this.checkOnline()) return;
this.isRecording = true;
this.recorderManager.start({
format: 'mp3',
duration: 60000,
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
frameSize: 1
});
},
stopRecording() {
if (!this.isRecording) return;
this.isRecording = false;
this.recorderManager.stop();
},
cancelRecording() {
if (!this.isRecording) return;
this.isRecording = false;
this.recorderManager.stop();
uni.showToast({
title: '已取消录音',
icon: 'none'
});
},
async uploadAudio(filePath) {
try {
uni.showLoading({
title: '正在上传...'
});
const uploadRes = await uni.uploadFile({
url: 'YOUR_UPLOAD_API_URL',
filePath: filePath,
name: 'file',
formData: {
deviceId: this.device.deviceId
}
});
if (uploadRes.statusCode === 200) {
const result = JSON.parse(uploadRes.data);
const audioListModel = this.device.thingsModels.find(item => item.id === 'audio_list');
if (audioListModel) {
audioListModel.shadow = JSON.stringify({
...JSON.parse(audioListModel.shadow || '{}'),
files: [...(JSON.parse(audioListModel.shadow || '{}').files || []), {
name: result.fileName,
url: result.fileUrl,
duration: result.duration
}]
});
await this.mqttPublish(this.device, audioListModel);
}
uni.showToast({
title: '上传成功',
icon: 'success'
});
} else {
throw new Error('上传失败');
}
} catch (error) {
console.error('上传音频失败:', error);
uni.showToast({
title: '上传失败: ' + error.message,
icon: 'none'
});
} finally {
uni.hideLoading();
}
},
showAddAudioModal() {
this.showAddAudio = true;
},
showAddDefaultModal() {
this.showAddDefault = true;
},
async confirmAddAudio() {
if (!this.checkOnline()) return;
try {
// 获取当前的 mp3_list 模型
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model.id === 'mp3_list');
if (!mp3ListModel) {
throw new Error('未找到 mp3_list 模型');
}
// 解析当前的 mp3_list 数据
const jsonStr = mp3ListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
// 获取当前最大的 id
let maxId = 0;
if (data.sound_card && data.sound_card.mp3_list) {
data.sound_card.mp3_list.forEach(item => {
const id = parseInt(item.split('_')[0]);
if (!isNaN(id) && id > maxId) {
maxId = id;
}
});
}
// 生成新的 id
const newId = maxId + 1;
// 构建 TTS 请求数据
const ttsData = {
JSON_id: 1,
sound_card: {
TTS: {
per: parseInt(this.newAudio.per),
spd: parseInt(this.newAudio.spd),
pit: parseInt(this.newAudio.pit),
vol: parseInt(this.newAudio.vol),
tex_utf8: this.newAudio.text,
filename: `${newId}_${this.newAudio.name}`
}
}
};
// 更新 shadow 值并发送
mp3ListModel.shadow = 'JSON=' + JSON.stringify(ttsData);
await this.mqttPublish(this.device, mp3ListModel);
// 重置表单
this.newAudio = {
name: '',
per: '0',
spd: '5',
pit: '5',
vol: '5',
text: '',
file: null
};
// 关闭弹窗
this.showAddAudio = false;
uni.showToast({
title: '添加成功',
icon: 'success'
});
} catch (error) {
console.error('添加音频失败:', error);
uni.showToast({
title: '添加失败: ' + error.message,
icon: 'none'
});
}
},
confirmAddDefault() {
if (!this.checkOnline()) return;
uni.showToast({
title: '默认音频添加成功',
icon: 'success'
});
this.showAddDefault = false;
},
async deleteAudio(index) {
if (!this.checkOnline()) return;
try {
uni.showModal({
title: '提示',
content: '确定要删除该音频吗?',
cancelText: '取消',
confirmText: '确定',
success: async (res) => {
if (res.confirm) {
// 获取当前的 mp3_list 模型
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);
// 从音频列表中删除指定索引的项
if (data.sound_card && data.sound_card.mp3_list) {
data.sound_card.mp3_list.splice(index, 1);
// 更新 shadow 值
mp3ListModel.shadow = 'JSON=' + JSON.stringify(data);
// 发送更新到设备
await this.mqttPublish(this.device, mp3ListModel);
// 更新本地音频列表
this.audioList.splice(index, 1);
uni.showToast({
title: '删除成功',
icon: 'success'
});
}
} catch (error) {
console.error('删除音频失败:', error);
uni.showToast({
title: '删除失败: ' + error.message,
icon: 'none'
});
}
}
}
}
});
} catch (error) {
console.error('删除音频失败:', error);
uni.showToast({
title: '删除失败: ' + error.message,
icon: 'none'
});
}
},
deleteDefault(index) {
uni.showModal({
title: '提示',
content: '确定要删除该默认音频吗?',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
this.defaultList.splice(index, 1);
}
}
});
},
afterAudioRead(file) {
this.audioFiles.push(file);
},
deleteAudioFile(index) {
this.audioFiles.splice(index, 1);
},
beforeAudioUpload(file) {
const isValidType = file.name.toLowerCase().endsWith('.mp3');
const isValidSize = file.size / 1024 / 1024 < 10;
if (!isValidType) {
uni.showToast({
title: '只能上传MP3格式',
icon: 'none'
});
return false;
}
if (!isValidSize) {
uni.showToast({
title: '文件大小不能超过10MB',
icon: 'none'
});
return false;
}
return true;
},
async audioSwitchChange() {
if (!this.checkOnline()) return;
try {
const playEnModel = this.device.thingsModels.find(item => item.id === 'play_en');
if (playEnModel) {
playEnModel.shadow = this.audioEnabled ? '1' : '0';
await this.mqttPublish(this.device, playEnModel);
}
} catch (error) {
console.error('切换音频开关失败:', error);
uni.showToast({
title: '操作失败: ' + error.message,
icon: 'none'
});
}
},
formatTime(timestamp) {
const date = new Date(timestamp);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
},
startTimeChange(e) {
this.newDefault.startTime = e.detail.value;
},
endTimeChange(e) {
this.newDefault.endTime = e.detail.value;
},
toggleWeekDay(index) {
const position = this.newDefault.repeatDays.indexOf(index);
if (position === -1) {
this.newDefault.repeatDays.push(index);
} else {
this.newDefault.repeatDays.splice(position, 1);
}
},
radarSwitchChange(value) {
this.newDefault.radarEnabled = value;
if (!value) {
this.newDefault.minSpeed = '';
this.newDefault.maxSpeed = '';
}
},
audioChange(e) {
this.audioIndex = e.detail.value;
this.newDefault.audioFile = this.audioList[this.audioIndex];
},
updateBasicSettings() {
if (!this.deviceInfo.thingsModels) return;
// 更新音频开关状态
const playEnModel = this.deviceInfo.thingsModels.find(model => model.id === 'play_en');
if (playEnModel) {
this.audioEnabled = playEnModel.shadow === '1';
}
// 更新音量设置
const volumeModel = this.deviceInfo.thingsModels.find(model => model.id === 'volume');
if (volumeModel) {
this.volume = parseInt(volumeModel.shadow) || 50;
}
// 更新音频列表
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model.id === 'mp3_list');
console.log('mp3ListModel:', mp3ListModel);
if (mp3ListModel && mp3ListModel.shadow) {
try {
// 解析 JSON 字符串
const jsonStr = mp3ListModel.shadow.replace('JSON=', '');
console.log('jsonStr:', jsonStr);
const data = JSON.parse(jsonStr);
console.log('parsed data:', data);
// 获取 mp3_list 数组
if (data.sound_card && data.sound_card.mp3_list) {
console.log('mp3_list:', data.sound_card.mp3_list);
// 更新音频列表
this.audioList = data.sound_card.mp3_list.map((item, index) => {
// 从 "1_def" 格式中提取名称
const name = item.split('_')[1] || item;
console.log('item:', item, 'name:', name);
return {
id: index + 1,
name: name
};
});
console.log('updated audioList:', this.audioList);
} else {
console.log('no mp3_list found in data');
}
} catch (error) {
console.error('解析音频列表失败:', error);
console.error('原始数据:', mp3ListModel.shadow);
}
} else {
console.log('no mp3ListModel or shadow is empty');
}
}
},
computed: {
startTimeLabel() {
return this.newDefault.startTime ? this.formatTime(this.newDefault.startTime) : '请选择开始时间';
},
endTimeLabel() {
return this.newDefault.endTime ? this.formatTime(this.newDefault.endTime) : '请选择结束时间';
},
selectedAudioName() {
return this.audioIndex >= 0 ? this.audioList[this.audioIndex].name : '';
}
}
};
</script>
<style lang="scss" scoped>
// @import "@/uni_modules/uview-ui/scss/index.scss";
.voice-control {
padding: 20rpx;
background-color: #f7f8fa;
min-height: 100vh;
.card {
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.status-titletop {
font-weight: bold;
font-size: 15px;
color: #333333;
line-height: 42rpx;
text-align: left;
padding: 15rpx 28rpx;
background-color: #eef6ff;
border-bottom: 1rpx solid #dceaff;
}
.version-wrap {
background-color: #F7F7F7;
border-radius: 10rpx;
padding: 0 42rpx;
font-size: 24rpx;
color: #000000;
line-height: 42rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
padding: 24rpx;
border-bottom: 1rpx solid #f5f5f5;
display: flex;
align-items: center;
.add-icon {
width: 40rpx;
height: 40rpx;
margin-left: auto;
}
}
.info-content {
padding: 16rpx 24rpx 24rpx;
}
.volume-slider {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 20rpx;
padding: 16rpx 0;
background-color: #f9f9f9;
border-radius: 16rpx;
.volume-icon {
width: 40rpx;
height: 40rpx;
display: flex;
justify-content: center;
align-items: center;
.volume-svg {
width: 40rpx;
height: 40rpx;
}
}
.slider-container {
flex: 1;
position: relative;
.volume-marks {
display: flex;
justify-content: space-between;
margin-top: 8rpx;
padding: 0 8rpx;
text {
font-size: 22rpx;
color: #999;
}
}
}
.volume-value {
min-width: 72rpx;
text-align: right;
color: #2979ff;
font-size: 26rpx;
font-weight: 500;
}
}
.audio-switch {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 0;
text {
font-size: 28rpx;
color: #333;
}
}
.audio-list,
.default-list {
.list-container {
padding: 0 24rpx 16rpx;
}
.empty-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
color: #c0c4cc;
font-size: 28rpx;
gap: 20rpx;
opacity: 0.8;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
opacity: 0.6;
}
}
.audio-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.audio-info {
display: flex;
align-items: center;
gap: 16rpx;
flex: 1;
overflow: hidden;
.audio-name {
font-size: 26rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.audio-actions {
display: flex;
align-items: center;
gap: 24rpx;
margin-left: 16rpx;
.audio-duration {
font-size: 24rpx;
color: #999;
min-width: 72rpx;
text-align: right;
}
}
}
}
.remote-talk {
.talk-container {
padding: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
.talk-tip {
font-size: 26rpx;
color: #999;
text-align: center;
}
.talk-button {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background-color: #f0f6ff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10rpx;
transition: all 0.3s ease;
&.recording {
background-color: #fff1f0;
transform: scale(1.05);
}
text {
font-size: 24rpx;
color: #666;
}
}
}
}
.add-audio-modal {
width: 80vw;
max-width: 600rpx;
padding: 0;
border-radius: 16rpx;
background-color: #fff;
overflow: hidden;
.modal-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
padding: 28rpx 28rpx 20rpx;
text-align: center;
border-bottom: 1rpx solid #f5f5f5;
}
.modal-content {
padding: 0 28rpx;
margin: 20rpx 0;
max-height: 60vh;
overflow-y: auto;
.u-form-item {
padding: 20rpx 0;
:deep(.u-form-item__body) {
padding: 0;
}
:deep(.u-form-item__body__left) {
width: 160rpx;
}
:deep(.u-form-item__body__right) {
flex: 1;
}
}
.slider-with-value {
display: flex;
align-items: center;
width: 100%;
.custom-slider {
flex: 1;
margin-right: 20rpx;
}
}
:deep(.u-radio-group) {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
}
.modal-footer {
display: flex;
justify-content: space-between;
padding: 16rpx 28rpx 28rpx;
border-top: 1rpx solid #f5f5f5;
.u-button {
width: 48%;
height: 72rpx;
font-size: 26rpx;
border-radius: 40rpx;
}
}
}
.week-picker {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
padding: 10rpx 0;
.week-item {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 30rpx;
background-color: #f5f5f5;
font-size: 24rpx;
color: #666;
transition: all 0.3s ease;
&.active {
background-color: #2979ff;
color: #fff;
}
}
}
.speed-range {
display: flex;
align-items: center;
gap: 10rpx;
.u-input {
flex: 1;
}
.separator {
color: #666;
padding: 0 10rpx;
}
.unit {
color: #666;
font-size: 24rpx;
margin-left: 10rpx;
}
}
.picker-value {
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
color: #333;
padding: 0 20rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
}
}
</style>