1329 lines
37 KiB
Vue
Raw Normal View History

2025-06-06 06:23:49 +08:00
<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>