2025-06-26 14:55:08 +08:00
|
|
|
|
<template>
|
2025-07-20 17:23:49 +08:00
|
|
|
|
<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>
|
2025-06-26 14:55:08 +08:00
|
|
|
|
</view>
|
2025-07-20 17:23:49 +08:00
|
|
|
|
<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>
|
|
|
|
|
<!-- 分区配置(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>
|
2025-06-26 14:55:08 +08:00
|
|
|
|
<view class="form-row">
|
2025-07-20 17:23:49 +08:00
|
|
|
|
<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>
|
2025-06-26 14:55:08 +08:00
|
|
|
|
</view>
|
2025-07-20 17:23:49 +08:00
|
|
|
|
</view>
|
|
|
|
|
<!-- 文字参数 -->
|
|
|
|
|
<view class="group-card" v-if="zone.playType == 0">
|
|
|
|
|
<view class="group-title">文字参数</view>
|
2025-06-26 14:55:08 +08:00
|
|
|
|
<view class="form-row">
|
2025-07-20 17:23:49 +08:00
|
|
|
|
<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>
|
2025-06-26 14:55:08 +08:00
|
|
|
|
</view>
|
2025-07-20 17:23:49 +08:00
|
|
|
|
<view class="form-row">
|
|
|
|
|
<u-input v-model="zone.displayText" type="textarea" placeholder="请输入展示信息" class="input-area" />
|
2025-06-26 14:55:08 +08:00
|
|
|
|
</view>
|
2025-07-20 17:23:49 +08:00
|
|
|
|
<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>
|
2025-06-26 14:55:08 +08:00
|
|
|
|
</view>
|
2025-07-20 17:23:49 +08:00
|
|
|
|
<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>
|
2025-06-26 14:55:08 +08:00
|
|
|
|
</view>
|
2025-07-20 17:23:49 +08:00
|
|
|
|
<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>
|
2025-06-26 14:55:08 +08:00
|
|
|
|
</view>
|
2025-07-20 17:23:49 +08:00
|
|
|
|
</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" />
|
2025-06-26 14:55:08 +08:00
|
|
|
|
</view>
|
2025-07-20 17:23:49 +08:00
|
|
|
|
<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>
|
2025-06-26 14:55:08 +08:00
|
|
|
|
</view>
|
2025-07-20 17:23:49 +08:00
|
|
|
|
</view>
|
|
|
|
|
<!-- 操作(常显卡片) -->
|
|
|
|
|
<view class="section">
|
|
|
|
|
<view class="section-title blue">操作</view>
|
2025-06-26 14:55:08 +08:00
|
|
|
|
<view class="button-row">
|
2025-07-20 17:23:49 +08:00
|
|
|
|
<u-button type="default" class="btn" @click="onRead">读取</u-button>
|
|
|
|
|
<u-button type="primary" class="btn" @click="onSet">设置</u-button>
|
2025-06-26 14:55:08 +08:00
|
|
|
|
</view>
|
2025-07-20 17:23:49 +08:00
|
|
|
|
</view>
|
2025-06-26 14:55:08 +08:00
|
|
|
|
</view>
|
2025-07-20 17:23:49 +08:00
|
|
|
|
</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
|
|
|
|
|
}]
|
2025-07-03 08:57:13 +08:00
|
|
|
|
},
|
2025-07-20 17:23:49 +08:00
|
|
|
|
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();
|
|
|
|
|
});
|
2025-06-26 14:55:08 +08:00
|
|
|
|
},
|
2025-07-20 17:23:49 +08:00
|
|
|
|
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
|
2025-06-26 14:55:08 +08:00
|
|
|
|
});
|
2025-07-20 17:23:49 +08:00
|
|
|
|
} 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;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
2025-07-20 17:23:49 +08:00
|
|
|
|
ctx.restore();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uni.getImageInfo({
|
|
|
|
|
src: state.imageUrl,
|
|
|
|
|
success: (res) => {
|
|
|
|
|
this.imageCache[state.imageUrl] = res.path;
|
|
|
|
|
this.drawCanvas();
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
2025-07-20 17:23:49 +08:00
|
|
|
|
});
|
|
|
|
|
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;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
2025-07-20 17:23:49 +08:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
};
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
2025-07-20 17:23:49 +08:00
|
|
|
|
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>
|
|
|
|
|
|
|
|
|
|
|
2025-06-26 14:55:08 +08:00
|
|
|
|
<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 {
|
2025-07-20 17:23:49 +08:00
|
|
|
|
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;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
2025-07-20 17:23:49 +08:00
|
|
|
|
.group-card:not(:last-child) {
|
|
|
|
|
margin-bottom: 18px;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
|
|
|
|
.group-title {
|
2025-07-20 17:23:49 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
font-size: 16px;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
font-weight: bold;
|
2025-07-20 17:23:49 +08:00
|
|
|
|
color: #2979ff;
|
|
|
|
|
margin-bottom: 14px;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
}
|
2025-07-20 17:23:49 +08:00
|
|
|
|
.group-title::before {
|
|
|
|
|
content: '';
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 4px;
|
|
|
|
|
height: 18px;
|
|
|
|
|
background: #409eff;
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
margin-right: 10px;
|
|
|
|
|
}
|
2025-06-26 14:55:08 +08:00
|
|
|
|
.form-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: flex-start;
|
2025-07-20 17:23:49 +08:00
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
padding: 0;
|
|
|
|
|
border-bottom: 1px dashed #e3eaf5;
|
|
|
|
|
padding-bottom: 10px;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
2025-07-20 17:23:49 +08:00
|
|
|
|
.form-row:last-child {
|
|
|
|
|
border-bottom: none;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
2025-07-20 17:23:49 +08:00
|
|
|
|
.form-label, .mini-label {
|
2025-06-26 14:55:08 +08:00
|
|
|
|
color: #333;
|
2025-07-20 17:23:49 +08:00
|
|
|
|
font-size: 15px;
|
|
|
|
|
width: 80px;
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
text-align: left;
|
|
|
|
|
margin-right: 10px;
|
|
|
|
|
font-weight: 500;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
2025-07-20 17:23:49 +08:00
|
|
|
|
.picker, .u-input, u-input {
|
2025-06-26 14:55:08 +08:00
|
|
|
|
flex: 1;
|
2025-07-20 17:23:49 +08:00
|
|
|
|
min-width: 100px;
|
|
|
|
|
margin-left: 0;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
|
|
|
|
.input-area {
|
|
|
|
|
width: 100%;
|
2025-07-20 17:23:49 +08:00
|
|
|
|
min-height: 48px;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
border-radius: 6px;
|
2025-07-20 17:23:49 +08:00
|
|
|
|
font-size: 15px;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
margin-top: 4px;
|
|
|
|
|
border: 1px solid #eaeaea;
|
2025-07-20 17:23:49 +08:00
|
|
|
|
background: #f5f7fa;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.button-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-around;
|
|
|
|
|
margin-top: 24px;
|
|
|
|
|
padding-top: 16px;
|
|
|
|
|
border-top: 1px dashed #eaeaea;
|
2025-07-20 17:23:49 +08:00
|
|
|
|
background: #fafdff;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
|
|
|
|
.btn {
|
2025-07-20 17:23:49 +08:00
|
|
|
|
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;
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
|
|
|
|
.btn:active {
|
2025-07-20 17:23:49 +08:00
|
|
|
|
transform: scale(0.98);
|
2025-06-26 14:55:08 +08:00
|
|
|
|
}
|
|
|
|
|
</style>
|