2025-07-23 14:34:57 +08:00

1053 lines
30 KiB
Vue
Raw Permalink 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="section">
<view class="section-title blue">节目参数</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 class="form-row">
<view class="mini-label">节目备注</view>
<u-input v-model="form.remark" placeholder="请输入节目备注" style="margin-left: 60px;max-width:220px;" />
</view>
</view>
<!-- 分区配置tab切换 -->
<view class="section">
<view class="section-title blue">分区配置</view>
<u-tabs :list="zoneTabs" :current="currentZoneTab" :scrollable="false"
:activeStyle="{color:'#2979ff',fontWeight:'bold'}" :lineColor="'#2979ff'" @click="onTabClick" />
<view v-for="(zone, index) in form.zones" :key="index" v-show="currentZoneTab === index">
<view class="group-card">
<view class="group-title">项目选择</view>
<view class="form-row">
<view class="form-label">播放类型</view>
<view class="picker" @click="openPicker('showPlayTypePicker')">
{{ 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')">
{{ 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')">{{ 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('showFontSizePicker')">{{ 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')">{{ fontColors[zone.fontColor] }}</view>
<u-picker :show="showFontColorPicker" :columns="[fontColors]" @confirm="onFontColorConfirm"
@cancel="showFontColorPicker = false"></u-picker>
</view>
</view>
<!-- 图片参数 -->
<view class="group-card" v-else>
<view class="group-title">图片参数</view>
<view class="form-row">
<view class="picker" @click="openPicker('showImagePicker')">
{{ zone.image ? zone.image : '请选择图片' }}
</view>
<u-picker :show="showImagePicker" :columns="[imageFiles]" @confirm="onImageConfirm"
@cancel="showImagePicker = false" />
</view>
<view class="form-row">
<view class="mini-label">尺寸</view>
<view class="picker" @click="openPicker('showImageSizePicker')">
{{ imageSizes[zone.imageSize] }}
</view>
<u-picker :show="showImageSizePicker" :columns="[imageSizes]" @confirm="onImageSizeConfirm"
@cancel="showImageSizePicker = false" />
</view>
<view class="form-row">
<view class="mini-label">颜色</view>
<view class="picker" @click="openPicker('showImageColorPicker')">
{{ fontColors[zone.imageColor] }}
</view>
<u-picker :show="showImageColorPicker" :columns="[fontColors]" @confirm="onImageColorConfirm"
@cancel="showImageColorPicker = false" />
</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')">
{{ 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')">{{ 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')">
{{ 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')">
{{ 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')">
{{ 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>
<!-- 操作常显卡片 -->
<view class="section">
<view class="section-title blue">操作</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>
</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);
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,
showImagePicker: false,
showImageColorPicker: false,
showImageSizePicker: 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: ['顶部对齐', '居中对齐', '底部对齐'],
imageFiles: [
'上箭头',
'两侧道路变窄',
'停车',
'减速慢行',
'减速让行',
'前方事故',
'前方施工',
'右下箭头',
'右侧道路变窄',
'右箭头',
'向右',
'向右转弯',
'向左',
'向左向右转弯',
'向左转弯',
'左下箭头',
'左侧道路变窄',
'左右两侧绕行',
'左箭头',
'掉头',
'检查',
'注意安全',
'禁止临时停车',
'禁止停车',
'禁止停车停',
'禁止右转',
'禁止左转',
'禁止掉头',
'禁止驶入',
'禁止鸣笛',
'箭头向上',
'箭头向下',
'解除限速',
'限速',
'靠右行驶',
'靠左行驶'
],
imageSizes: ['16x16', '32x32', '64x64'],
form: {
mode: 0,
duration: '',
zones: [{
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: 1,
vAlign: 1,
x: 0,
y: 0,
width: 32,
height: 64,
imageColor: 0,
imageSize: 1
}]
},
animationFrameId: null,
animationStates: [],
imageCache: {},
currentZoneTab: 0,
};
},
computed: {
zoneTabs() {
return this.form.zones.map((z, i) => ({ name: `分区${i + 1}` }));
}
},
watch: {
'form.zones': {
handler() {
this.$nextTick(() => {
this.initializeAnimations();
});
},
deep: true
}
},
methods: {
deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
return Array.isArray(obj)
? obj.map(item => this.deepClone(item))
: Object.keys(obj).reduce((acc, key) => {
acc[key] = this.deepClone(obj[key]);
return acc;
}, {});
},
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;
},
updateZoneField(field, value) {
this.form.zones[this.currentZoneTab][field] = value;
this.drawCanvas();
},
onModeConfirm(e) {
this.form.mode = this.getIndex(e);
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 = this.deepClone({
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: 1,
vAlign: 1,
x: 0,
y: 0,
width: screenW,
height: screenH,
imageColor: 0,
imageSize: 1
});
switch (this.form.mode) {
case 1:
templateZone.height = Math.floor(screenH / count);
templateZone.y = i * templateZone.height;
if (i === count - 1) {
templateZone.height = screenH - templateZone.y;
}
break;
case 2:
templateZone.width = Math.floor(screenW / count);
templateZone.x = i * templateZone.width;
if (i === count - 1) {
templateZone.width = screenW - templateZone.x;
}
break;
case 3:
templateZone.height = Math.floor(screenH / count);
templateZone.y = i * templateZone.height;
if (i === count - 1) {
templateZone.height = screenH - templateZone.y;
}
break;
}
if (this.form.zones[i]) {
newZones.push({
...this.deepClone(this.form.zones[i]),
x: templateZone.x,
y: templateZone.y,
width: templateZone.width,
height: templateZone.height
});
} else {
newZones.push(templateZone);
}
}
this.form.zones = newZones;
if (this.currentZoneTab >= newZones.length) {
this.currentZoneTab = newZones.length - 1;
}
},
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;
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;
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) return;
if (zone.playType === 1 && state.isImage && state.imageUrl) {
const zoneDrawX = offsetX + zone.x * scale;
const zoneDrawY = offsetY + zone.y * scale;
const zoneDrawWidth = zone.width * scale;
const zoneDrawHeight = zone.height * scale;
ctx.save();
ctx.beginPath();
ctx.rect(zoneDrawX, zoneDrawY, zoneDrawWidth, zoneDrawHeight);
ctx.clip();
const cachePath = this.imageCache[state.imageUrl];
if (cachePath) {
ctx.drawImage(cachePath, zoneDrawX, zoneDrawY, zoneDrawWidth, zoneDrawHeight);
if (zone.imageColor !== undefined) {
ctx.globalAlpha = 0.4;
ctx.setFillStyle(this.mapColor(zone.imageColor));
ctx.fillRect(zoneDrawX, zoneDrawY, zoneDrawWidth, zoneDrawHeight);
ctx.globalAlpha = 1;
}
ctx.restore();
return;
}
uni.getImageInfo({
src: state.imageUrl,
success: (res) => {
this.imageCache[state.imageUrl] = res.path;
this.drawCanvas();
}
});
ctx.restore();
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();
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);
const pageLines = state.pages[state.currentPage] || [];
const blockHeight = pageLines.length * scaledFontSize;
let blockStartY = zoneDrawY + state.currentY * scale;
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;
}
ctx.textBaseline = 'top';
pageLines.forEach((line, lineIndex) => {
let xPos = zoneDrawX + state.currentX * scale;
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);
});
if (zone.effect === 5) {
const secondCopyX = zoneDrawX + state.currentX * scale + state.assetWidth * scale;
pageLines.forEach((line, lineIndex) => {
const yPos = blockStartY + (lineIndex * scaledFontSize);
ctx.fillText(line, secondCopyX, yPos);
});
}
ctx.restore();
this.updateAnimationState(zone, state, index);
});
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;
const effect = zone.effect;
if (effect !== 5 && state.pages.length > 1) {
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);
}
}
switch (effect) {
case 0:
break;
case 1:
if (state.currentX > 0) state.currentX = Math.max(0, state.currentX - animationSpeed);
break;
case 2:
if (state.currentX < 0) state.currentX = Math.min(0, state.currentX + animationSpeed);
break;
case 3:
if (state.currentY > 0) state.currentY = Math.max(0, state.currentY - animationSpeed);
break;
case 4:
if (state.currentY < 0) state.currentY = Math.min(0, state.currentY + animationSpeed);
break;
case 5:
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 === 1 && zone.image) {
return {
isImage: true,
imageUrl: zone.image,
assetWidth: zone.width || 32,
assetHeight: zone.height || 32,
pages: [[]],
currentPage: 0
};
}
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)}`;
if (zone.effect === 5) {
const textWidth = this.calculateTextWidth(zone.displayText, fontSize);
return {
pages: [[zone.displayText]],
assetWidth: textWidth,
assetHeight: fontSize,
isText: true
};
}
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++) {
if (text.charCodeAt(i) > 127) {
width += fontSize;
} else {
width += fontSize / 2;
}
}
return width;
},
openPicker(pickerFlag) {
this[pickerFlag] = true;
},
onPlayTypeConfirm(e) {
this.updateZoneField('playType', this.getIndex(e));
this.showPlayTypePicker = false;
},
onPhraseConfirm(e) {
const index = this.getIndex(e);
const phrase = this.commonPhrases[index];
this.updateZoneField('commonPhrase', index);
if (index > 0) {
this.updateZoneField('displayText', phrase);
}
this.showPhrasePicker = false;
},
onFontConfirm(e) {
this.updateZoneField('font', this.getIndex(e));
this.showFontPicker = false;
},
onFontShapeConfirm(e) {
this.updateZoneField('fontShape', this.getIndex(e));
this.showFontShapePicker = false;
},
onFontSizeConfirm(e) {
this.updateZoneField('fontSize', this.getIndex(e));
this.showFontSizePicker = false;
},
onFontColorConfirm(e) {
this.updateZoneField('fontColor', this.getIndex(e));
this.showFontColorPicker = false;
},
onFontBoldConfirm(e) {
this.updateZoneField('fontBold', this.getIndex(e));
this.showFontBoldPicker = false;
},
onFontStretchConfirm(e) {
this.updateZoneField('fontStretch', this.getIndex(e));
this.showFontStretchPicker = false;
},
onEffectConfirm(e) {
this.updateZoneField('effect', this.getIndex(e));
this.showEffectPicker = false;
},
onHAlignConfirm(e) {
this.updateZoneField('hAlign', this.getIndex(e));
this.showHAlignPicker = false;
},
onVAlignConfirm(e) {
this.updateZoneField('vAlign', this.getIndex(e));
this.showVAlignPicker = false;
},
onSpeedConfirm(e) {
this.updateZoneField('speed', this.getIndex(e));
this.showSpeedPicker = false;
},
onStayTimeConfirm(e) {
this.updateZoneField('stayTime', this.getIndex(e));
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();
},
onImageConfirm(e) {
const index = this.getIndex(e);
const fileName = this.imageFiles[index];
this.updateZoneField('image', `https://xaznkj.cn/doc/traffic/ ${fileName}.png`);
this.showImagePicker = false;
},
onImageSizeConfirm(e) {
const idx = this.getIndex(e);
const sizeMap = [
{ w: 16, h: 16 },
{ w: 32, h: 32 },
{ w: 64, h: 64 }
];
this.updateZoneField('imageSize', idx);
this.updateZoneField('width', sizeMap[idx].w);
this.updateZoneField('height', sizeMap[idx].h);
this.showImageSizePicker = false;
},
onImageColorConfirm(e) {
this.updateZoneField('imageColor', this.getIndex(e));
this.showImageColorPicker = false;
},
onTabClick(item) {
this.currentZoneTab = item.index;
console.log(this.currentZoneTab);
},
mapFont(index) {
return ['SimSun', 'SimHei', 'KaiTi'][index] || 'SimSun';
},
mapColor(index) {
return ['#FF0000', '#00FF00', '#0000FF'][index] || '#FF0000';
},
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: #fafdff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.06);
margin: 18px 0 0 0;
padding: 18px 20px 12px 20px;
border: 1.5px solid #e3eaf5;
position: relative;
transition: box-shadow 0.2s;
}
.group-card:not(:last-child) {
margin-bottom: 18px;
}
.group-title {
display: flex;
align-items: center;
font-size: 16px;
font-weight: bold;
color: #2979ff;
margin-bottom: 14px;
letter-spacing: 1px;
}
.group-title::before {
content: '';
display: inline-block;
width: 4px;
height: 18px;
background: #409eff;
border-radius: 2px;
margin-right: 10px;
}
.form-row {
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: 14px;
padding: 0;
border-bottom: 1px dashed #e3eaf5;
padding-bottom: 10px;
}
.form-row:last-child {
border-bottom: none;
}
.form-label, .mini-label {
color: #333;
font-size: 15px;
width: 80px;
min-width: 80px;
text-align: left;
margin-right: 10px;
font-weight: 500;
}
.picker, .u-input, u-input {
flex: 1;
min-width: 100px;
margin-left: 0;
}
.input-area {
width: 100%;
min-height: 48px;
border-radius: 6px;
font-size: 15px;
margin-top: 4px;
border: 1px solid #eaeaea;
background: #f5f7fa;
}
.button-row {
display: flex;
justify-content: space-around;
margin-top: 24px;
padding-top: 16px;
border-top: 1px dashed #eaeaea;
background: #fafdff;
}
.btn {
width: 44%;
border-radius: 24px;
font-size: 17px;
font-weight: bold;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.10);
transition: all 0.2s;
}
.btn:active {
transform: scale(0.98);
}
</style>