Xazn-vue/src/views/iot/device/voicecard.vue
2025-06-10 17:22:29 +08:00

1810 lines
74 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>
<div class="running-status">
<el-row :gutter="20">
<el-col :xs="24" :sm="24" :md="24" :lg="16" :xl="14" class="status-col">
<!-- 设备模式和OTA升级部分 -->
<el-row :gutter="20" class="mode-section">
<!-- 设备模式 -->
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-card class="mode-card" shadow="hover">
<div class="mode-header">
<i class="el-icon-menu"></i>
<span class="mode-title">{{ $t('device.running-status.866086-0') }}</span>
</div>
<div class="mode-content">
<span class="title" :style="{ color: statusColor.background }">{{ title }}</span>
<el-button type="text" @click="printThingsModels" style="margin-left: 10px">
<i class="el-icon-printer"></i> 打印物模型
</el-button>
</div>
</el-card>
</el-col>
<!-- 设备升级 -->
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-card class="mode-card" shadow="hover">
<div class="mode-header">
<svg-icon icon-class="ota" />
<span class="mode-title">{{ $t('device.running-status.866086-1') }}</span>
</div>
<div class="mode-content">
<el-button type="primary" size="mini" :plain="true" @click="viewVersion()">
{{ $t('device.running-status.866086-44') }}
</el-button>
</div>
</el-card>
</el-col>
</el-row>
<!-- 声卡基础设置 -->
<el-card class="settings-card" shadow="hover">
<div slot="header" class="settings-header">
<span class="settings-title">基础设置</span>
</div>
<el-form :model="basicSettings" label-width="100px" style="margin-top: 20px;">
<el-form-item label="音频开关">
<el-switch v-model="basicSettings.audioEnabled" active-color="#13ce66"
inactive-color="#ff4949" @change="handleAudioSwitchChange">
</el-switch>
</el-form-item>
<el-form-item label="音量设置">
<el-slider v-model="basicSettings.volume" :min="0" :max="100" :format-tooltip="formatVolume"
@change="handleVolumeChange" style="width: 80%" :disabled="!basicSettings.audioEnabled">
</el-slider>
</el-form-item>
</el-form>
</el-card>
<!-- 音频列表 -->
<el-card class="audio-list-card" shadow="hover">
<div slot="header" class="audio-list-header">
<span class="audio-list-title">音频列表</span>
<el-button type="primary" size="mini" icon="el-icon-plus" @click="showAddAudioDialog">
添加音频
</el-button>
</div>
<el-table :data="audioList" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }"
border>
<el-table-column prop="id" label="序号" width="80" align="center">
</el-table-column>
<el-table-column prop="name" label="音频名称" min-width="150">
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template slot-scope="scope">
<el-button type="text" icon="el-icon-delete" @click="handleDeleteAudio(scope.row)">
</el-button>
</template>
</el-table-column>
<template slot="empty">
<div style="padding: 20px 0;">
<el-empty description="暂无音频数据"></el-empty>
</div>
</template>
</el-table>
</el-card>
<!-- 默认列表 -->
<el-card class="default-list-card" shadow="hover">
<div slot="header" class="default-list-header">
<span class="default-list-title">播放列表</span>
<el-button type="primary" size="mini" icon="el-icon-plus" @click="showAddPlaylistDialog">
添加音频
</el-button>
</div>
<el-table :data="defaultList" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }"
border>
<el-table-column prop="id" label="序号" width="80" align="center">
</el-table-column>
<el-table-column prop="name" label="音频名称" min-width="150">
</el-table-column>
<el-table-column prop="playTime" label="播放时间" width="120" align="center">
</el-table-column>
<el-table-column prop="weekdays" label="重复" width="200" align="center">
</el-table-column>
<el-table-column label="雷达" width="100" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.radarEnabled ? 'success' : 'info'">
{{ scope.row.radarEnabled ? '开启' : '关闭' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="120" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" :active-value="'启用'" :inactive-value="'禁用'"
@change="handleStatusChange(scope.row)">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template slot-scope="scope">
<el-button type="text" icon="el-icon-delete" @click="handleDeletePlaylist(scope.row)">
</el-button>
</template>
</el-table-column>
<template slot="empty">
<div style="padding: 20px 0;">
<el-empty description="暂无播放列表数据"></el-empty>
</div>
</template>
</el-table>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
<!-- 远程喊话控制面板 -->
<el-card class="voice-control-card" shadow="hover">
<div slot="header" class="voice-control-header">
<span class="voice-control-title">远程喊话</span>
</div>
<div class="voice-control-content">
<div class="recorder-status">
<div class="status-indicator" :class="{ 'recording': isRecording }">
<i class="el-icon-microphone"></i>
</div>
<span class="status-text">{{ recordingStatus }}</span>
</div>
<div class="timer-display" v-if="isRecording">
{{ formatTime(recordingTime) }}
</div>
<div class="control-buttons">
<el-button type="primary" icon="el-icon-video-play" circle @click="startRecording"
:disabled="isRecording">
</el-button>
<el-button type="danger" icon="el-icon-video-pause" circle @click="stopRecording"
:disabled="!isRecording">
</el-button>
<el-button type="success" icon="el-icon-upload2" circle @click="uploadRecording"
:disabled="!hasRecording || isRecording">
</el-button>
</div>
<!-- 录音预览 -->
<div class="recording-preview" v-if="hasRecording">
<div class="preview-title">录音预览</div>
<div class="audio-player">
<audio ref="audioPlayer" :src="audioUrl" controls></audio>
<div class="preview-controls">
<el-button type="text" icon="el-icon-refresh" @click="reRecord"
:disabled="isRecording">
重新录制
</el-button>
</div>
</div>
</div>
<div class="recording-list" v-if="recordings.length > 0">
<div class="list-title">最近录音</div>
<el-scrollbar style="height: 200px">
<div v-for="(recording, index) in recordings" :key="index" class="recording-item">
<span class="recording-name">{{ recording.name }}</span>
<span class="recording-time">{{ recording.time }}</span>
<el-button type="text" icon="el-icon-delete" @click="deleteRecording(index)">
</el-button>
</div>
</el-scrollbar>
</div>
</div>
</el-card>
<!-- 设备监测图表-->
<el-row :gutter="20" v-if="deviceInfo.chartList.length > 0">
<el-col :xs="24" :sm="12" :md="12" :lg="24" :xl="12" v-for="(item, index) in deviceInfo.chartList"
:key="index">
<el-card shadow="hover" style="border-radius: 8px; margin-bottom: 20px">
<div ref="map" style="height: 230px; width: 185px; margin: 0 auto; margin-bottom: 15px">
</div>
</el-card>
</el-col>
</el-row>
</el-col>
</el-row>
<!-- 固件版本查看对话框 -->
<el-dialog :title="$t('device.running-status.866086-10')" :visible.sync="openVersion" width="550px"
append-to-body>
<el-form ref="firmwareForm" label-width="100px" :model="firmwareParams" :inline="true" :rules="rules">
<el-form-item :label="$t('device.running-status.866086-38')" prop="firmwareType">
<el-select v-model="deviceInfo.firmwareType" :placeholder="$t('firmware.index.222541-51')"
@change="handleVersionInputChange" style="width: 350px" disabled>
<el-option v-for="item in firmwareTypeList" :key="item.value" :label="item.label"
:value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('device.running-status.866086-39')" prop="">
<el-input :placeholder="$t('device.running-status.866086-40')" v-model="deviceInfo.firmwareVersion"
style="width: 350px" disabled>
<template slot="prepend">Version</template>
</el-input>
</el-form-item>
</el-form>
<div slot="footer">
<el-tooltip effect="dark" :content="$t('device.running-status.866086-41')" placement="top-start">
<el-button type="primary" @click="getLatestFirmware" :disabled="device.status !== 3">{{
$t('device.running-status.866086-42') }}</el-button>
</el-tooltip>
<el-button @click="cancel1">{{ $t('cancel') }}</el-button>
</div>
</el-dialog>
<!-- 添加或修改产品固件对话框 -->
<el-dialog :title="$t('device.running-status.866086-10')" :visible.sync="openFirmware" width="600px"
append-to-body>
<div v-if="firmware == null" style="text-align: center; font-size: 16px">
<i class="el-icon-success" style="color: #67c23a"></i>
{{ $t('device.running-status.866086-11') }}
</div>
<el-descriptions :column="1" border size="large"
v-if="firmware != null && deviceInfo.firmwareVersion < firmware.version"
:labelStyle="{ width: '150px', 'font-weight': 'bold' }">
<template slot="title">
<el-link icon="el-icon-success" type="success" :underline="false">{{
$t('device.running-status.866086-12') }}</el-link>
</template>
<el-descriptions-item :label="$t('device.running-status.866086-13')">{{ firmware.firmwareName
}}</el-descriptions-item>
<el-descriptions-item :label="$t('device.device-edit.148398-4')">{{ firmware.productName
}}</el-descriptions-item>
<el-descriptions-item :label="$t('device.device-edit.148398-12')">Version {{ firmware.version
}}</el-descriptions-item>
<el-descriptions-item :label="$t('device.running-status.866086-16')">
<el-link :href="getDownloadUrl(firmware.filePath)" :underline="false" type="primary">{{
getDownloadUrl(firmware.filePath) }}</el-link>
</el-descriptions-item>
<el-descriptions-item :label="$t('device.running-status.866086-17')">{{ firmware.remark
}}</el-descriptions-item>
</el-descriptions>
<div slot="footer" class="dialog-footer">
<el-button type="success" @click="otaUpgrade"
v-if="firmware != null && deviceInfo.firmwareVersion < firmware.version">{{
$t('device.running-status.866086-18') }}</el-button>
<el-button @click="cancel">{{ $t('cancel') }}</el-button>
</div>
</el-dialog>
<!-- 添加音频对话框 -->
<el-dialog title="添加音频" :visible.sync="addAudioDialogVisible" width="600px" append-to-body>
<el-form :model="newAudio" :rules="audioRules" ref="audioForm" label-width="120px">
<el-form-item label="备注" prop="remark">
<el-input v-model="newAudio.remark" placeholder="请输入备注"></el-input>
</el-form-item>
<el-form-item label="主持人声音" prop="per">
<el-select v-model="newAudio.per" placeholder="请选择主持人声音" style="width: 100%">
<el-option label="小美" :value="0"></el-option>
<el-option label="小宇" :value="1"></el-option>
<el-option label="逍遥" :value="3"></el-option>
<el-option label="丫丫" :value="4"></el-option>
</el-select>
</el-form-item>
<el-form-item label="合成语速" prop="spd">
<el-slider v-model="newAudio.spd" :min="0" :max="15" :step="1" :format-tooltip="formatSpeed">
</el-slider>
</el-form-item>
<el-form-item label="合成音调" prop="pit">
<el-slider v-model="newAudio.pit" :min="0" :max="15" :step="1" :format-tooltip="formatPitch">
</el-slider>
</el-form-item>
<el-form-item label="合成音量" prop="vol">
<el-slider v-model="newAudio.vol" :min="0" :max="15" :step="1" :format-tooltip="formatVolume">
</el-slider>
</el-form-item>
<el-form-item label="合成文本" prop="tex_utf8">
<el-input type="textarea" :rows="4" v-model="newAudio.tex_utf8" placeholder="请输入需要合成的文本">
</el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="addAudioDialogVisible = false"> </el-button>
<el-button type="primary" @click="submitAudioForm"> </el-button>
</div>
</el-dialog>
<!-- 添加播放列表对话框 -->
<el-dialog title="添加播放列表" :visible.sync="addPlaylistDialogVisible" width="600px" append-to-body>
<el-form :model="newPlaylist" :rules="playlistRules" ref="playlistForm" label-width="120px">
<el-form-item label="音频选择" prop="audioId">
<el-select v-model="newPlaylist.audioId" placeholder="请选择音频" style="width: 100%">
<el-option v-for="item in audioList" :key="item.id" :label="item.name" :value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="播放时间" prop="playTime">
<div class="time-range">
<el-time-picker v-model="newPlaylist.playTimeStart" format="HH:mm" placeholder="开始时间"
style="width: 45%">
</el-time-picker>
<span class="time-separator"></span>
<el-time-picker v-model="newPlaylist.playTimeEnd" format="HH:mm" placeholder="结束时间"
style="width: 45%">
</el-time-picker>
</div>
</el-form-item>
<el-form-item label="星期重复" prop="weekdays">
<el-checkbox-group v-model="newPlaylist.weekdays">
<el-checkbox label="1">周一</el-checkbox>
<el-checkbox label="2">周二</el-checkbox>
<el-checkbox label="3">周三</el-checkbox>
<el-checkbox label="4">周四</el-checkbox>
<el-checkbox label="5">周五</el-checkbox>
<el-checkbox label="6">周六</el-checkbox>
<el-checkbox label="0">周日</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="雷达开关" prop="radarEnabled">
<el-switch v-model="newPlaylist.radarEnabled" active-color="#13ce66" inactive-color="#ff4949">
</el-switch>
</el-form-item>
<el-collapse-transition>
<div v-show="newPlaylist.radarEnabled" class="radar-settings">
<el-form-item label="速度范围" prop="radarSpeed">
<div class="speed-range">
<el-input-number v-model="newPlaylist.radarSpeedMin" :min="0"
:max="newPlaylist.radarSpeedMax" :step="1" placeholder="最小速度">
</el-input-number>
<span class="speed-separator">-</span>
<el-input-number v-model="newPlaylist.radarSpeedMax" :min="newPlaylist.radarSpeedMin"
:max="200" :step="1" placeholder="最大速度">
</el-input-number>
<span class="speed-unit">km/h</span>
</div>
</el-form-item>
</div>
</el-collapse-transition>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="addPlaylistDialogVisible = false"> </el-button>
<el-button type="primary" @click="submitPlaylistForm"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getLatestFirmware } from '@/api/iot/firmware';
import { serviceInvoke, serviceInvokeReply } from '@/api/iot/runstatus';
import { getOrderControl } from '@/api/iot/control';
export default {
name: 'running-status',
props: {
device: {
type: Object,
default: null,
},
},
watch: {
device: {
handler(newVal) {
if (newVal && newVal.deviceId != 0) {
this.deviceInfo = newVal;
this.updateDeviceStatus(this.deviceInfo);
this.$nextTick(function () {
this.MonitorChart();
});
console.log("物模型", JSON.stringify(this.deviceInfo.thingsModels));
if (this.deviceInfo.thingsModels && this.deviceInfo.thingsModels.length > 0) {
this.deviceInfo.thingsModels = this.device.thingsModels.sort((a, b) => b.order - a.order);
}
if (this.deviceInfo.chartList && this.deviceInfo.chartList.length > 0) {
this.deviceInfo.chartList = this.deviceInfo.chartList.sort((a, b) => b.order - a.order);
}
}
},
},
},
data() {
return {
title: '设备控制',
shadowUnEnable: false,
statusColor: {
background: '#67C23A',
color: '#fff',
maxWidth: '200px',
},
firmware: {},
openFirmware: false,
loading: true,
deviceInfo: {
deviceId: 0,
serialNumber: '',
productId: '',
productName: '',
status: 0,
isShadow: 0,
rssi: 0,
firmwareVersion: '',
wirelessVersion: '',
firmwareType: 1,
protocolCode: '',
thingsModels: [],
chartList: [],
},
firmwareParams: {
firmwareType: '',
versionInput: '',
},
monitorChart: [
{
chart: {},
data: {
id: '',
name: '',
value: '',
},
},
],
openVersion: false,
firmwareTypeList: [
{
label: this.$t('firmware.index.222541-52'),
value: 1,
},
{
label: 'HTTP',
value: 2,
},
],
rules: {
firmwareType: [
{
required: true,
message: this.$t('device.running-status.866086-43'),
trigger: 'blur',
},
],
},
// 声卡相关数据
basicSettings: {
volume: 50,
audioEnabled: true
},
audioList: [],
defaultList: [],
// 录音相关数据
isRecording: false,
recordingTime: 0,
recordingStatus: '准备就绪',
hasRecording: false,
mediaRecorder: null,
audioChunks: [],
recordings: [],
timer: null,
audioUrl: null,
// 音频列表相关数据
addAudioDialogVisible: false,
newAudio: {
remark: '',
per: 0,
spd: 5,
pit: 5,
vol: 5,
tex_utf8: '',
filename: ''
},
audioRules: {
remark: [
{ required: true, message: '请输入备注', trigger: 'blur' }
],
per: [
{ required: true, message: '请选择主持人声音', trigger: 'change' }
],
spd: [
{ required: true, message: '请设置合成语速', trigger: 'change' }
],
pit: [
{ required: true, message: '请设置合成音调', trigger: 'change' }
],
vol: [
{ required: true, message: '请设置合成音量', trigger: 'change' }
],
tex_utf8: [
{ required: true, message: '请输入合成文本', trigger: 'blur' }
]
},
// 播放列表相关数据
addPlaylistDialogVisible: false,
newPlaylist: {
name: '',
type: '用户',
status: '启用',
audioId: '',
playTimeStart: null,
playTimeEnd: null,
weekdays: [],
radarEnabled: false,
radarSpeedMin: 0,
radarSpeedMax: 120
},
playlistRules: {
audioId: [
{ required: true, message: '请选择音频', trigger: 'change' }
],
playTime: [
{
validator: (rule, value, callback) => {
if (!this.newPlaylist.playTimeStart || !this.newPlaylist.playTimeEnd) {
callback(new Error('请选择播放时间段'));
} else if (this.newPlaylist.playTimeStart >= this.newPlaylist.playTimeEnd) {
callback(new Error('开始时间必须小于结束时间'));
} else {
callback();
}
},
trigger: 'change'
}
],
weekdays: [
{ required: true, message: '请选择重复日期', trigger: 'change' }
],
radarSpeed: [
{
validator: (rule, value, callback) => {
if (this.newPlaylist.radarEnabled) {
if (!this.newPlaylist.radarSpeedMin || !this.newPlaylist.radarSpeedMax) {
callback(new Error('请设置速度范围'));
} else if (this.newPlaylist.radarSpeedMin >= this.newPlaylist.radarSpeedMax) {
callback(new Error('最小速度必须小于最大速度'));
} else {
callback();
}
} else {
callback();
}
},
trigger: 'change'
}
]
},
};
},
mounted() {
if (this.device && this.device.deviceId) {
this.handleDeviceChange(this.device);
this.initDataStatus();
this.initData();
}
},
methods: {
//发送指令
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) {
this.$message({
type: 'warning',
message: 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,
};
//设备在线状态判断
if (this.device.status !== 3 && this.device.isShadow !== 1) {
if (this.device.status === 1) {
title = this.$t('device.device-variable.930930-0');
} else if (this.device.status === 2) {
title = this.$t('device.device-variable.930930-1');
} else {
title = this.$t('device.device-variable.930930-2');
}
this.$message({
type: 'warning',
message: title,
});
return;
}
if ((this.deviceInfo.protocolCode === 'MODBUS-TCP' || this.deviceInfo.protocolCode === 'MODBUS-RTU') && this.device.status === 3) {
await serviceInvokeReply(data).then((response) => {
if (response.code === 200) {
this.$message({
type: 'success',
message: this.$t('device.running-status.866086-25'),
});
} else {
this.$message.error(response.msg);
}
});
} else {
await serviceInvoke(data).then((response) => {
if (response.code === 200) {
this.$message({
type: 'success',
message: this.$t('device.running-status.866086-25'),
});
} else {
this.$message.error(response.msg);
}
});
}
},
// 保留原有的设备状态相关方法
handleDeviceChange(device) {
if (device && device.deviceId != 0) {
const { firmwareVersion, wirelessVersion, firmwareType, ...res } = device;
const data = {
version: firmwareType === 1 ? firmwareVersion : wirelessVersion,
firmwareType,
...res,
};
this.deviceInfo = data;
this.updateDeviceStatus(this.deviceInfo);
this.$nextTick(() => {
this.MonitorChart();
});
if (this.deviceInfo.thingsModels && this.deviceInfo.thingsModels.length > 0) {
this.deviceInfo.thingsModels = this.deviceInfo.thingsModels.sort((a, b) => b.order - a.order);
this.updateBasicSettings(); // 更新基础设置
this.printThingsModels();
}
if (this.deviceInfo.chartList && this.deviceInfo.chartList.length > 0) {
this.deviceInfo.chartList = this.deviceInfo.chartList.sort((a, b) => b.order - a.order);
}
}
},
// 更新基础设置
updateBasicSettings() {
if (!this.deviceInfo.thingsModels) return;
// 更新音频开关状态
const playEnModel = this.deviceInfo.thingsModels.find(model => model.id === 'play_en');
if (playEnModel) {
this.basicSettings.audioEnabled = playEnModel.shadow === '1';
}
// 更新音量设置
const volumeModel = this.deviceInfo.thingsModels.find(model => model.id === 'volume');
if (volumeModel) {
this.basicSettings.volume = parseInt(volumeModel.shadow) || 50;
}
// 更新音频列表
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model.id === 'mp3_list');
if (mp3ListModel && mp3ListModel.shadow) {
try {
// 解析 JSON 字符串
const jsonStr = mp3ListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
// 获取 mp3_list 数组
if (data.sound_card && data.sound_card.mp3_list) {
// 更新音频列表
this.audioList = data.sound_card.mp3_list.map((item, index) => {
// 从 "1_def" 格式中提取名称
const name = item.split('_')[1] || item;
return {
id: index + 1,
name: name
};
});
}
} catch (error) {
console.error('解析音频列表失败:', error);
}
}
// 更新播放列表
const playListModel = this.deviceInfo.thingsModels.find(model => model.id === 'play_list');
if (playListModel && playListModel.shadow) {
try {
const jsonStr = playListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
if (data.sound_card && data.sound_card.play_list) {
this.defaultList = data.sound_card.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` : ''
};
});
}
} catch (error) {
console.error('解析播放列表失败:', error);
}
}
},
printThingsModels() {
console.log('当前物模型数据:', JSON.stringify(this.deviceInfo.thingsModels, null, 2));
this.$message({
message: '物模型数据已打印到控制台',
type: 'success'
});
},
// 声卡特有方法
formatVolume(val) {
return val + '%';
},
handleVolumeChange(val) {
const volumeModel = this.deviceInfo.thingsModels.find(model => model.id === 'volume');
if (volumeModel) {
volumeModel.shadow = val.toString();
this.mqttPublish(this.deviceInfo, volumeModel);
}
},
// 保留其他必要的方法
initData() {
this.$busEvent.$on('updateData', (params) => {
this.updateParam(params);
});
},
// 处理设备上报的数据更新
// updateParam(params) {
// console.log(1111111111)
// if (!params || !this.deviceInfo.thingsModels) return;
// const { serialNumber, productId, data } = params;
// if (data && this.deviceInfo.serialNumber === serialNumber) {
// // 更新物模型数据
// this.deviceInfo.thingsModels.forEach(model => {
// if (data[model.id] !== undefined) {
// model.shadow = data[model.id];
// }
// });
// // 更新基础设置
// this.updateBasicSettings();
// }
// },
//更新参数值
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') {
// 1.对象类型数组,id为数组中一个元素,例如array_01_gateway_temperature
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 {
// 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 + 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) {
// 数组类型匹配,例如array_00_gateway_temperature
if (this.deviceInfo.chartList[k].id == data[j].id) {
this.deviceInfo.chartList[k].shadow = data[j].value;
// 更新图表
for (let m = 0; m < this.monitorChart.length; m++) {
if (data[j].id == this.monitorChart[m].data.id) {
let data = [
{
value: this.deviceInfo.chartList[k].shadow,
name: this.monitorChart[m].data.name,
},
];
this.monitorChart[m].chart.setOption({
series: [
{
data: data,
},
],
});
break;
}
}
}
} else {
// 普通类型匹配
if (this.deviceInfo.chartList[k].id == data[j].id) {
this.deviceInfo.chartList[k].shadow = data[j].value;
// 更新图表
for (let m = 0; m < this.monitorChart.length; m++) {
if (data[j].id == this.monitorChart[m].data.id) {
let data = [
{
value: this.deviceInfo.chartList[k].shadow,
name: this.monitorChart[m].data.name,
},
];
this.monitorChart[m].chart.setOption({
series: [
{
data: data,
},
],
});
break;
}
}
}
}
if (isComplete) {
break;
}
}
}
}
this.updateBasicSettings();
},
initDataStatus() {
this.$busEvent.$on('updateStatus', (status) => {
this.updateStatus(status);
});
},
updateStatus(status) {
let { serialNumber, productId, data } = status;
if (data) {
if (this.deviceInfo.serialNumber == serialNumber) {
this.deviceInfo.status = data.status;
this.deviceInfo.isShadow = data.isShadow;
this.deviceInfo.rssi = data.rssi;
this.updateDeviceStatus(this.deviceInfo);
}
}
},
updateDeviceStatus(device) {
if (device.status == 3) {
this.statusColor.background = '#12d09f';
this.title = this.$t('device.running-status.866086-26');
this.shadowUnEnable = false;
} else {
if (device.isShadow == 1) {
this.statusColor.background = '#486FF2';
this.title = this.$t('device.running-status.866086-27');
this.shadowUnEnable = false;
} else {
this.statusColor.background = '#909399';
this.title = this.$t('device.running-status.866086-28');
this.shadowUnEnable = true;
}
}
this.$emit('statusEvent', this.deviceInfo.status);
},
// 保留固件更新相关方法
viewVersion() {
this.openVersion = true;
this.firmwareParams.firmwareType = 1;
this.firmwareParams.versionInput = '';
this.handleVersionInputChange();
},
handleVersionInputChange() {
if (this.firmwareParams.firmwareType == 1) {
this.firmwareParams.versionInput = 'Version' + this.device.firmwareVersion;
} else {
this.firmwareParams.versionInput = 'Version' + this.device.wirelessVersion;
}
},
cancel1() {
this.openVersion = false;
},
getLatestFirmware() {
const { deviceId, firmwareType } = this.deviceInfo;
getLatestFirmware(deviceId, firmwareType).then((response) => {
if (response.code === 200) {
this.firmware = response.data;
this.openFirmware = true;
}
});
},
cancel() {
this.openFirmware = false;
},
getDownloadUrl(path) {
return window.location.origin + process.env.VUE_APP_BASE_API + path;
},
MonitorChart() {
for (let i = 0; i < this.deviceInfo.chartList.length; i++) {
this.monitorChart[i] = {
chart: this.$echarts.init(this.$refs.map[i]),
data: {
id: this.deviceInfo.chartList[i].id,
name: this.deviceInfo.chartList[i].name,
value: this.deviceInfo.chartList[i].shadow ? this.deviceInfo.chartList[i].shadow : this.deviceInfo.chartList[i].datatype.min,
},
};
var option;
option = {
tooltip: {
formatter: ' {b} <br/> {c}' + this.deviceInfo.chartList[i].datatype.unit,
},
series: [
{
name: this.deviceInfo.chartList[i].datatype.type,
type: 'gauge',
min: this.deviceInfo.chartList[i].datatype.min,
max: this.deviceInfo.chartList[i].datatype.max,
colorBy: 'data',
splitNumber: 10,
radius: '100%',
splitLine: {
distance: 4,
},
axisLabel: {
fontSize: 10,
distance: 10,
},
axisTick: {
distance: 4,
},
axisLine: {
lineStyle: {
width: 8,
color: [
[0.2, '#409EFF'],
[0.8, '#12d09f'],
[1, '#F56C6C'],
],
opacity: 0.3,
},
},
pointer: {
icon: 'triangle',
length: '60%',
width: 7,
},
progress: {
show: true,
width: 8,
},
detail: {
valueAnimation: true,
formatter: '{value}' + ' ' + this.deviceInfo.chartList[i].datatype.unit,
offsetCenter: [0, '80%'],
fontSize: 20,
},
data: [
{
value: this.deviceInfo.chartList[i].shadow ? this.deviceInfo.chartList[i].shadow : this.deviceInfo.chartList[i].datatype.min,
name: this.deviceInfo.chartList[i].name,
},
],
title: {
offsetCenter: [0, '115%'],
fontSize: 16,
},
},
],
};
option && this.monitorChart[i].chart.setOption(option);
}
},
// 录音相关方法
async startRecording() {
if (this.isRecording) return;
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.mediaRecorder = new MediaRecorder(stream);
this.audioChunks = [];
this.mediaRecorder.ondataavailable = (event) => {
this.audioChunks.push(event.data);
};
this.mediaRecorder.onstop = () => {
const audioBlob = new Blob(this.audioChunks, { type: 'audio/mp3' });
this.hasRecording = true;
this.recordingStatus = '录音完成';
// 创建音频预览URL
this.audioUrl = URL.createObjectURL(audioBlob);
};
// 设置数据收集间隔为100ms
this.mediaRecorder.start(100);
this.isRecording = true;
this.recordingStatus = '正在录音...';
this.recordingTime = 0;
// 开始计时
this.timer = setInterval(() => {
this.recordingTime++;
}, 1000);
} catch (error) {
this.$message.error('无法访问麦克风');
console.error('录音错误:', error);
}
},
stopRecording() {
if (this.mediaRecorder && this.isRecording) {
this.mediaRecorder.stop();
this.isRecording = false;
clearInterval(this.timer);
// 停止所有音轨
this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
},
formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
},
reRecord() {
// 释放之前的音频URL
if (this.audioUrl) {
URL.revokeObjectURL(this.audioUrl);
this.audioUrl = null;
}
this.hasRecording = false;
this.audioChunks = [];
this.recordingStatus = '准备就绪';
this.recordingTime = 0; // 重置录音时长
},
async uploadRecording() {
if (!this.hasRecording) return;
try {
const audioBlob = new Blob(this.audioChunks, { type: 'audio/mp3' });
const formData = new FormData();
formData.append('file', audioBlob, `recording_${Date.now()}.mp3`);
// TODO: 替换为实际的上传API
// const response = await uploadFile(formData);
this.$message.success('上传成功');
this.recordings.unshift({
name: `录音_${this.formatTime(this.recordingTime)}`,
time: new Date().toLocaleString()
});
// 重置录音状态
this.reRecord();
} catch (error) {
this.$message.error('上传失败');
console.error('上传错误:', error);
}
},
deleteRecording(index) {
this.recordings.splice(index, 1);
},
handleAudioSwitchChange(val) {
const playEnModel = this.deviceInfo.thingsModels.find(model => model.id === 'play_en');
if (playEnModel) {
playEnModel.shadow = val ? '1' : '0';
this.mqttPublish(this.deviceInfo, playEnModel);
}
},
showAddAudioDialog() {
this.addAudioDialogVisible = true;
this.newAudio = {
remark: '',
per: 0,
spd: 5,
pit: 5,
vol: 5,
tex_utf8: '',
filename: ''
};
},
// 获取未使用的最小ID
getNextAvailableId() {
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model.id === 'mp3_list');
if (mp3ListModel) {
try {
const jsonStr = mp3ListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
if (data.sound_card && data.sound_card.mp3_list) {
// 获取所有已使用的ID
const usedIds = data.sound_card.mp3_list.map(item => {
const id = parseInt(item.split('_')[0]);
return isNaN(id) ? 0 : id;
});
// 找到最小的未使用ID
let nextId = 1;
while (usedIds.includes(nextId)) {
nextId++;
}
return nextId;
}
} catch (error) {
console.error('解析mp3_list失败:', error);
}
}
return 1; // 如果出错返回1
},
submitAudioForm() {
this.$refs.audioForm.validate((valid) => {
if (valid) {
// 找到mp3_list物模型
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model.id === 'mp3_list');
if (mp3ListModel) {
try {
// 获取下一个可用的ID
const nextId = this.getNextAvailableId();
// 生成文件名
const filename = `${nextId}_${this.newAudio.remark}`;
// 构建TTS对象
const ttsData = {
JSON_id: 1,
sound_card: {
TTS: {
per: this.newAudio.per,
spd: this.newAudio.spd,
pit: this.newAudio.pit,
vol: this.newAudio.vol,
tex_utf8: this.newAudio.tex_utf8,
filename: filename
}
}
};
// 更新物模型shadow值
mp3ListModel.shadow = 'JSON=' + JSON.stringify(ttsData);
// 发送更新
this.mqttPublish(this.deviceInfo, mp3ListModel);
this.addAudioDialogVisible = false;
this.$message.success('添加成功');
} catch (error) {
console.error('添加音频失败:', error);
this.$message.error('添加失败');
}
}
}
});
},
handleDeleteAudio(row) {
this.$confirm('确认删除该音频吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 找到mp3_list物模型
const mp3ListModel = this.deviceInfo.thingsModels.find(model => model.id === 'mp3_list');
if (mp3ListModel) {
try {
// 解析当前JSON
const jsonStr = mp3ListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
// 从音频名称中提取完整ID
const audioId = row.name;
// 从mp3_list中移除对应音频
if (data.sound_card && data.sound_card.mp3_list) {
data.sound_card.mp3_list = data.sound_card.mp3_list.filter(item => !item.endsWith(audioId));
// 更新物模型shadow值
const newShadow = 'JSON=' + JSON.stringify(data);
mp3ListModel.shadow = newShadow;
// 发送更新
this.mqttPublish(this.deviceInfo, mp3ListModel).then(() => {
this.$message.success('删除成功');
}).catch(error => {
console.error('发送删除命令失败:', error);
this.$message.error('删除失败');
});
}
} catch (error) {
console.error('解析或更新mp3_list失败:', error);
this.$message.error('删除失败');
}
}
}).catch(() => { });
},
showAddPlaylistDialog() {
this.addPlaylistDialogVisible = true;
this.newPlaylist = {
name: '',
type: '用户',
status: '启用',
audioId: '',
playTimeStart: null,
playTimeEnd: null,
weekdays: [],
radarEnabled: false,
radarSpeedMin: 0,
radarSpeedMax: 120
};
},
submitPlaylistForm() {
this.$refs.playlistForm.validate((valid) => {
if (valid) {
// 找到play_list物模型
const playListModel = this.deviceInfo.thingsModels.find(model => model.id === 'play_list');
if (playListModel) {
try {
// 解析当前JSON
const jsonStr = playListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
// 获取选中的音频信息
const selectedAudio = this.audioList.find(audio => audio.id === this.newPlaylist.audioId);
if (!selectedAudio) {
this.$message.error('未找到选中的音频');
return;
}
// 构建新的播放项
const newPlayItem = {
play: {
en: 1,
num: this.defaultList.length + 1,
sou: 0,
filename: `${selectedAudio.id}_${selectedAudio.name}`,
play_time: 1,
pause_time: 0
},
time: {
en: 1,
begin: this.convertTimeToSeconds(this.newPlaylist.playTimeStart),
end: this.convertTimeToSeconds(this.newPlaylist.playTimeEnd),
week: this.convertWeekArrayToValue(this.newPlaylist.weekdays)
},
speed: {
en: this.newPlaylist.radarEnabled ? 1 : 0,
min: this.newPlaylist.radarEnabled ? this.newPlaylist.radarSpeedMin : 0,
max: this.newPlaylist.radarEnabled ? this.newPlaylist.radarSpeedMax : 0
}
};
// 添加到播放列表
if (!data.sound_card) {
data.sound_card = {};
}
if (!data.sound_card.play_list) {
data.sound_card.play_list = [];
}
data.sound_card.play_list.push(newPlayItem);
// 更新物模型shadow值
playListModel.shadow = 'JSON=' + JSON.stringify(data);
// 发送更新
this.mqttPublish(this.deviceInfo, playListModel).then(() => {
this.addPlaylistDialogVisible = false;
this.$message.success('添加成功');
}).catch(error => {
console.error('发送播放列表更新失败:', error);
this.$message.error('添加失败');
});
} catch (error) {
console.error('解析或更新播放列表失败:', error);
this.$message.error('添加失败');
}
}
}
});
},
// 将时间转换为秒数
convertTimeToSeconds(time) {
if (!time) return 0;
return time.getHours() * 3600 + time.getMinutes() * 60;
},
// 将星期数组转换为位值
convertWeekArrayToValue(weekdays) {
let value = 0;
weekdays.forEach(day => {
value |= (1 << parseInt(day));
});
return value;
},
formatSpeed(val) {
return val;
},
formatPitch(val) {
return val;
},
formatVolume(val) {
return val;
},
// 将秒数转换为时间格式
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 weekMap = {
1: '周一',
2: '周二',
3: '周三',
4: '周四',
5: '周五',
6: '周六',
0: '周日'
};
const result = [];
for (let i = 0; i < 7; i++) {
if (week & (1 << i)) {
result.push(weekMap[i]);
}
}
return result;
},
handleStatusChange(row) {
const playListModel = this.deviceInfo.thingsModels.find(model => model.id === 'play_list');
if (playListModel) {
try {
const jsonStr = playListModel.shadow.replace('JSON=', '');
const data = JSON.parse(jsonStr);
if (data.sound_card && data.sound_card.play_list) {
// 找到对应的播放项
const playItem = data.sound_card.play_list[row.id - 1];
if (playItem) {
// 更新状态
playItem.play.en = row.status === '启用' ? 1 : 0;
// 更新物模型shadow值
playListModel.shadow = 'JSON=' + JSON.stringify(data);
// 发送更新
this.mqttPublish(this.deviceInfo, playListModel).then(() => {
this.$message.success('状态更新成功');
}).catch(error => {
console.error('发送状态更新失败:', error);
this.$message.error('状态更新失败');
});
}
}
} catch (error) {
console.error('解析或更新播放列表状态失败:', error);
this.$message.error('状态更新失败');
}
}
},
handleDeletePlaylist(row) {
this.$confirm('确认删除该播放项吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const playListModel = this.deviceInfo.thingsModels.find(model => model.id === 'play_list');
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(row.id - 1, 1);
// 更新序号
data.sound_card.play_list.forEach((item, index) => {
item.play.num = index + 1;
});
// 更新物模型shadow值
playListModel.shadow = 'JSON=' + JSON.stringify(data);
// 发送更新
this.mqttPublish(this.deviceInfo, playListModel).then(() => {
this.$message.success('删除成功');
}).catch(error => {
console.error('发送删除命令失败:', error);
this.$message.error('删除失败');
});
}
} catch (error) {
console.error('解析或更新播放列表失败:', error);
this.$message.error('删除失败');
}
}
}).catch(() => {});
},
},
};
</script>
<style lang="scss" scoped>
.running-status {
padding: 20px;
.status-col {
.title {
line-height: 28px;
font-size: 16px;
}
}
.mode-section {
margin-bottom: 30px;
}
.mode-card {
margin-bottom: 20px;
transition: all 0.3s;
padding: 20px;
&:hover {
transform: translateY(-5px);
}
.mode-header {
display: flex;
align-items: center;
margin-bottom: 15px;
i,
.svg-icon {
font-size: 20px;
margin-right: 8px;
color: #409EFF;
}
.mode-title {
font-size: 16px;
font-weight: bold;
color: #303133;
}
}
.mode-content {
text-align: center;
padding: 10px 0;
.title {
font-size: 16px;
font-weight: bold;
}
}
}
.settings-card,
.audio-list-card,
.default-list-card {
margin-bottom: 20px;
transition: all 0.3s;
&:hover {
transform: translateY(-5px);
}
.settings-header,
.audio-list-header,
.default-list-header {
display: flex;
justify-content: space-between;
align-items: center;
.settings-title,
.audio-list-title,
.default-list-title {
font-size: 16px;
font-weight: bold;
color: #303133;
}
}
}
.el-table {
margin-top: 15px;
}
.el-slider {
margin-top: 10px;
}
}
.voice-control-card {
margin-bottom: 20px;
transition: all 0.3s;
&:hover {
transform: translateY(-5px);
}
.voice-control-header {
display: flex;
justify-content: space-between;
align-items: center;
.voice-control-title {
font-size: 16px;
font-weight: bold;
color: #303133;
}
}
.voice-control-content {
padding: 20px;
text-align: center;
.recorder-status {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
.status-indicator {
width: 60px;
height: 60px;
border-radius: 50%;
background: #f4f4f5;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
transition: all 0.3s;
i {
font-size: 30px;
color: #909399;
}
&.recording {
background: #fef0f0;
animation: pulse 1.5s infinite;
i {
color: #f56c6c;
}
}
}
.status-text {
font-size: 14px;
color: #606266;
}
}
.timer-display {
font-size: 24px;
font-weight: bold;
color: #303133;
margin-bottom: 20px;
}
.control-buttons {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 20px;
.el-button {
width: 50px;
height: 50px;
font-size: 20px;
}
}
.recording-preview {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
.preview-title {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
text-align: left;
}
.audio-player {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
audio {
width: 100%;
height: 40px;
}
.preview-controls {
display: flex;
justify-content: center;
margin-top: 5px;
.el-button {
padding: 8px 15px;
i {
margin-right: 5px;
}
}
}
}
}
.recording-list {
text-align: left;
border-top: 1px solid #ebeef5;
padding-top: 15px;
.list-title {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
}
.recording-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #ebeef5;
.recording-name {
flex: 1;
font-size: 14px;
color: #303133;
}
.recording-time {
font-size: 12px;
color: #909399;
margin-right: 10px;
}
}
}
}
}
@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 10px rgba(245, 108, 108, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(245, 108, 108, 0);
}
}
.radar-settings {
margin-top: -20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
.speed-range {
display: flex;
align-items: center;
gap: 10px;
.el-input-number {
width: 120px;
}
.speed-separator {
color: #606266;
font-size: 16px;
}
.speed-unit {
color: #606266;
margin-left: 5px;
}
}
}
.time-range {
display: flex;
align-items: center;
gap: 10px;
.time-separator {
color: #606266;
font-size: 14px;
}
}
</style>