显卡逻辑完善
This commit is contained in:
parent
ec7e9ff6b9
commit
c470b9074e
@ -76,10 +76,10 @@
|
||||
:disabled="!basicSettings.screenEnabled">
|
||||
</el-slider>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<!-- <el-form-item>
|
||||
<el-button type="danger" @click="handleClearScreen"
|
||||
:disabled="!basicSettings.screenEnabled">清除屏幕</el-button>
|
||||
</el-form-item>
|
||||
</el-form-item> -->
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
@ -110,7 +110,7 @@
|
||||
<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">
|
||||
<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>
|
||||
@ -356,11 +356,10 @@
|
||||
<!-- 添加/编辑自定义节目对话框 -->
|
||||
<el-dialog :title="isEditProgram ? '编辑自定义节目' : '添加自定义节目'" :visible.sync="addProgramDialogVisible" width="700px"
|
||||
append-to-body>
|
||||
<!-- <div
|
||||
style="position:sticky;top:0;z-index:10;background:#fff;padding-top:24px;padding-bottom:32px;box-shadow:0 2px 8px #e0e0e0;text-align:center;margin-bottom:36px;width:600px;margin-left:auto;margin-right:auto;border-radius:16px;"> -->
|
||||
<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> -->
|
||||
<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">
|
||||
@ -442,6 +441,14 @@
|
||||
</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%">
|
||||
@ -522,22 +529,26 @@
|
||||
<el-row :gutter="8">
|
||||
<el-col :span="5">
|
||||
<el-form-item label="X" label-width="20px">
|
||||
<el-input-number v-model="zone.x" :min="0" style="width:100%" />
|
||||
<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 v-model="zone.width" :min="1" style="width:100%" />
|
||||
<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 v-model="zone.y" :min="0" style="width:100%" />
|
||||
<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 v-model="zone.height" :min="1" style="width:100%" />
|
||||
<el-input-number :value="zoneDisplay(zIdx).height" :min="1" style="width:100%"
|
||||
@input="setZoneDisplay(zIdx, 'height', $event)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -726,14 +737,15 @@ export default {
|
||||
commonPhrase: 0,
|
||||
displayText: '',
|
||||
font: 0,
|
||||
fontEn: 0, // 新增英文字体
|
||||
fontShape: 0,
|
||||
fontSize: 0,
|
||||
fontColor: 0,
|
||||
fontColor: 0, // 红色(默认)
|
||||
fontBold: 0,
|
||||
fontStretch: 0,
|
||||
image: '',
|
||||
imageSize: 1, // 32x32
|
||||
imageColor: 0, // Red
|
||||
imageColor: 0, // 红色(默认)
|
||||
effect: 0,
|
||||
speed: 4,
|
||||
stayTime: 5,
|
||||
@ -767,7 +779,7 @@ export default {
|
||||
addProgramFonts: ['宋体(中)', '黑体(中)', '楷体(中)'],
|
||||
addProgramFontShapes: ['圆角(英)', '直角(英)'],
|
||||
addProgramFontSizes: ['16px', '24px', '32px'],
|
||||
addProgramFontColors: ['黑色', '红色', '绿色', '黄色'],
|
||||
addProgramFontColors: ['红色', '绿色', '黄色'],
|
||||
addProgramFontBold: ['不加粗', '加粗'],
|
||||
addProgramFontStretch: ['不拉伸', '横向拉伸', '纵向拉伸'],
|
||||
addProgramEffects: ['立即显示', '左移', '右移', '上移', '下移', '连续左移', '闪烁换页'],
|
||||
@ -776,7 +788,10 @@ export default {
|
||||
addProgramHorizontalAlign: ['居左对齐', '居中对齐', '居右对齐'],
|
||||
addProgramVerticalAlign: ['顶部对齐', '居中对齐', '底部对齐'],
|
||||
addProgramImageSizes: ['16x16', '32x32', '48x48', '64x64'],
|
||||
addProgramImageColors: ['黑色', '红色', '绿色', '黄色'],
|
||||
addProgramImageColors: ['红色', '绿色', '黄色'],
|
||||
addProgramFontEns: [
|
||||
'打字(英)', '长黑(英)', '白斜(英)', '线形(英)', '方斜(英)', '歌德(英)', '黑正(英)', '美术(英)', '手写(英)', '正圆(英)', '时钟(英)圆角(英)'
|
||||
],
|
||||
// 自动分页相关
|
||||
addProgramPreviewPage: [0], // 每个分区当前页
|
||||
addProgramPreviewTimer: [], // 每个分区定时器
|
||||
@ -961,13 +976,8 @@ export default {
|
||||
const progListModel = this.deviceInfo.thingsModels.find(model => model.id === '102#progList');
|
||||
if (progListModel) {
|
||||
try {
|
||||
const jsonStr = progListModel.shadow.replace('JSON=', '');
|
||||
const data = JSON.parse(jsonStr);
|
||||
|
||||
// 删除指定序号的节目
|
||||
data.del_prog = program.order;
|
||||
|
||||
progListModel.shadow = 'JSON=' + JSON.stringify(data);
|
||||
// 只发送删除指令,不带上整个节目列表
|
||||
progListModel.shadow = 'JSON=' + JSON.stringify({ del_prog: program.order });
|
||||
this.mqttPublish(this.deviceInfo, progListModel).then(() => {
|
||||
this.$message.success('删除指令已发送');
|
||||
}).catch(error => {
|
||||
@ -1014,28 +1024,37 @@ export default {
|
||||
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,
|
||||
fontColor: 0, // 红色(默认)
|
||||
fontBold: 0,
|
||||
fontStretch: 0,
|
||||
image: '',
|
||||
imageSize: 1,
|
||||
imageColor: 0,
|
||||
imageColor: 0, // 红色(默认)
|
||||
effect: 0,
|
||||
speed: 4,
|
||||
stayTime: 5,
|
||||
hAlign: 1,
|
||||
vAlign: 1,
|
||||
x: area.size ? area.size.x : 0,
|
||||
y: area.size ? area.size.y : 0,
|
||||
width: area.size ? area.size.l : 32,
|
||||
height: area.size ? area.size.h : 64
|
||||
x: x,
|
||||
y: y,
|
||||
width: w,
|
||||
height: h
|
||||
};
|
||||
|
||||
// 解析播放项
|
||||
@ -1046,12 +1065,13 @@ export default {
|
||||
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 || 0; // 文本颜色
|
||||
zone.fontBold = item.txt.fW || 0; // 字体加粗
|
||||
zone.fontStretch = item.txt.stch || 0; // 拉伸方向
|
||||
zone.hAlign = item.txt.hPos || 1; // 水平对齐
|
||||
zone.vAlign = item.txt.vPos || 1; // 垂直对齐
|
||||
zone.hAlign = typeof item.txt.hPos === 'number' ? Math.max(item.txt.hPos - 1, 0) : 1; // 水平对齐
|
||||
zone.vAlign = typeof item.txt.vPos === 'number' ? Math.max(item.txt.vPos - 1, 0) : 1; // 垂直对齐
|
||||
} else if (item.typ === 1 && item.img) {
|
||||
// 图片类型
|
||||
zone.playType = 1;
|
||||
@ -1077,14 +1097,15 @@ export default {
|
||||
commonPhrase: 0,
|
||||
displayText: '',
|
||||
font: 0,
|
||||
fontEn: 0, // 新增英文字体
|
||||
fontShape: 0,
|
||||
fontSize: 0,
|
||||
fontColor: 0,
|
||||
fontColor: 0, // 红色(默认)
|
||||
fontBold: 0,
|
||||
fontStretch: 0,
|
||||
image: '',
|
||||
imageSize: 1,
|
||||
imageColor: 0,
|
||||
imageColor: 0, // 红色(默认)
|
||||
effect: 0,
|
||||
speed: 4,
|
||||
stayTime: 5,
|
||||
@ -1099,6 +1120,7 @@ export default {
|
||||
|
||||
// 更新预览
|
||||
this.$nextTick(() => {
|
||||
this.onAddProgramModeChange(this.addProgramForm.mode); // 保证分区布局和模式同步
|
||||
this.drawAddProgramPreview();
|
||||
});
|
||||
},
|
||||
@ -2087,6 +2109,7 @@ export default {
|
||||
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) {
|
||||
@ -2187,14 +2210,15 @@ export default {
|
||||
commonPhrase: 0,
|
||||
displayText: '',
|
||||
font: 0,
|
||||
fontEn: 0, // 新增英文字体
|
||||
fontShape: 0,
|
||||
fontSize: 0,
|
||||
fontColor: 0,
|
||||
fontColor: 0, // 红色(默认)
|
||||
fontBold: 0,
|
||||
fontStretch: 0,
|
||||
image: '',
|
||||
imageSize: 1,
|
||||
imageColor: 0,
|
||||
imageColor: 0, // 红色(默认)
|
||||
effect: 0,
|
||||
speed: 4,
|
||||
stayTime: 5,
|
||||
@ -2222,14 +2246,15 @@ export default {
|
||||
commonPhrase: 0,
|
||||
displayText: '',
|
||||
font: 0,
|
||||
fontEn: 0, // 新增英文字体
|
||||
fontShape: 0,
|
||||
fontSize: 0,
|
||||
fontColor: 0,
|
||||
fontColor: 0, // 红色(默认)
|
||||
fontBold: 0,
|
||||
fontStretch: 0,
|
||||
image: '',
|
||||
imageSize: 1,
|
||||
imageColor: 0,
|
||||
imageColor: 0, // 红色(默认)
|
||||
effect: 0,
|
||||
speed: 4,
|
||||
stayTime: 5,
|
||||
@ -2282,77 +2307,97 @@ export default {
|
||||
const progListModel = this.deviceInfo.thingsModels.find(model => model.id === '102#progList');
|
||||
if (progListModel) {
|
||||
try {
|
||||
// 构建符合显卡接口规范的节目数据
|
||||
const programData = {
|
||||
del_prog: 0, // 不删除节目
|
||||
prog_list: [{
|
||||
order: this.isEditProgram ? this.editingProgram.order : 0, // 编辑模式使用原序号
|
||||
rem: (this.addProgramForm.remark || `自定义节目${Date.now()}`) + '\0', // 节目备注,需要包含\0
|
||||
dur: this.addProgramForm.duration, // 播放时长(秒)
|
||||
areaM: this.addProgramForm.mode, // 分区模式
|
||||
aLst: this.addProgramForm.zones.map(zone => {
|
||||
// 构建分区数据
|
||||
const area = {
|
||||
size: {
|
||||
x: zone.x, // 左下角X坐标
|
||||
y: zone.y, // 左下角Y坐标
|
||||
l: zone.width, // 宽度
|
||||
h: zone.height // 高度
|
||||
// 计算 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 // 播放时间
|
||||
},
|
||||
pLst: []
|
||||
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, // 水平对齐
|
||||
vPos: zone.vAlign - 1 // 垂直对齐
|
||||
}
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
// 根据播放类型构建播放项
|
||||
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, // 文本颜色
|
||||
fS: this.getFontSize(zone.fontSize), // 字体大小
|
||||
fCn: zone.font + 1, // 中文字体
|
||||
fEn: 7, // 英文字体默认值
|
||||
fW: zone.fontBold, // 字体加粗
|
||||
stch: zone.fontStretch, // 拉伸方向
|
||||
hPos: zone.hAlign, // 水平对齐
|
||||
vPos: zone.vAlign // 垂直对齐
|
||||
}
|
||||
};
|
||||
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;
|
||||
})
|
||||
}]
|
||||
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;
|
||||
@ -2376,8 +2421,14 @@ export default {
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 获取当前屏幕参数
|
||||
const screenW = this.screenParams.width || 32;
|
||||
const screenH = this.screenParams.height || 64;
|
||||
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; // 调大预览宽度
|
||||
@ -2414,11 +2465,19 @@ export default {
|
||||
const asset = this.addProgramPreviewAssets[zIdx];
|
||||
if (!asset) return;
|
||||
|
||||
// 计算分区在画布中的位置
|
||||
const zoneX = offsetX + zone.x * scale;
|
||||
const zoneY = offsetY + zone.y * scale;
|
||||
const zoneWidth = zone.width * scale;
|
||||
const zoneHeight = zone.height * scale;
|
||||
// 计算分区在画布中的位置(角度为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();
|
||||
@ -2443,10 +2502,19 @@ export default {
|
||||
const pageLines = asset.pages[page] || [];
|
||||
|
||||
// 设置字体样式
|
||||
const colors = ['#000000', '#FF0000', '#00FF00', '#FFFF00'];
|
||||
const colors = ['#FF0000', '#00FF00', '#FFFF00'];
|
||||
ctx.fillStyle = colors[zone.fontColor] || '#000000';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.font = `${asset.fontWeight} ${asset.scaledFontSize}px ${asset.fontFamily}`;
|
||||
// 这里根据zone.fontEn选择英文字体
|
||||
let fontFamily = ['SimSun', 'SimHei', 'KaiTi'][zone.font] || 'SimSun';
|
||||
if (zone.fontEn !== undefined && zone.fontEn !== null) {
|
||||
// 你可以根据实际需求映射到具体英文字体
|
||||
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'
|
||||
];
|
||||
fontFamily = enFontMap[zone.fontEn] || fontFamily;
|
||||
}
|
||||
ctx.font = `${asset.fontWeight} ${asset.scaledFontSize}px ${fontFamily}`;
|
||||
|
||||
// 计算行高
|
||||
const lineHeight = asset.scaledFontSize * 1.2;
|
||||
@ -2570,8 +2638,14 @@ export default {
|
||||
if (!canvas) return;
|
||||
|
||||
// 动态获取屏幕参数
|
||||
const screenW = this.screenParams.width || 32;
|
||||
const screenH = this.screenParams.height || 64;
|
||||
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);
|
||||
|
||||
@ -2610,7 +2684,8 @@ export default {
|
||||
measureCtx.font = `${fontWeight} ${scaledFontSize}px ${fontFamily}`;
|
||||
|
||||
// 分行逻辑
|
||||
const maxWidth = zone.width * scale - 4; // 预留4px边距
|
||||
const zoneRenderWidth = swapWH ? zone.height : zone.width;
|
||||
const maxWidth = zoneRenderWidth * scale - 4; // 预留4px边距
|
||||
let currentLine = '';
|
||||
const lines = [];
|
||||
|
||||
@ -2629,8 +2704,9 @@ export default {
|
||||
if (currentLine) lines.push(currentLine);
|
||||
|
||||
// 计算每页最大行数
|
||||
const zoneRenderHeight = swapWH ? zone.width : zone.height;
|
||||
const lineHeight = scaledFontSize * 1.2;
|
||||
const maxLines = Math.max(1, Math.floor((zone.height * scale - 4) / lineHeight));
|
||||
const maxLines = Math.max(1, Math.floor((zoneRenderHeight * scale - 4) / lineHeight));
|
||||
|
||||
// 分页
|
||||
const pages = [];
|
||||
@ -2901,6 +2977,40 @@ export default {
|
||||
}
|
||||
},
|
||||
// ================= 显卡物模型适配方法 END =================
|
||||
// 获取分区显示用的坐标和尺寸,index为分区下标
|
||||
zoneDisplay(zIdx) {
|
||||
const zone = this.addProgramForm.zones[zIdx];
|
||||
if (!zone) return { x: 0, y: 0, width: 0, height: 0 };
|
||||
if (this.screenParams.angle === 90 || this.screenParams.angle === 270) {
|
||||
return {
|
||||
x: zone.y,
|
||||
y: zone.x,
|
||||
width: zone.height,
|
||||
height: zone.width
|
||||
};
|
||||
} else {
|
||||
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;
|
||||
if (this.screenParams.angle === 90 || this.screenParams.angle === 270) {
|
||||
// 对调
|
||||
if (key === 'x') zone.y = value;
|
||||
else if (key === 'y') zone.x = value;
|
||||
else if (key === 'width') zone.height = value;
|
||||
else if (key === 'height') zone.width = value;
|
||||
} else {
|
||||
zone[key] = value;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -3321,4 +3431,17 @@ export default {
|
||||
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;
|
||||
}
|
||||
</style>
|
@ -76,20 +76,21 @@
|
||||
<!-- 子设备标题 -->
|
||||
|
||||
<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
|
||||
<el-tab-pane label="显示屏控制" name="display" style="line-height: 42px; height: 42px;">
|
||||
<el-tab-pane v-if="availableTabs.includes('display')" label="显示屏控制" name="display"
|
||||
style="line-height: 42px; height: 42px;">
|
||||
<div class="tab-content">
|
||||
<display-component :device="device" ref="displayComponent" v-if="activeTab === 'display'">
|
||||
</display-component>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="声卡控制" name="voicecard">
|
||||
<el-tab-pane v-if="availableTabs.includes('voicecard')" label="声卡控制" name="voicecard">
|
||||
<div class="tab-content">
|
||||
<voicecard-component :device="device" ref="voicecardComponent" v-if="activeTab === 'voicecard'">
|
||||
</voicecard-component>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="网关设置" name="gatewaySet">
|
||||
<el-tab-pane v-if="availableTabs.includes('gatewaySet')" label="网关设置" name="gatewaySet">
|
||||
<div class="tab-content">
|
||||
<gatewaySet :device="device" ref="gatewaySet" v-if="activeTab === 'gatewaySet'">
|
||||
</gatewaySet>
|
||||
@ -145,17 +146,17 @@
|
||||
$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>
|
||||
<el-descriptions-item :label="$t('device.device-edit.148398-4')">{{ firmware.productName
|
||||
}}</el-descriptions-item>
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('device.device-edit.148398-12')">Version {{ firmware.version
|
||||
}}</el-descriptions-item>
|
||||
}}</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-item>
|
||||
</el-descriptions>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="success" @click="otaUpgrade"
|
||||
@ -175,6 +176,14 @@ import { getOrderControl } from '@/api/iot/control';
|
||||
import DisplayComponent from './display.vue';
|
||||
import VoicecardComponent from './voicecard.vue';
|
||||
import gatewaySet from './gateway-set.vue';
|
||||
|
||||
// 子产品类型映射
|
||||
const PRODUCT_TYPE_MAP = {
|
||||
102: 'display', // 显卡
|
||||
103: 'voicecard', // 声卡
|
||||
1: 'gatewaySet', // 网关卡
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'gateway',
|
||||
components: {
|
||||
@ -242,6 +251,14 @@ export default {
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 计算当前设备支持的子产品 Tab
|
||||
availableTabs() {
|
||||
// deviceInfo.product 可能为 undefined/null
|
||||
const arr = (this.deviceInfo.product || []).slice(1); // 跳过第一个
|
||||
return arr.map(type => PRODUCT_TYPE_MAP[type]).filter(Boolean);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
device: {
|
||||
handler(newVal) {
|
||||
@ -256,18 +273,60 @@ export default {
|
||||
if (this.deviceInfo.chartList && this.deviceInfo.chartList.length > 0) {
|
||||
this.deviceInfo.chartList = this.deviceInfo.chartList.sort((a, b) => b.order - a.order);
|
||||
}
|
||||
|
||||
// 调用解析函数
|
||||
this.parseProductFromThingsModels();
|
||||
}
|
||||
// activeTab 不在 availableTabs 里时,切换到第一个可用 Tab
|
||||
this.$nextTick(() => {
|
||||
if (!this.availableTabs.includes(this.activeTab) && this.availableTabs.length > 0) {
|
||||
this.activeTab = this.availableTabs[0];
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
// 监听 availableTabs 变化,保证 activeTab 合理
|
||||
availableTabs(newTabs) {
|
||||
if (!newTabs.includes(this.activeTab) && newTabs.length > 0) {
|
||||
this.activeTab = newTabs[0];
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.device && this.device.deviceId) {
|
||||
this.handleDeviceChange(this.device);
|
||||
this.initDataStatus();
|
||||
this.initData();
|
||||
// mounted 时也解析一次
|
||||
this.parseProductFromThingsModels();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 解析 thingsModels 里的 1#productpram,提取 product 数组
|
||||
parseProductFromThingsModels() {
|
||||
const productModel = (this.deviceInfo.thingsModels || []).find(
|
||||
item => item.id === '1#productpram'
|
||||
);
|
||||
if (productModel) {
|
||||
let valueStr = productModel.value || productModel.shadow || '';
|
||||
let match = valueStr.match(/JSON=(.*)/);
|
||||
if (match) {
|
||||
try {
|
||||
let json = JSON.parse(match[1]);
|
||||
if (json.product) {
|
||||
this.deviceInfo.product = json.product;
|
||||
// 打印调试
|
||||
console.log('解析到的 product:', this.deviceInfo.product);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('解析 product JSON 失败:', e, valueStr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('未找到 1#productpram 物模型');
|
||||
}
|
||||
},
|
||||
|
||||
// Tab切换处理
|
||||
handleTabClick(tab) {
|
||||
console.log('切换到:', tab.name);
|
||||
|
@ -12,12 +12,7 @@
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">信号强度:</span>
|
||||
<el-rate
|
||||
v-model="signalStrength"
|
||||
disabled
|
||||
:colors="['#99A9BF', '#F7BA2A', '#FF9900']"
|
||||
:max="3"
|
||||
/>
|
||||
<el-rate v-model="signalStrength" disabled :colors="['#99A9BF', '#F7BA2A', '#FF9900']" :max="3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -27,20 +22,10 @@
|
||||
<h3 class="section-title">
|
||||
<i class="el-icon-s-platform"></i> 产品选择
|
||||
</h3>
|
||||
<el-select
|
||||
v-model="selectedProduct"
|
||||
placeholder="请选择产品"
|
||||
class="enhanced-select"
|
||||
size="medium"
|
||||
filterable
|
||||
@focus="loadProductList"
|
||||
>
|
||||
<el-option
|
||||
v-for="product in productModels"
|
||||
:key="product.id"
|
||||
:label="`${product.name} (${product.id})`"
|
||||
:value="product.id"
|
||||
/>
|
||||
<el-select v-model="selectedProduct" placeholder="请选择产品" class="enhanced-select" size="medium" filterable
|
||||
@focus="loadProductList">
|
||||
<el-option v-for="product in productModels" :key="product.id" :label="`${product.name} (${product.id})`"
|
||||
:value="product.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@ -49,13 +34,8 @@
|
||||
<i class="el-icon-menu"></i> 功能选择
|
||||
</h3>
|
||||
<div class="page-control-grid">
|
||||
<div
|
||||
v-for="(page, index) in pageList"
|
||||
:key="index"
|
||||
class="page-item"
|
||||
:class="{ 'active': selectedPages[index] }"
|
||||
@click="togglePage(index)"
|
||||
>
|
||||
<div v-for="(page, index) in pageList" :key="index" class="page-item"
|
||||
:class="{ 'active': selectedPages[index] }" @click="togglePage(index)">
|
||||
<div class="page-icon">
|
||||
<i :class="page.icon"></i>
|
||||
</div>
|
||||
@ -88,22 +68,11 @@
|
||||
|
||||
<!-- 操作按钮区 -->
|
||||
<div class="action-buttons">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-s-promotion"
|
||||
@click="sendControlCommand"
|
||||
class="action-button"
|
||||
:loading="sending"
|
||||
:disabled="!selectedProduct || !deviceInfo.deviceId"
|
||||
>
|
||||
<el-button type="primary" icon="el-icon-s-promotion" @click="sendControlCommand" class="action-button"
|
||||
:loading="sending" :disabled="!selectedProduct || !deviceInfo.deviceId">
|
||||
发送指令
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
icon="el-icon-refresh"
|
||||
@click="resetSettings"
|
||||
class="action-button"
|
||||
>
|
||||
<el-button type="info" icon="el-icon-refresh" @click="resetSettings" class="action-button">
|
||||
重置选择
|
||||
</el-button>
|
||||
</div>
|
||||
@ -139,14 +108,14 @@ export default {
|
||||
signalStrength: 2,
|
||||
productModels: [],
|
||||
pageList: [
|
||||
{ name: '显示设置', icon: 'el-icon-data-line' },
|
||||
{ name: '声卡设置', icon: 'el-icon-headset' },
|
||||
{ name: '车牌识别', icon: 'el-icon-camera' },
|
||||
{ name: '预警设置', icon: 'el-icon-warning' },
|
||||
{ name: '远程喊话', icon: 'el-icon-microphone' },
|
||||
{ name: '网关设置', icon: 'el-icon-connection' },
|
||||
{ name: '路锥控制', icon: 'el-icon-place' },
|
||||
{ name: '翻转屏', icon: 'el-icon-monitor' }
|
||||
{ name: '显示单元', icon: 'el-icon-data-line', code: 102 },
|
||||
{ name: '声卡设置', icon: 'el-icon-headset', code: 103 },
|
||||
// { name: '车牌识别', icon: 'el-icon-camera' },
|
||||
// { name: '预警设置', icon: 'el-icon-warning' },
|
||||
// { name: '远程喊话', icon: 'el-icon-microphone' },
|
||||
{ name: '网关设置', icon: 'el-icon-connection', code: 1 },
|
||||
{ name: '路锥控制', icon: 'el-icon-place', code: 104 },
|
||||
{ name: '激光单元', icon: 'el-icon-monitor', code: 105 }
|
||||
],
|
||||
loadingProducts: false,
|
||||
deviceInfo: {
|
||||
@ -188,6 +157,15 @@ export default {
|
||||
if (!this.selectedProduct) return '未选择';
|
||||
const product = this.productModels.find(p => p.id === this.selectedProduct);
|
||||
return product ? `${product.name} (${product.id})` : this.selectedProduct;
|
||||
},
|
||||
productArray() {
|
||||
const arr = [Number(this.selectedProduct)];
|
||||
this.selectedPages.forEach((selected, idx) => {
|
||||
if (selected && this.pageList[idx] && this.pageList[idx].code) {
|
||||
arr.push(this.pageList[idx].code);
|
||||
}
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -259,6 +237,7 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async sendControlCommand() {
|
||||
if (!this.selectedProduct) {
|
||||
this.$message.warning('请选择产品');
|
||||
@ -270,75 +249,92 @@ export default {
|
||||
}
|
||||
|
||||
this.sending = true;
|
||||
|
||||
const payload = { product: this.productArray };
|
||||
try {
|
||||
const distributeModel = this.deviceInfo.thingsModels.find(
|
||||
model => model.id === 'productpram'
|
||||
);
|
||||
// 发送102#model指令,值为1
|
||||
const model = this.deviceInfo.thingsModels.find(m => m.id === '1#productpram');
|
||||
if (model) {
|
||||
model.shadow = 'JSON=' + JSON.stringify(payload);
|
||||
await this.mqttPublish(this.deviceInfo, model);
|
||||
this.$message.success('已发送刷新指令,编译信息将自动更新');
|
||||
this.sending = false;
|
||||
|
||||
} else {
|
||||
this.$message.warning('未找到102#model物模型');
|
||||
this.sending = false;
|
||||
|
||||
if (!distributeModel) {
|
||||
throw new Error('未找到分发控制模型');
|
||||
}
|
||||
|
||||
// 确保shadow值是最新的
|
||||
distributeModel.shadow = this.fullCommand;
|
||||
|
||||
await this.mqttPublish(this.deviceInfo, distributeModel);
|
||||
|
||||
this.$message.success('控制指令发送成功');
|
||||
} catch (error) {
|
||||
console.error('发送控制指令失败:', error);
|
||||
this.$message.error(`控制指令发送失败: ${error.message}`);
|
||||
} finally {
|
||||
console.error('发送刷新指令失败:', error);
|
||||
this.sending = false;
|
||||
|
||||
this.$message.error('发送刷新指令失败');
|
||||
}
|
||||
},
|
||||
|
||||
//发送指令
|
||||
async mqttPublish(device, model) {
|
||||
try {
|
||||
if (!device || !device.deviceId) {
|
||||
throw new Error('无效的设备信息');
|
||||
}
|
||||
if (!model || !model.id) {
|
||||
throw new Error('无效的模型信息');
|
||||
}
|
||||
|
||||
const command = {};
|
||||
command[model.id] = model.shadow;
|
||||
|
||||
const params = {
|
||||
deviceId: device.deviceId,
|
||||
modelId: model.modelId,
|
||||
};
|
||||
|
||||
const response = await getOrderControl(params);
|
||||
if (response.code != 200) {
|
||||
throw new Error(response.msg || '获取控制指令失败');
|
||||
}
|
||||
|
||||
const data = {
|
||||
serialNumber: device.serialNumber,
|
||||
productId: device.productId,
|
||||
remoteCommand: command,
|
||||
identifier: model.id,
|
||||
modelName: model.name,
|
||||
isShadow: device.status != 3,
|
||||
type: model.type,
|
||||
};
|
||||
|
||||
if (device.status !== 3 && device.isShadow !== 1) {
|
||||
throw new Error('设备不在线且未启用影子模式');
|
||||
}
|
||||
|
||||
if ((device.protocolCode === 'MODBUS-TCP' || device.protocolCode === 'MODBUS-RTU') && device.status === 3) {
|
||||
await serviceInvokeReply(data);
|
||||
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 {
|
||||
await serviceInvoke(data);
|
||||
title = this.$t('device.device-variable.930930-2');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('MQTT发布失败:', error);
|
||||
throw error;
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user