|
@@ -2,6 +2,36 @@
|
|
|
<view class="poster-overlay" v-if="show" @click.self="$emit('close')">
|
|
<view class="poster-overlay" v-if="show" @click.self="$emit('close')">
|
|
|
<view class="poster-container">
|
|
<view class="poster-container">
|
|
|
<canvas canvas-id="contentPosterCanvas" class="poster-canvas" id="contentPosterCanvas"></canvas>
|
|
<canvas canvas-id="contentPosterCanvas" class="poster-canvas" id="contentPosterCanvas"></canvas>
|
|
|
|
|
+ <!-- 模板选择(管理端定义的模板;未配置任何模板时不显示选择行) -->
|
|
|
|
|
+ <view class="poster-tpl-row" v-if="serverTemplates.length > 0">
|
|
|
|
|
+ <view class="tpl-label">海报模板</view>
|
|
|
|
|
+ <view class="tpl-options">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="tpl in serverTemplates"
|
|
|
|
|
+ :key="'tpl_' + tpl.id"
|
|
|
|
|
+ class="tpl-option"
|
|
|
|
|
+ :class="{ 'tpl-active': activeTemplateId === tpl.id }"
|
|
|
|
|
+ @click="switchTemplate(tpl)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="tpl-dot" :style="{ background: tpl.mainColor }"></view>
|
|
|
|
|
+ <text class="tpl-text">{{ tpl.name }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <!-- 背景颜色选择(渐变模板可用) -->
|
|
|
|
|
+ <view class="poster-bg-row" v-if="template === 'gradient'">
|
|
|
|
|
+ <view class="tpl-label">背景色</view>
|
|
|
|
|
+ <view class="bg-options">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(c, i) in bgOptions"
|
|
|
|
|
+ :key="'bg_' + i"
|
|
|
|
|
+ class="bg-option"
|
|
|
|
|
+ :class="{ 'bg-active': bgColor === c }"
|
|
|
|
|
+ :style="{ background: c }"
|
|
|
|
|
+ @click="switchBgColor(c)"
|
|
|
|
|
+ ></view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
<view class="poster-actions">
|
|
<view class="poster-actions">
|
|
|
<button class="poster-btn save" @click="savePoster">保存到相册</button>
|
|
<button class="poster-btn save" @click="savePoster">保存到相册</button>
|
|
|
</view>
|
|
</view>
|
|
@@ -35,7 +65,10 @@ export default {
|
|
|
// 传 null 时不画数据卡,按 coverImage -> brandFallback -> 占位图 顺序降级(原有行为不变)
|
|
// 传 null 时不画数据卡,按 coverImage -> brandFallback -> 占位图 顺序降级(原有行为不变)
|
|
|
energyData: { type: Object, default: null },
|
|
energyData: { type: Object, default: null },
|
|
|
// 无 energyData 且 coverImage 为空时,是否用品牌渐变卡替代"暂无封面图"占位
|
|
// 无 energyData 且 coverImage 为空时,是否用品牌渐变卡替代"暂无封面图"占位
|
|
|
- brandFallback: { type: Boolean, default: false }
|
|
|
|
|
|
|
+ brandFallback: { type: Boolean, default: false },
|
|
|
|
|
+ // Web 管理端定义的模板列表([{id, name, templateKey, mainColor, darkColor, lightColor, bgColors, qrSize}])
|
|
|
|
|
+ // 传空数组时使用内置三套模板(原有行为不变)
|
|
|
|
|
+ templates: { type: Array, default: () => [] }
|
|
|
},
|
|
},
|
|
|
watch: {
|
|
watch: {
|
|
|
show(val) {
|
|
show(val) {
|
|
@@ -44,10 +77,61 @@ export default {
|
|
|
setTimeout(this.drawPoster.bind(this), 300)
|
|
setTimeout(this.drawPoster.bind(this), 300)
|
|
|
}.bind(this))
|
|
}.bind(this))
|
|
|
}
|
|
}
|
|
|
|
|
+ },
|
|
|
|
|
+ // 模板列表异步到达:无选中或选中项已失效时默认选第一个,并通过 emit 同步父页面
|
|
|
|
|
+ templates: {
|
|
|
|
|
+ immediate: true,
|
|
|
|
|
+ handler: function(list) {
|
|
|
|
|
+ if (this.serverTemplates.length === 0) return
|
|
|
|
|
+ if (!this.getActiveTemplate()) {
|
|
|
|
|
+ this.applyTemplate(this.serverTemplates[0])
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ serverTemplates: function() {
|
|
|
|
|
+ var list = this.templates || []
|
|
|
|
|
+ var out = []
|
|
|
|
|
+ for (var i = 0; i < list.length; i++) {
|
|
|
|
|
+ if (list[i] && list[i].id) out.push(list[i])
|
|
|
|
|
+ }
|
|
|
|
|
+ return out
|
|
|
|
|
+ },
|
|
|
|
|
+ // 当前选中的管理端模板(无则为 null)
|
|
|
|
|
+ activeTemplate: function() {
|
|
|
|
|
+ if (!this.activeTemplateId) return null
|
|
|
|
|
+ var list = this.serverTemplates
|
|
|
|
|
+ for (var i = 0; i < list.length; i++) {
|
|
|
|
|
+ if (String(list[i].id) === String(this.activeTemplateId)) return list[i]
|
|
|
|
|
+ }
|
|
|
|
|
+ return null
|
|
|
|
|
+ },
|
|
|
|
|
+ // 背景色板:优先用管理端模板的配置,其次用内置七色
|
|
|
|
|
+ bgOptions: function() {
|
|
|
|
|
+ var tpl = this.activeTemplate
|
|
|
|
|
+ if (tpl && tpl.bgColors) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var arr = JSON.parse(tpl.bgColors)
|
|
|
|
|
+ if (arr && arr.length > 0) return arr
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ return this.bgColors
|
|
|
|
|
+ },
|
|
|
|
|
+ // 二维码尺寸:管理端模板配置优先,其次 qrSizeOverride prop
|
|
|
|
|
+ effectiveQrSize: function() {
|
|
|
|
|
+ var tpl = this.activeTemplate
|
|
|
|
|
+ if (tpl && tpl.qrSize && tpl.qrSize > 0) return tpl.qrSize
|
|
|
|
|
+ return this.qrSizeOverride || 150
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
data() {
|
|
data() {
|
|
|
- return {}
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ // 选中的管理端模板 id(未使用管理端模板时为 null)
|
|
|
|
|
+ activeTemplateId: null,
|
|
|
|
|
+ // 可选的背景色(渐变模板下展示)
|
|
|
|
|
+ bgColors: ['#FF8C42', '#6366F1', '#F59E0B', '#10B981', '#FF6B9D', '#1E293B', '#FFFFFF']
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
methods: {
|
|
methods: {
|
|
|
// 海报画布文字按用户字号系数放大(画布有固定排版,放大区间比页面更保守,防止溢出裁切)
|
|
// 海报画布文字按用户字号系数放大(画布有固定排版,放大区间比页面更保守,防止溢出裁切)
|
|
@@ -57,7 +141,43 @@ export default {
|
|
|
if (scale > 1.25) scale = 1.25
|
|
if (scale > 1.25) scale = 1.25
|
|
|
return Math.round(size * scale)
|
|
return Math.round(size * scale)
|
|
|
},
|
|
},
|
|
|
- getColors() {
|
|
|
|
|
|
|
+ // 选中的管理端模板(返回对象或 null)
|
|
|
|
|
+ getActiveTemplate: function() {
|
|
|
|
|
+ if (!this.activeTemplateId) return null
|
|
|
|
|
+ var list = this.serverTemplates
|
|
|
|
|
+ for (var i = 0; i < list.length; i++) {
|
|
|
|
|
+ if (String(list[i].id) === String(this.activeTemplateId)) return list[i]
|
|
|
|
|
+ }
|
|
|
|
|
+ return null
|
|
|
|
|
+ },
|
|
|
|
|
+ // 应用管理端模板:选中 + 同步模板类型/背景色到父页面(背景色取模板色板第一个)
|
|
|
|
|
+ applyTemplate: function(tpl) {
|
|
|
|
|
+ if (!tpl) return
|
|
|
|
|
+ this.activeTemplateId = tpl.id
|
|
|
|
|
+ this.$emit('update:template', tpl.templateKey)
|
|
|
|
|
+ var bg = tpl.mainColor
|
|
|
|
|
+ if (tpl.bgColors) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var arr = JSON.parse(tpl.bgColors)
|
|
|
|
|
+ if (arr && arr.length > 0) bg = arr[0]
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ this.$emit('update:bgColor', bg)
|
|
|
|
|
+ var self = this
|
|
|
|
|
+ this.$nextTick(function() {
|
|
|
|
|
+ setTimeout(self.drawPoster.bind(self), 100)
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ // 海报模板配色(管理端模板配色优先,其次按维度取内置配色)
|
|
|
|
|
+ getColors: function() {
|
|
|
|
|
+ var tpl = this.getActiveTemplate()
|
|
|
|
|
+ if (tpl) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ main: tpl.mainColor || '#F97316',
|
|
|
|
|
+ dark: tpl.darkColor || '#EA580C',
|
|
|
|
|
+ light: tpl.lightColor || '#FFF7ED'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
var map = {
|
|
var map = {
|
|
|
body: { main: '#FF8C42', dark: '#EA580C', light: '#FFF7ED' },
|
|
body: { main: '#FF8C42', dark: '#EA580C', light: '#FFF7ED' },
|
|
|
mind: { main: '#FF6B9D', dark: '#DB2777', light: '#FDF2F8' },
|
|
mind: { main: '#FF6B9D', dark: '#DB2777', light: '#FDF2F8' },
|
|
@@ -76,10 +196,11 @@ export default {
|
|
|
if (template === 'gradient') {
|
|
if (template === 'gradient') {
|
|
|
return { template: 'gradient', bgColor: bgColor || colors.main, main: colors.main, dark: colors.dark, light: colors.light }
|
|
return { template: 'gradient', bgColor: bgColor || colors.main, main: colors.main, dark: colors.dark, light: colors.light }
|
|
|
}
|
|
}
|
|
|
- // 极简模板:深色背景+白色卡片
|
|
|
|
|
|
|
+ // 极简模板:深色背景+白色卡片(管理端模板的 darkColor 作为默认底色)
|
|
|
if (template === 'minimal') {
|
|
if (template === 'minimal') {
|
|
|
- var dark = bgColor || '#1E293B'
|
|
|
|
|
- return { template: 'minimal', bgColor: dark, main: '#F97316', dark: '#EF4444', light: '#F3F4F6' }
|
|
|
|
|
|
|
+ var tpl = this.getActiveTemplate()
|
|
|
|
|
+ var dark = tpl ? (bgColor || colors.dark) : (bgColor || '#1E293B')
|
|
|
|
|
+ return { template: 'minimal', bgColor: dark, main: colors.main, dark: colors.dark, light: colors.light }
|
|
|
}
|
|
}
|
|
|
// 卡片模板:浅色背景+彩色顶栏
|
|
// 卡片模板:浅色背景+彩色顶栏
|
|
|
return { template: 'card', bgColor: bgColor || '#FFFFFF', main: colors.main, dark: colors.dark, light: colors.light }
|
|
return { template: 'card', bgColor: bgColor || '#FFFFFF', main: colors.main, dark: colors.dark, light: colors.light }
|
|
@@ -430,8 +551,8 @@ export default {
|
|
|
ctx.setTextBaseline('top')
|
|
ctx.setTextBaseline('top')
|
|
|
ctx.fillText(this.inviteText || '长按识别二维码 · 查看详情', w / 2, inviteY)
|
|
ctx.fillText(this.inviteText || '长按识别二维码 · 查看详情', w / 2, inviteY)
|
|
|
|
|
|
|
|
- // 二维码 - 可配置尺寸(默认150,可通过 qrSizeOverride 调整)
|
|
|
|
|
- var qrSize = this.qrSizeOverride || 150
|
|
|
|
|
|
|
+ // 二维码 - 可配置尺寸(管理端模板 qrSize 优先,其次 qrSizeOverride prop,默认150)
|
|
|
|
|
+ var qrSize = this.effectiveQrSize
|
|
|
var qrX = (w - qrSize) / 2
|
|
var qrX = (w - qrSize) / 2
|
|
|
var qrY = inviteY + 24
|
|
var qrY = inviteY + 24
|
|
|
|
|
|
|
@@ -586,6 +707,19 @@ export default {
|
|
|
ctx.closePath()
|
|
ctx.closePath()
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ switchTemplate(tpl) {
|
|
|
|
|
+ // 仅支持管理端定义模板(对象);未配置模板时无选择行,不会触发
|
|
|
|
|
+ if (!tpl || typeof tpl !== 'object') return
|
|
|
|
|
+ this.applyTemplate(tpl)
|
|
|
|
|
+ },
|
|
|
|
|
+ switchBgColor(color) {
|
|
|
|
|
+ this.$emit('update:bgColor', color)
|
|
|
|
|
+ var self = this
|
|
|
|
|
+ this.$nextTick(function() {
|
|
|
|
|
+ setTimeout(self.drawPoster.bind(self), 100)
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
savePoster() {
|
|
savePoster() {
|
|
|
var self = this
|
|
var self = this
|
|
|
uni.canvasToTempFilePath({
|
|
uni.canvasToTempFilePath({
|
|
@@ -671,5 +805,40 @@ export default {
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
|
}
|
|
}
|
|
|
.poster-btn.save:active { opacity: 0.85; }
|
|
.poster-btn.save:active { opacity: 0.85; }
|
|
|
|
|
+.poster-tpl-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin: 12rpx 0;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.tpl-label {
|
|
|
|
|
+ margin-right: 20rpx;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+}
|
|
|
|
|
+.tpl-options {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+}
|
|
|
|
|
+.tpl-option {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ margin: 0 4rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+.tpl-dot {
|
|
|
|
|
+ width: 24rpx;
|
|
|
|
|
+ height: 24rpx;
|
|
|
|
|
+ border-radius: 999rpx;
|
|
|
|
|
+ margin-bottom: 4rpx;
|
|
|
|
|
+ border: 1rpx solid rgba(0, 0, 0, 0.08);
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+.tpl-text {
|
|
|
|
|
+ font-size: 14rpx;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+}
|
|
|
|
|
+.tpl-active .tpl-text { color: #F97316; }
|
|
|
|
|
|
|
|
</style>
|
|
</style>
|