显卡逻辑完善

This commit is contained in:
1 2025-07-23 14:35:40 +08:00
parent ec7e9ff6b9
commit c470b9074e
4 changed files with 421 additions and 243 deletions

BIN
dist.zip

Binary file not shown.

View File

@ -76,10 +76,10 @@
:disabled="!basicSettings.screenEnabled"> :disabled="!basicSettings.screenEnabled">
</el-slider> </el-slider>
</el-form-item> </el-form-item>
<el-form-item> <!-- <el-form-item>
<el-button type="danger" @click="handleClearScreen" <el-button type="danger" @click="handleClearScreen"
:disabled="!basicSettings.screenEnabled">清除屏幕</el-button> :disabled="!basicSettings.screenEnabled">清除屏幕</el-button>
</el-form-item> </el-form-item> -->
</el-form> </el-form>
</el-card> </el-card>
@ -110,7 +110,7 @@
<el-input-number v-model="screenParams.height" :min="1" :max="10000"></el-input-number> <el-input-number v-model="screenParams.height" :min="1" :max="10000"></el-input-number>
</el-form-item> </el-form-item>
<el-form-item label="屏幕角度"> <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="0">0°</el-radio>
<el-radio :label="90">90°</el-radio> <el-radio :label="90">90°</el-radio>
<el-radio :label="180">180°</el-radio> <el-radio :label="180">180°</el-radio>
@ -356,11 +356,10 @@
<!-- 添加/编辑自定义节目对话框 --> <!-- 添加/编辑自定义节目对话框 -->
<el-dialog :title="isEditProgram ? '编辑自定义节目' : '添加自定义节目'" :visible.sync="addProgramDialogVisible" width="700px" <el-dialog :title="isEditProgram ? '编辑自定义节目' : '添加自定义节目'" :visible.sync="addProgramDialogVisible" width="700px"
append-to-body> append-to-body>
<!-- <div <div class="sticky-canvas-preview">
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"
<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>
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> -->
<el-form :model="addProgramForm" ref="addProgramFormRef" label-width="60px" size="mini" <el-form :model="addProgramForm" ref="addProgramFormRef" label-width="60px" size="mini"
style="margin-bottom:0;"> style="margin-bottom:0;">
<el-form-item label="节目模式" label-width="70px"> <el-form-item label="节目模式" label-width="70px">
@ -442,6 +441,14 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </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-col :span="5">
<el-form-item label="字号" label-width="42px"> <el-form-item label="字号" label-width="42px">
<el-select v-model="zone.fontSize" placeholder="字号" style="width:100%"> <el-select v-model="zone.fontSize" placeholder="字号" style="width:100%">
@ -522,22 +529,26 @@
<el-row :gutter="8"> <el-row :gutter="8">
<el-col :span="5"> <el-col :span="5">
<el-form-item label="X" label-width="20px"> <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-form-item>
</el-col> </el-col>
<el-col :span="5"> <el-col :span="5">
<el-form-item label="宽" label-width="24px"> <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-form-item>
</el-col> </el-col>
<el-col :span="5"> <el-col :span="5">
<el-form-item label="Y" label-width="20px"> <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-form-item>
</el-col> </el-col>
<el-col :span="5"> <el-col :span="5">
<el-form-item label="高" label-width="24px"> <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-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -726,14 +737,15 @@ export default {
commonPhrase: 0, commonPhrase: 0,
displayText: '', displayText: '',
font: 0, font: 0,
fontEn: 0, //
fontShape: 0, fontShape: 0,
fontSize: 0, fontSize: 0,
fontColor: 0, fontColor: 0, //
fontBold: 0, fontBold: 0,
fontStretch: 0, fontStretch: 0,
image: '', image: '',
imageSize: 1, // 32x32 imageSize: 1, // 32x32
imageColor: 0, // Red imageColor: 0, //
effect: 0, effect: 0,
speed: 4, speed: 4,
stayTime: 5, stayTime: 5,
@ -767,7 +779,7 @@ export default {
addProgramFonts: ['宋体(中)', '黑体(中)', '楷体(中)'], addProgramFonts: ['宋体(中)', '黑体(中)', '楷体(中)'],
addProgramFontShapes: ['圆角(英)', '直角(英)'], addProgramFontShapes: ['圆角(英)', '直角(英)'],
addProgramFontSizes: ['16px', '24px', '32px'], addProgramFontSizes: ['16px', '24px', '32px'],
addProgramFontColors: ['黑色', '红色', '绿色', '黄色'], addProgramFontColors: ['红色', '绿色', '黄色'],
addProgramFontBold: ['不加粗', '加粗'], addProgramFontBold: ['不加粗', '加粗'],
addProgramFontStretch: ['不拉伸', '横向拉伸', '纵向拉伸'], addProgramFontStretch: ['不拉伸', '横向拉伸', '纵向拉伸'],
addProgramEffects: ['立即显示', '左移', '右移', '上移', '下移', '连续左移', '闪烁换页'], addProgramEffects: ['立即显示', '左移', '右移', '上移', '下移', '连续左移', '闪烁换页'],
@ -776,7 +788,10 @@ export default {
addProgramHorizontalAlign: ['居左对齐', '居中对齐', '居右对齐'], addProgramHorizontalAlign: ['居左对齐', '居中对齐', '居右对齐'],
addProgramVerticalAlign: ['顶部对齐', '居中对齐', '底部对齐'], addProgramVerticalAlign: ['顶部对齐', '居中对齐', '底部对齐'],
addProgramImageSizes: ['16x16', '32x32', '48x48', '64x64'], addProgramImageSizes: ['16x16', '32x32', '48x48', '64x64'],
addProgramImageColors: ['黑色', '红色', '绿色', '黄色'], addProgramImageColors: ['红色', '绿色', '黄色'],
addProgramFontEns: [
'打字(英)', '长黑(英)', '白斜(英)', '线形(英)', '方斜(英)', '歌德(英)', '黑正(英)', '美术(英)', '手写(英)', '正圆(英)', '时钟(英)圆角(英)'
],
// //
addProgramPreviewPage: [0], // addProgramPreviewPage: [0], //
addProgramPreviewTimer: [], // addProgramPreviewTimer: [], //
@ -961,13 +976,8 @@ export default {
const progListModel = this.deviceInfo.thingsModels.find(model => model.id === '102#progList'); const progListModel = this.deviceInfo.thingsModels.find(model => model.id === '102#progList');
if (progListModel) { if (progListModel) {
try { try {
const jsonStr = progListModel.shadow.replace('JSON=', ''); //
const data = JSON.parse(jsonStr); progListModel.shadow = 'JSON=' + JSON.stringify({ del_prog: program.order });
//
data.del_prog = program.order;
progListModel.shadow = 'JSON=' + JSON.stringify(data);
this.mqttPublish(this.deviceInfo, progListModel).then(() => { this.mqttPublish(this.deviceInfo, progListModel).then(() => {
this.$message.success('删除指令已发送'); this.$message.success('删除指令已发送');
}).catch(error => { }).catch(error => {
@ -1014,28 +1024,37 @@ export default {
if (programData.aLst && programData.aLst.length > 0) { if (programData.aLst && programData.aLst.length > 0) {
console.log('解析分区数据:', JSON.stringify(programData.aLst)); console.log('解析分区数据:', JSON.stringify(programData.aLst));
this.addProgramForm.zones = programData.aLst.map(area => { 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 = { const zone = {
playType: 0, playType: 0,
commonPhrase: 0, commonPhrase: 0,
displayText: '', displayText: '',
font: 0, font: 0,
fontEn: 0, //
fontShape: 0, fontShape: 0,
fontSize: 0, fontSize: 0,
fontColor: 0, fontColor: 0, //
fontBold: 0, fontBold: 0,
fontStretch: 0, fontStretch: 0,
image: '', image: '',
imageSize: 1, imageSize: 1,
imageColor: 0, imageColor: 0, //
effect: 0, effect: 0,
speed: 4, speed: 4,
stayTime: 5, stayTime: 5,
hAlign: 1, hAlign: 1,
vAlign: 1, vAlign: 1,
x: area.size ? area.size.x : 0, x: x,
y: area.size ? area.size.y : 0, y: y,
width: area.size ? area.size.l : 32, width: w,
height: area.size ? area.size.h : 64 height: h
}; };
// //
@ -1046,12 +1065,13 @@ export default {
zone.playType = 0; zone.playType = 0;
zone.displayText = item.txt.str ? item.txt.str.replace(/\0/g, '') : ''; zone.displayText = item.txt.str ? item.txt.str.replace(/\0/g, '') : '';
zone.font = item.txt.fCn ? item.txt.fCn - 1 : 0; // 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.fontSize = item.txt.fS ? this.getFontSizeIndex(item.txt.fS) : 0; //
zone.fontColor = item.txt.col || 0; // zone.fontColor = item.txt.col || 0; //
zone.fontBold = item.txt.fW || 0; // zone.fontBold = item.txt.fW || 0; //
zone.fontStretch = item.txt.stch || 0; // zone.fontStretch = item.txt.stch || 0; //
zone.hAlign = item.txt.hPos || 1; // zone.hAlign = typeof item.txt.hPos === 'number' ? Math.max(item.txt.hPos - 1, 0) : 1; //
zone.vAlign = item.txt.vPos || 1; // zone.vAlign = typeof item.txt.vPos === 'number' ? Math.max(item.txt.vPos - 1, 0) : 1; //
} else if (item.typ === 1 && item.img) { } else if (item.typ === 1 && item.img) {
// //
zone.playType = 1; zone.playType = 1;
@ -1077,14 +1097,15 @@ export default {
commonPhrase: 0, commonPhrase: 0,
displayText: '', displayText: '',
font: 0, font: 0,
fontEn: 0, //
fontShape: 0, fontShape: 0,
fontSize: 0, fontSize: 0,
fontColor: 0, fontColor: 0, //
fontBold: 0, fontBold: 0,
fontStretch: 0, fontStretch: 0,
image: '', image: '',
imageSize: 1, imageSize: 1,
imageColor: 0, imageColor: 0, //
effect: 0, effect: 0,
speed: 4, speed: 4,
stayTime: 5, stayTime: 5,
@ -1099,6 +1120,7 @@ export default {
// //
this.$nextTick(() => { this.$nextTick(() => {
this.onAddProgramModeChange(this.addProgramForm.mode); //
this.drawAddProgramPreview(); this.drawAddProgramPreview();
}); });
}, },
@ -2087,6 +2109,7 @@ export default {
this.$set(this.screenParams, 'width', params.w); this.$set(this.screenParams, 'width', params.w);
this.$set(this.screenParams, 'height', params.h); this.$set(this.screenParams, 'height', params.h);
this.$set(this.screenParams, 'angle', params.angle); this.$set(this.screenParams, 'angle', params.angle);
this.drawAddProgramPreview(); //
} }
}, },
handleOePolarityChange(val) { handleOePolarityChange(val) {
@ -2187,14 +2210,15 @@ export default {
commonPhrase: 0, commonPhrase: 0,
displayText: '', displayText: '',
font: 0, font: 0,
fontEn: 0, //
fontShape: 0, fontShape: 0,
fontSize: 0, fontSize: 0,
fontColor: 0, fontColor: 0, //
fontBold: 0, fontBold: 0,
fontStretch: 0, fontStretch: 0,
image: '', image: '',
imageSize: 1, imageSize: 1,
imageColor: 0, imageColor: 0, //
effect: 0, effect: 0,
speed: 4, speed: 4,
stayTime: 5, stayTime: 5,
@ -2222,14 +2246,15 @@ export default {
commonPhrase: 0, commonPhrase: 0,
displayText: '', displayText: '',
font: 0, font: 0,
fontEn: 0, //
fontShape: 0, fontShape: 0,
fontSize: 0, fontSize: 0,
fontColor: 0, fontColor: 0, //
fontBold: 0, fontBold: 0,
fontStretch: 0, fontStretch: 0,
image: '', image: '',
imageSize: 1, imageSize: 1,
imageColor: 0, imageColor: 0, //
effect: 0, effect: 0,
speed: 4, speed: 4,
stayTime: 5, stayTime: 5,
@ -2282,77 +2307,97 @@ export default {
const progListModel = this.deviceInfo.thingsModels.find(model => model.id === '102#progList'); const progListModel = this.deviceInfo.thingsModels.find(model => model.id === '102#progList');
if (progListModel) { if (progListModel) {
try { try {
// // order
const programData = { let order;
del_prog: 0, // if (this.isEditProgram && this.editingProgram) {
prog_list: [{ order = this.editingProgram.order;
order: this.isEditProgram ? this.editingProgram.order : 0, // 使 } else {
rem: (this.addProgramForm.remark || `自定义节目${Date.now()}`) + '\0', // \0 // order
dur: this.addProgramForm.duration, // () const usedOrders = this.programList.map(p => p.order);
areaM: this.addProgramForm.mode, // order = 0;
aLst: this.addProgramForm.zones.map(zone => { while (usedOrders.includes(order) && order < 10) {
// order++;
const area = { }
size: { }
x: zone.x, // X
y: zone.y, // Y //
l: zone.width, // const programObj = {
h: zone.height // 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);
}
// return area;
if (zone.playType === 0) { })
// };
const textItem = {
typ: 0, //
anim: { const programData = {
typ: zone.effect + 1, // del_prog: 0, //
spd: zone.speed, // prog_list: [programObj]
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;
})
}]
}; };
//
progListModel.shadow = 'JSON=' + JSON.stringify(programData); progListModel.shadow = 'JSON=' + JSON.stringify(programData);
this.mqttPublish(this.deviceInfo, progListModel).then(() => { this.mqttPublish(this.deviceInfo, progListModel).then(() => {
this.addProgramDialogVisible = false; this.addProgramDialogVisible = false;
@ -2376,8 +2421,14 @@ export default {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
// //
const screenW = this.screenParams.width || 32; let screenW = this.screenParams.width || 32;
const screenH = this.screenParams.height || 64; let screenH = this.screenParams.height || 64;
// 90270
const angle = this.screenParams.angle;
let swapWH = angle === 90 || angle === 270;
if (swapWH) {
[screenW, screenH] = [screenH, screenW];
}
// //
const renderWidth = 640; // const renderWidth = 640; //
@ -2414,11 +2465,19 @@ export default {
const asset = this.addProgramPreviewAssets[zIdx]; const asset = this.addProgramPreviewAssets[zIdx];
if (!asset) return; if (!asset) return;
// // 90270x/y/width/height
const zoneX = offsetX + zone.x * scale; let zoneX, zoneY, zoneWidth, zoneHeight;
const zoneY = offsetY + zone.y * scale; if (swapWH) {
const zoneWidth = zone.width * scale; zoneX = offsetX + (zone.y || 0) * scale;
const zoneHeight = zone.height * 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.save();
@ -2443,10 +2502,19 @@ export default {
const pageLines = asset.pages[page] || []; const pageLines = asset.pages[page] || [];
// //
const colors = ['#000000', '#FF0000', '#00FF00', '#FFFF00']; const colors = ['#FF0000', '#00FF00', '#FFFF00'];
ctx.fillStyle = colors[zone.fontColor] || '#000000'; ctx.fillStyle = colors[zone.fontColor] || '#000000';
ctx.textBaseline = 'top'; 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; const lineHeight = asset.scaledFontSize * 1.2;
@ -2570,8 +2638,14 @@ export default {
if (!canvas) return; if (!canvas) return;
// //
const screenW = this.screenParams.width || 32; let screenW = this.screenParams.width || 32;
const screenH = this.screenParams.height || 64; let screenH = this.screenParams.height || 64;
// 90270
const angle = this.screenParams.angle;
const swapWH = angle === 90 || angle === 270;
if (swapWH) {
[screenW, screenH] = [screenH, screenW];
}
const renderWidth = 640; // const renderWidth = 640; //
const scale = Math.min(renderWidth / screenW, 240 / screenH); const scale = Math.min(renderWidth / screenW, 240 / screenH);
@ -2610,7 +2684,8 @@ export default {
measureCtx.font = `${fontWeight} ${scaledFontSize}px ${fontFamily}`; 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 = ''; let currentLine = '';
const lines = []; const lines = [];
@ -2629,8 +2704,9 @@ export default {
if (currentLine) lines.push(currentLine); if (currentLine) lines.push(currentLine);
// //
const zoneRenderHeight = swapWH ? zone.width : zone.height;
const lineHeight = scaledFontSize * 1.2; 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 = []; const pages = [];
@ -2901,6 +2977,40 @@ export default {
} }
}, },
// ================= END ================= // ================= 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> </script>
@ -3321,4 +3431,17 @@ export default {
background: #a8a8a8; 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> </style>

View File

@ -76,20 +76,21 @@
<!-- 子设备标题 --> <!-- 子设备标题 -->
<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick"> <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"> <div class="tab-content">
<display-component :device="device" ref="displayComponent" v-if="activeTab === 'display'"> <display-component :device="device" ref="displayComponent" v-if="activeTab === 'display'">
</display-component> </display-component>
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="声卡控制" name="voicecard"> <el-tab-pane v-if="availableTabs.includes('voicecard')" label="声卡控制" name="voicecard">
<div class="tab-content"> <div class="tab-content">
<voicecard-component :device="device" ref="voicecardComponent" v-if="activeTab === 'voicecard'"> <voicecard-component :device="device" ref="voicecardComponent" v-if="activeTab === 'voicecard'">
</voicecard-component> </voicecard-component>
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="网关设置" name="gatewaySet"> <el-tab-pane v-if="availableTabs.includes('gatewaySet')" label="网关设置" name="gatewaySet">
<div class="tab-content"> <div class="tab-content">
<gatewaySet :device="device" ref="gatewaySet" v-if="activeTab === 'gatewaySet'"> <gatewaySet :device="device" ref="gatewaySet" v-if="activeTab === 'gatewaySet'">
</gatewaySet> </gatewaySet>
@ -145,17 +146,17 @@
$t('device.running-status.866086-12') }}</el-link> $t('device.running-status.866086-12') }}</el-link>
</template> </template>
<el-descriptions-item :label="$t('device.running-status.866086-13')">{{ firmware.firmwareName <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 :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 :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-descriptions-item :label="$t('device.running-status.866086-16')">
<el-link :href="getDownloadUrl(firmware.filePath)" :underline="false" type="primary">{{ <el-link :href="getDownloadUrl(firmware.filePath)" :underline="false" type="primary">{{
getDownloadUrl(firmware.filePath) }}</el-link> getDownloadUrl(firmware.filePath) }}</el-link>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :label="$t('device.running-status.866086-17')">{{ firmware.remark <el-descriptions-item :label="$t('device.running-status.866086-17')">{{ firmware.remark
}}</el-descriptions-item> }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button type="success" @click="otaUpgrade" <el-button type="success" @click="otaUpgrade"
@ -175,6 +176,14 @@ import { getOrderControl } from '@/api/iot/control';
import DisplayComponent from './display.vue'; import DisplayComponent from './display.vue';
import VoicecardComponent from './voicecard.vue'; import VoicecardComponent from './voicecard.vue';
import gatewaySet from './gateway-set.vue'; import gatewaySet from './gateway-set.vue';
//
const PRODUCT_TYPE_MAP = {
102: 'display', //
103: 'voicecard', //
1: 'gatewaySet', //
};
export default { export default {
name: 'gateway', name: 'gateway',
components: { 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: { watch: {
device: { device: {
handler(newVal) { handler(newVal) {
@ -256,18 +273,60 @@ export default {
if (this.deviceInfo.chartList && this.deviceInfo.chartList.length > 0) { if (this.deviceInfo.chartList && this.deviceInfo.chartList.length > 0) {
this.deviceInfo.chartList = this.deviceInfo.chartList.sort((a, b) => b.order - a.order); 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() { mounted() {
if (this.device && this.device.deviceId) { if (this.device && this.device.deviceId) {
this.handleDeviceChange(this.device); this.handleDeviceChange(this.device);
this.initDataStatus(); this.initDataStatus();
this.initData(); this.initData();
// mounted
this.parseProductFromThingsModels();
} }
}, },
methods: { 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 // Tab
handleTabClick(tab) { handleTabClick(tab) {
console.log('切换到:', tab.name); console.log('切换到:', tab.name);

View File

@ -12,12 +12,7 @@
</div> </div>
<div class="status-item"> <div class="status-item">
<span class="status-label">信号强度</span> <span class="status-label">信号强度</span>
<el-rate <el-rate v-model="signalStrength" disabled :colors="['#99A9BF', '#F7BA2A', '#FF9900']" :max="3" />
v-model="signalStrength"
disabled
:colors="['#99A9BF', '#F7BA2A', '#FF9900']"
:max="3"
/>
</div> </div>
</div> </div>
@ -27,20 +22,10 @@
<h3 class="section-title"> <h3 class="section-title">
<i class="el-icon-s-platform"></i> 产品选择 <i class="el-icon-s-platform"></i> 产品选择
</h3> </h3>
<el-select <el-select v-model="selectedProduct" placeholder="请选择产品" class="enhanced-select" size="medium" filterable
v-model="selectedProduct" @focus="loadProductList">
placeholder="请选择产品" <el-option v-for="product in productModels" :key="product.id" :label="`${product.name} (${product.id})`"
class="enhanced-select" :value="product.id" />
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> </el-select>
</div> </div>
@ -49,13 +34,8 @@
<i class="el-icon-menu"></i> 功能选择 <i class="el-icon-menu"></i> 功能选择
</h3> </h3>
<div class="page-control-grid"> <div class="page-control-grid">
<div <div v-for="(page, index) in pageList" :key="index" class="page-item"
v-for="(page, index) in pageList" :class="{ 'active': selectedPages[index] }" @click="togglePage(index)">
:key="index"
class="page-item"
:class="{ 'active': selectedPages[index] }"
@click="togglePage(index)"
>
<div class="page-icon"> <div class="page-icon">
<i :class="page.icon"></i> <i :class="page.icon"></i>
</div> </div>
@ -88,22 +68,11 @@
<!-- 操作按钮区 --> <!-- 操作按钮区 -->
<div class="action-buttons"> <div class="action-buttons">
<el-button <el-button type="primary" icon="el-icon-s-promotion" @click="sendControlCommand" class="action-button"
type="primary" :loading="sending" :disabled="!selectedProduct || !deviceInfo.deviceId">
icon="el-icon-s-promotion"
@click="sendControlCommand"
class="action-button"
:loading="sending"
:disabled="!selectedProduct || !deviceInfo.deviceId"
>
发送指令 发送指令
</el-button> </el-button>
<el-button <el-button type="info" icon="el-icon-refresh" @click="resetSettings" class="action-button">
type="info"
icon="el-icon-refresh"
@click="resetSettings"
class="action-button"
>
重置选择 重置选择
</el-button> </el-button>
</div> </div>
@ -139,14 +108,14 @@ export default {
signalStrength: 2, signalStrength: 2,
productModels: [], productModels: [],
pageList: [ pageList: [
{ name: '显示设置', icon: 'el-icon-data-line' }, { name: '显示单元', icon: 'el-icon-data-line', code: 102 },
{ name: '声卡设置', icon: 'el-icon-headset' }, { name: '声卡设置', icon: 'el-icon-headset', code: 103 },
{ name: '车牌识别', icon: 'el-icon-camera' }, // { name: '', icon: 'el-icon-camera' },
{ name: '预警设置', icon: 'el-icon-warning' }, // { name: '', icon: 'el-icon-warning' },
{ name: '远程喊话', icon: 'el-icon-microphone' }, // { name: '', icon: 'el-icon-microphone' },
{ name: '网关设置', icon: 'el-icon-connection' }, { name: '网关设置', icon: 'el-icon-connection', code: 1 },
{ name: '路锥控制', icon: 'el-icon-place' }, { name: '路锥控制', icon: 'el-icon-place', code: 104 },
{ name: '翻转屏', icon: 'el-icon-monitor' } { name: '激光单元', icon: 'el-icon-monitor', code: 105 }
], ],
loadingProducts: false, loadingProducts: false,
deviceInfo: { deviceInfo: {
@ -188,6 +157,15 @@ export default {
if (!this.selectedProduct) return '未选择'; if (!this.selectedProduct) return '未选择';
const product = this.productModels.find(p => p.id === this.selectedProduct); const product = this.productModels.find(p => p.id === this.selectedProduct);
return product ? `${product.name} (${product.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: { watch: {
@ -205,7 +183,7 @@ export default {
isShadow: newVal.isShadow, isShadow: newVal.isShadow,
protocolCode: newVal.protocolCode protocolCode: newVal.protocolCode
}; };
// distributeshadow // distributeshadow
const distributeModel = this.deviceInfo.thingsModels.find( const distributeModel = this.deviceInfo.thingsModels.find(
model => model.id === 'distribute' model => model.id === 'distribute'
@ -225,7 +203,7 @@ export default {
methods: { methods: {
async loadProductList() { async loadProductList() {
if (this.productModels.length > 0 || this.loadingProducts) return; if (this.productModels.length > 0 || this.loadingProducts) return;
this.loadingProducts = true; this.loadingProducts = true;
try { try {
const params = { const params = {
@ -243,13 +221,13 @@ export default {
this.loadingProducts = false; this.loadingProducts = false;
} }
}, },
togglePage(index) { togglePage(index) {
this.$set(this.selectedPages, index, this.selectedPages[index] ? 0 : 1); this.$set(this.selectedPages, index, this.selectedPages[index] ? 0 : 1);
// distributeshadow // distributeshadow
this.updateDistributeModel(); this.updateDistributeModel();
}, },
updateDistributeModel() { updateDistributeModel() {
const distributeModel = this.deviceInfo.thingsModels.find( const distributeModel = this.deviceInfo.thingsModels.find(
model => model.id === 'distribute' model => model.id === 'distribute'
@ -258,7 +236,8 @@ export default {
distributeModel.shadow = this.fullCommand; distributeModel.shadow = this.fullCommand;
} }
}, },
async sendControlCommand() { async sendControlCommand() {
if (!this.selectedProduct) { if (!this.selectedProduct) {
this.$message.warning('请选择产品'); this.$message.warning('请选择产品');
@ -268,80 +247,97 @@ export default {
this.$message.warning('设备信息未初始化'); this.$message.warning('设备信息未初始化');
return; return;
} }
this.sending = true; this.sending = true;
const payload = { product: this.productArray };
try { try {
const distributeModel = this.deviceInfo.thingsModels.find( // 102#model1
model => model.id === 'productpram' const model = this.deviceInfo.thingsModels.find(m => m.id === '1#productpram');
); if (model) {
model.shadow = 'JSON=' + JSON.stringify(payload);
if (!distributeModel) { await this.mqttPublish(this.deviceInfo, model);
throw new Error('未找到分发控制模型'); this.$message.success('已发送刷新指令,编译信息将自动更新');
} this.sending = false;
// 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 {
this.sending = false;
}
},
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);
} else { } else {
await serviceInvoke(data); this.$message.warning('未找到102#model物模型');
this.sending = false;
} }
} catch (error) { } catch (error) {
console.error('MQTT发布失败:', error); console.error('发送刷新指令失败:', error);
throw error; this.sending = false;
this.$message.error('发送刷新指令失败');
} }
}, },
//
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);
}
});
}
},
resetSettings() { resetSettings() {
this.selectedPages = [0, 0, 0, 0, 0, 0, 0, 0]; this.selectedPages = [0, 0, 0, 0, 0, 0, 0, 0];
this.updateDistributeModel(); this.updateDistributeModel();