Xazn-vue/src/views/iot/device/display.vue

3763 lines
167 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="running-status">
<el-row :gutter="20">
<el-col :xs="24" :sm="24" :md="24" :lg="16" :xl="14" class="status-col">
<!-- 设备模式和OTA升级部分 -->
<el-row :gutter="20" class="mode-section">
<!-- 设备模式 -->
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-card class="mode-card" shadow="hover">
<div class="mode-header">
<i class="el-icon-menu" 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/3UI下拉框为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>