Xazn-app/pagesA/home/device/status/addProgram.vue

1053 lines
31 KiB
Vue
Raw Normal View History

2025-06-26 14:55:08 +08:00
<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 {
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);
});
},
data() {
return {
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(左右)', '模式4(上中下)', '模式5', '模式6'],
commonPhrases: ['自定义', '欢迎光临', '限速'],
fonts: ['宋体(中)', '黑体(中)', '楷体(中)'],
fontShapes: ['圆角(英)', '直角(英)'],
fontSizes: ['16px', '24px', '32px'],
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);
});
this.animationStates = this.form.zones.map(zone => {
const assetInfo = this.prepareZoneAssets(zone);
return {
...assetInfo,
currentPage: 0,
currentX: 0,
currentY: 0,
standTimer: 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;
switch (effect) {
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;
}
},
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) : 0;
const pages = [];
if (maxLinesPerPage > 0) {
for (let i = 0; i < lines.length; i += maxLinesPerPage) {
pages.push(lines.slice(i, i + maxLinesPerPage));
}
}
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() {
this.$u.toast('设置功能待实现');
},
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>