3763 lines
167 KiB
Vue
3763 lines
167 KiB
Vue
<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" style="font-size: 30px;color: #409EFF;"></i>
|
||
<span class="mode-title">
|
||
<template v-if="boardOnline">{{ $t('device.running-status.866086-0') }}</template>
|
||
<template v-else>设备离线</template>
|
||
</span>
|
||
</div>
|
||
<div class="mode-content">
|
||
<template v-if="boardOnline">
|
||
<span class="title" :style="{ color: statusColor.background }">{{ title }}</span>
|
||
</template>
|
||
<template v-else>
|
||
<span class="title offline">设备离线</span>
|
||
</template>
|
||
</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" style="font-size: 30px; color: #409EFF;" />
|
||
<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="mode-card" shadow="hover">
|
||
<div class="mode-header">
|
||
<div class="header-left">
|
||
<i class="el-icon-info"></i>
|
||
<span class="mode-title">编译信息</span>
|
||
</div>
|
||
<el-button type="text" @click="refreshBuildInfo" size="normal">
|
||
<i class="el-icon-refresh"></i> 刷新
|
||
</el-button>
|
||
</div>
|
||
<div class="mode-content">
|
||
<div class="info-item" v-loading="buildInfoLoading">
|
||
<span class="info-label">编译版本:</span>
|
||
<span class="info-value">{{ buildVersion || '加载中...' }}</span>
|
||
</div>
|
||
<div class="info-item" v-loading="buildInfoLoading">
|
||
<span class="info-label">编译时间:</span>
|
||
<span class="info-value">{{ buildTime || '加载中...' }}</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- 基础设置(显卡) -->
|
||
<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.screenEnabled" active-color="#13ce66"
|
||
inactive-color="#ff4949" @change="handleScreenSwitchChange">
|
||
</el-switch>
|
||
</el-form-item>
|
||
<el-form-item label="屏幕亮度">
|
||
<el-slider v-model="basicSettings.brightness" :min="0" :max="100"
|
||
:format-tooltip="formatBrightness" @change="handleBrightnessChange" style="width: 80%"
|
||
:disabled="!basicSettings.screenEnabled">
|
||
</el-slider>
|
||
</el-form-item>
|
||
<!-- <el-form-item>
|
||
<el-button type="danger" @click="handleClearScreen"
|
||
:disabled="!basicSettings.screenEnabled">清除屏幕</el-button>
|
||
</el-form-item> -->
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<!-- 屏幕参数卡片 -->
|
||
<el-card class="settings-card" shadow="hover">
|
||
<div slot="header" class="settings-header">
|
||
<span class="settings-title">屏幕参数</span>
|
||
</div>
|
||
<el-form :model="screenParams" label-width="120px" style="margin-top: 20px;">
|
||
<el-form-item label="屏幕模板">
|
||
<el-select v-model="screenParams.template" @change="handleTemplateChange">
|
||
<el-option v-for="tpl in screenTemplates" :key="tpl.value" :label="tpl.label"
|
||
:value="tpl.value"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="OE极性">
|
||
<el-switch v-model="screenParams.oePolarity" :active-value="1"
|
||
:inactive-value="0"></el-switch>
|
||
</el-form-item>
|
||
<el-form-item label="DATA极性">
|
||
<el-switch v-model="screenParams.dataPolarity" :active-value="1"
|
||
:inactive-value="0"></el-switch>
|
||
</el-form-item>
|
||
<el-form-item label="屏宽">
|
||
<el-input-number v-model="screenParams.width" :min="1" :max="10000"></el-input-number>
|
||
</el-form-item>
|
||
<el-form-item label="屏高">
|
||
<el-input-number v-model="screenParams.height" :min="1" :max="10000"></el-input-number>
|
||
</el-form-item>
|
||
<el-form-item label="屏幕角度">
|
||
<el-radio-group v-model="screenParams.angle" @change="drawAddProgramPreview">
|
||
<el-radio :label="0">0°</el-radio>
|
||
<el-radio :label="90">90°</el-radio>
|
||
<el-radio :label="180">180°</el-radio>
|
||
<el-radio :label="270">270°</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="handleSendScreenParams">下发参数</el-button>
|
||
</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" align="center">
|
||
</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-col>
|
||
|
||
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
|
||
<!-- 开机参数卡片(右侧) -->
|
||
<el-card class="settings-card" shadow="hover">
|
||
<div slot="header" class="settings-header">
|
||
<span class="settings-title">开机参数</span>
|
||
</div>
|
||
<el-form :model="bootSettings" label-width="120px" style="margin-top: 20px;">
|
||
<el-form-item label="开机界面可选">
|
||
<el-switch v-model="bootSettings.showBootScreen"
|
||
@change="handleBootScreenSwitchChange"></el-switch>
|
||
</el-form-item>
|
||
<el-form-item label="重置开关">
|
||
<el-switch v-model="bootSettings.resetEnabled"
|
||
@change="handleResetSwitchChange"></el-switch>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<!-- 传感器阈值设置卡片 -->
|
||
<el-card class="settings-card" shadow="hover">
|
||
<div slot="header" class="settings-header">
|
||
<span class="settings-title">传感器阈值设置</span>
|
||
</div>
|
||
<el-form :model="sensorThreshold" label-width="120px" style="margin-top: 20px;">
|
||
<el-form-item label="速度阈值">
|
||
|
||
<el-input-number v-model="sensorThreshold.speed" :min="0" :max="200" :step="1"
|
||
style="width: 100%">
|
||
</el-input-number>
|
||
</el-form-item>
|
||
<el-form-item label="温度阈值">
|
||
<el-input-number v-model="sensorThreshold.temp" :min="-50" :max="100" :step="1"
|
||
style="width: 100%">
|
||
</el-input-number>
|
||
<!-- <span style="margin-left: 8px; color: #909399;">°C</span> -->
|
||
</el-form-item>
|
||
<el-form-item label="湿度阈值">
|
||
<el-input-number v-model="sensorThreshold.humi" :min="0" :max="100" :step="1"
|
||
style="width: 100%">
|
||
</el-input-number>
|
||
<!-- <span style="margin-left: 8px; color: #909399;">%</span> -->
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<div style="text-align: center;">
|
||
<el-button type="primary" @click="handleSendSensorThreshold">下发阈值</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
</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-row :gutter="20" style="margin-top: 20px;">
|
||
<el-col :xs="22" :sm="22" :md="22" :lg="22" :xl="22">
|
||
<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> -->
|
||
<el-button type="success" size="mini" icon="el-icon-plus" @click="showAddProgramDialog">
|
||
添加自定义节目
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 显卡节目列表 -->
|
||
<div v-if="programList.length > 0" style="margin-bottom: 20px;">
|
||
<h4 style="margin-bottom: 15px; color: #409EFF;">显卡节目列表</h4>
|
||
<el-table :data="programList" 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="120" align="center">
|
||
</el-table-column>
|
||
<el-table-column prop="duration" label="时长(秒)" width="100" align="center">
|
||
</el-table-column>
|
||
<el-table-column prop="mode" label="模式" width="120" align="center">
|
||
</el-table-column>
|
||
<el-table-column prop="zones" label="分区数" width="100" align="center">
|
||
</el-table-column>
|
||
<el-table-column prop="content" label="内容" min-width="200" align="center">
|
||
<template slot-scope="scope">
|
||
<el-tooltip :content="scope.row.content" placement="top">
|
||
<span>{{ scope.row.content.length > 30 ? scope.row.content.substring(0, 30) +
|
||
'...' : scope.row.content }}</span>
|
||
</el-tooltip>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="使能" width="100" align="center">
|
||
<template slot-scope="scope">
|
||
<el-switch v-model="scope.row.enabled" :active-value="1" :inactive-value="0"
|
||
@change="handleProgramEnableChange">
|
||
</el-switch>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="150" align="center">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" icon="el-icon-edit" @click="editProgram(scope.row)">
|
||
编辑
|
||
</el-button>
|
||
<el-button type="text" icon="el-icon-delete" @click="deleteProgram(scope.row)">
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<template v-if="programList.length === 0">
|
||
<div style="padding: 20px 0;">
|
||
<el-empty description="暂无节目列表数据"></el-empty>
|
||
</div>
|
||
</template>
|
||
</el-card>
|
||
</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')"
|
||
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" style="text-align: center; font-size: 16px">
|
||
<i class="el-icon-success" style="color: #67c23a"></i>
|
||
{{ $t('device.running-status.866086-11') }}
|
||
</div>
|
||
<div v-else-if="!compareVersions(deviceInfo.firmwareVersion, firmware.version)"
|
||
style="text-align: center; font-size: 16px">
|
||
<i class="el-icon-warning" style="color: #E6A23C"></i>
|
||
{{ $t('device.running-status.866086-47') }}
|
||
</div>
|
||
<el-descriptions v-else :column="1" border size="large"
|
||
: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 && compareVersions(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="isEditProgram ? '编辑自定义节目' : '添加自定义节目'" :visible.sync="addProgramDialogVisible" width="700px"
|
||
append-to-body>
|
||
<div class="sticky-canvas-preview">
|
||
<canvas ref="addProgramPreviewCanvas" width="640px" height="240px"
|
||
style="width:100%;height:240px;border:1px solid #e0e0e0;border-radius:16px;box-shadow:0 2px 8px #e0e0e0;background:#FFFFFF00; margin-bottom: 20px;"></canvas>
|
||
</div>
|
||
<el-form :model="addProgramForm" ref="addProgramFormRef" label-width="60px" size="mini"
|
||
style="margin-bottom:0;">
|
||
<el-form-item label="节目模式" label-width="70px">
|
||
<el-select v-model="addProgramForm.mode" placeholder="请选择模式" @change="onAddProgramModeChange"
|
||
style="width:140px">
|
||
<el-option v-for="(item, idx) in addProgramModes" :key="idx" :label="item" :value="idx" />
|
||
</el-select>
|
||
<el-form-item label="节目时长" label-width="70px" style="display:inline-block;margin-left:24px;">
|
||
<el-input-number v-model="addProgramForm.duration" :min="1" :max="600" style="width:90px" /> 秒
|
||
</el-form-item>
|
||
</el-form-item>
|
||
<el-form-item label="节目备注" label-width="70px">
|
||
<el-input v-model="addProgramForm.remark" placeholder="请输入节目备注名称" style="width:100%" />
|
||
</el-form-item>
|
||
<el-divider style="margin:10px 0;" />
|
||
<div v-for="(zone, zIdx) in addProgramForm.zones" :key="zIdx"
|
||
style="margin-bottom:10px;padding:10px 12px 8px 12px;border:1px solid #f0f0f0;border-radius:8px;background:#fafbfc;">
|
||
<div style="font-weight:bold;font-size:14px;margin-bottom:6px;color:#409EFF;">分区{{ zIdx + 1 }}</div>
|
||
<!-- 第一行:类型、常用语句、展示信息/图片路径 -->
|
||
<el-row :gutter="12" style="margin-bottom:2px;">
|
||
<el-col :span="6">
|
||
<el-form-item label="类型" label-width="42px">
|
||
<el-select v-model="zone.playType" placeholder="类型" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramPlayTypes" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6" v-if="zone.playType === 0">
|
||
<el-form-item label="常用" label-width="42px">
|
||
<el-select v-model="zone.commonPhrase" placeholder="常用语句"
|
||
@change="onAddProgramPhraseChange(zIdx)" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramCommonPhrases" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="15" v-if="zone.playType === 0">
|
||
<el-form-item label="展示信息" label-width="70px">
|
||
<el-input v-model="zone.displayText" type="textarea" placeholder="请输入展示信息" autosize
|
||
style="width:100%" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="19" v-if="zone.playType === 1">
|
||
<el-form-item label="图片选择" label-width="70px">
|
||
<el-select v-model="zone.image" placeholder="请选择图片" style="width:100%">
|
||
<el-option v-for="item in addProgramTrafficImages" :key="item.value"
|
||
:label="item.label" :value="item.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 新增:图片尺寸和颜色 -->
|
||
<el-row :gutter="12" v-if="zone.playType === 1" style="margin-bottom:2px;">
|
||
<el-col :span="8">
|
||
<el-form-item label="图片尺寸" label-width="70px">
|
||
<el-select v-model="zone.imageSize" placeholder="请选择尺寸" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramImageSizes" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="图片颜色" label-width="70px">
|
||
<el-select v-model="zone.imageColor" placeholder="请选择颜色" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramImageColors" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 第二行:字体、字号、颜色、加粗、拉伸(仅文字) -->
|
||
<el-row :gutter="12" v-if="zone.playType === 0" style="margin-bottom:2px;">
|
||
<el-col :span="6">
|
||
<el-form-item label="字体" label-width="42px">
|
||
<el-select v-model="zone.font" placeholder="字体" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramFonts" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="7">
|
||
<el-form-item label="字体(英)" label-width="70px">
|
||
<el-select v-model="zone.fontEn" placeholder="字体(英)" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramFontEns" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="字号" label-width="42px">
|
||
<el-select v-model="zone.fontSize" placeholder="字号" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramFontSizes" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="颜色" label-width="42px">
|
||
<el-select v-model="zone.fontColor" placeholder="颜色" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramFontColors" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="加粗" label-width="42px">
|
||
<el-select v-model="zone.fontBold" placeholder="加粗" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramFontBold" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="拉伸" label-width="42px">
|
||
<el-select v-model="zone.fontStretch" placeholder="拉伸" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramFontStretch" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 第三行:动画特效、速度、停留、水平、垂直 -->
|
||
<el-row :gutter="8" style="margin-bottom:2px;">
|
||
<el-col :span="6">
|
||
<el-form-item label="特效" label-width="42px">
|
||
<el-select v-model="zone.effect" placeholder="特效" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramEffects" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="速度" label-width="42px">
|
||
<el-select v-model="zone.speed" placeholder="速度" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramSpeeds" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="停留" label-width="42px">
|
||
<el-select v-model="zone.stayTime" placeholder="停留" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramStayTimes" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="水平" label-width="42px">
|
||
<el-select v-model="zone.hAlign" placeholder="水平" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramHorizontalAlign" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="垂直" label-width="42px">
|
||
<el-select v-model="zone.vAlign" placeholder="垂直" style="width:100%">
|
||
<el-option v-for="(item, idx) in addProgramVerticalAlign" :key="idx" :label="item"
|
||
:value="idx" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 第四行:X、宽、Y、高 -->
|
||
<el-row :gutter="8">
|
||
<el-col :span="5">
|
||
<el-form-item label="X" label-width="20px">
|
||
<el-input-number :value="zoneDisplay(zIdx).x" :min="0" style="width:100%"
|
||
@input="setZoneDisplay(zIdx, 'x', $event)" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="宽" label-width="24px">
|
||
<el-input-number :value="zoneDisplay(zIdx).width" :min="1" style="width:100%"
|
||
@input="setZoneDisplay(zIdx, 'width', $event)" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="Y" label-width="20px">
|
||
<el-input-number :value="zoneDisplay(zIdx).y" :min="0" style="width:100%"
|
||
@input="setZoneDisplay(zIdx, 'y', $event)" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="高" label-width="24px">
|
||
<el-input-number :value="zoneDisplay(zIdx).height" :min="1" style="width:100%"
|
||
@input="setZoneDisplay(zIdx, 'height', $event)" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
<div style="text-align: right;margin-top:8px;">
|
||
<el-button @click="addProgramDialogVisible = false" size="mini">取 消</el-button>
|
||
<el-button type="primary" @click="submitAddProgram" size="mini">{{ isEditProgram ? '更新' : '确定'
|
||
}}</el-button>
|
||
</div>
|
||
</el-form>
|
||
</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);
|
||
}
|
||
}
|
||
},
|
||
},
|
||
// 监听自定义节目参数变化,实时刷新预览
|
||
addProgramForm: {
|
||
handler() {
|
||
this.$nextTick(() => {
|
||
this.prepareAddProgramPreviewAssets();
|
||
this.resetAddProgramPreviewPage();
|
||
this.resetAddProgramPreviewAnimState();
|
||
this.drawAddProgramPreview();
|
||
this.startAddProgramPreviewAutoPage();
|
||
this.startAddProgramPreviewAnimLoop();
|
||
});
|
||
},
|
||
deep: true
|
||
},
|
||
addProgramDialogVisible(val) {
|
||
if (val) {
|
||
this.$nextTick(() => {
|
||
this.prepareAddProgramPreviewAssets();
|
||
this.resetAddProgramPreviewPage();
|
||
this.resetAddProgramPreviewAnimState();
|
||
this.drawAddProgramPreview();
|
||
this.startAddProgramPreviewAutoPage();
|
||
this.startAddProgramPreviewAnimLoop();
|
||
});
|
||
} else {
|
||
this.stopAddProgramPreviewAutoPage();
|
||
this.stopAddProgramPreviewAnimLoop();
|
||
}
|
||
},
|
||
screenParams: {
|
||
handler() {
|
||
// 屏幕参数变化时,重置分区、刷新预览
|
||
this.onAddProgramModeChange(this.addProgramForm.mode);
|
||
this.prepareAddProgramPreviewAssets();
|
||
this.resetAddProgramPreviewPage();
|
||
this.resetAddProgramPreviewAnimState();
|
||
this.drawAddProgramPreview();
|
||
},
|
||
deep: true
|
||
},
|
||
},
|
||
|
||
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: {
|
||
screenEnabled: true,
|
||
brightness: 50
|
||
},
|
||
bootSettings: {
|
||
showBootScreen: false,
|
||
resetEnabled: false
|
||
},
|
||
// 传感器阈值设置
|
||
sensorThreshold: {
|
||
speed: 60,
|
||
temp: 30,
|
||
humi: 50
|
||
},
|
||
|
||
screenParams: {
|
||
template: '', // 屏幕模板
|
||
oePolarity: '', // OE极性
|
||
dataPolarity: '', // DATA极性
|
||
width: '', // 屏宽
|
||
height: '', // 屏高
|
||
angle: '' // 屏幕角度
|
||
},
|
||
// 新增:自定义节目相关
|
||
addProgramDialogVisible: false,
|
||
isEditProgram: false, // 是否为编辑模式
|
||
editingProgram: null, // 当前编辑的节目
|
||
addProgramForm: {
|
||
mode: 0,
|
||
duration: 10,
|
||
remark: '',
|
||
zones: [
|
||
{
|
||
playType: 0,
|
||
commonPhrase: 0,
|
||
displayText: '',
|
||
font: 0,
|
||
fontEn: 0, // 新增英文字体
|
||
fontShape: 0,
|
||
fontSize: 0,
|
||
fontColor: 0, // 红色(默认)
|
||
fontBold: 0,
|
||
fontStretch: 0,
|
||
image: '',
|
||
imageSize: 1, // 32x32
|
||
imageColor: 0, // 红色(默认)
|
||
effect: 0,
|
||
speed: 4,
|
||
stayTime: 5,
|
||
hAlign: 1,
|
||
vAlign: 1,
|
||
x: 0,
|
||
y: 0,
|
||
width: 32,
|
||
height: 64
|
||
}
|
||
]
|
||
},
|
||
addProgramModes: ['模式1', '模式2(上下)', '模式3(左右)', '模式4(上中下)', '模式5', '模式6'],
|
||
addProgramPlayTypes: ['文字', '图片'],
|
||
addProgramCommonPhrases: ['自定义', "公安交警正在巡逻",
|
||
"公安交警停车检查",
|
||
"前方事故减速慢行",
|
||
"警察临检请您配合",
|
||
"交警临检请您配合",
|
||
"《《《《《",
|
||
"》》》》》",
|
||
"禁止停车",
|
||
"交通管制禁止通行",
|
||
"公安检查靠边停车",
|
||
"前方事故道路封闭",
|
||
"禁止掉头",
|
||
"雨雪天气注意安全",
|
||
"大雾天气减速慢行",
|
||
"靠右停车接受检查",
|
||
"警察临检靠边停车"],
|
||
addProgramFonts: ['宋体(中)', '黑体(中)', '楷体(中)'],
|
||
addProgramFontShapes: ['圆角(英)', '直角(英)'],
|
||
addProgramFontSizes: ['16px', '24px', '32px'],
|
||
addProgramFontColors: ['红色', '绿色', '黄色'],
|
||
addProgramFontBold: ['不加粗', '加粗'],
|
||
addProgramFontStretch: ['不拉伸', '横向拉伸', '纵向拉伸'],
|
||
addProgramEffects: ['立即显示', '左移', '右移', '上移', '下移', '连续左移', '闪烁换页'],
|
||
addProgramSpeeds: ['1X', '2X', '3X', '4X', '5X'],
|
||
addProgramStayTimes: ['不停顿', '100毫秒', '500毫秒', '1秒', '2秒', '3秒', '4秒', '5秒', '6秒', '8秒', '10秒'],
|
||
addProgramHorizontalAlign: ['居左对齐', '居中对齐', '居右对齐'],
|
||
addProgramVerticalAlign: ['顶部对齐', '居中对齐', '底部对齐'],
|
||
addProgramImageSizes: ['16x16', '32x32', '48x48', '64x64'],
|
||
addProgramImageColors: ['红色', '绿色', '黄色'],
|
||
addProgramFontEns: [
|
||
'打字(英)', '长黑(英)', '白斜(英)', '线形(英)', '方斜(英)', '歌德(英)', '黑正(英)', '美术(英)', '手写(英)', '正圆(英)', '时钟(英)圆角(英)'
|
||
],
|
||
// 自动分页相关
|
||
addProgramPreviewPage: [0], // 每个分区当前页
|
||
addProgramPreviewTimer: [], // 每个分区定时器
|
||
addProgramPreviewAssets: [], // 每个分区的分行分页缓存
|
||
addProgramPreviewAnimState: [], // 每个分区动画状态
|
||
addProgramPreviewAnimFrame: null, // 动画主循环ID
|
||
addProgramTrafficImages: [],
|
||
screenTemplates: [
|
||
{ label: '自定义', value: '', params: { oe: '', data: '', w: '', h: '', angle: '' } },
|
||
{ label: '伸缩屏', value: 'flex', params: { oe: 1, data: 1, w: 64, h: 32, angle: 270 } },
|
||
{ label: '大屏', value: 'big', params: { oe: 1, data: 1, w: 64, h: 32, angle: 180 } },
|
||
{ label: '小屏', value: 'small', params: { oe: 1, data: 1, w: 64, h: 16, angle: 0 } }
|
||
],
|
||
// 编译信息
|
||
buildVersion: '',
|
||
buildTime: '',
|
||
buildInfoLoading: false,
|
||
// 节目列表
|
||
programList: [],
|
||
// 节目使能状态数组
|
||
programEnableList: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], // 默认全部开启
|
||
boardOnline: false,
|
||
};
|
||
},
|
||
mounted() {
|
||
this.loadTrafficImages();
|
||
if (this.device && this.device.deviceId) {
|
||
this.handleDeviceChange(this.device);
|
||
this.initDataStatus();
|
||
this.initData();
|
||
}
|
||
// 初始化自定义节目预览
|
||
this.$nextTick(() => {
|
||
this.drawAddProgramPreview();
|
||
});
|
||
},
|
||
|
||
methods: {
|
||
loadTrafficImages() {
|
||
try {
|
||
// 使用webpack的require.context动态加载图片
|
||
const files = require.context('@/assets/traffic', false, /\.(png|jpe?g|svg|bmp)$/);
|
||
this.addProgramTrafficImages = files.keys().map(key => {
|
||
const label = key.replace('./', '');
|
||
return {
|
||
label: label, // 例如: '1.png'
|
||
value: files(key) // Webpack处理后的路径
|
||
};
|
||
});
|
||
} catch (e) {
|
||
console.error("加载交通标志图片失败, 请确认 'src/assets/traffic' 目录下有图片文件。", e);
|
||
this.addProgramTrafficImages = [];
|
||
}
|
||
},
|
||
|
||
// 获取暂停时间(毫秒)
|
||
getPauseTime(stayTimeIndex) {
|
||
const pauseTimeMap = [0, 100, 500, 1000, 2000, 3000, 4000, 5000, 6000, 8000, 10000];
|
||
return pauseTimeMap[stayTimeIndex] || 1000;
|
||
},
|
||
|
||
// 获取字体大小
|
||
getFontSize(fontSizeIndex) {
|
||
const fontSizeMap = [16, 24, 32];
|
||
return fontSizeMap[fontSizeIndex] || 16;
|
||
},
|
||
|
||
// 获取图片编号(根据图片路径提取编号)
|
||
getImageNumber(imagePath) {
|
||
if (!imagePath) return 0;
|
||
// 从图片路径中提取编号,根据显卡.md文档中的图片编号映射
|
||
const imageNameMap = {
|
||
'减速慢行.png': 0, // 注意安全
|
||
'减速让行.png': 1, // 禁止驶入
|
||
'掉头.png': 2, // 禁止掉头
|
||
'禁止停车.png': 3, // 禁止停车
|
||
'禁止通行.png': 4, // 禁止通行
|
||
'限速40.png': 5, // 限速40
|
||
'限速60.png': 6, // 限速60
|
||
'限速80.png': 7, // 限速80
|
||
'限速100.png': 8, // 限速100
|
||
'限速120.png': 9, // 限速120
|
||
'取消限速40.png': 25, // 取消限速40
|
||
'取消限速60.png': 26, // 取消限速60
|
||
'取消限速80.png': 27, // 取消限速80
|
||
'取消限速100.png': 28, // 取消限速100
|
||
'取消限速120.png': 29, // 取消限速120
|
||
// 可以根据实际图片添加更多映射
|
||
};
|
||
const fileName = imagePath.split('/').pop();
|
||
return imageNameMap[fileName] || 0;
|
||
},
|
||
|
||
// 获取图片尺寸
|
||
getImageSize(imageSizeIndex) {
|
||
const imageSizeMap = [16, 32, 48, 64];
|
||
return imageSizeMap[imageSizeIndex] || 32;
|
||
},
|
||
|
||
// 解析节目名称(去除\0结束符)
|
||
parseProgramName(rem) {
|
||
if (!rem) return '未命名节目';
|
||
return rem.replace(/\0/g, '');
|
||
},
|
||
|
||
// 获取模式名称
|
||
getModeName(areaM) {
|
||
const modeNames = ['模式1', '模式2(上下)', '模式3(左右)', '模式4(上中下)', '模式5', '模式6'];
|
||
return modeNames[areaM] || `模式${areaM}`;
|
||
},
|
||
|
||
// 解析节目内容
|
||
parseProgramContent(aLst) {
|
||
if (!aLst || aLst.length === 0) return '无内容';
|
||
|
||
const contents = [];
|
||
aLst.forEach((area, areaIndex) => {
|
||
if (area.pLst && area.pLst.length > 0) {
|
||
area.pLst.forEach((item, itemIndex) => {
|
||
if (item.typ === 0 && item.txt) {
|
||
// 文本类型
|
||
const text = item.txt.str ? item.txt.str.replace(/\0/g, '') : '';
|
||
if (text) {
|
||
contents.push(`分区${areaIndex + 1}: ${text}`);
|
||
}
|
||
} else if (item.typ === 1 && item.img) {
|
||
// 图片类型
|
||
const imageName = this.getImageNameByNumber(item.img.num);
|
||
contents.push(`分区${areaIndex + 1}: 图片${item.img.num}(${imageName})`);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
return contents.length > 0 ? contents.join('; ') : '无内容';
|
||
},
|
||
|
||
// 根据图片编号获取图片名称
|
||
getImageNameByNumber(num) {
|
||
const imageNameMap = {
|
||
0: '注意安全',
|
||
1: '禁止驶入',
|
||
2: '禁止掉头',
|
||
3: '禁止停车',
|
||
4: '禁止通行',
|
||
5: '限速40',
|
||
6: '限速60',
|
||
7: '限速80',
|
||
8: '限速100',
|
||
9: '限速120',
|
||
25: '取消限速40',
|
||
26: '取消限速60',
|
||
27: '取消限速80',
|
||
28: '取消限速100',
|
||
29: '取消限速120'
|
||
};
|
||
return imageNameMap[num] || `图片${num}`;
|
||
},
|
||
|
||
// 查看节目详情
|
||
viewProgram(program) {
|
||
this.$alert(`
|
||
<div style="text-align: left;">
|
||
<p><strong>节目名称:</strong>${program.name}</p>
|
||
<p><strong>播放时长:</strong>${program.duration}秒</p>
|
||
<p><strong>显示模式:</strong>${program.mode}</p>
|
||
<p><strong>分区数量:</strong>${program.zones}个</p>
|
||
<p><strong>节目内容:</strong>${program.content}</p>
|
||
</div>
|
||
`, '节目详情', {
|
||
dangerouslyUseHTMLString: true,
|
||
confirmButtonText: '确定'
|
||
});
|
||
},
|
||
|
||
// 删除节目
|
||
deleteProgram(program) {
|
||
this.$confirm(`确认删除节目"${program.name}"吗?`, '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
const progListModel = this.deviceInfo.thingsModels.find(model => model.id === '102#progList');
|
||
if (progListModel) {
|
||
try {
|
||
// 只发送删除指令,不带上整个节目列表
|
||
progListModel.shadow = 'JSON=' + JSON.stringify({ del_prog: program.order });
|
||
this.mqttPublish(this.deviceInfo, progListModel).then(() => {
|
||
this.$message.success('删除指令已发送');
|
||
}).catch(error => {
|
||
console.error('发送删除指令失败:', error);
|
||
this.$message.error('删除失败');
|
||
});
|
||
} catch (error) {
|
||
console.error('构建删除数据失败:', error);
|
||
this.$message.error('删除失败');
|
||
}
|
||
} else {
|
||
this.$message.warning('未找到102#progList物模型');
|
||
}
|
||
}).catch(() => { });
|
||
},
|
||
|
||
// 编辑节目
|
||
editProgram(program) {
|
||
this.isEditProgram = true;
|
||
this.editingProgram = program;
|
||
|
||
// 解析节目数据并填充表单
|
||
this.fillProgramForm(program.raw);
|
||
|
||
// 显示弹窗
|
||
this.addProgramDialogVisible = true;
|
||
},
|
||
|
||
// 填充节目表单数据
|
||
fillProgramForm(programData) {
|
||
if (!programData) return;
|
||
|
||
// console.log('填充节目表单数据:', JSON.stringify(programData));
|
||
|
||
// 重置表单
|
||
this.addProgramForm = {
|
||
mode: programData.areaM || 0,
|
||
duration: programData.dur || 10,
|
||
remark: this.parseProgramName(programData.rem) || '',
|
||
zones: []
|
||
};
|
||
|
||
// 解析分区数据
|
||
if (programData.aLst && programData.aLst.length > 0) {
|
||
// console.log('解析分区数据:', JSON.stringify(programData.aLst));
|
||
this.addProgramForm.zones = programData.aLst.map(area => {
|
||
let x = area.size ? area.size.x : 0;
|
||
let y = area.size ? area.size.y : 0;
|
||
let w = area.size ? area.size.l : 32;
|
||
let h = area.size ? area.size.h : 64;
|
||
if (this.screenParams.angle === 90 || this.screenParams.angle === 270) {
|
||
[x, y] = [y, x];
|
||
[w, h] = [h, w];
|
||
}
|
||
const zone = {
|
||
playType: 0,
|
||
commonPhrase: 0,
|
||
displayText: '',
|
||
font: 0,
|
||
fontEn: 0, // 新增英文字体
|
||
fontShape: 0,
|
||
fontSize: 0,
|
||
fontColor: 0, // 红色(默认)
|
||
fontBold: 0,
|
||
fontStretch: 0,
|
||
image: '',
|
||
imageSize: 1,
|
||
imageColor: 0, // 红色(默认)
|
||
effect: 0,
|
||
speed: 4,
|
||
stayTime: 5,
|
||
hAlign: 1,
|
||
vAlign: 1,
|
||
x: x,
|
||
y: y,
|
||
width: w,
|
||
height: h
|
||
};
|
||
|
||
// 解析播放项
|
||
if (area.pLst && area.pLst.length > 0) {
|
||
const item = area.pLst[0]; // 取第一个播放项
|
||
if (item.typ === 0 && item.txt) {
|
||
// 文本类型
|
||
zone.playType = 0;
|
||
zone.displayText = item.txt.str ? item.txt.str.replace(/\0/g, '') : '';
|
||
zone.font = item.txt.fCn ? item.txt.fCn - 1 : 0; // 中文字体索引
|
||
zone.fontEn = item.txt.fEn ? item.txt.fEn - 1 : 0; // 英文字体索引
|
||
zone.fontSize = item.txt.fS ? this.getFontSizeIndex(item.txt.fS) : 0; // 字体大小索引
|
||
zone.fontColor = item.txt.col - 1 || 0; // 文本颜色
|
||
zone.fontBold = item.txt.fW || 0; // 字体加粗
|
||
zone.fontStretch = item.txt.stch || 0; // 拉伸方向
|
||
// 调试输出
|
||
// console.log('编辑节目item.txt.hPos:', item.txt.hPos, 'item.txt.vPos:', item.txt.vPos);
|
||
// 修正:物模型hPos/vPos为1/2/3,UI下拉框为0/1/2
|
||
zone.hAlign = typeof item.txt.hPos === 'number' ? (item.txt.hPos - 1) : 0; // 水平对齐(1左,2中,3右)
|
||
zone.vAlign = typeof item.txt.vPos === 'number' ? (item.txt.vPos - 1) : 0; // 垂直对齐(1顶,2中,3底)
|
||
// console.log('zone.hAlign:', zone.hAlign, 'zone.vAlign:', zone.vAlign);
|
||
} else if (item.typ === 1 && item.img) {
|
||
// 图片类型
|
||
zone.playType = 1;
|
||
zone.image = this.getImagePathByNumber(item.img.num);
|
||
zone.imageSize = this.getImageSizeIndex(item.img.w); // 使用宽度作为尺寸
|
||
zone.imageColor = item.img.col || 0; // 图片颜色
|
||
}
|
||
|
||
// 解析动画效果
|
||
if (item.anim) {
|
||
zone.effect = item.anim.typ ? item.anim.typ - 1 : 0; // 动画类型需要减1
|
||
zone.speed = item.anim.spd || 4; // 动画速度
|
||
zone.stayTime = item.anim.pauseT ? this.getStayTimeIndex(item.anim.pauseT) : 5; // 暂停时间索引
|
||
}
|
||
}
|
||
|
||
return zone;
|
||
});
|
||
} else {
|
||
// 如果没有分区数据,创建默认分区
|
||
this.addProgramForm.zones = [{
|
||
playType: 0,
|
||
commonPhrase: 0,
|
||
displayText: '',
|
||
font: 0,
|
||
fontEn: 0, // 新增英文字体
|
||
fontShape: 0,
|
||
fontSize: 0,
|
||
fontColor: 0, // 红色(默认)
|
||
fontBold: 0,
|
||
fontStretch: 0,
|
||
image: '',
|
||
imageSize: 1,
|
||
imageColor: 0, // 红色(默认)
|
||
effect: 0,
|
||
speed: 4,
|
||
stayTime: 5,
|
||
hAlign: 1,
|
||
vAlign: 1,
|
||
x: 0,
|
||
y: 0,
|
||
width: 32,
|
||
height: 64
|
||
}];
|
||
}
|
||
|
||
// 更新预览
|
||
this.$nextTick(() => {
|
||
this.onAddProgramModeChange(this.addProgramForm.mode); // 保证分区布局和模式同步
|
||
this.drawAddProgramPreview();
|
||
});
|
||
},
|
||
|
||
// 根据图片编号获取图片路径
|
||
getImagePathByNumber(imageNumber) {
|
||
const imageNumberMap = {
|
||
0: '减速慢行.png',
|
||
1: '减速让行.png',
|
||
2: '掉头.png',
|
||
3: '禁止停车.png',
|
||
4: '禁止通行.png',
|
||
5: '限速40.png',
|
||
6: '限速60.png',
|
||
7: '限速80.png',
|
||
8: '限速100.png',
|
||
9: '限速120.png',
|
||
25: '取消限速40.png',
|
||
26: '取消限速60.png',
|
||
27: '取消限速80.png',
|
||
28: '取消限速100.png',
|
||
29: '取消限速120.png'
|
||
};
|
||
const fileName = imageNumberMap[imageNumber] || '减速慢行.png';
|
||
return this.addProgramTrafficImages.find(img => img.label === fileName)?.value || '';
|
||
},
|
||
|
||
// 根据图片尺寸获取尺寸索引
|
||
getImageSizeIndex(imageSize) {
|
||
const sizeMap = [16, 32, 48, 64];
|
||
return sizeMap.indexOf(imageSize) || 1;
|
||
},
|
||
|
||
// 根据字体大小获取字体大小索引
|
||
getFontSizeIndex(fontSize) {
|
||
const fontSizeMap = [16, 24, 32];
|
||
return fontSizeMap.indexOf(fontSize) || 0;
|
||
},
|
||
|
||
// 根据暂停时间获取停留时间索引
|
||
getStayTimeIndex(pauseTime) {
|
||
const pauseTimeMap = [0, 100, 500, 1000, 2000, 3000, 4000, 5000, 6000, 8000, 10000];
|
||
return pauseTimeMap.indexOf(pauseTime) || 5;
|
||
},
|
||
|
||
// 根据图片编号获取图片名称
|
||
getImageNameByNumber(imageNumber) {
|
||
const imageNameMap = {
|
||
0: '减速慢行',
|
||
1: '减速让行',
|
||
2: '掉头',
|
||
3: '禁止停车',
|
||
4: '禁止通行',
|
||
5: '限速40',
|
||
6: '限速60',
|
||
7: '限速80',
|
||
8: '限速100',
|
||
9: '限速120',
|
||
25: '取消限速40',
|
||
26: '取消限速60',
|
||
27: '取消限速80',
|
||
28: '取消限速100',
|
||
29: '取消限速120'
|
||
};
|
||
return imageNameMap[imageNumber] || `图片${imageNumber}`;
|
||
},
|
||
|
||
// 处理节目使能状态变化
|
||
handleProgramEnableChange() {
|
||
// 构建10个元素的使能状态数组
|
||
const enableArray = new Array(10).fill(0); // 默认全部为0
|
||
|
||
// 根据节目列表更新使能状态
|
||
this.programList.forEach((program, index) => {
|
||
if (index < 10) { // 确保不超过10个
|
||
enableArray[index] = program.enabled;
|
||
}
|
||
});
|
||
|
||
this.programEnableList = enableArray;
|
||
|
||
// 发送使能状态到设备
|
||
const progEnModel = this.deviceInfo.thingsModels.find(model => model.id === '102#progEn');
|
||
if (progEnModel) {
|
||
try {
|
||
const progEnData = {
|
||
prog_en: this.programEnableList
|
||
};
|
||
progEnModel.shadow = 'JSON=' + JSON.stringify(progEnData);
|
||
this.mqttPublish(this.deviceInfo, progEnModel).then(() => {
|
||
this.$message.success('节目使能状态已更新');
|
||
}).catch(error => {
|
||
console.error('发送节目使能状态失败:', error);
|
||
this.$message.error('更新失败');
|
||
});
|
||
} catch (error) {
|
||
console.error('构建节目使能数据失败:', error);
|
||
this.$message.error('更新失败');
|
||
}
|
||
} else {
|
||
this.$message.warning('未找到102#progEn物模型');
|
||
}
|
||
},
|
||
|
||
// 刷新编译信息
|
||
async refreshBuildInfo() {
|
||
// console.log('programList', JSON.stringify(this.programList))
|
||
try {
|
||
// 发送102#model指令,值为1
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#model');
|
||
if (model) {
|
||
model.shadow = '1';
|
||
await this.mqttPublish(this.deviceInfo, model);
|
||
this.$message.success('已发送刷新指令,编译信息将自动更新');
|
||
} else {
|
||
this.$message.warning('未找到102#model物模型');
|
||
}
|
||
} catch (error) {
|
||
console.error('发送刷新指令失败:', error);
|
||
this.$message.error('发送刷新指令失败');
|
||
}
|
||
},
|
||
|
||
// 获取编译信息
|
||
getBuildInfo() {
|
||
this.buildInfoLoading = true;
|
||
try {
|
||
// 从物模型102#info获取编译信息
|
||
const infoModel = this.deviceInfo.thingsModels.find(model => model.id === '102#info');
|
||
if (infoModel && infoModel.shadow) {
|
||
try {
|
||
const jsonStr = infoModel.shadow.replace('JSON=', '');
|
||
const data = JSON.parse(jsonStr);
|
||
this.buildVersion = data.versions || '未知版本';
|
||
this.buildTime = data.compile_time || '未知时间';
|
||
} catch (parseError) {
|
||
console.error('解析编译信息JSON失败:', parseError);
|
||
this.buildVersion = '未知版本';
|
||
this.buildTime = '未知时间';
|
||
}
|
||
} else {
|
||
// 如果物模型不存在,使用默认值
|
||
this.buildVersion = '未知版本';
|
||
this.buildTime = '未知时间';
|
||
}
|
||
} catch (error) {
|
||
console.error('获取编译信息失败:', error);
|
||
this.buildVersion = '未知版本';
|
||
this.buildTime = '未知时间';
|
||
} finally {
|
||
this.buildInfoLoading = false;
|
||
}
|
||
},
|
||
//发送指令
|
||
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) {
|
||
let title = '';
|
||
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();
|
||
this.getBuildInfo(); // 获取编译信息
|
||
}
|
||
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;
|
||
this.updateBoardOnlineStatus();
|
||
|
||
// 屏幕开关
|
||
const screenEnModel = this.deviceInfo.thingsModels.find(model => model.id === '102#screenEn');
|
||
if (screenEnModel) {
|
||
this.basicSettings.screenEnabled = screenEnModel.shadow === '1';
|
||
}
|
||
|
||
// 屏幕亮度
|
||
const luminanceModel = this.deviceInfo.thingsModels.find(model => model.id === '102#luminance');
|
||
if (luminanceModel) {
|
||
this.basicSettings.brightness = parseInt(luminanceModel.shadow) || 50;
|
||
}
|
||
|
||
// 开机参数
|
||
const bootEnModel = this.deviceInfo.thingsModels.find(model => model.id === '102#bootEn');
|
||
if (bootEnModel) {
|
||
this.bootSettings.showBootScreen = bootEnModel.shadow === '1';
|
||
}
|
||
const factoryEnModel = this.deviceInfo.thingsModels.find(model => model.id === '102#factoryEn');
|
||
if (factoryEnModel) {
|
||
this.bootSettings.resetEnabled = factoryEnModel.shadow === '1';
|
||
}
|
||
|
||
// 屏幕参数
|
||
const scrParamModel = this.deviceInfo.thingsModels.find(model => model.id === '102#scrParam');
|
||
if (scrParamModel && scrParamModel.shadow) {
|
||
try {
|
||
const jsonStr = scrParamModel.shadow.replace('JSON=', '');
|
||
const data = JSON.parse(jsonStr);
|
||
if (data.scr_param) {
|
||
this.screenParams.oePolarity = data.scr_param.oe;
|
||
this.screenParams.dataPolarity = data.scr_param.data;
|
||
this.screenParams.width = data.scr_param.w;
|
||
this.screenParams.height = data.scr_param.h;
|
||
this.screenParams.angle = data.scr_param.angle;
|
||
}
|
||
} catch (error) {
|
||
console.error('解析屏幕参数失败:', error);
|
||
}
|
||
}
|
||
|
||
// 传感器阈值设置
|
||
const sensorThresholdModel = this.deviceInfo.thingsModels.find(model => model.id === '102#snsrThr');
|
||
if (sensorThresholdModel && sensorThresholdModel.shadow) {
|
||
try {
|
||
const jsonStr = sensorThresholdModel.shadow.replace('JSON=', '');
|
||
const data = JSON.parse(jsonStr);
|
||
if (data.snsr_thr) {
|
||
this.sensorThreshold.speed = data.snsr_thr.speed || 60;
|
||
this.sensorThreshold.temp = data.snsr_thr.temp || 30;
|
||
this.sensorThreshold.humi = data.snsr_thr.humi || 50;
|
||
}
|
||
} catch (error) {
|
||
console.error('解析传感器阈值失败:', error);
|
||
}
|
||
}
|
||
|
||
// 更新节目列表(如果有102#progList物模型)
|
||
const progListModel = this.deviceInfo.thingsModels.find(model => model.id === '102#progList');
|
||
// console.log("progListModel", JSON.stringify(progListModel))
|
||
if (progListModel && progListModel.shadow) {
|
||
try {
|
||
const jsonStr = progListModel.shadow.replace('JSON=', '');
|
||
const data = JSON.parse(jsonStr);
|
||
if (data.prog_list && data.prog_list.length > 0) {
|
||
// 解析节目列表并更新显示
|
||
this.programList = data.prog_list.map((program, index) => {
|
||
return {
|
||
id: index + 1,
|
||
order: program.order,
|
||
name: this.parseProgramName(program.rem),
|
||
duration: program.dur,
|
||
mode: this.getModeName(program.areaM),
|
||
zones: program.aLst ? program.aLst.length : 0,
|
||
content: this.parseProgramContent(program.aLst),
|
||
enabled: this.programEnableList[index] || 0, // 使用使能状态数组,默认为0
|
||
raw: program
|
||
};
|
||
});
|
||
// console.log('解析后的节目列表:', this.programList);
|
||
} else {
|
||
this.programList = [];
|
||
}
|
||
} catch (error) {
|
||
console.error('解析节目列表失败:', error);
|
||
this.programList = [];
|
||
}
|
||
}
|
||
|
||
// 更新节目使能状态(如果有102#progEn物模型)
|
||
const progEnModel = this.deviceInfo.thingsModels.find(model => model.id === '102#progEn');
|
||
if (progEnModel && progEnModel.shadow) {
|
||
try {
|
||
const jsonStr = progEnModel.shadow.replace('JSON=', '');
|
||
const data = JSON.parse(jsonStr);
|
||
if (data.prog_en && Array.isArray(data.prog_en)) {
|
||
// 确保使能状态数组有10个元素
|
||
this.programEnableList = new Array(10).fill(0);
|
||
data.prog_en.forEach((enabled, index) => {
|
||
if (index < 10) {
|
||
this.programEnableList[index] = enabled;
|
||
}
|
||
});
|
||
|
||
// 更新节目列表中的使能状态
|
||
this.programList.forEach((program, index) => {
|
||
program.enabled = this.programEnableList[index] || 0;
|
||
});
|
||
// console.log('节目使能状态:', this.programEnableList);
|
||
}
|
||
} catch (error) {
|
||
console.error('解析节目使能状态失败:', error);
|
||
// 如果解析失败,初始化为10个0
|
||
this.programEnableList = new Array(10).fill(0);
|
||
}
|
||
//在线离线
|
||
}
|
||
},
|
||
|
||
printThingsModels() {
|
||
console.log('当前物模型数据:', JSON.stringify(this.deviceInfo.thingsModels, null, 2));
|
||
this.$message({
|
||
message: '物模型数据已打印到控制台',
|
||
type: 'success'
|
||
});
|
||
},
|
||
|
||
|
||
|
||
// 保留其他必要的方法
|
||
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();
|
||
this.getBuildInfo(); // 更新编译信息
|
||
},
|
||
|
||
|
||
|
||
|
||
|
||
|
||
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;
|
||
},
|
||
|
||
// 添加版本比较方法
|
||
compareVersions(version1, version2) {
|
||
// 移除可能的'Version'前缀
|
||
version1 = String(version1).replace('Version', '').trim();
|
||
version2 = String(version2).replace('Version', '').trim();
|
||
|
||
const v1Parts = version1.split('.').map(Number);
|
||
const v2Parts = version2.split('.').map(Number);
|
||
|
||
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
||
const v1 = v1Parts[i] || 0;
|
||
const v2 = v2Parts[i] || 0;
|
||
|
||
if (v1 < v2) return true;
|
||
if (v1 > v2) return false;
|
||
}
|
||
return false;
|
||
},
|
||
|
||
/** 获取最新固件 */
|
||
getLatestFirmware() {
|
||
const { deviceId, firmwareType } = this.deviceInfo;
|
||
if (!deviceId || !firmwareType) {
|
||
this.$message.error('设备信息不完整');
|
||
return;
|
||
}
|
||
getLatestFirmware(deviceId, firmwareType).then((response) => {
|
||
if (response.code === 200) {
|
||
if (response.data) {
|
||
this.firmware = response.data;
|
||
// 添加版本比较的调试信息
|
||
console.log('当前版本:', this.deviceInfo.firmwareVersion);
|
||
console.log('新版本:', this.firmware.version);
|
||
console.log('版本比较结果:', this.compareVersions(this.deviceInfo.firmwareVersion, this.firmware.version));
|
||
this.openFirmware = true;
|
||
} else {
|
||
this.$message.info('当前已是最新版本');
|
||
this.openFirmware = true;
|
||
}
|
||
} else {
|
||
this.$message.error(response.msg || '获取固件信息失败');
|
||
}
|
||
}).catch(error => {
|
||
console.error('获取固件信息失败:', error);
|
||
this.$message.error('获取固件信息失败');
|
||
});
|
||
},
|
||
/** 设备升级 */
|
||
otaUpgrade() {
|
||
// OTA升级
|
||
let topic = '/' + this.deviceInfo.productId + '/' + this.deviceInfo.serialNumber + '/ota/get';
|
||
let message = '{"version":' + this.firmware.version + ',"downloadUrl":"' + this.getDownloadUrl(this.firmware.filePath) + '"}';
|
||
// 发布
|
||
this.$mqttTool
|
||
.publish(topic, message, this.$t('device.running-status.866086-31'))
|
||
.then((res) => {
|
||
this.$modal.notifySuccess(res);
|
||
})
|
||
.catch((res) => {
|
||
this.$modal.notifyError(res);
|
||
});
|
||
this.openFirmware = false;
|
||
},
|
||
|
||
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);
|
||
}
|
||
},
|
||
|
||
// 录音相关方法
|
||
floatTo16BitPCM(input) {
|
||
const output = new Int16Array(input.length);
|
||
for (let i = 0; i < input.length; i++) {
|
||
let s = Math.max(-1, Math.min(1, input[i]));
|
||
output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
||
}
|
||
return output;
|
||
},
|
||
async startRecording() {
|
||
if (this.isRecording) return;
|
||
try {
|
||
const stream = await navigator.mediaDevices.getUserMedia({
|
||
audio: {
|
||
sampleRate: 44100,
|
||
channelCount: 2,
|
||
sampleSize: 16,
|
||
autoGainControl: false,
|
||
noiseSuppression: false,
|
||
echoCancellation: false
|
||
}
|
||
});
|
||
const Mp3Encoder = window.lamejs.Mp3Encoder;
|
||
this._mp3Encoder = new Mp3Encoder(2, 44100, 128);
|
||
this._audioChunks = [];
|
||
this._audioContext = new AudioContext({ sampleRate: 44100 });
|
||
this._sourceNode = this._audioContext.createMediaStreamSource(stream);
|
||
this._processorNode = this._audioContext.createScriptProcessor(4096, 2, 2);
|
||
this._stream = stream;
|
||
this._processorNode.onaudioprocess = (event) => {
|
||
const leftChannel = event.inputBuffer.getChannelData(0);
|
||
const rightChannel = event.inputBuffer.getChannelData(1);
|
||
// 修正:Float32转Int16
|
||
const left = this.floatTo16BitPCM(leftChannel);
|
||
const right = this.floatTo16BitPCM(rightChannel);
|
||
const mp3Buffer = this._mp3Encoder.encodeBuffer(left, right);
|
||
if (mp3Buffer.length > 0) {
|
||
this._audioChunks.push(mp3Buffer);
|
||
}
|
||
};
|
||
this._sourceNode.connect(this._processorNode);
|
||
this._processorNode.connect(this._audioContext.destination);
|
||
this.isRecording = true;
|
||
this.recordingStatus = '正在录音...';
|
||
this.recordingTime = 0;
|
||
this.timer = setInterval(() => this.recordingTime++, 1000);
|
||
} catch (error) {
|
||
console.error('录音失败:', error);
|
||
this.$message.error(`录音失败: ${error.message}`);
|
||
}
|
||
},
|
||
|
||
stopRecording() {
|
||
if (!this.isRecording) return;
|
||
clearInterval(this.timer);
|
||
if (this._sourceNode) this._sourceNode.disconnect();
|
||
if (this._processorNode) this._processorNode.disconnect();
|
||
if (this._stream) this._stream.getTracks().forEach(track => track.stop());
|
||
// 刷新编码器缓冲区
|
||
if (this._mp3Encoder) {
|
||
const lastChunk = this._mp3Encoder.flush();
|
||
if (lastChunk.length > 0) this._audioChunks.push(lastChunk);
|
||
}
|
||
// 生成 MP3 文件
|
||
const mp3Blob = new Blob(this._audioChunks, { type: 'audio/mp3' });
|
||
this.audioUrl = URL.createObjectURL(mp3Blob);
|
||
this.hasRecording = true;
|
||
this.recordingStatus = '录音完成';
|
||
this.audioChunks = this._audioChunks;
|
||
this.isRecording = false;
|
||
},
|
||
|
||
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`);
|
||
|
||
// 强制使用 HTTP 协议,因为接口只支持 HTTP
|
||
const uploadUrl = 'https://iot-xcwl.cn/common/upload/audio';
|
||
// const uploadUrl = 'http://1.94.62.14:8080/common/upload/audio';
|
||
|
||
// 调用上传接口
|
||
const response = await fetch(uploadUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': 'Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImU3MWM2OTg4LTNlMzMtNDYyMy05M2M3LWE4YzZmMTNlMjZkZSJ9.wgsL8b3WDmyuesG8JTA3LcNFp2FigkB90h6Inwxt7OFadH6rc5np5TjAyU1pzU2_b5cmG8BYXMEdAqEdJzoDcA'
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
if (result.code === 200) {
|
||
this.$message.success('上传成功');
|
||
this.recordings.unshift({
|
||
name: `录音_${this.formatTime(this.recordingTime)}`,
|
||
time: new Date().toLocaleString()
|
||
});
|
||
|
||
// 提取返回的 URL 并通过 MQTT 下发到 103#onlinePlay
|
||
if (result.url) {
|
||
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,
|
||
},
|
||
interrupt: 98
|
||
});
|
||
onlinePlayModel.shadow = playData;
|
||
await this.mqttPublish(this.deviceInfo, onlinePlayModel);
|
||
this.$message.success('音频已下发到设备');
|
||
} else {
|
||
this.$message.warning('未找到 103#onlinePlay 物模型');
|
||
}
|
||
}
|
||
|
||
// 重置录音状态
|
||
this.reRecord();
|
||
} else {
|
||
this.$message.error(result.msg || '上传失败');
|
||
}
|
||
} else {
|
||
this.$message.error('上传失败,请检查网络连接');
|
||
}
|
||
} catch (error) {
|
||
// 处理混合内容错误
|
||
if (error.message && error.message.includes('Mixed Content')) {
|
||
this.$message.error('由于安全策略,无法在 HTTPS 页面访问 HTTP 接口。请联系管理员配置接口支持 HTTPS。');
|
||
} else {
|
||
this.$message.error('上传失败');
|
||
}
|
||
console.error('上传错误:', error);
|
||
}
|
||
},
|
||
|
||
deleteRecording(index) {
|
||
this.recordings.splice(index, 1);
|
||
},
|
||
|
||
handleAudioSwitchChange(val) {
|
||
const playEnModel = this.deviceInfo.thingsModels.find(model => model.id === '103#playEn');
|
||
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: ''
|
||
};
|
||
},
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
formatBrightness(val) {
|
||
return val + '%';
|
||
},
|
||
handleScreenSwitchChange(val) {
|
||
// 占位物模型ID: '102#screenEn'
|
||
const screenEnModel = this.deviceInfo.thingsModels.find(model => model.id === '102#screenEn');
|
||
if (screenEnModel) {
|
||
screenEnModel.shadow = val ? '1' : '0';
|
||
this.mqttPublish(this.deviceInfo, screenEnModel);
|
||
}
|
||
},
|
||
handleBrightnessChange(val) {
|
||
// 占位物模型ID: '102#brightness'
|
||
const brightnessModel = this.deviceInfo.thingsModels.find(model => model.id === '102#luminance');
|
||
if (brightnessModel) {
|
||
brightnessModel.shadow = val.toString();
|
||
this.mqttPublish(this.deviceInfo, brightnessModel);
|
||
}
|
||
},
|
||
handleClearScreen() {
|
||
// 占位物模型ID: '102#clearScreen',下发清除指令
|
||
const clearScreenModel = this.deviceInfo.thingsModels.find(model => model.id === '102#clearScreen');
|
||
if (clearScreenModel) {
|
||
clearScreenModel.shadow = '1';
|
||
this.mqttPublish(this.deviceInfo, clearScreenModel);
|
||
} else {
|
||
this.$message.warning('未找到清除屏幕物模型');
|
||
}
|
||
},
|
||
handleBootScreenSwitchChange(val) {
|
||
// 占位物模型ID: '102#bootScreen'
|
||
const bootScreenModel = this.deviceInfo.thingsModels.find(model => model.id === '102#bootEn');
|
||
if (bootScreenModel) {
|
||
bootScreenModel.shadow = val ? '1' : '0';
|
||
this.mqttPublish(this.deviceInfo, bootScreenModel);
|
||
}
|
||
},
|
||
handleResetSwitchChange(val) {
|
||
// 占位物模型ID: '102#reset'
|
||
const resetModel = this.deviceInfo.thingsModels.find(model => model.id === '102#factoryEn');
|
||
if (resetModel) {
|
||
resetModel.shadow = val ? '1' : '0';
|
||
this.mqttPublish(this.deviceInfo, resetModel);
|
||
}
|
||
},
|
||
handleTemplateChange(val) {
|
||
const template = this.screenTemplates.find(t => t.value === val);
|
||
if (template) {
|
||
const params = template.params;
|
||
this.$set(this.screenParams, 'oePolarity', params.oe);
|
||
this.$set(this.screenParams, 'dataPolarity', params.data);
|
||
this.$set(this.screenParams, 'width', params.w);
|
||
this.$set(this.screenParams, 'height', params.h);
|
||
this.$set(this.screenParams, 'angle', params.angle);
|
||
this.drawAddProgramPreview(); // 切换模板时强制刷新预览
|
||
}
|
||
},
|
||
handleOePolarityChange(val) {
|
||
// 占位物模型ID: '102#oePolarity'
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#oePolarity');
|
||
if (model) {
|
||
model.shadow = val.toString();
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
handleDataPolarityChange(val) {
|
||
// 占位物模型ID: '102#dataPolarity'
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#dataPolarity');
|
||
if (model) {
|
||
model.shadow = val.toString();
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
handleWidthChange(val) {
|
||
// 占位物模型ID: '102#width'
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#width');
|
||
if (model) {
|
||
model.shadow = val.toString();
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
handleHeightChange(val) {
|
||
// 占位物模型ID: '102#height'
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#height');
|
||
if (model) {
|
||
model.shadow = val.toString();
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
handleAngleChange(val) {
|
||
// 占位物模型ID: '102#angle'
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#angle');
|
||
if (model) {
|
||
model.shadow = val.toString();
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
handleSendScreenParams() {
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#scrParam');
|
||
if (model) {
|
||
const param = {
|
||
scr_param: {
|
||
oe: this.screenParams.oePolarity,
|
||
data: this.screenParams.dataPolarity,
|
||
w: this.screenParams.width,
|
||
h: this.screenParams.height,
|
||
angle: this.screenParams.angle
|
||
}
|
||
};
|
||
model.shadow = 'JSON=' + JSON.stringify(param);
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
this.$message.success('参数已下发');
|
||
} else {
|
||
this.$message.warning('未找到屏幕参数物模型');
|
||
}
|
||
},
|
||
|
||
// 下发传感器阈值
|
||
handleSendSensorThreshold() {
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#snsrThr');
|
||
if (model) {
|
||
const param = {
|
||
snsr_thr: {
|
||
speed: this.sensorThreshold.speed,
|
||
temp: this.sensorThreshold.temp,
|
||
humi: this.sensorThreshold.humi
|
||
}
|
||
};
|
||
model.shadow = 'JSON=' + JSON.stringify(param);
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
this.$message.success('传感器阈值已下发');
|
||
} else {
|
||
this.$message.warning('未找到传感器阈值物模型');
|
||
}
|
||
},
|
||
showAddProgramDialog() {
|
||
this.isEditProgram = false;
|
||
this.editingProgram = null;
|
||
this.addProgramDialogVisible = true;
|
||
this.resetAddProgramForm();
|
||
},
|
||
resetAddProgramForm() {
|
||
// 使用当前屏幕参数
|
||
const screenW = this.screenParams.width || 32;
|
||
const screenH = this.screenParams.height || 64;
|
||
this.addProgramForm = {
|
||
mode: 0,
|
||
duration: 10,
|
||
remark: '',
|
||
zones: [
|
||
{
|
||
playType: 0,
|
||
commonPhrase: 0,
|
||
displayText: '',
|
||
font: 0,
|
||
fontEn: 0, // 新增英文字体
|
||
fontShape: 0,
|
||
fontSize: 0,
|
||
fontColor: 0, // 红色(默认)
|
||
fontBold: 0,
|
||
fontStretch: 0,
|
||
image: '',
|
||
imageSize: 1,
|
||
imageColor: 0, // 红色(默认)
|
||
effect: 0,
|
||
speed: 4,
|
||
stayTime: 5,
|
||
hAlign: 1,
|
||
vAlign: 1,
|
||
x: 0,
|
||
y: 0,
|
||
width: screenW,
|
||
height: screenH
|
||
}
|
||
]
|
||
};
|
||
},
|
||
onAddProgramModeChange(val) {
|
||
// 根据模式调整分区数量和布局
|
||
const isSwap = this.screenParams.angle === 90 || this.screenParams.angle === 270;
|
||
let actualMode = val;
|
||
//神经逻辑,反人类逻辑
|
||
if (isSwap) {
|
||
if (val === 1) actualMode = 2; // 上下变左右
|
||
else if (val === 2) actualMode = 1; // 左右变上下
|
||
else if (val === 3) actualMode = 2; // 上中下变左右
|
||
}
|
||
const zoneCounts = [1, 2, 2, 3, 1, 1];
|
||
const count = zoneCounts[actualMode] || 1;
|
||
// 使用当前屏幕参数
|
||
const screenW = this.screenParams.width || 32;
|
||
const screenH = this.screenParams.height || 64;
|
||
const newZones = [];
|
||
for (let i = 0; i < count; i++) {
|
||
const templateZone = {
|
||
playType: 0,
|
||
commonPhrase: 0,
|
||
displayText: '',
|
||
font: 0,
|
||
fontEn: 0, // 新增英文字体
|
||
fontShape: 0,
|
||
fontSize: 0,
|
||
fontColor: 0, // 红色(默认)
|
||
fontBold: 0,
|
||
fontStretch: 0,
|
||
image: '',
|
||
imageSize: 1,
|
||
imageColor: 0, // 红色(默认)
|
||
effect: 0,
|
||
speed: 4,
|
||
stayTime: 5,
|
||
hAlign: 1,
|
||
vAlign: 1,
|
||
x: 0,
|
||
y: 0,
|
||
width: screenW,
|
||
height: screenH
|
||
};
|
||
switch (actualMode) {
|
||
case 1: // 上下
|
||
templateZone.height = Math.floor(screenH / count);
|
||
templateZone.y = i * templateZone.height;
|
||
if (i === count - 1) templateZone.height = screenH - templateZone.y;
|
||
break;
|
||
case 2: // 左右
|
||
templateZone.width = Math.floor(screenW / count);
|
||
templateZone.x = i * templateZone.width;
|
||
if (i === count - 1) templateZone.width = screenW - templateZone.x;
|
||
break;
|
||
case 3: // 上中下
|
||
templateZone.height = Math.floor(screenH / count);
|
||
templateZone.y = i * templateZone.height;
|
||
if (i === count - 1) templateZone.height = screenH - templateZone.y;
|
||
break;
|
||
}
|
||
if (this.addProgramForm.zones[i]) {
|
||
newZones.push({
|
||
...this.addProgramForm.zones[i],
|
||
x: templateZone.x,
|
||
y: templateZone.y,
|
||
width: templateZone.width,
|
||
height: templateZone.height
|
||
});
|
||
} else {
|
||
newZones.push(templateZone);
|
||
}
|
||
}
|
||
this.addProgramForm.zones = newZones;
|
||
},
|
||
onAddProgramPhraseChange(zIdx) {
|
||
const idx = this.addProgramForm.zones[zIdx].commonPhrase;
|
||
if (idx > 0) {
|
||
this.addProgramForm.zones[zIdx].displayText = this.addProgramCommonPhrases[idx];
|
||
}
|
||
},
|
||
submitAddProgram() {
|
||
// 根据显卡.md文档格式组装节目参数
|
||
const progListModel = this.deviceInfo.thingsModels.find(model => model.id === '102#progList');
|
||
if (progListModel) {
|
||
try {
|
||
// 计算 order
|
||
let order;
|
||
if (this.isEditProgram && this.editingProgram) {
|
||
order = this.editingProgram.order;
|
||
} else {
|
||
// 新建,找未用的最小 order
|
||
const usedOrders = this.programList.map(p => p.order);
|
||
order = 0;
|
||
while (usedOrders.includes(order) && order < 10) {
|
||
order++;
|
||
}
|
||
}
|
||
|
||
// 构建节目对象
|
||
const programObj = {
|
||
order: order,
|
||
rem: (this.addProgramForm.remark || `自定义节目${Date.now()}`) + '\0', // 节目备注,需要包含\0
|
||
dur: this.addProgramForm.duration, // 播放时长(秒)
|
||
areaM: this.addProgramForm.mode, // 分区模式
|
||
aLst: this.addProgramForm.zones.map(zone => {
|
||
// 构建分区数据
|
||
let x = zone.x, y = zone.y, w = zone.width, h = zone.height;
|
||
// if (this.screenParams.angle === 90 || this.screenParams.angle === 270) {
|
||
// [x, y] = [y, x];
|
||
// [w, h] = [h, w];
|
||
// }
|
||
const area = {
|
||
size: {
|
||
x: x, // 左下角X坐标
|
||
y: y, // 左下角Y坐标
|
||
l: w, // 宽度
|
||
h: h // 高度
|
||
},
|
||
pLst: []
|
||
};
|
||
|
||
// 根据播放类型构建播放项
|
||
if (zone.playType === 0) {
|
||
// 文本类型
|
||
const textItem = {
|
||
typ: 0,
|
||
anim: {
|
||
typ: zone.effect + 1, // 动画类型映射
|
||
spd: zone.speed, // 动画速度
|
||
pauseT: this.getPauseTime(zone.stayTime), // 暂停时间
|
||
playT: 2000 // 播放时间
|
||
},
|
||
txt: {
|
||
str: zone.displayText + '\0', // 文本内容,需要包含\0
|
||
col: zone.fontColor + 1, // 文本颜色
|
||
fS: this.getFontSize(zone.fontSize), // 字体大小
|
||
fCn: zone.font + 1, // 中文字体
|
||
fEn: zone.fontEn + 1, // 英文字体
|
||
fW: zone.fontBold, // 字体加粗
|
||
stch: zone.fontStretch, // 拉伸方向
|
||
hPos: zone.hAlign + 1, // 水平对齐(1左,2中,3右)
|
||
vPos: zone.vAlign + 1 // 垂直对齐(1顶,2中,3底)
|
||
}
|
||
};
|
||
area.pLst.push(textItem);
|
||
} else if (zone.playType === 1) {
|
||
// 图片类型
|
||
const imageItem = {
|
||
typ: 1,
|
||
anim: {
|
||
typ: zone.effect + 1,
|
||
spd: zone.speed,
|
||
pauseT: this.getPauseTime(zone.stayTime),
|
||
playT: 2000
|
||
},
|
||
img: {
|
||
num: this.getImageNumber(zone.image), // 图片编号
|
||
col: zone.imageColor, // 图片颜色
|
||
data: '', // 预留字段
|
||
w: this.getImageSize(zone.imageSize), // 图片宽度
|
||
h: this.getImageSize(zone.imageSize) // 图片高度
|
||
}
|
||
};
|
||
area.pLst.push(imageItem);
|
||
}
|
||
|
||
return area;
|
||
})
|
||
};
|
||
|
||
// 只下发当前节目
|
||
const programData = {
|
||
del_prog: 0, // 不删除节目
|
||
prog_list: [programObj]
|
||
};
|
||
|
||
progListModel.shadow = 'JSON=' + JSON.stringify(programData);
|
||
this.mqttPublish(this.deviceInfo, progListModel).then(() => {
|
||
this.addProgramDialogVisible = false;
|
||
this.$message.success(this.isEditProgram ? '自定义节目更新成功' : '自定义节目添加成功');
|
||
}).catch(error => {
|
||
console.error('发送节目数据失败:', error);
|
||
this.$message.error(this.isEditProgram ? '自定义节目更新失败' : '自定义节目添加失败');
|
||
});
|
||
} catch (error) {
|
||
console.error('构建节目数据失败:', error);
|
||
this.$message.error(this.isEditProgram ? '自定义节目更新失败' : '自定义节目添加失败');
|
||
}
|
||
} else {
|
||
this.$message.warning('未找到102#progList物模型');
|
||
}
|
||
},
|
||
// 画面预览渲染
|
||
drawAddProgramPreview() {
|
||
const canvas = this.$refs.addProgramPreviewCanvas;
|
||
if (!canvas) return;
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
// 获取当前屏幕参数
|
||
let screenW = this.screenParams.width || 32;
|
||
let screenH = this.screenParams.height || 64;
|
||
// 如果角度为90或270,渲染时宽高互换
|
||
const angle = this.screenParams.angle;
|
||
let swapWH = angle === 90 || angle === 270;
|
||
if (swapWH) {
|
||
[screenW, screenH] = [screenH, screenW];
|
||
}
|
||
|
||
// 获取实际渲染尺寸
|
||
const renderWidth = 640; // 调大预览宽度
|
||
const renderHeight = 240; // 调大预览高度
|
||
|
||
// 设置高清渲染
|
||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||
canvas.width = renderWidth * devicePixelRatio;
|
||
canvas.height = renderHeight * devicePixelRatio;
|
||
canvas.style.width = `${renderWidth}px`;
|
||
canvas.style.height = `${renderHeight}px`;
|
||
ctx.setTransform(1, 0, 0, 1, 0, 0); // 重置变换
|
||
ctx.scale(devicePixelRatio, devicePixelRatio);
|
||
|
||
// 清空整个画布为透明
|
||
ctx.clearRect(0, 0, renderWidth, renderHeight);
|
||
|
||
// 计算缩放比例(基于screenParams的LED屏幕尺寸)
|
||
const scale = Math.min(
|
||
renderWidth / screenW,
|
||
renderHeight / screenH
|
||
);
|
||
|
||
// 居中偏移量
|
||
const offsetX = (renderWidth - screenW * scale) / 2;
|
||
const offsetY = (renderHeight - screenH * scale) / 2;
|
||
|
||
// 绘制LED屏幕区域(黑色背景)
|
||
ctx.fillStyle = '#000';
|
||
ctx.fillRect(offsetX, offsetY, screenW * scale, screenH * scale);
|
||
|
||
// 遍历所有分区
|
||
this.addProgramForm.zones.forEach((zone, zIdx) => {
|
||
const asset = this.addProgramPreviewAssets[zIdx];
|
||
if (!asset) return;
|
||
|
||
// 计算分区在画布中的位置(角度为90或270时,x/y/width/height互换)
|
||
let zoneX, zoneY, zoneWidth, zoneHeight;
|
||
if (swapWH) {
|
||
zoneX = offsetX + (zone.y || 0) * scale;
|
||
zoneY = offsetY + (zone.x || 0) * scale;
|
||
zoneWidth = (zone.height || 0) * scale;
|
||
zoneHeight = (zone.width || 0) * scale;
|
||
} else {
|
||
zoneX = offsetX + (zone.x || 0) * scale;
|
||
zoneY = offsetY + (zone.y || 0) * scale;
|
||
zoneWidth = (zone.width || 0) * scale;
|
||
zoneHeight = (zone.height || 0) * scale;
|
||
}
|
||
|
||
// 绘制分区边框(蓝色虚线)
|
||
ctx.save();
|
||
ctx.strokeStyle = '#409EFF';
|
||
ctx.lineWidth = 1.5;
|
||
ctx.setLineDash([5, 3]);
|
||
ctx.strokeRect(zoneX, zoneY, zoneWidth, zoneHeight);
|
||
ctx.restore();
|
||
|
||
// 裁剪区域,防止动画溢出
|
||
ctx.save();
|
||
ctx.beginPath();
|
||
ctx.rect(zoneX, zoneY, zoneWidth, zoneHeight);
|
||
ctx.clip();
|
||
|
||
// 获取动画状态
|
||
const state = this.addProgramPreviewAnimState[zIdx];
|
||
|
||
if (zone.playType === 0 && asset.isText) {
|
||
// 文本类型分区
|
||
const page = this.addProgramPreviewPage[zIdx] || 0;
|
||
const pageLines = asset.pages[page] || [];
|
||
|
||
// 设置字体样式
|
||
const colors = ['#FF0000', '#00FF00', '#FFFF00'];
|
||
ctx.fillStyle = colors[zone.fontColor] || '#000000';
|
||
ctx.textBaseline = 'top';
|
||
// 这里根据zone.fontEn选择英文字体
|
||
const fontFamilies = [
|
||
'SimSun, 宋体, Songti SC, serif',
|
||
'SimHei, 黑体, Heiti SC, sans-serif',
|
||
'KaiTi, 楷体, Kaiti SC, serif'
|
||
];
|
||
const enFontMap = [
|
||
'Courier New', 'Arial Black', 'Arial Italic', 'Lucida Console', 'Impact', 'Gothic', 'Arial Narrow', 'Comic Sans MS', 'Brush Script MT', 'Century Gothic', 'Times New Roman'
|
||
];
|
||
let fontFamily;
|
||
if (/^[A-Za-z0-9\s]+$/.test(zone.displayText)) {
|
||
fontFamily = enFontMap[zone.fontEn] || fontFamilies[0];
|
||
} else {
|
||
fontFamily = fontFamilies[zone.font] || fontFamilies[0];
|
||
}
|
||
ctx.font = `${asset.fontWeight} ${asset.scaledFontSize}px ${fontFamily}`;
|
||
|
||
// 计算行高
|
||
const lineHeight = asset.scaledFontSize * 1.2;
|
||
|
||
// 计算文本起始位置(考虑垂直对齐)
|
||
let startY = zoneY;
|
||
const verticalAlign = zone.vAlign;
|
||
const totalHeight = pageLines.length * lineHeight;
|
||
|
||
if (verticalAlign === 1) { // 垂直居中
|
||
startY = zoneY + (zoneHeight - totalHeight) / 2;
|
||
} else if (verticalAlign === 2) { // 底部对齐
|
||
startY = zoneY + zoneHeight - totalHeight;
|
||
} else { // 顶部对齐
|
||
startY = zoneY;
|
||
}
|
||
|
||
// 绘制每一行文本
|
||
pageLines.forEach((line, lineIdx) => {
|
||
// 计算水平位置(考虑水平对齐)
|
||
const textWidth = ctx.measureText(line).width;
|
||
let startX = zoneX;
|
||
|
||
if (zone.hAlign === 1) { // 水平居中
|
||
startX = zoneX + (zoneWidth - textWidth) / 2;
|
||
} else if (zone.hAlign === 2) { // 右对齐
|
||
startX = zoneX + zoneWidth - textWidth;
|
||
} else { // 左对齐
|
||
startX = zoneX;
|
||
}
|
||
|
||
// 计算动画偏移
|
||
const effect = zone.effect;
|
||
let animOffsetX = 0;
|
||
let animOffsetY = 0;
|
||
if (state) {
|
||
if (effect === 1 || effect === 2 || effect === 5) animOffsetX = state.currentX;
|
||
if (effect === 3 || effect === 4) animOffsetY = state.currentY;
|
||
}
|
||
|
||
ctx.fillText(line, startX + animOffsetX, startY + lineIdx * lineHeight + animOffsetY);
|
||
});
|
||
} else if (zone.playType === 1 && asset.isImage) {
|
||
// 图片类型分区
|
||
if (asset.loaded && asset.img.complete) {
|
||
const img = asset.img;
|
||
|
||
// 获取用户选择的目标尺寸
|
||
const sizeValues = [16, 32, 48, 64];
|
||
const targetSize = sizeValues[zone.imageSize] || 32;
|
||
|
||
// 根据原始宽高比计算缩放后的尺寸
|
||
let newWidth, newHeight;
|
||
if (img.width > img.height) {
|
||
newWidth = targetSize;
|
||
newHeight = (img.height / img.width) * targetSize;
|
||
} else {
|
||
newWidth = (img.width / img.height) * targetSize;
|
||
newHeight = targetSize;
|
||
}
|
||
|
||
// 创建临时画布用于调整尺寸和颜色
|
||
const tempCanvas = document.createElement('canvas');
|
||
tempCanvas.width = newWidth;
|
||
tempCanvas.height = newHeight;
|
||
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
|
||
|
||
// 绘制图片到临时画布(实现缩放)
|
||
tempCtx.drawImage(img, 0, 0, newWidth, newHeight);
|
||
|
||
// 获取像素数据并应用颜色
|
||
const imageData = tempCtx.getImageData(0, 0, newWidth, newHeight);
|
||
const data = imageData.data;
|
||
// 修正颜色顺序:0=红色, 1=绿色, 2=黄色
|
||
const colors = [
|
||
[255, 0, 0], // 红色
|
||
[0, 255, 0], // 绿色
|
||
[255, 255, 0] // 黄色
|
||
];
|
||
const selectedColor = colors[zone.imageColor];
|
||
if (selectedColor) {
|
||
for (let i = 0; i < data.length; i += 4) {
|
||
// 优化线条识别:通过计算灰度并使用更宽松的阈值来捕捉完整的笔画
|
||
const grayscale = (data[i] + data[i + 1] + data[i + 2]) / 3;
|
||
const isLine = grayscale < 128 && data[i + 3] > 50;
|
||
|
||
if (isLine) {
|
||
// 将线条着色
|
||
data[i] = selectedColor[0]; // R
|
||
data[i + 1] = selectedColor[1]; // G
|
||
data[i + 2] = selectedColor[2]; // B
|
||
data[i + 3] = 255; // 确保线条完全不透明
|
||
} else {
|
||
// 将背景设为透明
|
||
data[i + 3] = 0;
|
||
}
|
||
}
|
||
}
|
||
// 将处理后的图像数据放回临时画布
|
||
tempCtx.putImageData(imageData, 0, 0);
|
||
|
||
// 计算在主画布上绘制的最终尺寸和位置
|
||
const finalWidth = newWidth * scale;
|
||
const finalHeight = newHeight * scale;
|
||
const dX = zoneX + (zoneWidth - finalWidth) / 2;
|
||
const dY = zoneY + (zoneHeight - finalHeight) / 2;
|
||
|
||
// 将着色和缩放后的临时画布内容绘制到主画布
|
||
ctx.drawImage(tempCanvas, dX, dY, finalWidth, finalHeight);
|
||
|
||
} else {
|
||
// Image not loaded yet, draw placeholder
|
||
ctx.fillStyle = '#222';
|
||
ctx.fillRect(zoneX + 4, zoneY + 4, zoneWidth - 8, zoneHeight - 8);
|
||
|
||
ctx.fillStyle = '#fff';
|
||
ctx.font = `${Math.round(12 * scale)}px Arial`;
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText('加载中...', zoneX + zoneWidth / 2, zoneY + zoneHeight / 2);
|
||
}
|
||
}
|
||
|
||
// 恢复裁剪区域
|
||
ctx.restore();
|
||
});
|
||
},
|
||
prepareAddProgramPreviewAssets() {
|
||
const canvas = this.$refs.addProgramPreviewCanvas;
|
||
if (!canvas) return;
|
||
|
||
// 动态获取屏幕参数
|
||
let screenW = this.screenParams.width || 32;
|
||
let screenH = this.screenParams.height || 64;
|
||
// 如果角度为90或270,渲染时宽高互换
|
||
const angle = this.screenParams.angle;
|
||
const swapWH = angle === 90 || angle === 270;
|
||
if (swapWH) {
|
||
[screenW, screenH] = [screenH, screenW];
|
||
}
|
||
const renderWidth = 640; // 与实际渲染宽度匹配
|
||
const scale = Math.min(renderWidth / screenW, 240 / screenH);
|
||
|
||
this.addProgramPreviewAssets = this.addProgramForm.zones.map(zone => {
|
||
if (zone.playType === 1 && zone.image) {
|
||
console.log('加载图片', zone.image);
|
||
// 图片类型,异步加载图片
|
||
const img = new window.Image();
|
||
// img.crossOrigin = 'Anonymous';
|
||
const asset = { img, isImage: true, loaded: false };
|
||
img.onload = () => {
|
||
asset.loaded = true;
|
||
this.drawAddProgramPreview();
|
||
};
|
||
img.onerror = (e) => {
|
||
console.error("Preview image failed to load:", e);
|
||
};
|
||
img.src = zone.image;
|
||
return asset;
|
||
}
|
||
if (zone.playType !== 0 || !zone.displayText) {
|
||
return { pages: [[]], isText: false };
|
||
}
|
||
|
||
// 获取原始字体大小(像素值)
|
||
const fontSize = parseInt(this.addProgramFontSizes[zone.fontSize] || '16px');
|
||
|
||
// 计算缩放后的字体大小
|
||
const scaledFontSize = Math.max(10, Math.round(fontSize * scale * 0.8)); // 添加0.8缩放系数
|
||
|
||
const fontFamily = [
|
||
'SimSun, 宋体, Songti SC, serif',
|
||
'SimHei, 黑体, Heiti SC, sans-serif',
|
||
'KaiTi, 楷体, Kaiti SC, serif'
|
||
];
|
||
const fontWeight = zone.fontBold ? 'bold' : 'normal';
|
||
|
||
// 创建临时canvas测量文本
|
||
const measureCanvas = document.createElement('canvas');
|
||
const measureCtx = measureCanvas.getContext('2d');
|
||
const fontFamilies2 = [
|
||
'SimSun, 宋体, Songti SC, serif',
|
||
'SimHei, 黑体, Heiti SC, sans-serif',
|
||
'KaiTi, 楷体, Kaiti SC, serif'
|
||
];
|
||
const enFontMap2 = [
|
||
'Courier New', 'Arial Black', 'Arial Italic', 'Lucida Console', 'Impact', 'Gothic', 'Arial Narrow', 'Comic Sans MS', 'Brush Script MT', 'Century Gothic', 'Times New Roman'
|
||
];
|
||
let fontFamily2;
|
||
if (/^[A-Za-z0-9\s]+$/.test(zone.displayText)) {
|
||
fontFamily2 = enFontMap2[zone.fontEn] || fontFamilies2[0];
|
||
} else {
|
||
fontFamily2 = fontFamilies2[zone.font] || fontFamilies2[0];
|
||
}
|
||
measureCtx.font = `${fontWeight} ${scaledFontSize}px ${fontFamily2}`;
|
||
|
||
// 分行逻辑
|
||
const zoneRenderWidth = swapWH ? zone.height : zone.width;
|
||
const maxWidth = zoneRenderWidth * scale - 4; // 预留4px边距
|
||
let currentLine = '';
|
||
const lines = [];
|
||
|
||
for (const char of zone.displayText) {
|
||
const testLine = currentLine + char;
|
||
const metrics = measureCtx.measureText(testLine);
|
||
|
||
if (metrics.width > maxWidth) {
|
||
if (currentLine) lines.push(currentLine);
|
||
currentLine = char;
|
||
} else {
|
||
currentLine = testLine;
|
||
}
|
||
}
|
||
|
||
if (currentLine) lines.push(currentLine);
|
||
|
||
// 计算每页最大行数
|
||
const zoneRenderHeight = swapWH ? zone.width : zone.height;
|
||
const lineHeight = scaledFontSize * 1.2;
|
||
const maxLines = Math.max(1, Math.floor((zoneRenderHeight * scale - 4) / lineHeight));
|
||
|
||
// 分页
|
||
const pages = [];
|
||
for (let i = 0; i < lines.length; i += maxLines) {
|
||
pages.push(lines.slice(i, i + maxLines));
|
||
}
|
||
|
||
if (pages.length === 0) pages.push([]);
|
||
|
||
// 新增:计算总宽高和区域宽高,供动画用
|
||
const totalWidth = Math.max(...lines.map(line => measureCtx.measureText(line).width), 0);
|
||
const totalHeight = lines.length * lineHeight;
|
||
return {
|
||
pages,
|
||
isText: true,
|
||
scaledFontSize,
|
||
fontFamily: fontFamily2,
|
||
fontWeight,
|
||
lineHeight,
|
||
totalWidth,
|
||
totalHeight,
|
||
zoneRenderWidth: zoneRenderWidth * scale,
|
||
zoneRenderHeight: zoneRenderHeight * scale
|
||
};
|
||
});
|
||
},
|
||
resetAddProgramPreviewPage() {
|
||
this.addProgramPreviewPage = this.addProgramForm.zones.map(() => 0);
|
||
},
|
||
stopAddProgramPreviewAutoPage() {
|
||
if (this.addProgramPreviewTimer && this.addProgramPreviewTimer.length) {
|
||
this.addProgramPreviewTimer.forEach(timer => timer && clearInterval(timer));
|
||
}
|
||
this.addProgramPreviewTimer = [];
|
||
},
|
||
startAddProgramPreviewAutoPage() {
|
||
this.stopAddProgramPreviewAutoPage();
|
||
this.addProgramPreviewTimer = this.addProgramForm.zones.map((zone, zIdx) => {
|
||
const asset = this.addProgramPreviewAssets[zIdx];
|
||
if (!asset || !asset.isText) return null;
|
||
const totalPages = asset.pages.length;
|
||
if (totalPages <= 1) return null;
|
||
// 停留时间映射
|
||
const stayTimeMap = [0, 100, 500, 1000, 2000, 3000, 4000, 5000, 6000, 8000, 10000];
|
||
let stayMs = 1000;
|
||
if (zone.stayTime >= 0 && zone.stayTime < stayTimeMap.length) {
|
||
stayMs = stayTimeMap[zone.stayTime];
|
||
}
|
||
if (stayMs === 0) stayMs = 1000;
|
||
return setInterval(() => {
|
||
this.addProgramPreviewPage[zIdx] = (this.addProgramPreviewPage[zIdx] + 1) % totalPages;
|
||
this.drawAddProgramPreview();
|
||
}, stayMs);
|
||
});
|
||
},
|
||
getZoneLinesAndMax(zone) {
|
||
// 计算分区内容分行和每页最大行数
|
||
const canvas = this.$refs.addProgramPreviewCanvas;
|
||
if (!canvas) return { lines: [], maxLines: 1 };
|
||
const ctx = canvas.getContext('2d');
|
||
const screenW = 32, screenH = 64;
|
||
const canvasW = canvas.width, canvasH = canvas.height;
|
||
const scale = Math.min(canvasW / screenW, canvasH / screenH);
|
||
let fontSize = parseInt(this.addProgramFontSizes[zone.fontSize] || '16px');
|
||
let scaledFontSize = Math.max(10, Math.round(fontSize * scale));
|
||
const fontFamily = [
|
||
'SimSun, 宋体, Songti SC, serif',
|
||
'SimHei, 黑体, Heiti SC, sans-serif',
|
||
'KaiTi, 楷体, Kaiti SC, serif'
|
||
];
|
||
const fontWeight = zone.fontBold ? 'bold' : 'normal';
|
||
ctx.font = `${fontWeight} ${scaledFontSize}px ${fontFamily[zone.font]}`;
|
||
let lines = [];
|
||
let text = zone.displayText;
|
||
let zw = zone.width * scale;
|
||
let zh = zone.height * scale;
|
||
while (text && text.length > 0) {
|
||
let fitLen = 0;
|
||
let testStr = '';
|
||
for (let i = 1; i <= text.length; i++) {
|
||
testStr = text.slice(0, i);
|
||
if (ctx.measureText(testStr).width > zw - 4) {
|
||
break;
|
||
}
|
||
fitLen = i;
|
||
}
|
||
if (fitLen === 0) fitLen = 1;
|
||
lines.push(text.slice(0, fitLen));
|
||
text = text.slice(fitLen);
|
||
}
|
||
const maxLines = Math.floor((zh - 4) / scaledFontSize);
|
||
return { lines, maxLines };
|
||
},
|
||
resetAddProgramPreviewAnimState() {
|
||
// 初始化每个分区动画状态
|
||
this.addProgramPreviewAnimState = this.addProgramForm.zones.map((zone, idx) => {
|
||
return {
|
||
currentX: 0,
|
||
currentY: 0,
|
||
currentPage: this.addProgramPreviewPage[idx] || 0,
|
||
pageTimer: null,
|
||
effect: zone.effect,
|
||
};
|
||
});
|
||
},
|
||
stopAddProgramPreviewAnimLoop() {
|
||
if (this.addProgramPreviewAnimFrame) {
|
||
cancelAnimationFrame(this.addProgramPreviewAnimFrame);
|
||
this.addProgramPreviewAnimFrame = null;
|
||
}
|
||
},
|
||
startAddProgramPreviewAnimLoop() {
|
||
this.stopAddProgramPreviewAnimLoop();
|
||
const loop = () => {
|
||
this.updateAddProgramPreviewAnimState();
|
||
this.drawAddProgramPreview();
|
||
this.addProgramPreviewAnimFrame = requestAnimationFrame(loop);
|
||
};
|
||
this.addProgramPreviewAnimFrame = requestAnimationFrame(loop);
|
||
},
|
||
updateAddProgramPreviewAnimState() {
|
||
// 动画主循环,更新每个分区的动画状态
|
||
const renderWidth = 640;
|
||
const renderHeight = 240;
|
||
// const scale = Math.min(renderWidth / 32, renderHeight / 64);
|
||
// 使用与 prepareAddProgramPreviewAssets 中相同的 scale 计算方式
|
||
let screenW = this.screenParams.width || 32;
|
||
let screenH = this.screenParams.height || 64;
|
||
const angle = this.screenParams.angle;
|
||
const swapWH = angle === 90 || angle === 270;
|
||
if (swapWH) {
|
||
[screenW, screenH] = [screenH, screenW];
|
||
}
|
||
const scale = Math.min(renderWidth / screenW, renderHeight / screenH);
|
||
|
||
|
||
this.addProgramForm.zones.forEach((zone, idx) => {
|
||
const asset = this.addProgramPreviewAssets[idx];
|
||
const state = this.addProgramPreviewAnimState[idx];
|
||
|
||
if (!asset || !state || !asset.isText) return;
|
||
|
||
const effect = zone.effect;
|
||
const page = this.addProgramPreviewPage[idx] || 0;
|
||
const pageLines = asset.pages[page] || [];
|
||
const lineHeight = asset.lineHeight;
|
||
// const totalHeight = pageLines.length * lineHeight; // 原始计算
|
||
// const totalWidth = Math.max(...pageLines.map(line => measureCtx.measureText(line).width), 0); // 原始计算
|
||
// 使用 prepareAddProgramPreviewAssets 中预先计算好的值
|
||
const totalHeight = asset.totalHeight;
|
||
const totalWidth = asset.totalWidth;
|
||
|
||
|
||
// 动画速度
|
||
const speedMap = [1, 2, 3, 4, 5];
|
||
const animSpeed = (speedMap[zone.speed] || 3) * scale * 0.5; // 像素/帧
|
||
|
||
// 区域宽高 (使用预先计算并缩放的值)
|
||
const zoneWidth = asset.zoneRenderWidth;
|
||
const zoneHeight = asset.zoneRenderHeight;
|
||
|
||
// 暂停时间 (毫秒)
|
||
const pauseMs = this.getPauseTime(zone.stayTime);
|
||
|
||
// 确保初始状态值存在
|
||
if (typeof state.isPausing !== 'boolean') state.isPausing = false;
|
||
if (typeof state.pauseStart !== 'number') state.pauseStart = 0;
|
||
if (typeof state.pausePhase !== 'string') state.pausePhase = 'start'; // 'start' | 'move'
|
||
|
||
|
||
switch (effect) {
|
||
case 1: // 左移
|
||
// 确保初始 state.currentX 值存在
|
||
if (typeof state.currentX !== 'number') state.currentX = 0;
|
||
// 确保初始 pausePhase 值存在
|
||
if (!state.pausePhase) state.pausePhase = 'start'; // 'start' | 'move'
|
||
|
||
// 初始位置停留阶段
|
||
if (!state.isPausing && state.pausePhase === 'start') {
|
||
state.isPausing = true;
|
||
state.pauseStart = Date.now();
|
||
}
|
||
|
||
// 检查初始停留是否结束
|
||
if (state.isPausing && state.pausePhase === 'start') {
|
||
if (Date.now() - state.pauseStart >= pauseMs) {
|
||
state.isPausing = false;
|
||
state.pausePhase = 'move';
|
||
}
|
||
// 移动阶段
|
||
} else if (!state.isPausing && state.pausePhase === 'move') {
|
||
// --- 强制多移动的逻辑 ---
|
||
// !!! 核心修改点 !!!
|
||
// 原判断: if (state.currentX + totalWidth > 0)
|
||
// 新判断: 计算从起始位置(0)到当前位置已经移动的距离 (-state.currentX)
|
||
// 并与一个我们认为足够"移出"的距离进行比较
|
||
const pixelsMoved = -state.currentX; // 从 X=0 开始,向左移动了多远
|
||
const distanceToTriggerReset = zoneWidth + 100; // <--- 关键:移出区域宽度 + 额外距离
|
||
// 例如:区域宽 32 + 额外 100 = 132
|
||
// 你可以调整这个 100 为你觉得合适的值
|
||
|
||
if (pixelsMoved < distanceToTriggerReset) {
|
||
// 移动距离还不够,继续移动
|
||
state.currentX -= animSpeed;
|
||
} else {
|
||
// 移动距离足够了(强制认为已经移出),立即重置
|
||
// console.log(`Zone ${idx} - Left Move: Forcefully reset after moving ${pixelsMoved.toFixed(2)} pixels.`); // 调试信息
|
||
state.currentX = 0; // 回到起始位置
|
||
state.pausePhase = 'start'; // 回到初始停留阶段
|
||
}
|
||
// --- 强制多移动逻辑结束 ---
|
||
}
|
||
// Y轴不移动
|
||
state.currentY = 0;
|
||
break;
|
||
|
||
|
||
case 2: // 右移
|
||
// 确保初始 state.currentX 值存在
|
||
if (typeof state.currentX !== 'number') state.currentX = 0; // 通常从左侧开始向右移
|
||
// 确保初始 pausePhase 值存在
|
||
if (!state.pausePhase) state.pausePhase = 'start'; // 'start' | 'move'
|
||
|
||
// 初始位置停留阶段
|
||
if (!state.isPausing && state.pausePhase === 'start') {
|
||
state.isPausing = true;
|
||
state.pauseStart = Date.now();
|
||
}
|
||
|
||
// 检查初始停留是否结束
|
||
if (state.isPausing && state.pausePhase === 'start') {
|
||
if (Date.now() - state.pauseStart >= pauseMs) {
|
||
state.isPausing = false;
|
||
state.pausePhase = 'move';
|
||
}
|
||
// 移动阶段
|
||
} else if (!state.isPausing && state.pausePhase === 'move') {
|
||
// --- 修正后的逻辑 ---
|
||
// 判断是否还可以继续向右移动
|
||
// 条件:当前文本左边缘距离完全移出右边界还有距离 (currentX < zoneWidth)
|
||
if (state.currentX < zoneWidth) {
|
||
state.currentX += animSpeed;
|
||
// 可选:边界检查
|
||
// if (state.currentX > zoneWidth) {
|
||
// state.currentX = zoneWidth; // 精确设置到边界
|
||
// }
|
||
} else {
|
||
// 内容完全移出右侧,立即重置,准备下一轮
|
||
// 不再进入 'end' 停留阶段
|
||
state.currentX = 0; // 回到左侧起始位置
|
||
state.pausePhase = 'start'; // 回到初始停留阶段
|
||
}
|
||
// --- 修正结束 ---
|
||
}
|
||
// Y轴不移动
|
||
state.currentY = 0;
|
||
break;
|
||
|
||
case 3: // 上移
|
||
// 确保初始 state.currentY 值存在
|
||
if (typeof state.currentY !== 'number') state.currentY = 0;
|
||
// 确保初始 pausePhase 值存在
|
||
if (!state.pausePhase) state.pausePhase = 'start'; // 'start' | 'move'
|
||
|
||
// 初始位置停留阶段
|
||
if (!state.isPausing && state.pausePhase === 'start') {
|
||
state.isPausing = true;
|
||
state.pauseStart = Date.now();
|
||
}
|
||
|
||
// 检查初始停留是否结束
|
||
if (state.isPausing && state.pausePhase === 'start') {
|
||
if (Date.now() - state.pauseStart >= pauseMs) {
|
||
state.isPausing = false;
|
||
state.pausePhase = 'move';
|
||
}
|
||
// 移动阶段
|
||
} else if (!state.isPausing && state.pausePhase === 'move') {
|
||
// --- 修正后的逻辑 ---
|
||
// 判断是否还可以继续向上移动
|
||
// 条件:当前文本底边距离完全移出顶边还有距离 (currentY + totalHeight > 0)
|
||
if (state.currentY + totalHeight > 0) {
|
||
state.currentY -= animSpeed;
|
||
// 可选:边界检查
|
||
// if (state.currentY + totalHeight < 0) {
|
||
// state.currentY = -totalHeight;
|
||
// }
|
||
} else {
|
||
// 内容完全移出顶部,立即重置,准备下一轮
|
||
// 不再进入 'end' 停留阶段
|
||
state.currentY = 0; // 回到顶部起始位置
|
||
state.pausePhase = 'start'; // 回到初始停留阶段
|
||
}
|
||
// --- 修正结束 ---
|
||
}
|
||
// X轴不移动
|
||
state.currentX = 0;
|
||
break;
|
||
|
||
case 4: // 下移
|
||
// 确保初始 state.currentY 值存在
|
||
if (typeof state.currentY !== 'number') state.currentY = 0; // 通常从顶部开始向下移
|
||
// 确保初始 pausePhase 值存在
|
||
if (!state.pausePhase) state.pausePhase = 'start'; // 'start' | 'move'
|
||
|
||
// 初始位置停留阶段
|
||
if (!state.isPausing && state.pausePhase === 'start') {
|
||
state.isPausing = true;
|
||
state.pauseStart = Date.now();
|
||
}
|
||
|
||
// 检查初始停留是否结束
|
||
if (state.isPausing && state.pausePhase === 'start') {
|
||
if (Date.now() - state.pauseStart >= pauseMs) {
|
||
state.isPausing = false;
|
||
state.pausePhase = 'move';
|
||
}
|
||
// 移动阶段
|
||
} else if (!state.isPausing && state.pausePhase === 'move') {
|
||
// --- 修正后的逻辑 ---
|
||
// 判断是否还可以继续向下移动
|
||
// 条件:当前文本顶边距离完全移出底边还有距离 (currentY < zoneHeight)
|
||
if (state.currentY < zoneHeight) {
|
||
state.currentY += animSpeed;
|
||
// 可选:边界检查
|
||
// if (state.currentY > zoneHeight) {
|
||
// state.currentY = zoneHeight;
|
||
// }
|
||
} else {
|
||
// 内容完全移出底部,立即重置,准备下一轮
|
||
// 不再进入 'end' 停留阶段
|
||
state.currentY = 0; // 回到顶部起始位置
|
||
state.pausePhase = 'start'; // 回到初始停留阶段
|
||
}
|
||
// --- 修正结束 ---
|
||
}
|
||
// X轴不移动
|
||
state.currentX = 0;
|
||
break;
|
||
|
||
case 5: // 连续左移 (立即显示 -> 左移 -> 立即显示 -> ...)
|
||
// 确保初始 state.currentX 值存在
|
||
if (typeof state.currentX !== 'number') state.currentX = 0;
|
||
// 确保初始 pausePhase 值存在
|
||
if (!state.pausePhase) state.pausePhase = 'start'; // 'start' | 'move'
|
||
|
||
// 初始立即显示阶段 (或一轮结束后的显示阶段)
|
||
if (!state.isPausing && state.pausePhase === 'start') {
|
||
// 立即显示阶段,不移动,停留 pauseMs 时间
|
||
state.isPausing = true;
|
||
state.pauseStart = Date.now();
|
||
}
|
||
|
||
// 检查立即显示阶段是否结束
|
||
if (state.isPausing && state.pausePhase === 'start') {
|
||
if (Date.now() - state.pauseStart >= pauseMs) {
|
||
state.isPausing = false;
|
||
state.pausePhase = 'move'; // 进入移动阶段
|
||
}
|
||
// 移动阶段 (左移)
|
||
} else if (!state.isPausing && state.pausePhase === 'move') {
|
||
// --- 修正后的逻辑 (连续左移的移动逻辑与普通左移一致) ---
|
||
// 判断是否还可以继续向左移动
|
||
if (state.currentX > -totalWidth) {
|
||
state.currentX -= animSpeed;
|
||
// 可选边界检查
|
||
// if (state.currentX < -totalWidth) {
|
||
// state.currentX = -totalWidth;
|
||
// }
|
||
} else {
|
||
// 内容完全移出,进入下一个立即显示阶段
|
||
// 连续左移的效果是在这里停留 pauseMs 后再显示下一轮
|
||
state.isPausing = true;
|
||
state.pauseStart = Date.now();
|
||
state.currentX = 0; // 回到起始位置显示
|
||
state.pausePhase = 'start'; // 回到 'start' 阶段进行显示停留
|
||
}
|
||
// --- 修正结束 ---
|
||
}
|
||
state.currentY = 0; // Y轴不移动
|
||
break;
|
||
|
||
case 6: // 闪烁换页
|
||
// 由自动翻页定时器控制,此处不处理动画位移
|
||
state.currentX = 0;
|
||
state.currentY = 0;
|
||
break;
|
||
|
||
default: // 立即显示 (effect === 0) 或其他未知效果
|
||
state.currentX = 0;
|
||
state.currentY = 0;
|
||
// 可以考虑添加立即显示的停留逻辑,如果需要的话
|
||
// if (!state.isPausing && state.pausePhase === 'start') {
|
||
// state.isPausing = true;
|
||
// state.pauseStart = Date.now();
|
||
// }
|
||
// if (state.isPausing && state.pausePhase === 'start') {
|
||
// if (Date.now() - state.pauseStart >= pauseMs) {
|
||
// state.isPausing = false;
|
||
// state.pausePhase = 'end'; // 或者重置为 'start'
|
||
// }
|
||
// }
|
||
// state.pausePhase = 'start'; // 重置状态以便下次循环
|
||
break;
|
||
}
|
||
});
|
||
},
|
||
measureTextWidth(text, asset) {
|
||
// 用于动画宽度测量
|
||
const canvas = document.createElement('canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.font = `${asset.fontWeight} ${asset.scaledFontSize}px ${asset.fontFamily}`;
|
||
return ctx.measureText(text).width;
|
||
},
|
||
// ================= 显卡物模型适配方法 BEGIN =================
|
||
// 屏幕总开关
|
||
handleScreenSwitchChange(val) {
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#screenEn');
|
||
if (model) {
|
||
model.shadow = val ? '1' : '0';
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
// 开机界面开关
|
||
handleBootEnChange(val) {
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#bootEn');
|
||
if (model) {
|
||
model.shadow = val ? '1' : '0';
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
// 恢复出厂
|
||
handleFactoryEnChange(val) {
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#factoryEn');
|
||
if (model) {
|
||
model.shadow = val ? '1' : '0';
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
// 屏幕亮度
|
||
handleLuminanceChange(val) {
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#luminance');
|
||
if (model) {
|
||
model.shadow = val.toString();
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
// 屏参设置
|
||
handleScrParamChange(param) {
|
||
// param: { w, h, oe, data, angle }
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#scrParam');
|
||
if (model) {
|
||
model.shadow = JSON.stringify({ scr_param: param });
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
// 节目列表
|
||
handleProgListChange(progList) {
|
||
// progList: [{order, rem, dur, areaM, aLst: [...] }]
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#progList');
|
||
if (model) {
|
||
model.shadow = JSON.stringify({ prog_list: progList });
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
// 节目开关列表
|
||
handleProgEnChange(progEnArr) {
|
||
// progEnArr: [1,0,1,1,0]
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#progEn');
|
||
if (model) {
|
||
model.shadow = JSON.stringify(progEnArr);
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
// 删除节目
|
||
handleDelProg(index) {
|
||
// index: 0=删第1条, -1=全部清空
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === 'del_prog');
|
||
if (model) {
|
||
model.shadow = index.toString();
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
// 传感器阈值
|
||
handleSnsrThrChange(thr) {
|
||
// thr: { speed: 60, temp: 30, humi: 50 }
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#snsrThr');
|
||
if (model) {
|
||
model.shadow = JSON.stringify({ snsr_thr: thr });
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
// 触发一次全量上报
|
||
handleRequestModelReport() {
|
||
const model = this.deviceInfo.thingsModels.find(m => m.id === '102#model');
|
||
if (model) {
|
||
model.shadow = '1';
|
||
this.mqttPublish(this.deviceInfo, model);
|
||
}
|
||
},
|
||
// ================= 显卡物模型适配方法 END =================
|
||
// 获取分区显示用的坐标和尺寸,index为分区下标
|
||
zoneDisplay(zIdx) {
|
||
const zone = this.addProgramForm.zones[zIdx];
|
||
if (!zone) return { x: 0, y: 0, width: 0, height: 0 };
|
||
// 已去除角度为90或270时的xy/宽高对调逻辑,UI显示与实际参数一致
|
||
return {
|
||
x: zone.x,
|
||
y: zone.y,
|
||
width: zone.width,
|
||
height: zone.height
|
||
};
|
||
},
|
||
// 设置分区的物理坐标,UI输入时不再自动对调
|
||
setZoneDisplay(zIdx, key, value) {
|
||
const zone = this.addProgramForm.zones[zIdx];
|
||
if (!zone) return;
|
||
// 已去除角度为90或270时的xy/宽高对调逻辑,UI输入与实际参数一致
|
||
zone[key] = value;
|
||
},
|
||
updateBoardOnlineStatus() {
|
||
if (!this.deviceInfo || !this.deviceInfo.thingsModels) {
|
||
this.boardOnline = false;
|
||
return;
|
||
}
|
||
const onlineModel = this.deviceInfo.thingsModels.find(m => m.id === '1#onlineBoard');
|
||
if (!onlineModel || !(onlineModel.shadow || onlineModel.value)) { // 先检查 shadow
|
||
this.boardOnline = false;
|
||
return;
|
||
}
|
||
try {
|
||
const valueStr = onlineModel.shadow || onlineModel.value; // 优先使用 shadow
|
||
const jsonStr = valueStr.replace(/^JSON=/, '');
|
||
const data = JSON.parse(jsonStr);
|
||
const onlineArr = data.online || [];
|
||
this.boardOnline = onlineArr.includes(102); // 显卡编号
|
||
} catch (e) {
|
||
this.boardOnline = false;
|
||
}
|
||
// console.log("测试是否受到消息", JSON.stringify(this.boardOnline));
|
||
// console.log("数据来源:", onlineModel.shadow ? "shadow" : "value");
|
||
|
||
}
|
||
},
|
||
computed: {
|
||
// ...原有computed
|
||
},
|
||
};
|
||
</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;
|
||
justify-content: space-between;
|
||
margin-bottom: 15px;
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
i,
|
||
.svg-icon {
|
||
font-size: 30px;
|
||
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;
|
||
}
|
||
|
||
.info-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
margin-bottom: 15px;
|
||
padding: 8px 12px;
|
||
background: #f8f9fa;
|
||
border-radius: 6px;
|
||
border-left: 3px solid #409EFF;
|
||
position: relative;
|
||
min-height: 40px;
|
||
|
||
.info-label {
|
||
font-size: 14px;
|
||
color: #606266;
|
||
margin-right: 12px;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 14px;
|
||
color: #303133;
|
||
font-weight: 600;
|
||
flex: 1;
|
||
}
|
||
|
||
&.el-loading-parent--relative {
|
||
.el-loading-mask {
|
||
background-color: rgba(255, 255, 255, 0.8);
|
||
border-radius: 6px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.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;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
|
||
.time-header {
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.time-pickers {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
|
||
.time-separator {
|
||
margin: 0 10px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.weekday-select {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
// 自定义表格滚动条样式
|
||
.el-table {
|
||
|
||
// 设置表格容器的滚动条样式
|
||
&::-webkit-scrollbar {
|
||
width: 12px;
|
||
height: 12px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-track {
|
||
background: #f1f1f1;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background: #c1c1c1;
|
||
border-radius: 6px;
|
||
|
||
&:hover {
|
||
background: #a8a8a8;
|
||
}
|
||
}
|
||
|
||
// 表格内部的滚动条样式
|
||
.el-table__body-wrapper {
|
||
&::-webkit-scrollbar {
|
||
width: 28px;
|
||
height: 28px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-track {
|
||
background: #f5f5f5;
|
||
border-radius: 29px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background: #bfbfbf;
|
||
border-radius: 29px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 全局滚动条样式(影响所有滚动条)
|
||
::-webkit-scrollbar {
|
||
width: 12px;
|
||
height: 12px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: #f1f1f1;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: #c1c1c1;
|
||
border-radius: 6px;
|
||
|
||
&:hover {
|
||
background: #a8a8a8;
|
||
}
|
||
}
|
||
|
||
/* 弹窗内canvas预览区固定顶部 */
|
||
.sticky-canvas-preview {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
background: #fff;
|
||
padding-top: 24px;
|
||
padding-bottom: 16px;
|
||
box-shadow: 0 2px 8px #e0e0e0;
|
||
border-radius: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.board-status-single {
|
||
margin-top: 6px;
|
||
font-size: 15px;
|
||
|
||
span {
|
||
font-weight: 600;
|
||
|
||
&.online {
|
||
color: #67C23A;
|
||
}
|
||
|
||
&.offline {
|
||
color: #bbb;
|
||
}
|
||
}
|
||
}
|
||
|
||
.title.offline {
|
||
color: #bbb;
|
||
}
|
||
</style> |