2025-07-23 14:34:57 +08:00

1642 lines
46 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>
<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://xaznkj.cn/doc/photo/brightness.png" 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" style="margin-top: 20rpx;">
<text>屏幕开关</text>
<u-switch v-model="audioEnabled" @change="audioSwitchChange" :disabled="device.status !== 3"
size="22"></u-switch>
</view>
<!-- 屏幕参数 -->
<view class="clear-screen-btn-wrap">
<u-button type="error" size="medium" shape="circle" @click="clearScreen"
customStyle="box-shadow:0 4rpx 12rpx rgba(255,77,79,0.15);font-weight:600;">
<u-icon name="trash" size="18" color="#fff" style="margin-right:8rpx;" />
清除屏幕
</u-button>
</view>
</view>
</view>
<!-- 屏幕参数卡片 -->
<view class="card screen-params">
<view class="section-title">屏幕参数</view>
<view class="screen-params-form">
<u-cell-group>
<picker mode="selector" :range="templateColumns" range-key="label" @change="onNativePickerChange">
<view class="picker-cell u-cell u-cell--border-bottom"
style="display:flex;align-items:center;justify-content:space-between;padding:24rpx 32rpx;margin-bottom:12rpx;">
<text style="font-size:30rpx;color:#333;">屏幕模板</text>
<text style="font-size:30rpx;color:#2979ff;">{{ screenParams.templateLabel || '请选择屏幕模板'
}}</text>
</view>
</picker>
<u-cell @click="showPicker('screenRotate')" title="屏幕旋转"
:value="screenParams.screenRotateLabel || '请选择屏幕旋转'" isLink></u-cell>
</u-cell-group>
<u-form :model="screenParams" labelPosition="left" labelWidth="120">
<u-form-item label="OE极性" prop="oePolarity" borderBottom>
<view style="display:flex;align-items:center;">
<u-switch v-model="screenParams.oePolarity" :active-value="'1'" :inactive-value="'0'"
@change="val => screenParams.oePolarityLabel = val === '1' ? '1' : '0'" size="22"
style="margin-right:12rpx;" />
<u-input v-model="screenParams.oePolarityLabel" readonly border="none" style="flex:1;" />
</view>
</u-form-item>
<u-form-item label="DATA极性" prop="dataPolarity" borderBottom>
<view style="display:flex;align-items:center;">
<u-switch v-model="screenParams.dataPolarity" :active-value="'1'" :inactive-value="'0'"
@change="val => screenParams.dataPolarityLabel = val === '1' ? '1' : '0'" size="22"
style="margin-right:12rpx;" />
<u-input v-model="screenParams.dataPolarityLabel" readonly border="none" style="flex:1;" />
</view>
</u-form-item>
<u-form-item label="屏宽" prop="width" borderBottom>
<u-input v-model="screenParams.width" type="number" placeholder="请输入屏宽" />
</u-form-item>
<u-form-item label="屏高" prop="height" borderBottom>
<u-input v-model="screenParams.height" type="number" placeholder="请输入屏高" />
</u-form-item>
</u-form>
<view style="text-align:center;margin:20rpx 0;">
<u-button type="primary" size="normal" @click="handleSendScreenParams">下发参数</u-button>
</view>
</view>
<!-- <view style="text-align:center;margin:20rpx 0;">
<u-button type="primary" size="mini" @click="testOpenPicker">测试弹窗</u-button>
</view> -->
</view>
<!-- 节目列表 -->
<view class="card audio-list">
<view class="section-title">
<text>节目列表</text>
<image src="https://xaznkj.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="qzone" 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">
<u-switch v-model="item.enabled" :active-value="1" :inactive-value="0"
@change="onProgramEnableChange(index, item.enabled)" size="22"
:disabled="device.status !== 3"></u-switch>
<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>
</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">
<text class="audio-name">{{ item.name }}</text>
</view>
<view class="audio-actions">
<u-switch v-model="item.status" :active-value="'启用'" :inactive-value="'禁用'"
@change="(value) => handleStatusChange(index, value)" size="22"></u-switch>
</view>
</view>
</view>
</view> -->
<!-- 开机参数 -->
<view class="card remote-talk">
<view class="section-title">开机参数</view>
<view class="info-content">
<u-form :model="bootSettings" labelPosition="left" labelWidth="120">
<u-form-item label="开机界面可选">
<u-switch v-model="bootSettings.showBootScreen" @change="handleBootScreenSwitchChange"
size="22"></u-switch>
</u-form-item>
<u-form-item label="重置开关">
<u-switch v-model="bootSettings.resetEnabled" @change="handleResetSwitchChange"
size="22"></u-switch>
</u-form-item>
</u-form>
</view>
</view>
<!-- 传感器阈值卡片 -->
<view class="card sensor-threshold">
<view class="section-title">传感器阈值</view>
<view class="sensor-form">
<u-form :model="sensorThreshold" labelPosition="left" labelWidth="120">
<u-form-item label="速度阈值">
<u-input v-model="sensorThreshold.speed" type="number" placeholder="请输入速度阈值" />
</u-form-item>
<u-form-item label="温度阈值">
<u-input v-model="sensorThreshold.temp" type="number" placeholder="请输入温度阈值" />
</u-form-item>
<u-form-item label="湿度阈值">
<u-input v-model="sensorThreshold.humi" type="number" placeholder="请输入湿度阈值" />
</u-form-item>
</u-form>
<view style="text-align:center;margin:20rpx 0;">
<u-button type="primary" size="normal" @click="handleSendSensorThreshold">下发阈值</u-button>
</view>
</view>
</view>
<!-- 测试按钮 -->
<!-- Pickers -->
<u-picker :show="pickerShow.screenRotate" :columns="[screenRotateOptions]" keyName="label"
@confirm="onPickerConfirm('screenRotate')" @cancel="closePicker('screenRotate')"
safe-area-inset-bottom></u-picker>
<!-- 编译信息卡片 -->
<view class="card build-info">
<view class="section-title" style="display:flex;align-items:center;justify-content:space-between;">
<text>编译信息</text>
<u-icon name="reload" size="32" color="#2979ff" @click="refreshBuildInfo"
style="margin-left:12rpx;cursor:pointer;" />
</view>
<view class="info-content">
<view class="info-item">
<text class="info-label">编译版本</text>
<text class="info-value">{{ buildVersion || '加载中...' }}</text>
</view>
<view class="info-item">
<text class="info-label">编译时间</text>
<text class="info-value">{{ buildTime || '加载中...' }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import {
serviceInvoke
} from '@/apis/modules/runtime.js';
export default {
name: 'VoiceControl',
props: {
device: {
type: Object,
required: true
}
},
data() {
return {
title: '设备离线',
volume: 50,
audioEnabled: true,
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: [],
defaultList: [],
audioUrl: '',
uploadFailed: false,
screenParams: {
template: '',
templateLabel: '',
oePolarity: '0',
oePolarityLabel: '0',
dataPolarity: '0',
dataPolarityLabel: '0',
screenRotate: '',
screenRotateLabel: '',
width: '',
height: ''
},
pickerShow: {
screenRotate: false
},
templateColumns: [
{ label: '自定义', value: '1', params: { oe: '', data: '', w: '', h: '', angle: '' } },
{ label: '伸缩屏', value: '2', params: { oe: 1, data: 1, w: 64, h: 32, angle: 270 } },
{ label: '大屏', value: '3', params: { oe: 1, data: 1, w: 64, h: 32, angle: 180 } },
{ label: '小屏', value: '4', params: { oe: 1, data: 1, w: 64, h: 16, angle: 0 } }
],
screenRotateOptions: [{
label: '0°',
value: '0'
},
{
label: '90°',
value: '90'
},
{
label: '180°',
value: '180'
},
{
label: '270°',
value: '270'
}
],
bootSettings: {
showBootScreen: false,
resetEnabled: false
},
sensorThreshold: {
speed: 60,
temp: 30,
humi: 50
},
buildVersion: '',
buildTime: '',
};
},
created() {
if (this.device !== null && Object.keys(this.device).length !== 0) {
this.deviceInfo = this.device;
this.updateDeviceStatus(this.deviceInfo);
};
this.mqttCallback();
this.recorderManager = uni.getRecorderManager();
this.updateBasicSettings();
// 监听节目数据准备完成事件
uni.$on('programDataReady', this.handleProgramData);
},
beforeDestroy() {
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
// 移除事件监听
uni.$off('programDataReady', this.handleProgramData);
},
methods: {
showPicker(type) {
this.pickerShow[type] = true
},
closePicker(type) {
this.pickerShow[type] = false
},
onPickerConfirm(type, e) {
console.log('Picker confirm:', type, e);
const value = e && e.value && Array.isArray(e.value) && e.value.length > 0 ? e.value[0] : undefined;
if (!value) {
uni.showToast({ title: '选择数据异常', icon: 'none' });
this.closePicker(type);
return;
}
this.screenParams[type] = value.value;
this.screenParams[type + 'Label'] = value.label;
this.closePicker(type);
if (type === 'screenRotate') {
this.handleScreenRotateChange(value.value);
}
},
clearScreen() {
uni.showToast({
title: '已清除屏幕',
icon: 'success'
});
},
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 luminanceModel = this.device.thingsModels.find(item => item.id === '102#luminance');
if (luminanceModel) {
luminanceModel.shadow = value.toString();
await this.mqttPublish(this.device, luminanceModel);
}
} catch (error) {
console.error('调整亮度失败:', error);
uni.showToast({
title: '操作失败: ' + error.message,
icon: 'none'
});
}
},
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') {
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();
}
}
if (topics[4] == 'reply') {
uni.showToast({
icon: 'none',
title: message,
})
}
if (topics[3] == 'property' || topics[3] == 'function' || topic.endsWith('ws/service')) {
if (this.deviceInfo.serialNumber == deviceNum) {
this.updateParam({ serialNumber: this.deviceInfo.serialNumber, productId: this.deviceInfo.productId, data: message.message });
}
}
});
},
// 1. 新增/同步 updateParam 方法,接收设备上报后刷新页面数据
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') {
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 {
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) {
if (this.deviceInfo.chartList[k].id == data[j].id) {
this.deviceInfo.chartList[k].shadow = data[j].value;
}
} else {
if (this.deviceInfo.chartList[k].id == data[j].id) {
this.deviceInfo.chartList[k].shadow = data[j].value;
}
}
if (isComplete) {
break;
}
}
}
}
this.updateBasicSettings();
},
// 2. 同步 updateBasicSettings 方法,解析新版物模型 shadow 字段,字段与电脑版保持一致
updateBasicSettings() {
if (!this.deviceInfo.thingsModels) return;
// 屏幕开关
const screenEnModel = this.deviceInfo.thingsModels.find(model => model.id === '102#screenEn');
if (screenEnModel) {
this.audioEnabled = screenEnModel.shadow === '1';
}
// 屏幕亮度
const luminanceModel = this.deviceInfo.thingsModels.find(model => model.id === '102#luminance');
if (luminanceModel) {
this.volume = parseInt(luminanceModel.shadow) || 50;
}
// 屏幕参数
const scrParamModel = this.deviceInfo.thingsModels.find(model => model.id === '102#scrParam');
if (scrParamModel && scrParamModel.shadow) {
try {
const jsonStr = scrParamModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
if (data.scr_param) {
this.screenParams.oePolarity = data.scr_param.oe;
this.screenParams.dataPolarity = data.scr_param.data;
this.screenParams.width = data.scr_param.w;
this.screenParams.height = data.scr_param.h;
this.screenParams.screenRotate = data.scr_param.angle;
}
} catch (error) {
console.error('解析屏幕参数失败:', error);
}
}
// 节目列表
const progListModel = this.deviceInfo.thingsModels.find(model => model.id === '102#progList');
if (progListModel && progListModel.shadow) {
try {
const jsonStr = progListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
if (data.prog_list && data.prog_list.length > 0) {
this.audioList = data.prog_list.map((program, index) => {
return {
id: index + 1,
name: program.rem ? program.rem.replace(/\0/g, '') : '未命名节目',
duration: program.dur,
mode: program.areaM,
zones: program.aLst ? program.aLst.length : 0,
content: program.aLst ? JSON.stringify(program.aLst) : '',
enabled: 1,
raw: program
};
});
} else {
this.audioList = [];
}
} catch (error) {
console.error('解析节目列表失败:', error);
this.audioList = [];
}
}
// 节目使能状态
const progEnModel = this.deviceInfo.thingsModels.find(model => model.id === '102#progEn');
let enableArr = [];
if (progEnModel && progEnModel.shadow) {
try {
const jsonStr = progEnModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
if (data.prog_en && Array.isArray(data.prog_en)) {
enableArr = data.prog_en;
}
} catch (e) { }
}
if (progListModel && progListModel.shadow) {
try {
const jsonStr = progListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
if (data.prog_list && data.prog_list.length > 0) {
this.audioList = data.prog_list.map((program, index) => {
return {
id: index + 1,
name: program.rem ? program.rem.replace(/\0/g, '') : '未命名节目',
duration: program.dur,
mode: program.areaM,
zones: program.aLst ? program.aLst.length : 0,
content: program.aLst ? JSON.stringify(program.aLst) : '',
enabled: enableArr[index] === 1 ? 1 : 0,
raw: program
};
});
} else {
this.audioList = [];
}
} catch (error) {
console.error('解析节目列表失败:', error);
this.audioList = [];
}
}
// 开机参数
const bootEnModel = this.deviceInfo.thingsModels.find(model => model.id === '102#bootEn');
if (bootEnModel) {
this.bootSettings.showBootScreen = bootEnModel.shadow === '1';
}
const factoryEnModel = this.deviceInfo.thingsModels.find(model => model.id === '102#factoryEn');
if (factoryEnModel) {
this.bootSettings.resetEnabled = factoryEnModel.shadow === '1';
}
// 传感器阈值
const snsrThrModel = this.deviceInfo.thingsModels.find(model => model.id === '102#snsrThr');
if (snsrThrModel && snsrThrModel.shadow) {
try {
const jsonStr = snsrThrModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
if (data.snsr_thr) {
this.sensorThreshold.speed = data.snsr_thr.speed;
this.sensorThreshold.temp = data.snsr_thr.temp;
this.sensorThreshold.humi = data.snsr_thr.humi;
}
} catch (error) {
console.error('解析传感器阈值失败:', error);
}
}
// 编译信息
const infoModel = this.deviceInfo.thingsModels.find(model => model.id === '102#info');
if (infoModel && infoModel.shadow) {
try {
const jsonStr = infoModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
this.buildVersion = data.versions || '未知版本';
this.buildTime = data.compile_time || '未知时间';
} catch (e) {
this.buildVersion = '未知版本';
this.buildTime = '未知时间';
}
} else {
this.buildVersion = '未知版本';
this.buildTime = '未知时间';
}
},
// 3. 同步 mqttPublish 方法,参数结构与电脑版一致,适配移动端
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) {
// uni.$u.toast(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,
}
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');
}
},
showAddAudioModal() {
// 将设备信息存储到本地,供 addProgram 页面使用
uni.setStorageSync('currentDevice', this.device);
uni.setStorageSync('currentDeviceInfo', this.deviceInfo);
uni.navigateTo({
url: '/pagesA/home/device/status/addProgram'
});
console.log('页面跳转');
},
showAddDefaultModal() {
this.showAddDefault = true;
},
async confirmAddAudio() {
if (!this.checkOnline()) return;
try {
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model.id ===
'103#mp3List');
if (!mp3ListModel) {
throw new Error('未找到 mp3_list 模型');
}
const jsonStr = mp3ListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
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;
}
});
}
const newId = maxId + 1;
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}`
}
}
};
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'
});
}
},
async confirmAddDefault() {
if (!this.checkOnline()) return;
if (!this.newDefault.startTime || !this.newDefault.endTime || !this.newDefault.audioFile) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
});
return;
}
if (this.newDefault.radarEnabled) {
if (!this.newDefault.minSpeed || !this.newDefault.maxSpeed) {
uni.showToast({
title: '请填写速度范围',
icon: 'none'
});
return;
}
if (parseInt(this.newDefault.minSpeed) >= parseInt(this.newDefault.maxSpeed)) {
uni.showToast({
title: '最小速度必须小于最大速度',
icon: 'none'
});
return;
}
}
const playListModel = this.deviceInfo.thingsModels.find(model => model.id ===
'103#playList');
if (playListModel) {
try {
const jsonStr = playListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
if (!data.sound_card) {
data.sound_card = {};
}
if (!data.sound_card.play_list) {
data.sound_card.play_list = [];
}
let maxNum = 0;
if (data.sound_card.play_list.length > 0) {
maxNum = Math.max(...data.sound_card.play_list.map(item => item.play.num ||
0));
}
const [startHour, startMinute] = this.newDefault.startTime.split(':').map(Number);
const [endHour, endMinute] = this.newDefault.endTime.split(':').map(Number);
const startSeconds = startHour * 3600 + startMinute * 60;
const endSeconds = endHour * 3600 + endMinute * 60;
let weekValue = 0;
this.newDefault.repeatDays.forEach(day => {
weekValue |= (1 << day);
});
const newPlayItem = {
play: {
num: maxNum + 1,
filename: this.newDefault.audioFile.name,
en: 1
},
time: {
begin: startSeconds,
end: endSeconds,
week: weekValue
},
speed: {
en: this.newDefault.radarEnabled ? 1 : 0,
min: this.newDefault.radarEnabled ? parseInt(this.newDefault
.minSpeed) : 0,
max: this.newDefault.radarEnabled ? parseInt(this.newDefault
.maxSpeed) : 0
}
};
data.sound_card.play_list.push(newPlayItem);
playListModel.shadow = 'JSON=' + JSON.stringify(data);
try {
await this.mqttPublish(this.device, playListModel);
uni.showToast({
title: '添加成功',
icon: 'success'
});
this.showAddDefault = false;
this.newDefault = {
startTime: '',
endTime: '',
repeatDays: [],
radarEnabled: false,
minSpeed: '',
maxSpeed: '',
audioFile: null
};
} catch (error) {
console.error('发送添加命令失败:', error);
uni.showToast({
title: '添加失败',
icon: 'none'
});
}
} catch (error) {
console.error('解析或更新播放列表失败:', error);
uni.showToast({
title: '添加失败',
icon: 'none'
});
}
}
},
async deleteAudio(index) {
if (!this.checkOnline()) return;
try {
uni.showModal({
title: '提示',
content: '确定要删除该音频吗?',
cancelText: '取消',
confirmText: '确定',
success: async (res) => {
if (res.confirm) {
const mp3ListModel = this.deviceInfo.thingsModels.find(
model =>
model
.id === '103#mp3List');
if (mp3ListModel && mp3ListModel.shadow) {
try {
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);
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: async (res) => {
if (res.confirm) {
const playListModel = this.deviceInfo.thingsModels.find(
model =>
model.id ===
'103#playList');
if (playListModel) {
try {
const jsonStr = playListModel.shadow.replace('JSON=',
'');
const data = JSON.parse(jsonStr);
if (data.sound_card && data.sound_card.play_list) {
data.sound_card.play_list.splice(index, 1);
data.sound_card.play_list.forEach((item,
index) => {
item.play.num = index + 1;
});
playListModel.shadow = 'JSON=' + JSON.stringify(
data);
try {
await this.mqttPublish(this.device,
playListModel);
uni.showToast({
title: '删除成功',
icon: 'success'
});
} catch (error) {
console.error('发送删除命令失败:', error);
uni.showToast({
title: '删除失败',
icon: 'none'
});
}
}
} catch (error) {
console.error('解析或更新播放列表失败:', error);
uni.showToast({
title: '删除失败',
icon: 'none'
});
}
}
}
}
});
},
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 screenEnModel = this.device.thingsModels.find(item => item.id === '102#screenEn');
if (screenEnModel) {
screenEnModel.shadow = this.audioEnabled ? '1' : '0';
await this.mqttPublish(this.device, screenEnModel);
}
} catch (error) {
console.error('切换屏幕开关失败:', error);
uni.showToast({
title: '操作失败: ' + error.message,
icon: 'none'
});
}
},
formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
},
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];
},
// 4. 同步 handleStatusChange 方法,更新播放项状态
async handleStatusChange(index, value) {
const playListModel = this.deviceInfo.thingsModels.find(model => model.id ===
'103#playList');
if (playListModel) {
try {
const jsonStr = playListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
if (data.sound_card && data.sound_card.play_list) {
data.sound_card.play_list[index].play.en = value === '启用' ? 1 : 0;
playListModel.shadow = 'JSON=' + JSON.stringify(data);
try {
await this.mqttPublish(this.device, playListModel);
uni.showToast({
title: '更新成功',
icon: 'success'
});
} catch (error) {
console.error('发送状态更新命令失败:', error);
uni.showToast({
title: '更新失败',
icon: 'none'
});
this.defaultList[index].status = value === '启用' ? '禁用' : '启用';
}
}
} catch (error) {
console.error('解析或更新播放列表失败:', error);
uni.showToast({
title: '更新失败',
icon: 'none'
});
this.defaultList[index].status = value === '启用' ? '禁用' : '启用';
}
}
},
deleteRecording(index) {
uni.showModal({
title: '提示',
content: '确定要删除该录音吗?',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
this.recordings.splice(index, 1);
uni.showToast({
title: '删除成功',
icon: 'success'
});
}
}
});
},
// testOpenPicker(type) {
// this.pickerShow[type] = true;
// console.log('测试按钮pickerShow.' + type + ' =', this.pickerShow[type]);
// },
// 处理从 addProgram 页面传来的节目数据
async handleProgramData(data) {
try {
if (!this.checkOnline()) return;
// 1. 找到 progList 物模型
const progListModel = this.device.thingsModels.find(model => model.id === '102#progList');
if (!progListModel) {
uni.showToast({ title: '未找到节目物模型', icon: 'none' });
return;
}
// 2. 解析原有节目列表
let progListArr = [];
if (progListModel.shadow) {
try {
const jsonStr = progListModel.shadow.replace('JSON=', '');
const dataJson = JSON.parse(jsonStr);
if (dataJson.prog_list && Array.isArray(dataJson.prog_list)) {
progListArr = dataJson.prog_list;
}
} catch (e) {}
}
// 3. 组装新节目
const program = data.form;
const newProgram = {
order: progListArr.length, // 新增节目order为当前长度
rem: (program.remark || `自定义节目${Date.now()}`) + '\0',
dur: parseInt(program.duration) || 10,
areaM: program.mode,
aLst: program.zones.map(zone => {
const area = {
size: {
x: parseInt(zone.x) || 0,
y: parseInt(zone.y) || 0,
l: parseInt(zone.width) || 32,
h: parseInt(zone.height) || 64
},
pLst: []
};
if (zone.playType === 0) {
area.pLst.push({
typ: 0,
txt: {
str: zone.displayText || '',
fCn: (zone.font || 0) + 1,
fS: parseInt((this.fontSizes && this.fontSizes[zone.fontSize]) ? this.fontSizes[zone.fontSize] : 16),
col: zone.fontColor || 0,
fW: zone.fontBold || 0,
stch: zone.fontStretch || 0,
hPos: zone.hAlign || 1,
vPos: zone.vAlign || 1
}
});
} else if (zone.playType === 1) {
area.pLst.push({
typ: 1,
img: {
num: 0,
col: zone.imageColor || 0,
w: parseInt((this.imageSizes && this.imageSizes[zone.imageSize]) ? this.imageSizes[zone.imageSize] : 32),
h: parseInt((this.imageSizes && this.imageSizes[zone.imageSize]) ? this.imageSizes[zone.imageSize] : 32),
data: ''
},
anim: {
typ: (zone.effect || 0) + 1,
spd: zone.speed || 4,
pauseT: 1000,
playT: 2000
}
});
}
return area;
})
};
// 4. 添加到原有列表
progListArr.push(newProgram);
// 5. 下发
const progListData = { del_prog: 0, prog_list: progListArr };
progListModel.shadow = 'JSON=' + JSON.stringify(progListData);
await this.mqttPublish(this.device, progListModel);
uni.showToast({ title: '节目设置成功', icon: 'success' });
this.updateBasicSettings();
} catch (error) {
console.error('处理节目数据失败:', error);
uni.showToast({ title: '设置失败: ' + error.message, icon: 'none' });
}
},
// 新增:开机参数开关下发
handleBootScreenSwitchChange(val) {
const bootEnModel = this.device.thingsModels.find(model => model.id === '102#bootEn');
if (bootEnModel) {
bootEnModel.shadow = val ? '1' : '0';
this.mqttPublish(this.device, bootEnModel);
}
},
handleResetSwitchChange(val) {
const factoryEnModel = this.device.thingsModels.find(model => model.id === '102#factoryEn');
if (factoryEnModel) {
factoryEnModel.shadow = val ? '1' : '0';
this.mqttPublish(this.device, factoryEnModel);
}
},
// 屏幕模板选择逻辑
handleTemplateChange(val) {
const template = this.templateColumns.find(t => t.value === val);
if (template && template.params) {
const params = template.params;
// 保证为字符串'1'或'0'适配u-switch
this.screenParams.oePolarity = params.oe === 1 ? '1' : (params.oe === 0 ? '0' : '0');
this.screenParams.dataPolarity = params.data === 1 ? '1' : (params.data === 0 ? '0' : '0');
this.screenParams.width = params.w;
this.screenParams.height = params.h;
this.screenParams.screenRotate = params.angle;
// 同步旋转label
const rotateOption = this.screenRotateOptions.find(opt => opt.value == params.angle);
this.screenParams.screenRotateLabel = rotateOption ? rotateOption.label : '';
}
},
// 下发屏幕参数
handleSendScreenParams() {
const model = this.device.thingsModels.find(m => m.id === '102#scrParam');
if (model) {
const param = {
scr_param: {
oe: this.screenParams.oePolarity,
data: this.screenParams.dataPolarity,
w: this.screenParams.width,
h: this.screenParams.height,
angle: this.screenParams.screenRotate
}
};
model.shadow = 'JSON=' + JSON.stringify(param);
this.mqttPublish(this.device, model);
uni.showToast({ title: '参数已下发', icon: 'success' });
} else {
uni.showToast({ title: '未找到屏幕参数物模型', icon: 'none' });
}
},
// 新增原生picker回调
onNativePickerChange(e) {
const index = e.detail.value;
const value = this.templateColumns[index];
this.screenParams.template = value.value;
this.screenParams.templateLabel = value.label;
this.handleTemplateChange(value.value);
},
handleSendSensorThreshold() {
const model = this.device.thingsModels.find(m => m.id === '102#snsrThr');
if (model) {
const param = {
snsr_thr: {
speed: Number(this.sensorThreshold.speed),
temp: Number(this.sensorThreshold.temp),
humi: Number(this.sensorThreshold.humi)
}
};
model.shadow = 'JSON=' + JSON.stringify(param);
this.mqttPublish(this.device, model);
uni.showToast({ title: '阈值已下发', icon: 'success' });
} else {
uni.showToast({ title: '未找到传感器阈值物模型', icon: 'none' });
}
},
onProgramEnableChange(index, value) {
// 组装10个元素的使能数组
const enableArray = new Array(10).fill(0);
this.audioList.forEach((item, idx) => {
if (idx < 10) enableArray[idx] = item.enabled ? 1 : 0;
});
// 下发到 102#progEn
const progEnModel = this.device.thingsModels.find(model => model.id === '102#progEn');
if (progEnModel) {
const progEnData = { prog_en: enableArray };
progEnModel.shadow = 'JSON=' + JSON.stringify(progEnData);
this.mqttPublish(this.device, progEnModel);
}
},
refreshBuildInfo() {
// 下发102#model=1设备上报编译信息
const model = this.device.thingsModels.find(m => m.id === '102#model');
if (model) {
model.shadow = '1';
this.mqttPublish(this.device, model);
uni.showToast({ title: '已发送刷新指令', icon: 'success' });
} else {
uni.showToast({ title: '未找到编译信息物模型', icon: 'none' });
}
},
},
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>
.voice-control {
padding: 32rpx;
background: #f7f9fc;
min-height: 100vh;
font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
/* 统一卡片 */
.card {
background: #fff;
border-radius: 24rpx;
margin-bottom: 32rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, .06);
overflow: hidden;
transition: all .3s ease;
&:active {
transform: scale(.99);
}
}
/* 标题栏 */
.status-titletop {
font-size: 34rpx;
font-weight: 600;
color: #2979ff;
padding: 28rpx 40rpx;
background: linear-gradient(135deg, #e8f2ff 0%, #f0f7ff 100%);
border-bottom: 1rpx solid #dceaff;
}
.section-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
padding: 32rpx 40rpx;
border-bottom: 1rpx solid #f0f2f5;
background: #fafbff;
display: flex;
align-items: center;
.add-icon {
width: 48rpx;
height: 48rpx;
margin-left: auto;
transition: transform .2s;
&:active {
transform: scale(.85);
}
}
}
/* 内容区域统一内边距 */
.info-content {
padding: 32rpx 40rpx;
}
/* 音量滑条 */
.volume-slider {
display: flex;
align-items: center;
gap: 24rpx;
padding: 32rpx;
background: #f8fafc;
border-radius: 20rpx;
box-shadow: inset 0 0 0 1rpx #eaeef5;
.volume-icon image {
width: 48rpx;
height: 48rpx;
}
.slider-container {
flex: 1;
.volume-marks {
display: flex;
justify-content: space-between;
margin-top: 12rpx;
font-size: 24rpx;
color: #909399;
}
}
.volume-value {
min-width: 80rpx;
text-align: right;
font-size: 30rpx;
font-weight: 600;
color: #2979ff;
}
}
/* 开关行 */
.audio-switch {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
background: #f8fafc;
border-radius: 20rpx;
margin-bottom: 32rpx;
box-shadow: inset 0 0 0 1rpx #eaeef5;
font-size: 30rpx;
color: #333;
}
/* 按钮 */
.clear-screen-btn-wrap {
display: flex;
justify-content: center;
margin-top: 32rpx;
::v-deep .u-button {
width: 70%;
height: 88rpx;
font-size: 32rpx;
border-radius: 44rpx;
font-weight: 600;
}
}
/* 表单 / 列表 */
.screen-params-form {
padding: 32rpx 40rpx;
::v-deep .u-form-item {
margin-bottom: 24rpx;
&__body {
padding: 24rpx 0;
border-bottom: 1rpx solid #f0f2f5;
&__left {
font-size: 30rpx;
color: #606266;
}
&__right .u-input {
font-size: 30rpx;
color: #333;
text-align: right;
}
}
}
}
.list-container {
padding: 0 40rpx 32rpx;
.empty-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
color: #c0c4cc;
font-size: 30rpx;
gap: 24rpx;
}
.audio-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.audio-info {
display: flex;
align-items: center;
gap: 20rpx;
flex: 1;
overflow: hidden;
.audio-name {
font-size: 30rpx;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.audio-actions {
display: flex;
gap: 32rpx;
}
}
}
.picker-cell {
background: #fff;
border-radius: 24rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, .06);
margin-bottom: 12rpx;
transition: all .3s ease;
&:active {
transform: scale(.99);
}
}
.sensor-threshold {
background: #fff;
border-radius: 24rpx;
margin-bottom: 32rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, .06);
overflow: hidden;
transition: all .3s ease;
.section-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
padding: 32rpx 40rpx;
border-bottom: 1rpx solid #f0f2f5;
background: #fafbff;
display: flex;
align-items: center;
}
.sensor-form {
padding: 32rpx 40rpx;
.u-form-item {
margin-bottom: 32rpx;
}
.u-input {
background: #f8fafc;
border-radius: 16rpx;
padding: 0 20rpx;
font-size: 30rpx;
color: #333;
}
.u-button {
width: 70%;
height: 88rpx;
font-size: 32rpx;
border-radius: 44rpx;
font-weight: 600;
box-shadow: 0 4rpx 12rpx rgba(41, 121, 255, 0.15);
}
}
}
.build-info {
background: #fff;
border-radius: 24rpx;
margin-bottom: 32rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, .06);
overflow: hidden;
transition: all .3s ease;
.section-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
padding: 32rpx 40rpx;
border-bottom: 1rpx solid #f0f2f5;
background: #fafbff;
display: flex;
align-items: center;
}
.info-content {
padding: 32rpx 40rpx;
.info-item {
display: flex;
align-items: center;
margin-bottom: 18rpx;
.info-label {
font-size: 30rpx;
color: #606266;
min-width: 120rpx;
}
.info-value {
font-size: 30rpx;
color: #333;
}
}
}
}
}
</style>