This commit is contained in:
JayJiaJun 2025-05-30 16:23:45 +08:00
parent b4a2661c04
commit d832944320
5 changed files with 2166 additions and 5 deletions

BIN
dist.zip

Binary file not shown.

View File

@ -130,3 +130,7 @@ Element.Dialog.props.closeOnClickModal.default = false;
// 添加 token 打印(从 Cookies 或 localStorage 获取)
const token = Cookies.get('Admin-Token') || localStorage.getItem('Admin-Token');
console.log(' 当前 Token:', token); // 打印 token 到控制台

File diff suppressed because it is too large Load Diff

View File

@ -187,8 +187,12 @@
ref="gatewayRunningStatus" :device="form" @statusEvent="getDeviceStatusData($event)" /> ref="gatewayRunningStatus" :device="form" @statusEvent="getDeviceStatusData($event)" />
<relay v-else-if="form.productName && form.productName.toLowerCase().includes('多路控制器')" ref="relay" <relay v-else-if="form.productName && form.productName.toLowerCase().includes('多路控制器')" ref="relay"
:device="form" @statusEvent="getDeviceStatusData($event)" /> :device="form" @statusEvent="getDeviceStatusData($event)" />
<gatewaypre v-else-if="form.productName && form.productName.toLowerCase().includes('网关卡预配置')" ref="gatewaypre" <gatewaypre v-else-if="form.productName && form.productName.toLowerCase().includes('网关卡预配置')"
:device="form" @statusEvent="getDeviceStatusData($event)" /> ref="gatewaypre" :device="form" @statusEvent="getDeviceStatusData($event)" />
<acousto_optic v-else-if="form.productName && form.productName.toLowerCase().includes('声光')"
ref="acousto_optic" :device="form" @statusEvent="getDeviceStatusData($event)" />
<voicecard v-else-if="form.productName && form.productName.toLowerCase().includes('声卡')"
ref="voicecard" :device="form" @statusEvent="getDeviceStatusData($event)" />
<running-status v-else ref="runningStatus" :device="form" <running-status v-else ref="runningStatus" :device="form"
@statusEvent="getDeviceStatusData($event)" /> @statusEvent="getDeviceStatusData($event)" />
@ -453,6 +457,8 @@ import defaultSettings from '@/settings';
import gatewayRunningStatus from './gatewayrunning-status.vue'; import gatewayRunningStatus from './gatewayrunning-status.vue';
import relay from './relay.vue' import relay from './relay.vue'
import gatewaypre from './gatewaypre.vue' import gatewaypre from './gatewaypre.vue'
import acousto_optic from'./acousto_optic.vue'
import voicecard from './voicecard.vue';
export default { export default {
name: 'DeviceEdit', name: 'DeviceEdit',
dicts: ['iot_device_status', 'iot_location_way'], dicts: ['iot_device_status', 'iot_location_way'],
@ -486,7 +492,9 @@ export default {
deviceInlineVideo, deviceInlineVideo,
gatewayRunningStatus, gatewayRunningStatus,
relay, relay,
gatewaypre gatewaypre,
acousto_optic,
voicecard
}, },
watch: { watch: {
activeName(val) { activeName(val) {

View File

@ -0,0 +1,906 @@
<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>
</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">
<el-form-item label="音量设置">
<el-slider v-model="basicSettings.volume" :min="0" :max="100" :format-tooltip="formatVolume"
@change="handleVolumeChange" style="width: 80%">
</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>
</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 prop="duration" label="时长" width="120" align="center">
</el-table-column>
<el-table-column prop="size" label="大小" width="120" align="center">
</el-table-column>
</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>
</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="type" label="类型" width="120" align="center">
</el-table-column>
<el-table-column prop="status" label="状态" width="120" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status === '启用' ? 'success' : 'info'">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
</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>
</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();
});
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
},
audioList: [
{ id: 1, name: '音频1', duration: '00:30', size: '2.5MB' },
{ id: 2, name: '音频2', duration: '01:15', size: '3.8MB' },
{ id: 3, name: '音频3', duration: '00:45', size: '2.1MB' }
],
defaultList: [
{ id: 1, name: '默认音频1', type: '系统', status: '启用' },
{ id: 2, name: '默认音频2', type: '用户', status: '启用' },
{ id: 3, name: '默认音频3', type: '系统', status: '禁用' }
],
//
isRecording: false,
recordingTime: 0,
recordingStatus: '准备就绪',
hasRecording: false,
mediaRecorder: null,
audioChunks: [],
recordings: [],
timer: null,
audioUrl: null,
};
},
mounted() {
if (this.device && this.device.deviceId) {
this.handleDeviceChange(this.device);
this.initDataStatus();
this.initData();
}
},
methods: {
//
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);
}
if (this.deviceInfo.chartList && this.deviceInfo.chartList.length > 0) {
this.deviceInfo.chartList = this.deviceInfo.chartList.sort((a, b) => b.order - a.order);
}
}
},
//
formatVolume(val) {
return val + '%';
},
handleVolumeChange(val) {
//
console.log('音量变化:', val);
// TODO: API
},
//
initData() {
this.$busEvent.$on('updateData', (params) => {
this.updateParam(params);
});
},
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);
}
},
};
</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);
}
}
</style>