1748 lines
47 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>
2025-06-18 10:18:15 +08:00
<!-- <button @click="print()">测试打印</button -->
2025-06-06 06:23:49 +08:00
<!-- 基础信息 -->
<view class="card basic-info">
<view class="section-title">基础信息</view>
<view class="info-content">
<!-- 音量控制 -->
<view class="volume-slider">
<view class="volume-icon">
2025-07-03 08:57:13 +08:00
<image src="https://xaznkj.cn/doc/photo/voice.svg" mode="aspectFit" class="volume-svg">
2025-06-06 06:23:49 +08:00
</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>
2025-07-03 08:57:13 +08:00
<image src="https://xaznkj.cn/doc/photo/add.svg" mode="aspectFit" class="add-icon"
2025-06-06 06:23:49 +08:00
@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">
2025-06-18 10:18:15 +08:00
<text>播放列表</text>
2025-07-03 08:57:13 +08:00
<image src="https://xaznkj.cn/doc/photo/add.svg" mode="aspectFit" class="add-icon"
2025-06-06 06:23:49 +08:00
@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">
2025-06-18 10:18:15 +08:00
<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>
2025-06-06 06:23:49 +08:00
</view>
<view class="audio-actions">
2025-06-18 10:18:15 +08:00
<u-switch v-model="item.status" :active-value="'启用'" :inactive-value="'禁用'"
@change="(value) => handleStatusChange(index, value)" size="22"></u-switch>
2025-06-06 06:23:49 +08:00
<u-icon name="trash" size="18" color="#ff4d4f" @click="deleteDefault(index)"></u-icon>
</view>
</view>
</view>
</view>
<!-- 远程喊话 -->
<view class="card remote-talk">
<view class="talk-container">
2025-06-24 10:07:51 +08:00
<!-- 录音状态显示 -->
<view class="recorder-status">
<view class="status-indicator" :class="{ recording: isRecording }">
<image
2025-07-03 08:57:13 +08:00
:src="isRecording ? 'https://xaznkj.cn/doc/photo/recording.png' : 'https://xaznkj.cn/doc/photo/record.png'"
2025-06-24 10:07:51 +08:00
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="startRecording"
@touchend="stopRecording" @touchcancel="cancelRecording">
<image
2025-07-03 08:57:13 +08:00
:src="isRecording ? 'https://xaznkj.cn/doc/photo/micred.png' : 'https://xaznkj.cn/doc/photo/mic.png'"
2025-06-24 10:07:51 +08:00
class="mic-img" mode="aspectFit" />
<text>{{ isRecording ? '录音中...' : '按住说话' }}</text>
</view>
</view>
<!-- 录音状态提示 -->
<view class="recording-tips" v-if="!hasRecording && !isRecording">
<text class="tip-text">按住按钮开始录音松开自动结束并上传</text>
</view>
<!-- 录音预览仅在上传失败时显示 -->
2025-06-26 14:55:08 +08:00
<!-- <view class="recording-preview" v-if="hasRecording && uploadFailed">
2025-06-24 10:07:51 +08:00
<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>
2025-06-26 14:55:08 +08:00
</view> -->
2025-06-24 10:07:51 +08:00
<!-- 最近录音列表 -->
2025-06-26 14:55:08 +08:00
<!-- <view class="recording-list" v-if="recordings.length > 0">
2025-06-24 10:07:51 +08:00
<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>
2025-06-26 14:55:08 +08:00
</view> -->
2025-06-06 06:23:49 +08:00
</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">
2025-06-18 10:18:15 +08:00
<u-slider v-model="newAudio.spd" :min="0" :max="15" :step="1" :showValue="true"
class="custom-slider"></u-slider>
2025-06-06 06:23:49 +08:00
</view>
</u-form-item>
<u-form-item label="合成音调" prop="pit" borderBottom>
<view class="slider-with-value">
2025-06-18 10:18:15 +08:00
<u-slider v-model="newAudio.pit" :min="0" :max="15" :step="1" :showValue="true"
class="custom-slider"></u-slider>
2025-06-06 06:23:49 +08:00
</view>
</u-form-item>
<u-form-item label="合成音量" prop="vol" borderBottom>
<view class="slider-with-value">
2025-06-18 10:18:15 +08:00
<u-slider v-model="newAudio.vol" :min="0" :max="15" :step="1" :showValue="true"
class="custom-slider"></u-slider>
2025-06-06 06:23:49 +08:00
</view>
</u-form-item>
<u-form-item label="合成文本" prop="text" borderBottom>
2025-06-18 10:18:15 +08:00
<u-input v-model="newAudio.text" type="textarea" placeholder="请输入合成文本"
height="100"></u-input>
2025-06-06 06:23:49 +08:00
</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">
2025-06-18 10:18:15 +08:00
<view class="modal-title">添加播放列表</view>
2025-06-06 06:23:49 +08:00
<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((
2025-06-24 10:07:51 +08:00
item) => item.isReadonly == '0');
2025-06-06 06:23:49 +08:00
this.attributeList = this.deviceInfo.thingsModels && this.deviceInfo.thingsModels.filter((
2025-06-24 10:07:51 +08:00
item) => item.isReadonly == '1');
2025-06-06 06:23:49 +08:00
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,
2025-06-24 10:07:51 +08:00
recordingTime: 0,
recordingTimer: null,
2025-06-06 06:23:49 +08:00
tempFilePath: '',
2025-06-24 10:07:51 +08:00
hasRecording: false,
audioUrl: '',
uploadFailed: false,
recordings: [],
recordingStatus: '准备录音',
2025-06-06 06:23:49 +08:00
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: [],
2025-06-24 10:07:51 +08:00
audioList: [],
defaultList: [],
audioUrl: '',
uploadFailed: false
2025-06-06 06:23:49 +08:00
};
},
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();
2025-06-24 10:07:51 +08:00
},
beforeDestroy() {
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
2025-06-06 06:23:49 +08:00
},
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;
},
2025-06-24 10:07:51 +08:00
async uploadRecording() {
if (!this.tempFilePath) return;
try {
this.recordingStatus = '上传中...';
// 在 uni-app 中直接使用 uni.uploadFile不需要 FormData
2025-07-03 08:57:13 +08:00
const uploadUrl = 'https://xaznkj.cn/common/upload/audio';
2025-06-24 10:07:51 +08:00
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 = '准备录音';
},
2025-06-06 06:23:49 +08:00
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'
});
}
},
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,
})
}
2025-06-24 10:07:51 +08:00
if (topics[3] == 'property' || topics[3] == 'function' || topic.endsWith('ws/service')) {
2025-06-06 06:23:49 +08:00
if (this.deviceInfo.serialNumber == deviceNum) {
for (let j = 0; j < message.message.length; j++) {
let isComplete = false;
2025-06-24 10:07:51 +08:00
for (let k = 0; k < this.deviceInfo.thingsModels.length && !isComplete; k++) {
2025-06-06 06:23:49 +08:00
if (this.deviceInfo.thingsModels[k].id == message.message[j].id) {
this.deviceInfo.thingsModels[k].shadow = message.message[j].value;
isComplete = true;
break;
2025-06-24 10:07:51 +08:00
} else if (this.deviceInfo.thingsModels[k].datatype.type == "object") {
for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.params
2025-06-06 06:23:49 +08:00
.length; n++) {
2025-06-24 10:07:51 +08:00
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;
2025-06-06 06:23:49 +08:00
isComplete = true;
break;
}
}
2025-06-24 10:07:51 +08:00
} else if (this.deviceInfo.thingsModels[k].datatype.type == "array") {
if (this.deviceInfo.thingsModels[k].datatype.arrayType == "object") {
2025-06-06 06:23:49 +08:00
if (String(message.message[j].id).indexOf("array_") == 0) {
2025-06-24 10:07:51 +08:00
for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype
2025-06-06 06:23:49 +08:00
.arrayParams.length; n++) {
2025-06-24 10:07:51 +08:00
for (let m = 0; m < this.deviceInfo.thingsModels[k].datatype
2025-06-06 06:23:49 +08:00
.arrayParams[n].length; m++) {
2025-06-24 10:07:51 +08:00
if (this.deviceInfo.thingsModels[k].datatype.arrayParams[n]
2025-06-06 06:23:49 +08:00
[m].id == message.message[j].id) {
2025-06-24 10:07:51 +08:00
this.deviceInfo.thingsModels[k].datatype.arrayParams[n]
2025-06-06 06:23:49 +08:00
[m].shadow = message.message[j].value;
isComplete = true;
break;
}
}
if (isComplete) {
break;
}
}
} else {
2025-06-24 10:07:51 +08:00
for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype
2025-06-06 06:23:49 +08:00
.arrayParams.length; n++) {
2025-06-24 10:07:51 +08:00
for (let m = 0; m < this.deviceInfo.thingsModels[k].datatype
2025-06-06 06:23:49 +08:00
.arrayParams[n].length; m++) {
let index = n > 9 ? String(n) : '0' + k;
let prefix = 'array_' + index + '_';
2025-06-24 10:07:51 +08:00
if (this.deviceInfo.thingsModels[k].datatype.arrayParams[n]
2025-06-06 06:23:49 +08:00
[m].id == prefix + message.message[j].id) {
2025-06-24 10:07:51 +08:00
this.deviceInfo.thingsModels[k].datatype.arrayParams[n]
2025-06-06 06:23:49 +08:00
[m].shadow = message.message[j].value;
isComplete = true;
}
}
if (isComplete) {
break;
}
}
}
} else {
2025-06-24 10:07:51 +08:00
for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayModel
2025-06-06 06:23:49 +08:00
.length; n++) {
2025-06-24 10:07:51 +08:00
if (this.deviceInfo.thingsModels[k].datatype.arrayModel[n].id ==
2025-06-06 06:23:49 +08:00
message.message[j].id) {
2025-06-24 10:07:51 +08:00
this.deviceInfo.thingsModels[k].datatype.arrayModel[n].shadow =
2025-06-06 06:23:49 +08:00
message.message[j].value;
isComplete = true;
break;
}
}
}
}
};
2025-06-24 10:07:51 +08:00
for (let k = 0; k < this.deviceInfo.chartList.length && !isComplete; k++) {
2025-06-06 06:23:49 +08:00
if (this.deviceInfo.chartList[k].id.indexOf("array_") == 0) {
if (this.deviceInfo.chartList[k].id == message.message[j].id) {
this.deviceInfo.chartList[k].shadow = message.message[j].value;
isComplete = true;
break;
}
} else {
if (this.deviceInfo.chartList[k].id == message.message[j].id) {
this.deviceInfo.chartList[k].shadow = message.message[j].value;
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() {
2025-06-24 10:07:51 +08:00
if (!this.recorderManager) return;
this.recorderManager.onStart(() => {
this.recordingStatus = '正在录音...';
});
this.recorderManager.onStop((res) => {
2025-06-06 06:23:49 +08:00
this.tempFilePath = res.tempFilePath;
2025-06-24 10:07:51 +08:00
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();
2025-06-06 06:23:49 +08:00
});
2025-06-24 10:07:51 +08:00
this.recorderManager.onError((res) => {
this.isRecording = false;
this.recordingStatus = '录音失败';
2025-06-06 06:23:49 +08:00
uni.showToast({
2025-06-24 10:07:51 +08:00
title: res.errMsg || '录音失败',
icon: 'none',
2025-06-06 06:23:49 +08:00
});
2025-06-24 10:07:51 +08:00
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
2025-06-06 06:23:49 +08:00
});
},
startRecording() {
2025-06-24 10:07:51 +08:00
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',
});
}
2025-06-06 06:23:49 +08:00
},
stopRecording() {
2025-06-24 10:07:51 +08:00
if (!this.isRecording || !this.recorderManager) return;
2025-06-06 06:23:49 +08:00
this.isRecording = false;
2025-06-24 10:07:51 +08:00
this.recordingStatus = '录音完成,正在上传...';
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
2025-06-06 06:23:49 +08:00
this.recorderManager.stop();
},
cancelRecording() {
2025-06-24 10:07:51 +08:00
if (!this.isRecording || !this.recorderManager) return;
2025-06-06 06:23:49 +08:00
this.isRecording = false;
2025-06-24 10:07:51 +08:00
this.recordingStatus = '准备录音';
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
this.recordingTime = 0;
2025-06-06 06:23:49 +08:00
this.recorderManager.stop();
uni.showToast({
title: '已取消录音',
2025-06-24 10:07:51 +08:00
icon: 'none',
2025-06-06 06:23:49 +08:00
});
},
showAddAudioModal() {
this.showAddAudio = true;
},
showAddDefaultModal() {
this.showAddDefault = true;
},
async confirmAddAudio() {
if (!this.checkOnline()) return;
2025-06-18 10:18:15 +08:00
2025-06-06 06:23:49 +08:00
try {
2025-06-23 09:15:23 +08:00
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model.id === '103#mp3List');
2025-06-06 06:23:49 +08:00
if (!mp3ListModel) {
throw new Error('未找到 mp3_list 模型');
}
const jsonStr = mp3ListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
2025-06-18 10:18:15 +08:00
2025-06-06 06:23:49 +08:00
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;
}
});
}
2025-06-18 10:18:15 +08:00
2025-06-06 06:23:49 +08:00
const newId = maxId + 1;
2025-06-18 10:18:15 +08:00
2025-06-06 06:23:49 +08:00
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'
});
}
},
2025-06-18 10:18:15 +08:00
async confirmAddDefault() {
2025-06-06 06:23:49 +08:00
if (!this.checkOnline()) return;
2025-06-18 10:18:15 +08:00
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;
}
}
2025-06-23 09:15:23 +08:00
const playListModel = this.deviceInfo.thingsModels.find(model => model.id === '103#playList');
2025-06-18 10:18:15 +08:00
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'
});
}
}
2025-06-06 06:23:49 +08:00
},
async deleteAudio(index) {
if (!this.checkOnline()) return;
2025-06-18 10:18:15 +08:00
2025-06-06 06:23:49 +08:00
try {
uni.showModal({
title: '提示',
content: '确定要删除该音频吗?',
cancelText: '取消',
confirmText: '确定',
success: async (res) => {
if (res.confirm) {
2025-06-18 10:18:15 +08:00
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model
2025-06-24 10:07:51 +08:00
.id === '103#mp3List');
2025-06-06 06:23:49 +08:00
if (mp3ListModel && mp3ListModel.shadow) {
try {
const jsonStr = mp3ListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
2025-06-18 10:18:15 +08:00
2025-06-06 06:23:49 +08:00
if (data.sound_card && data.sound_card.mp3_list) {
data.sound_card.mp3_list.splice(index, 1);
2025-06-18 10:18:15 +08:00
2025-06-06 06:23:49 +08:00
mp3ListModel.shadow = 'JSON=' + JSON.stringify(data);
2025-06-18 10:18:15 +08:00
2025-06-06 06:23:49 +08:00
await this.mqttPublish(this.device, mp3ListModel);
2025-06-18 10:18:15 +08:00
2025-06-06 06:23:49 +08:00
this.audioList.splice(index, 1);
2025-06-18 10:18:15 +08:00
2025-06-06 06:23:49 +08:00
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: '提示',
2025-06-18 10:18:15 +08:00
content: '确认删除该播放项吗?',
2025-06-06 06:23:49 +08:00
cancelText: '取消',
confirmText: '确定',
2025-06-18 10:18:15 +08:00
success: async (res) => {
2025-06-06 06:23:49 +08:00
if (res.confirm) {
2025-06-18 10:18:15 +08:00
const playListModel = this.deviceInfo.thingsModels.find(model => model.id ===
2025-06-23 09:15:23 +08:00
'103#playList');
2025-06-18 10:18:15 +08:00
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'
});
}
}
2025-06-06 06:23:49 +08:00
}
}
});
},
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'
});
}
},
2025-06-24 10:07:51 +08:00
formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
2025-06-06 06:23:49 +08:00
},
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;
2025-06-23 09:15:23 +08:00
const playEnModel = this.deviceInfo.thingsModels.find(model => model.id === '103#playEn');
2025-06-06 06:23:49 +08:00
if (playEnModel) {
this.audioEnabled = playEnModel.shadow === '1';
}
2025-06-23 09:15:23 +08:00
const volumeModel = this.deviceInfo.thingsModels.find(model => model.id === '103#volume');
2025-06-06 06:23:49 +08:00
if (volumeModel) {
this.volume = parseInt(volumeModel.shadow) || 50;
}
2025-06-24 10:07:51 +08:00
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model.id === '103#mp3List');
2025-06-06 06:23:49 +08:00
if (mp3ListModel && mp3ListModel.shadow) {
try {
const jsonStr = mp3ListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
2025-06-23 09:15:23 +08:00
if (data && data.mp3_list) {
this.audioList = data.mp3_list.map((item, index) => {
2025-06-06 06:23:49 +08:00
const name = item.split('_')[1] || item;
return {
id: index + 1,
name: name
};
});
}
} catch (error) {
console.error('解析音频列表失败:', error);
}
}
2025-06-18 10:18:15 +08:00
2025-06-23 09:15:23 +08:00
const playListModel = this.deviceInfo.thingsModels.find(model => model.id === '103#playList');
2025-06-18 10:18:15 +08:00
if (playListModel && playListModel.shadow) {
try {
const jsonStr = playListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
2025-06-23 09:15:23 +08:00
if (data && data.play_list) {
this.defaultList = data.play_list.map((item, index) => {
2025-06-18 10:18:15 +08:00
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` :
''
};
});
}
} 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) {
2025-06-23 09:15:23 +08:00
const playListModel = this.deviceInfo.thingsModels.find(model => model.id === '103#playList');
2025-06-18 10:18:15 +08:00
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 === '启用' ? '禁用' : '启用';
}
}
2025-06-24 10:07:51 +08:00
},
deleteRecording(index) {
uni.showModal({
title: '提示',
content: '确定要删除该录音吗?',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
this.recordings.splice(index, 1);
uni.showToast({
title: '删除成功',
icon: 'success'
});
}
}
});
2025-06-06 06:23:49 +08:00
}
},
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;
2025-06-18 10:18:15 +08:00
align-items: flex-start;
2025-06-06 06:23:49 +08:00
gap: 16rpx;
flex: 1;
overflow: hidden;
2025-06-18 10:18:15 +08:00
.audio-details {
flex: 1;
2025-06-06 06:23:49 +08:00
overflow: hidden;
2025-06-18 10:18:15 +08:00
.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;
}
}
2025-06-06 06:23:49 +08:00
}
}
.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;
2025-06-24 10:07:51 +08:00
.recorder-status {
2025-06-06 06:23:49 +08:00
display: flex;
flex-direction: column;
align-items: center;
2025-06-24 10:07:51 +08:00
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;
2025-06-06 06:23:49 +08:00
justify-content: center;
2025-06-24 10:07:51 +08:00
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);
}
2025-06-06 06:23:49 +08:00
2025-06-24 10:07:51 +08:00
&:active {
transform: scale(0.95);
}
text {
font-size: 24rpx;
color: #666;
font-weight: 500;
}
2025-06-06 06:23:49 +08:00
}
2025-06-24 10:07:51 +08:00
}
2025-06-06 06:23:49 +08:00
2025-06-24 10:07:51 +08:00
.recording-tips {
margin-top: 20rpx;
padding: 24rpx;
background: #f8f9fa;
border-radius: 16rpx;
width: 100%;
.tip-text {
font-size: 28rpx;
color: #606266;
text-align: 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;
}
}
2025-06-06 06:23:49 +08:00
}
}
}
}
2025-06-24 10:07:51 +08:00
@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);
}
}
2025-06-06 06:23:49 +08:00
.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%;
2025-06-18 10:18:15 +08:00
2025-06-06 06:23:49 +08:00
.custom-slider {
flex: 1;
margin-right: 20rpx;
}
}
2025-06-18 10:18:15 +08:00
2025-06-06 06:23:49 +08:00
: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;
}
2025-06-24 10:07:51 +08:00
.mic-img {
width: 56rpx;
height: 56rpx;
margin-bottom: 8rpx;
}
2025-06-06 06:23:49 +08:00
}
</style>