Xazn-app/pagesA/home/device/status/addProgram.vue
2025-07-03 08:57:13 +08:00

1140 lines
34 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>
<view class="add-program-container ">
<!-- Hidden canvas for text measurement -->
<canvas canvas-id="__temp_prepare_canvas"
style="width: 0; height: 0; position: absolute; left: -9999px; top: -9999px;"></canvas>
<!-- 画面预览悬浮顶部 -->
<view class="section preview-section">
<view class="section-title blue">画面预览</view>
<canvas class="preview-canvas" canvas-id="previewCanvas"></canvas>
</view>
<!-- 节目参数 -->
<view class="zone-card">
<view class="zone-title">节目参数</view>
<view class="form-row">
<view class="mini-label">模式</view>
<view class="picker" style="margin-left: 90px;" @click="showModePicker = true">{{ modes[form.mode] }}
</view>
<u-picker :show="showModePicker" :columns="[modes]" @confirm="onModeConfirm"
@cancel="showModePicker = false"></u-picker>
</view>
<view class="form-row">
<view class="mini-label">时长</view>
<u-input v-model="form.duration" type="number" placeholder="请输入节目时长"
style="max-width:110px; margin-left: 60px;" />
<view style="line-height: 36px; margin-right: 14px; color: #666;">秒</view>
</view>
</view>
<!-- 分区1整体卡片 -->
<view class="zone-card" v-for="(zone, index) in form.zones" :key="index">
<view class="zone-title">分区{{ index + 1 }}</view>
<!-- 项目选择卡片 -->
<view class="group-card">
<view class="group-title">项目选择</view>
<view class="form-row ">
<view class="form-label">播放类型</view>
<view class="picker" @click="openPicker('showPlayTypePicker', index)">{{ playTypes[zone.playType] }}
</view>
<u-picker :show="showPlayTypePicker" :columns="[playTypes]" @confirm="onPlayTypeConfirm"
@cancel="showPlayTypePicker = false"></u-picker>
</view>
</view>
<!-- 文字参数卡片仅文字类型显示 -->
<view class="group-card" v-if="zone.playType == 0">
<view class="group-title">文字参数</view>
<view class="form-row">
<view class="form-label">常用语句</view>
<view class="picker" @click="openPicker('showPhrasePicker', index)">
{{ commonPhrases[zone.commonPhrase] || '常用语句' }}
</view>
<u-picker :show="showPhrasePicker" :columns="[commonPhrases]" @confirm="onPhraseConfirm"
@cancel="showPhrasePicker = false"></u-picker>
</view>
<view class="form-row">
<u-input v-model="zone.displayText" type="textarea" placeholder="请输入展示信息" class="input-area" />
</view>
<view class="form-row">
<view class="mini-label">字体</view>
<view class="picker" @click="openPicker('showFontPicker', index)">{{ fonts[zone.font] }}</view>
<u-picker :show="showFontPicker" :columns="[fonts]" @confirm="onFontConfirm"
@cancel="showFontPicker = false"></u-picker>
</view>
<view class="form-row">
<view class="mini-label">圆角</view>
<view class="picker" @click="openPicker('showFontShapePicker', index)">
{{ fontShapes[zone.fontShape] }}
</view>
<u-picker :show="showFontShapePicker" :columns="[fontShapes]" @confirm="onFontShapeConfirm"
@cancel="showFontShapePicker = false"></u-picker>
</view>
<view class="form-row">
<view class="mini-label">字号</view>
<view class="picker" @click="openPicker('showFontSizePicker', index)">{{ fontSizes[zone.fontSize] }}
</view>
<u-picker :show="showFontSizePicker" :columns="[fontSizes]" @confirm="onFontSizeConfirm"
@cancel="showFontSizePicker = false"></u-picker>
</view>
<view class="form-row">
<view class="mini-label">颜色</view>
<view class="picker" @click="openPicker('showFontColorPicker', index)">
{{ fontColors[zone.fontColor] }}
</view>
<u-picker :show="showFontColorPicker" :columns="[fontColors]" @confirm="onFontColorConfirm"
@cancel="showFontColorPicker = false"></u-picker>
</view>
<view class="form-row">
<view class="mini-label">加粗</view>
<view class="picker" @click="openPicker('showFontBoldPicker', index)">{{ fontBold[zone.fontBold] }}
</view>
<u-picker :show="showFontBoldPicker" :columns="[fontBold]" @confirm="onFontBoldConfirm"
@cancel="showFontBoldPicker = false"></u-picker>
</view>
<view class="form-row">
<view class="mini-label">拉伸</view>
<view class="picker" @click="openPicker('showFontStretchPicker', index)">
{{ fontStretch[zone.fontStretch] }}
</view>
<u-picker :show="showFontStretchPicker" :columns="[fontStretch]" @confirm="onFontStretchConfirm"
@cancel="showFontStretchPicker = false"></u-picker>
</view>
</view>
<!-- 图片参数卡片仅图片类型显示 -->
<view class="group-card" v-else>
<view class="group-title">图片参数</view>
<view class="form-row">
<u-input v-model="zone.image" placeholder="请输入图片路径或选择图片" />
</view>
</view>
<!-- 显示参数卡片 -->
<view class="group-card">
<view class="group-title">显示参数</view>
<view class="form-row">
<view class="form-label">动画特效</view>
<view class="picker" @click="openPicker('showEffectPicker', index)">
{{ effects[zone.effect] || '动画特效' }}
</view>
<u-picker :show="showEffectPicker" :columns="[effects]" @confirm="onEffectConfirm"
@cancel="showEffectPicker = false"></u-picker>
</view>
<view class="form-row">
<view class="form-label">速度</view>
<view class="picker" @click="openPicker('showSpeedPicker', index)">{{ speeds[zone.speed] || '速度' }}
</view>
<u-picker :show="showSpeedPicker" :columns="[speeds]" @confirm="onSpeedConfirm"
@cancel="showSpeedPicker = false"></u-picker>
</view>
<view class="form-row">
<view class="form-label">停留时间</view>
<view class="picker" @click="openPicker('showStayTimePicker', index)">
{{ stayTimes[zone.stayTime] || '停留时间' }}
</view>
<u-picker :show="showStayTimePicker" :columns="[stayTimes]" @confirm="onStayTimeConfirm"
@cancel="showStayTimePicker = false"></u-picker>
</view>
<view class="form-row">
<view class="form-label">水平对齐</view>
<view class="picker" @click="openPicker('showHAlignPicker', index)">
{{ horizontalAlign[zone.hAlign] || '水平对齐' }}
</view>
<u-picker :show="showHAlignPicker" :columns="[horizontalAlign]" @confirm="onHAlignConfirm"
@cancel="showHAlignPicker = false"></u-picker>
</view>
<view class="form-row">
<view class="form-label">垂直对齐</view>
<view class="picker" @click="openPicker('showVAlignPicker', index)">
{{ verticalAlign[zone.vAlign] || '垂直对齐' }}
</view>
<u-picker :show="showVAlignPicker" :columns="[verticalAlign]" @confirm="onVAlignConfirm"
@cancel="showVAlignPicker = false"></u-picker>
</view>
</view>
<!-- 高级参数卡片 -->
<view class="group-card">
<view class="group-title">高级参数</view>
<view class="form-row">
<view class="mini-label">X</view>
<u-input v-model="zone.x" placeholder="0" />
<view class="unit-label">PX</view>
</view>
<view class="form-row">
<view class="mini-label"></view>
<u-input v-model="zone.width" placeholder="32" />
<view class="unit-label">PX</view>
</view>
<view class="form-row">
<view class="mini-label">Y</view>
<u-input v-model="zone.y" placeholder="0" />
<view class="unit-label">PX</view>
</view>
<view class="form-row">
<view class="mini-label"></view>
<u-input v-model="zone.height" placeholder="64" />
<view class="unit-label">PX</view>
</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="button-row">
<u-button type="default" class="btn" @click="onRead">读取</u-button>
<u-button type="primary" class="btn" @click="onSet">设置</u-button>
</view>
</view>
</template>
<script>
export default {
name: 'AddProgram',
onLoad() {
// 从本地存储获取设备信息
const device = uni.getStorageSync('currentDevice');
const deviceInfo = uni.getStorageSync('currentDeviceInfo');
if (device) {
this.device = device;
}
if (deviceInfo) {
this.deviceInfo = deviceInfo;
}
},
onReady() {
this.$nextTick(() => {
this.initializeAnimations();
});
},
onUnload() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
// Clear any remaining timers
this.animationStates.forEach(state => {
if (state.standTimer) clearTimeout(state.standTimer);
if (state.pageTimer) clearTimeout(state.pageTimer);
});
},
data() {
return {
device: null,
deviceInfo: null,
screenWidth: 32,
screenHeight: 64,
// picker弹窗控制
showModePicker: false,
showPlayTypePicker: false,
showPhrasePicker: false,
showFontPicker: false,
showFontShapePicker: false,
showFontSizePicker: false,
showFontColorPicker: false,
showFontBoldPicker: false,
showFontStretchPicker: false,
showEffectPicker: false,
showHAlignPicker: false,
showVAlignPicker: false,
showSpeedPicker: false,
showStayTimePicker: false,
// 选项数组
playTypes: ['文字', '图片'],
modes: ['模式1', '模式2(上下)', '模式3(左右)', '模式1114(上中下)', '模式5', '模式6'],
commonPhrases: [
'自定义',
"公安交警正在巡逻",
"公安交警停车检查",
"前方事故减速慢行",
"警察临检请您配合",
"交警临检请您配合",
"《《《《《",
"》》》》》",
"禁止停车",
"交通管制禁止通行",
"公安检查靠边停车",
"前方事故道路封闭",
"禁止掉头",
"雨雪天气注意安全",
"大雾天气减速慢行",
"靠右停车接受检查",
"警察临检靠边停车"
],
fonts: ['宋体(中)', '黑体(中)', '楷体(中)'],
fontShapes: ['圆角(英)', '直角(英)'],
fontSizes: ['16px', '24px', '32px', '48px', '64px'],
fontColors: ['红色', '绿色', '蓝色'],
fontBold: ['不加粗', '加粗'],
fontStretch: ['不拉伸', '横向拉伸', '纵向拉伸'],
effects: ['立即显示', '左移', '右移', '上移', '下移', '连续左移', '闪烁换页'],
speeds: ['1X', '2X', '3X', '4X', '5X'],
stayTimes: ['不停顿', '100毫秒', '500毫秒', '1秒', '2秒', '3秒', '4秒', '5秒', '6秒', '8秒', '10秒'],
horizontalAlign: ['居左对齐', '居中对齐', '居右对齐'],
verticalAlign: ['顶部对齐', '居中对齐', '底部对齐'],
form: {
mode: 0,
duration: '',
zones: [{
playType: 0, // 0: 文字, 1: 图片
commonPhrase: 0,
displayText: '',
font: 0,
fontShape: 0,
fontSize: 0,
fontColor: 0,
fontBold: 0,
fontStretch: 0,
image: '', // 图片参数
effect: 0,
speed: 4, // Default to 5X
stayTime: 5, // Default to 3秒
hAlign: 0,
vAlign: 0,
x: 0,
y: 0,
width: 32,
height: 64
}]
},
currentZoneIndex: 0, // To track which zone's picker is open
animationFrameId: null,
animationStates: []
};
},
watch: {
'form.zones': {
handler() {
// Use nextTick to ensure canvas is ready after DOM updates
this.$nextTick(() => {
this.initializeAnimations();
});
},
deep: true
}
},
methods: {
getIndex(e) {
if (e && e.indexs && e.indexs[0] !== undefined) return e.indexs[0];
if (e && e.index && e.index[0] !== undefined) return e.index[0];
if (Array.isArray(e)) return e[0];
return 0;
},
onModeConfirm(e) {
console.log('onModeConfirm', e, e.index, e.value);
this.form.mode = this.getIndex(e);
console.log('form.mode', this.form.mode);
this.updateZones();
this.drawCanvas();
this.showModePicker = false;
},
updateZones() {
const zoneCounts = [1, 2, 2, 3, 1, 1];
const count = zoneCounts[this.form.mode] || 1;
const newZones = [];
const screenW = this.screenWidth;
const screenH = this.screenHeight;
for (let i = 0; i < count; i++) {
const templateZone = {
playType: 0,
commonPhrase: 0,
displayText: '',
font: 0,
fontShape: 0,
fontSize: 0,
fontColor: 0,
fontBold: 0,
fontStretch: 0,
image: '',
effect: 0,
speed: 4,
stayTime: 5,
hAlign: 0,
vAlign: 0,
x: 0,
y: 0,
width: screenW,
height: screenH
};
switch (this.form.mode) {
case 1: // Mode 2 (上下)
templateZone.height = Math.floor(screenH / count);
templateZone.y = i * templateZone.height;
// Last zone takes remaining height
if (i === count - 1) {
templateZone.height = screenH - templateZone.y;
}
break;
case 2: // Mode 3 (左右)
templateZone.width = Math.floor(screenW / count);
templateZone.x = i * templateZone.width;
// Last zone takes remaining width
if (i === count - 1) {
templateZone.width = screenW - templateZone.x;
}
break;
case 3: // Mode 4 (上中下)
templateZone.height = Math.floor(screenH / count);
templateZone.y = i * templateZone.height;
// Last zone takes remaining height
if (i === count - 1) {
templateZone.height = screenH - templateZone.y;
}
break;
}
// Try to keep user-edited data if a zone already exists, but update geometry
if (this.form.zones[i]) {
newZones.push({
...this.form.zones[i],
x: templateZone.x,
y: templateZone.y,
width: templateZone.width,
height: templateZone.height
});
} else {
newZones.push(templateZone);
}
}
this.form.zones = newZones;
},
async initializeAnimations() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
this.animationStates.forEach(state => {
if (state.standTimer) clearTimeout(state.standTimer);
if (state.pageTimer) clearTimeout(state.pageTimer);
});
this.animationStates = this.form.zones.map(zone => {
const assetInfo = this.prepareZoneAssets(zone);
return {
...assetInfo,
currentPage: 0,
currentX: 0,
currentY: 0,
standTimer: null,
pageTimer: null,
...this.getInitialPosition(zone, assetInfo)
};
});
this.animationLoop();
},
getInitialPosition(zone, assetInfo) {
const pos = {
currentX: 0,
currentY: 0
};
const effect = zone.effect; // 0:立即显示, 1:左移, 2:右移, 3:上移, 4:下移, 5:连续左移
if (effect === 1) pos.currentX = zone.width;
if (effect === 2) pos.currentX = -assetInfo.assetWidth;
if (effect === 3) pos.currentY = zone.height;
if (effect === 4) pos.currentY = -assetInfo.assetHeight;
if (effect === 5) pos.currentX = 0; // Start at the beginning for continuous scroll
return pos;
},
animationLoop() {
this.drawCanvas();
this.animationFrameId = requestAnimationFrame(this.animationLoop.bind(this));
},
drawCanvas() {
const ctx = uni.createCanvasContext('previewCanvas', this);
const query = uni.createSelectorQuery().in(this);
query.select('.preview-canvas').boundingClientRect(rect => {
if (!rect) return;
const canvasWidth = rect.width;
const canvasHeight = rect.height;
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
const scale = Math.min(canvasWidth / this.screenWidth, canvasHeight / this.screenHeight);
const screenDrawWidth = this.screenWidth * scale;
const screenDrawHeight = this.screenHeight * scale;
const offsetX = (canvasWidth - screenDrawWidth) / 2;
const offsetY = (canvasHeight - screenDrawHeight) / 2;
ctx.fillStyle = '#000000';
ctx.fillRect(offsetX, offsetY, screenDrawWidth, screenDrawHeight);
this.form.zones.forEach((zone, index) => {
const state = this.animationStates[index];
if (!state || !state.isText) return;
ctx.save();
const zoneDrawX = offsetX + zone.x * scale;
const zoneDrawY = offsetY + zone.y * scale;
const zoneDrawWidth = zone.width * scale;
const zoneDrawHeight = zone.height * scale;
ctx.beginPath();
ctx.rect(zoneDrawX, zoneDrawY, zoneDrawWidth, zoneDrawHeight);
ctx.clip();
// Setup font for drawing
const fontSize = parseInt(this.fontSizes[zone.fontSize] || '16px');
const scaledFontSize = Math.max(1, Math.round(fontSize * scale));
ctx.font =
`${this.mapBold(zone.fontBold)} ${scaledFontSize}px ${this.mapFont(zone.font)}`;
ctx.fillStyle = this.mapColor(zone.fontColor);
// Get the lines for the current page
const pageLines = state.pages[state.currentPage] || [];
// --- Correct Vertical Alignment Calculation ---
const blockHeight = pageLines.length * scaledFontSize;
let blockStartY = zoneDrawY + state.currentY * scale; // Apply Y animation offset
const verticalAlign = this.mapVAlign(zone.vAlign);
if (verticalAlign === 'middle') {
blockStartY = zoneDrawY + state.currentY * scale + (zoneDrawHeight -
blockHeight) / 2;
} else if (verticalAlign === 'bottom') {
blockStartY = zoneDrawY + state.currentY * scale + zoneDrawHeight -
blockHeight;
}
// Set textBaseline to top for consistent line-by-line drawing
ctx.textBaseline = 'top';
pageLines.forEach((line, lineIndex) => {
// Horizontal alignment
let xPos = zoneDrawX + state.currentX *
scale; // Apply X animation offset
const horizontalAlign = this.mapHAlign(zone.hAlign);
if (horizontalAlign === 'center') {
const lineWidth = this.calculateTextWidth(line, scaledFontSize);
xPos = zoneDrawX + state.currentX * scale + (zoneDrawWidth -
lineWidth) / 2;
} else if (horizontalAlign === 'right') {
const lineWidth = this.calculateTextWidth(line, scaledFontSize);
xPos = zoneDrawX + state.currentX * scale + zoneDrawWidth -
lineWidth;
}
const yPos = blockStartY + (lineIndex * scaledFontSize);
ctx.fillText(line, xPos, yPos);
});
// For continuous scroll, draw a second copy
if (zone.effect === 5) {
const secondCopyX = zoneDrawX + state.currentX * scale + state.assetWidth *
scale;
pageLines.forEach((line, lineIndex) => {
// Horizontal alignment for the second copy is always 'left' relative to its starting point
const yPos = blockStartY + (lineIndex * scaledFontSize);
ctx.fillText(line, secondCopyX, yPos);
});
}
ctx.restore();
this.updateAnimationState(zone, state, index);
});
// Drawing partition lines after all content
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 1;
ctx.save();
ctx.translate(offsetX, offsetY);
const mode = this.form.mode;
if (mode === 1) { //上下
ctx.beginPath();
ctx.moveTo(0, screenDrawHeight / 2);
ctx.lineTo(screenDrawWidth, screenDrawHeight / 2);
ctx.stroke();
} else if (mode === 2) { //左右
ctx.beginPath();
ctx.moveTo(screenDrawWidth / 2, 0);
ctx.lineTo(screenDrawWidth / 2, screenDrawHeight);
ctx.stroke();
} else if (mode === 3) { //上中下
ctx.beginPath();
ctx.moveTo(0, screenDrawHeight / 3);
ctx.lineTo(screenDrawWidth, screenDrawHeight / 3);
ctx.moveTo(0, (screenDrawHeight / 3) * 2);
ctx.lineTo(screenDrawWidth, (screenDrawHeight / 3) * 2);
ctx.stroke();
}
ctx.restore();
ctx.draw();
}).exec();
},
updateAnimationState(zone, state) {
if (state.standTimer) return;
const speedMap = [1, 2, 3, 4, 5];
const animationSpeed = (speedMap[zone.speed] || 5) / 5; // Normalize speed
const effect = zone.effect;
// Handle multi-page text cycling for non-scroll effects
if (effect !== 5 && state.pages.length > 1) {
// If we don't have a timer yet, start one
if (!state.pageTimer) {
const stayTimeMap = [0, 100, 500, 1000, 2000, 3000, 4000, 5000, 6000, 8000, 10000];
const stayTime = stayTimeMap[zone.stayTime] || 3000;
state.pageTimer = setTimeout(() => {
state.currentPage = (state.currentPage + 1) % state.pages.length;
state.pageTimer = null; // Clear timer so it can be set again
}, stayTime);
}
}
switch (effect) {
case 0: // 立即显示 - 不需要动画,只需要页面切换
break;
case 1: // Left
if (state.currentX > 0) state.currentX = Math.max(0, state.currentX - animationSpeed);
break;
case 2: // Right
if (state.currentX < 0) state.currentX = Math.min(0, state.currentX + animationSpeed);
break;
case 3: // Up
if (state.currentY > 0) state.currentY = Math.max(0, state.currentY - animationSpeed);
break;
case 4: // Down
if (state.currentY < 0) state.currentY = Math.min(0, state.currentY + animationSpeed);
break;
case 5: // Continuous Left
state.currentX -= animationSpeed;
if (state.currentX <= -state.assetWidth) {
state.currentX = 0;
}
break;
case 6: // 闪烁换页 - 特殊处理
if (!state.pageTimer) {
const stayTimeMap = [0, 100, 500, 1000, 2000, 3000, 4000, 5000, 6000, 8000, 10000];
const stayTime = stayTimeMap[zone.stayTime] || 3000;
state.pageTimer = setTimeout(() => {
state.currentPage = (state.currentPage + 1) % state.pages.length;
state.pageTimer = null;
}, stayTime);
}
break;
}
},
prepareZoneAssets(zone) {
if (zone.playType !== 0 || !zone.displayText) {
return {
pages: [],
assetWidth: 0,
assetHeight: 0,
isText: false
};
}
const ctx = uni.createCanvasContext('__temp_prepare_canvas', this);
const fontSize = parseInt(this.fontSizes[zone.fontSize] || '16px');
ctx.font = `${this.mapBold(zone.fontBold)} ${fontSize}px ${this.mapFont(zone.font)}`;
// For continuous scroll, treat as a single line
if (zone.effect === 5) {
const textWidth = this.calculateTextWidth(zone.displayText, fontSize);
return {
pages: [
[zone.displayText]
],
assetWidth: textWidth,
assetHeight: fontSize,
isText: true
};
}
// Word wrapping logic based on character code estimation
const lines = [];
let currentLine = '';
const text = zone.displayText;
for (let i = 0; i < text.length; i++) {
const char = text[i];
const testLine = currentLine + char;
const lineWidth = this.calculateTextWidth(testLine, fontSize);
if (lineWidth > zone.width && currentLine !== '') {
lines.push(currentLine);
currentLine = char;
} else {
currentLine = testLine;
}
}
if (currentLine) {
lines.push(currentLine);
}
const lineHeight = fontSize;
const maxLinesPerPage = zone.height > 0 ? Math.floor(zone.height / lineHeight) : 1;
const pages = [];
// 确保至少有一页,即使高度不够
if (maxLinesPerPage > 0) {
for (let i = 0; i < lines.length; i += maxLinesPerPage) {
pages.push(lines.slice(i, i + maxLinesPerPage));
}
} else {
// 如果高度不够,将所有行放在一页中
pages.push(lines);
}
// 确保至少有一页(即使是空的)
if (pages.length === 0) {
pages.push([]);
}
return {
pages: pages,
assetWidth: zone.width,
assetHeight: zone.height,
isText: true
};
},
calculateTextWidth(text, fontSize) {
let width = 0;
for (let i = 0; i < text.length; i++) {
// Treat non-ASCII characters as full-width (width = height)
if (text.charCodeAt(i) > 127) {
width += fontSize;
} else {
// Treat ASCII characters as half-width
width += fontSize / 2;
}
}
return width;
},
openPicker(pickerFlag, zoneIndex) {
this.currentZoneIndex = zoneIndex;
this[pickerFlag] = true;
},
onPlayTypeConfirm(e) {
console.log('onPlayTypeConfirm', e, e.index, e.value);
this.form.zones[this.currentZoneIndex].playType = this.getIndex(e);
this.showPlayTypePicker = false;
},
onPhraseConfirm(e) {
console.log('onPhraseConfirm', e, e.index, e.value);
const index = this.getIndex(e);
const phrase = this.commonPhrases[index];
this.form.zones[this.currentZoneIndex].commonPhrase = index;
if (index > 0) { // Not "自定义"
this.form.zones[this.currentZoneIndex].displayText = phrase;
}
this.drawCanvas();
this.showPhrasePicker = false;
},
onFontConfirm(e) {
console.log('onFontConfirm', e, e.index, e.value);
this.form.zones[this.currentZoneIndex].font = this.getIndex(e);
console.log('form.font', this.form.zones[this.currentZoneIndex].font);
this.drawCanvas();
this.showFontPicker = false;
},
onFontShapeConfirm(e) {
console.log('onFontShapeConfirm', e, e.index, e.value);
this.form.zones[this.currentZoneIndex].fontShape = this.getIndex(e);
console.log('form.fontShape', this.form.zones[this.currentZoneIndex].fontShape);
this.drawCanvas();
this.showFontShapePicker = false;
},
onFontSizeConfirm(e) {
console.log('onFontSizeConfirm', e, e.index, e.value);
this.form.zones[this.currentZoneIndex].fontSize = this.getIndex(e);
console.log('form.fontSize', this.form.zones[this.currentZoneIndex].fontSize);
this.drawCanvas();
this.showFontSizePicker = false;
},
onFontColorConfirm(e) {
console.log('onFontColorConfirm', e, e.index, e.value);
this.form.zones[this.currentZoneIndex].fontColor = this.getIndex(e);
console.log('form.fontColor', this.form.zones[this.currentZoneIndex].fontColor);
this.drawCanvas();
this.showFontColorPicker = false;
},
onFontBoldConfirm(e) {
console.log('onFontBoldConfirm', e, e.index, e.value);
this.form.zones[this.currentZoneIndex].fontBold = this.getIndex(e);
console.log('form.fontBold', this.form.zones[this.currentZoneIndex].fontBold);
this.drawCanvas();
this.showFontBoldPicker = false;
},
onFontStretchConfirm(e) {
console.log('onFontStretchConfirm', e, e.index, e.value);
this.form.zones[this.currentZoneIndex].fontStretch = this.getIndex(e);
console.log('form.fontStretch', this.form.zones[this.currentZoneIndex].fontStretch);
this.drawCanvas();
this.showFontStretchPicker = false;
},
onEffectConfirm(e) {
console.log('onEffectConfirm', e, e.index, e.value);
this.form.zones[this.currentZoneIndex].effect = this.getIndex(e);
console.log('form.effect', this.form.zones[this.currentZoneIndex].effect);
this.drawCanvas();
this.showEffectPicker = false;
},
onHAlignConfirm(e) {
console.log('onHAlignConfirm', e, e.index, e.value);
this.form.zones[this.currentZoneIndex].hAlign = this.getIndex(e);
console.log('form.hAlign', this.form.zones[this.currentZoneIndex].hAlign);
this.drawCanvas();
this.showHAlignPicker = false;
},
onVAlignConfirm(e) {
console.log('onVAlignConfirm', e, e.index, e.value);
this.form.zones[this.currentZoneIndex].vAlign = this.getIndex(e);
console.log('form.vAlign', this.form.zones[this.currentZoneIndex].vAlign);
this.drawCanvas();
this.showVAlignPicker = false;
},
onSpeedConfirm(e) {
console.log('onSpeedConfirm', e, e.index, e.value);
this.form.zones[this.currentZoneIndex].speed = this.getIndex(e);
console.log('form.speed', this.form.zones[this.currentZoneIndex].speed);
// No direct draw call, watcher will handle it.
this.showSpeedPicker = false;
},
onStayTimeConfirm(e) {
console.log('onStayTimeConfirm', e, e.index, e.value);
this.form.zones[this.currentZoneIndex].stayTime = this.getIndex(e);
console.log('form.stayTime', this.form.zones[this.currentZoneIndex].stayTime);
// No direct draw call, watcher will handle it.
this.showStayTimePicker = false;
},
onRead() {
this.$u.toast('读取功能待实现');
},
onSet() {
// 构建要发送的数据
const programData = {
form: this.form,
device: this.device,
deviceInfo: this.deviceInfo
};
// 发送数据到父页面
uni.$emit('programDataReady', programData);
this.$u.toast('设置成功');
// 返回上一页
uni.navigateBack();
},
mapFont(index) {
return ['SimSun', 'SimHei', 'KaiTi'][index] || 'SimSun';
},
mapColor(index) {
return ['#FF0000', '#00FF00', '#0000FF'][index] || '#FF0000'; // Red, Green, Blue
},
mapBold(index) {
return ['normal', 'bold'][index] || 'normal';
},
mapHAlign(index) {
return ['left', 'center', 'right'][index] || 'left';
},
mapVAlign(index) {
return ['top', 'middle', 'bottom'][index] || 'top';
}
}
};
</script>
<style scoped>
.preview-section {
position: sticky;
top: 0;
z-index: 99;
}
.add-program-container {
background: #f5f7fa;
padding: 16px;
/* margin-top: 160px; 增加顶部内边距,避免被标题栏遮挡 */
}
.section {
background: #fff;
border-radius: 10px;
margin-bottom: 16px;
box-shadow: 0 2px 8px #e0e0e0;
padding: 12px 16px 16px 16px;
}
.section-title {
font-weight: bold;
font-size: 16px;
margin-top: 10px;
margin-bottom: 8px;
color: #409eff;
}
.section-title.blue {
background: #409eff;
color: #fff;
padding: 6px 12px;
border-radius: 6px 6px 0 0;
margin: -12px -16px 8px -16px;
}
.section-title.preview-fixed {
position: fixed;
top: 0;
left: 0;
width: 100vw;
z-index: 999;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
border-radius: 0 0 12px 12px;
padding-top: env(safe-area-inset-top, 0);
background: linear-gradient(to bottom, #409eff, #5b9bd5);
height: auto;
/* 改为自动高度 */
min-height: 160px;
/* 设置最小高度 */
}
.preview-canvas {
width: 100%;
height: 120px;
border: 2px solid #e0e0e0;
border-radius: 8px;
margin-bottom: 8px;
box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.2);
display: block;
}
.zone-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
margin-bottom: 20px;
padding: 0 0 16px 0;
border: 1px solid #eaeaea;
overflow: hidden;
}
.zone-title {
background: #409eff;
color: #fff;
font-size: 20px;
font-weight: bold;
border-radius: 12px 12px 0 0;
padding: 10px 20px;
margin-bottom: 8px;
letter-spacing: 2px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.group-card {
background: #f8fafd;
border-radius: 8px;
box-shadow: 0 1px 4px #e0e0e0;
margin: 16px 16px 0 16px;
padding: 12px 16px 12px 16px;
border: 1px solid #f0f0f0;
transition: all 0.3s ease;
}
.group-card:hover {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
}
.group-title {
color: #409eff;
font-size: 15px;
font-weight: bold;
margin-bottom: 10px;
letter-spacing: 1px;
}
/* 修改 form-row 的 justify-content 为 flex-start 使内容靠左 */
.form-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding: 8px 0;
}
/* 保留 row-3 的特殊布局 */
.form-row.row-3 {
display: flex;
gap: 10px;
margin-bottom: 12px;
justify-content: flex-start;
/* 左对齐 */
}
.form-label,
.mini-label {
color: #666;
font-size: 20px;
text-align: left;
width: 90px;
min-width: 90px;
flex-shrink: 0;
margin-right: 0;
margin-left: 20px;
}
.picker {
background: #fff;
border-radius: 8px;
border: 1.5px solid #d0d7e5;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.06);
padding: 8px 32px 8px 16px;
min-width: 100px;
font-size: 15px;
color: #333;
position: relative;
transition: border-color 0.2s, box-shadow 0.2s;
cursor: pointer;
line-height: 1.6;
display: flex;
align-items: right;
margin-left: 65px;
max-width: 100%;
text-align: right;
}
.picker:after {
content: '';
display: block;
position: absolute;
right: 14px;
top: 50%;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 7px solid #b0b0b0;
transform: translateY(-50%);
pointer-events: none;
}
.picker:hover,
.picker:active {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.12);
background: #f4faff;
color: #2979ff;
}
.u-input,
u-input {
flex: 1;
min-width: 80px;
border-radius: 6px;
font-size: 15px;
border: 1px solid #eaeaea;
background: #fafafa;
}
.input-area {
width: 100%;
min-height: 60px;
border-radius: 6px;
font-size: 16px;
margin-top: 4px;
border: 1px solid #eaeaea;
}
.button-row {
display: flex;
justify-content: space-around;
margin-top: 24px;
padding-top: 16px;
border-top: 1px dashed #eaeaea;
}
.btn {
width: 40%;
border-radius: 20px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
}
.btn:active {
transform: translateY(1px);
}
/* 特别为 right-picker 类添加样式,使 picker 靠右 */
.right-picker {
display: flex;
justify-content: flex-end;
/* 使整个容器内容靠右 */
align-items: center;
gap: 10px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.right-picker .picker {
margin-left: auto;
/* 使 picker 元素自身向右对齐 */
flex: 0 0 160px;
/* 固定宽度,不伸缩 */
}
@media (max-width: 500px) {
.zone-title {
font-size: 16px;
padding: 8px 16px;
}
.group-title {
font-size: 14px;
}
.form-label {
min-width: 50px;
font-size: 13px;
}
.picker {
min-width: 80px;
}
.btn {
width: 45%;
/* 在小屏幕上按钮占更多空间 */
}
/* 在小屏幕上调整 right-picker 的样式 */
.right-picker {
flex-direction: row;
/* 确保水平排列 */
}
.right-picker .picker {
flex: 0 0 auto;
/* 不强制宽度,允许根据内容调整 */
min-width: 70px;
/* 最小宽度 */
}
}
.picker-group,
.form-row.row-3 {
display: block;
margin: 0;
padding: 0;
}
.unit-label {
font-size: 13px;
color: #888;
margin-left: 8px;
}
</style>