1933 lines
55 KiB
Vue
Raw 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://xaznkj.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>
<switch :checked="audioEnabled" @change="handleAudioSwitchChange($event)"
:disabled="device.status !== 3" color="#2979ff" />
</view>
</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="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://xaznkj.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="item.status === '启用' ? 'star-fill' : 'star'" size="18"
:color="item.status === '启用' ? '#ff9900' : '#c0c4cc'"></u-icon>
<view class="audio-details">
<text class="audio-name">{{ item.name }}</text>
<view class="audio-meta">
<text class="time-info">{{ item.playTime }}</text>
<text class="week-info">{{ item.weekdays }}</text>
<text v-if="item.radarEnabled" class="radar-info">{{ item.radarSpeed }}</text>
</view>
</view>
</view>
<view class="audio-actions">
<switch :checked="item.status === '启用'" @change="handleNativeSwitchChange($event, index)"
:disabled="device.status !== 3" color="#2979ff" />
<u-icon name="edit-pen" size="18" color="#2979ff" v-if="device.status === 3"
@click="editDefault(index)"></u-icon>
<u-icon name="edit-pen" size="18" color="#ccc" v-else></u-icon>
<u-icon name="trash" size="18" color="#ff4d4f" v-if="device.status === 3"
@click="deleteDefault(index)"></u-icon>
<u-icon name="trash" size="18" color="#ccc" v-else></u-icon>
</view>
</view>
</view>
</view>
<!-- 远程喊话 -->
<view class="card remote-talk">
<view class="talk-container">
<!-- 录音状态显示 -->
<view class="recorder-status">
<view class="status-indicator" :class="{ recording: isRecording }">
<image
:src="isRecording ? 'https://xaznkj.cn/doc/photo/recording.png' : 'https://xaznkj.cn/doc/photo/record.png'"
class="mic-img" mode="aspectFit" />
</view>
<text class="status-text">{{ recordingStatus }}</text>
</view>
<!-- 录音时长显示 -->
<view class="timer-display" v-if="isRecording">
{{ formatTime(recordingTime) }}
</view>
<!-- 录音控制按钮 -->
<view class="control-buttons">
<view class="talk-button" :class="{ recording: isRecording }" @touchstart="handleRecordPressStart"
@touchend="handleRecordPressEnd" @touchcancel="handleRecordPressCancel">
<image
:src="isRecording ? 'https://xaznkj.cn/doc/photo/micred.png' : 'https://xaznkj.cn/doc/photo/mic.png'"
class="mic-img" mode="aspectFit" />
<text>{{ isRecording ? '录音中...' : '按住说话' }}</text>
</view>
</view>
<!-- 录音状态提示 -->
<view class="recording-tips" v-if="!hasRecording && !isRecording">
<text class="tip-text">按住按钮开始录音松开自动结束并上传</text>
</view>
<!-- 录音预览仅在上传失败时显示 -->
<!-- <view class="recording-preview" v-if="hasRecording && uploadFailed">
<view class="preview-title">录音预览</view>
<view class="audio-player">
<audio :src="audioUrl" controls style="width: 100%; height: 40px;"></audio>
<view class="preview-controls">
<u-button type="primary" size="small" @click="uploadRecording">
重新上传
</u-button>
<u-button type="text" size="small" @click="reRecord">
重新录制
</u-button>
</view>
</view>
</view> -->
<!-- 最近录音列表 -->
<!-- <view class="recording-list" v-if="recordings.length > 0">
<view class="list-title">最近录音</view>
<scroll-view scroll-y style="height: 200px;">
<view v-for="(recording, index) in recordings" :key="index" class="recording-item">
<view class="recording-info">
<text class="recording-name">{{ recording.name }}</text>
<text class="recording-time">{{ recording.time }}</text>
</view>
<u-icon name="trash" size="18" color="#ff4d4f" @click="deleteRecording(index)"></u-icon>
</view>
</scroll-view>
</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="男声1" name="0"></u-radio>
<u-radio label="男声2" name="1"></u-radio>
<u-radio label="女声1" name="3"></u-radio>
<u-radio label="女声2" 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" height="12" blockSize="28" style="width: 100%;" />
</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" height="12" blockSize="28" style="width: 100%;" />
</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" height="12" blockSize="28" style="width: 100%;" />
</view>
</u-form-item>
<u-form-item label="合成文本" prop="text" borderBottom>
<u-input v-model="newAudio.text" type="textarea" placeholder="最多输入30个字"
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) {
// console.log("newVal", newVal)
// 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();
// }
// }
// },
data() {
return {
title: '设备离线',
volume: 50,
audioEnabled: true,
isRecording: false,
recorderManager: null,
recordingTime: 0,
recordingTimer: null,
tempFilePath: '',
hasRecording: false,
audioUrl: '',
uploadFailed: false,
recordings: [],
recordingStatus: '准备录音',
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,
isEditDefault: false,
editDefaultIndex: null,
// 新增变量
recordPressTimer: null,
isWaitingRecord: false,
};
},
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.initRecorder();
this.updateBasicSettings();
},
beforeDestroy() {
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
},
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 uploadRecording() {
if (!this.tempFilePath) return;
try {
this.recordingStatus = '上传中...';
// 在 uni-app 中直接使用 uni.uploadFile不需要 FormData
const uploadUrl = 'https://xaznkj.cn/common/upload/audio';
const token =
'Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImU3MWM2OTg4LTNlMzMtNDYyMy05M2M3LWE4YzZmMTNlMjZkZSJ9.wgsL8b3WDmyuesG8JTA3LcNFp2FigkB90h6Inwxt7OFadH6rc5np5TjAyU1pzU2_b5cmG8BYXMEdAqEdJzoDcA';
// 使用 uni.uploadFile 上传录音文件
const [error, res] = await new Promise((resolve) => {
uni.uploadFile({
url: uploadUrl,
filePath: this.tempFilePath,
name: 'file',
header: {
'Authorization': token
},
success: (res) => resolve([null, res]),
fail: (err) => resolve([err, null])
});
});
if (error) throw error;
if (res.statusCode === 200) {
const result = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
if (result.code === 200) {
this.recordingStatus = '上传成功';
this.uploadFailed = false;
uni.showToast({
title: '上传成功',
icon: 'success'
});
// 提取返回的 URL 并通过 MQTT 下发到 103#onlinePlay
if (result.url || result.resourcePath) {
const onlinePlayModel = this.deviceInfo.thingsModels?.find(model => model.id ===
'103#onlinePlay');
if (onlinePlayModel) {
// 构建包含 interrupt 参数的 JSON 数据
const playData = 'JSON=' + JSON.stringify({
online_play: {
url: "http://1.94.62.14:8080" + (result.resourcePath || result
.url),
},
interrupt: 98
});
onlinePlayModel.shadow = playData;
await this.mqttPublish(this.deviceInfo, onlinePlayModel);
uni.showToast({
title: '音频已下发到设备',
icon: 'success'
});
} else {
uni.showToast({
title: '未找到 103#onlinePlay 物模型',
icon: 'none'
});
}
}
} else {
throw new Error(result.msg || '上传失败');
}
} else {
throw new Error('上传失败,状态码: ' + res.statusCode);
}
} catch (error) {
console.error('上传失败:', error);
this.recordingStatus = '上传失败';
this.uploadFailed = true;
let errorMsg = '上传失败';
if (error.message && error.message.includes('Mixed Content')) {
errorMsg = '由于安全策略,无法在 HTTPS 页面访问 HTTP 接口。请联系管理员配置接口支持 HTTPS。';
}
uni.showToast({
title: errorMsg || error.message || '上传失败',
icon: 'none'
});
}
},
reRecord() {
this.hasRecording = false;
this.uploadFailed = false;
this.recordingStatus = '准备录音';
},
async volumeChange(value) {
if (!this.checkOnline()) return;
try {
const volumeModel = this.device.thingsModels.find(item => item.id === '103#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);
}
}
//兼容设备回复
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;
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() {
if (!this.recorderManager) return;
this.recorderManager.onStart(() => {
this.recordingStatus = '正在录音...';
});
this.recorderManager.onStop((res) => {
this.tempFilePath = res.tempFilePath;
this.audioUrl = res.tempFilePath;
this.hasRecording = true;
this.recordingStatus = '录音完成,正在上传...';
this.recordings.unshift({
name: `录音_${this.formatTime(res.duration ? Math.floor(res.duration/1000) : this.recordingTime)}`,
time: new Date().toLocaleString(),
path: res.tempFilePath,
duration: res.duration ? Math.floor(res.duration / 1000) : this.recordingTime
});
this.uploadRecording();
});
this.recorderManager.onError((res) => {
this.isRecording = false;
this.recordingStatus = '录音失败';
uni.showToast({
title: res.errMsg || '录音失败',
icon: 'none',
});
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
});
},
startRecording() {
if (!this.checkOnline() || !this.recorderManager) return;
try {
this.isRecording = true;
this.recordingStatus = '正在录音...';
this.recordingTime = 0;
this.recordingTimer = setInterval(() => {
this.recordingTime++;
}, 1000);
this.recorderManager.start({
duration: 60000,
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
format: 'mp3',
frameSize: 1,
});
} catch (e) {
this.isRecording = false;
this.recordingStatus = '录音启动失败';
uni.showToast({
title: '录音启动失败,请检查权限',
icon: 'none',
});
}
},
stopRecording() {
if (!this.isRecording || !this.recorderManager) return;
this.isRecording = false;
this.recordingStatus = '录音完成,正在上传...';
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
this.recorderManager.stop();
},
cancelRecording() {
if (!this.isRecording || !this.recorderManager) return;
this.isRecording = false;
this.recordingStatus = '准备录音';
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
this.recordingTime = 0;
this.recorderManager.stop();
uni.showToast({
title: '已取消录音',
icon: 'none',
});
},
showAddAudioModal() {
this.showAddAudio = true;
},
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;
console.log("data", JSON.stringify(data))
if (data.mp3_list) {
data.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,
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}`
},
interrupt: 99
};
console.log("103#mp3List", JSON.stringify(ttsData))
mp3ListModel.shadow = 'JSON=' + JSON.stringify(ttsData);
// 本地添加
this.audioList.push({
id: newId,
name: this.newAudio.name,
filename: `${newId}_${this.newAudio.name}`
});
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) {
data = {};
}
if (!data.play_list) {
data.play_list = [];
}
let maxNum = 0;
if (data.play_list.length > 0) {
maxNum = Math.max(...data.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 audioIndex = this.audioList.findIndex(a => a.name === this.newDefault.audioFile.name);
const filename = this.newDefault.audioFile.filename;
const newPlayItem = {
play: {
num: this.isEditDefault && this.editDefaultIndex !== null && data.play_list[this
.editDefaultIndex] ? data.play_list[this.editDefaultIndex].play.num :
maxNum + 1,
filename: filename,
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
}
};
if (this.isEditDefault && this.editDefaultIndex !== null) {
data.play_list[this.editDefaultIndex] = newPlayItem;
// 本地编辑
this.defaultList.splice(this.editDefaultIndex, 1, {
id: this.editDefaultIndex + 1,
name: filename,
playTime: `${this.newDefault.startTime} - ${this.newDefault.endTime}`,
weekdays: this.newDefault.repeatDays.map(i => this.weekDays[i]).join(', '),
radarEnabled: this.newDefault.radarEnabled,
status: '启用',
radarSpeed: this.newDefault.radarEnabled ?
`${this.newDefault.minSpeed}-${this.newDefault.maxSpeed}km/h` : ''
});
} else {
data.play_list.push(newPlayItem);
// 本地添加
this.defaultList.push({
id: this.defaultList.length + 1,
name: filename,
playTime: `${this.newDefault.startTime} - ${this.newDefault.endTime}`,
weekdays: this.newDefault.repeatDays.map(i => this.weekDays[i]).join(', '),
radarEnabled: this.newDefault.radarEnabled,
status: '启用',
radarSpeed: this.newDefault.radarEnabled ?
`${this.newDefault.minSpeed}-${this.newDefault.maxSpeed}km/h` : ''
});
}
console.log(JSON.stringify(data))
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
};
this.isEditDefault = false;
this.editDefaultIndex = 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;
// 先本地删除
const deleted = this.audioList.splice(index, 1);
// 弹窗确认
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.mp3_list) {
data.mp3_list.splice(index, 1);
mp3ListModel.shadow = 'JSON=' + JSON.stringify(data);
await this.mqttPublish(this.device, mp3ListModel);
uni.showToast({
title: '删除成功',
icon: 'success'
});
}
} catch (error) {
// 失败时回滚本地数据
this.audioList.splice(index, 0, ...deleted);
uni.showToast({
title: '删除失败: ' + error.message,
icon: 'none'
});
}
}
} else {
// 取消时回滚本地数据
this.audioList.splice(index, 0, ...deleted);
}
}
});
},
async deleteDefault(index) {
if (!this.checkOnline()) return;
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.play_list) {
data.play_list.splice(index, 1);
data.play_list.forEach((item, idx) => {
item.play.num = idx + 1;
});
playListModel.shadow = 'JSON=' + JSON.stringify(data);
await this.mqttPublish(this.device, playListModel);
uni.showToast({
title: '删除成功',
icon: 'success'
});
}
} catch (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;
// 本地切换已在handleAudioSwitchChange处理
try {
const playEnModel = this.device.thingsModels.find(item => item.id === '103#playEn');
if (playEnModel) {
playEnModel.shadow = this.audioEnabled ? '1' : '0';
await this.mqttPublish(this.device, playEnModel);
}
} catch (error) {
this.audioEnabled = !this.audioEnabled;
uni.showToast({
title: '操作失败: ' + error.message,
icon: 'none'
});
}
},
handleAudioSwitchChange(e) {
const value = e && e.detail && e.detail.value;
this.audioEnabled = value;
this.audioSwitchChange();
},
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];
},
updateBasicSettings() {
if (!this.deviceInfo.thingsModels) return;
const playEnModel = this.deviceInfo.thingsModels.find(model => model.id === '103#playEn');
if (playEnModel) {
this.audioEnabled = playEnModel.shadow === '1';
}
const volumeModel = this.deviceInfo.thingsModels.find(model => model.id === '103#volume');
if (volumeModel) {
this.volume = parseInt(volumeModel.shadow) || 50;
}
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 && data.mp3_list) {
this.audioList = data.mp3_list.map((item, index) => {
const [id, ...nameArr] = item.split('_');
const name = nameArr.join('_') || item;
return {
id: Number(id),
name: name,
filename: item
};
});
// 强制整体赋值,保证小程序端响应
this.audioList = [...this.audioList];
}
} catch (error) {
console.error('解析音频列表失败:', error);
}
}
const playListModel = this.deviceInfo.thingsModels.find(model => model.id === '103#playList');
if (playListModel && playListModel.shadow) {
try {
const jsonStr = playListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
if (data && data.play_list) {
this.defaultList = data.play_list.map((item, index) => {
const beginTime = this.formatSecondsToTime(item.time.begin);
const endTime = this.formatSecondsToTime(item.time.end);
const weekdays = this.convertWeekToArray(item.time.week);
return {
id: index + 1,
name: item.play.filename,
playTime: `${beginTime} - ${endTime}`,
weekdays: weekdays.join(', '),
radarEnabled: item.speed.en === 1,
status: item.play.en === 1 ? '启用' : '禁用',
radarSpeed: item.speed.en === 1 ? `${item.speed.min}-${item.speed.max}km/h` :
''
};
});
// 强制整体赋值,保证小程序端响应
this.defaultList = [...this.defaultList];
}
} catch (error) {
console.error('解析播放列表失败:', error);
}
}
},
formatSecondsToTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
},
convertWeekToArray(week) {
const weekdays = [];
for (let i = 0; i < 7; i++) {
if (week & (1 << i)) {
weekdays.push(this.weekDays[i]);
}
}
return weekdays;
},
async handleStatusChange(index, value) {
value = typeof value !== 'undefined' ? value : arguments[1];
// 本地切换
this.defaultList[index].status = 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.play_list) {
data.play_list[index].play.en = value === '启用' ? 1 : 0;
playListModel.shadow = 'JSON=' + JSON.stringify(data);
await this.mqttPublish(this.device, playListModel);
uni.showToast({
title: '更新成功',
icon: 'success'
});
}
} catch (error) {
this.defaultList[index].status = value === '启用' ? '禁用' : '启用';
uni.showToast({
title: '更新失败',
icon: 'none'
});
}
}
},
handleNativeSwitchChange(e, index) {
// e.detail.value 是布尔值
const value = e && e.detail && e.detail.value ? '启用' : '禁用';
// 本地切换
this.defaultList[index].status = value;
// 复用原有下发逻辑
this.handleStatusChange(index, value);
},
deleteRecording(index) {
uni.showModal({
title: '提示',
content: '确定要删除该录音吗?',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
this.recordings.splice(index, 1);
uni.showToast({
title: '删除成功',
icon: 'success'
});
}
}
});
},
editDefault(index) {
if (!this.checkOnline()) return;
const item = this.defaultList[index];
const audioFile = this.audioList.find(a => a.filename === item.name || a.name === item.name);
const audioIndex = this.audioList.findIndex(a => a.filename === item.name || a.name === item.name);
this.newDefault = {
startTime: item.playTime.split(' - ')[0],
endTime: item.playTime.split(' - ')[1],
repeatDays: this.weekDays.map((d, i) => item.weekdays.includes(d) ? i : -1).filter(i => i !== -1),
radarEnabled: item.radarEnabled,
minSpeed: item.radarEnabled && item.radarSpeed ? item.radarSpeed.split('-')[0] : '',
maxSpeed: item.radarEnabled && item.radarSpeed ? item.radarSpeed.split('-')[1].replace('km/h',
'') : '',
audioFile: audioFile
};
this.audioIndex = audioIndex;
this.isEditDefault = true;
this.editDefaultIndex = index;
this.showAddDefault = true;
},
// 新增方法
handleRecordPressStart() {
if (!this.checkOnline() || !this.recorderManager) return;
this.isWaitingRecord = true;
this.recordingStatus = '请继续按住,准备录音...';
this.recordPressTimer = setTimeout(() => {
this.isWaitingRecord = false;
this.startRecording();
}, 1000);
},
handleRecordPressEnd() {
if (this.recordPressTimer) {
clearTimeout(this.recordPressTimer);
this.recordPressTimer = null;
}
if (this.isWaitingRecord) {
this.isWaitingRecord = false;
this.recordingStatus = '按住时间太短,请重试';
return;
}
this.stopRecording();
},
handleRecordPressCancel() {
if (this.recordPressTimer) {
clearTimeout(this.recordPressTimer);
this.recordPressTimer = null;
}
if (this.isWaitingRecord) {
this.isWaitingRecord = false;
this.recordingStatus = '已取消录音';
return;
}
this.cancelRecording();
}
},
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: flex-start;
gap: 16rpx;
flex: 1;
overflow: hidden;
.audio-details {
flex: 1;
overflow: hidden;
.audio-name {
font-size: 26rpx;
color: #333;
margin-bottom: 8rpx;
display: block;
}
.audio-meta {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
font-size: 22rpx;
color: #666;
.time-info,
.week-info,
.radar-info {
background-color: #f5f5f5;
padding: 4rpx 12rpx;
border-radius: 4rpx;
}
}
}
}
.audio-actions {
display: flex;
align-items: center;
gap: 24rpx;
margin-left: 16rpx;
}
}
}
.remote-talk {
.talk-container {
padding: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
.recorder-status {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20rpx;
.status-indicator {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: #f4f4f5;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
transition: all 0.3s;
&.recording {
background: #fef0f0;
animation: pulse 1.5s infinite;
}
}
.status-text {
font-size: 26rpx;
color: #606266;
}
}
.timer-display {
font-size: 48rpx;
font-weight: bold;
color: #303133;
margin-bottom: 20rpx;
}
.control-buttons {
display: flex;
justify-content: center;
gap: 20rpx;
margin-bottom: 20rpx;
.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;
border: 2rpx solid #e1e8ff;
box-shadow: 0 4rpx 12rpx rgba(41, 121, 255, 0.2);
&.recording {
background-color: #fff1f0;
border-color: #ff4d4f;
transform: scale(1.05);
box-shadow: 0 6rpx 16rpx rgba(245, 77, 79, 0.3);
}
&:active {
transform: scale(0.95);
}
text {
font-size: 24rpx;
color: #666;
font-weight: 500;
}
}
}
.recording-tips {
margin-top: 20rpx;
padding: 24rpx;
background: #f8f9fa;
border-radius: 16rpx;
width: 100%;
.tip-text {
font-size: 28rpx;
color: #606266;
text-align: center;
justify-content: center;
}
}
.recording-preview {
margin-top: 20rpx;
padding: 24rpx;
background: #f8f9fa;
border-radius: 16rpx;
width: 100%;
.preview-title {
font-size: 28rpx;
color: #606266;
margin-bottom: 20rpx;
text-align: left;
}
.audio-player {
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
audio {
width: 100%;
height: 80rpx;
}
.preview-controls {
display: flex;
justify-content: center;
margin-top: 10rpx;
}
}
}
.recording-list {
text-align: left;
border-top: 1rpx solid #ebeef5;
padding-top: 20rpx;
width: 100%;
.list-title {
font-size: 28rpx;
color: #606266;
margin-bottom: 20rpx;
}
.recording-item {
display: flex;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #ebeef5;
.recording-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.recording-name {
font-size: 26rpx;
color: #303133;
}
.recording-time {
font-size: 22rpx;
color: #909399;
}
}
}
}
}
}
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(245, 108, 108, 0.4);
}
70% {
transform: scale(1.1);
box-shadow: 0 0 0 10rpx rgba(245, 108, 108, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(245, 108, 108, 0);
}
}
.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 {
width: 100% !important;
display: block !important;
.custom-slider {
width: 100% !important;
min-width: 0 !important;
max-width: 100% !important;
flex: 1 !important;
display: block !important;
margin-right: 0 !important;
}
}
: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;
}
.mic-img {
width: 56rpx;
height: 56rpx;
margin-bottom: 8rpx;
}
}
</style>