Files
lgp-wx-plus/components/theme/ThemeCanvasCard.vue

359 lines
10 KiB
Vue
Raw Permalink Normal View History

2026-08-19 08:17:18 +08:00
<template>
<!-- 按 card_scheme 图层还原商品卡;没有方案时走默认插槽 -->
<view v-if="!schemeLayers.length" class="tcc-fallback">
<slot />
</view>
<view v-else class="tcc" :style="shellStyle">
<view class="tcc-stage">
<view
v-for="layer in visibleLayers"
:key="layer.id"
class="tcc-layer"
:style="layerStyle(layer)"
>
<view
v-if="layer.type === 'shape'"
class="tcc-fill"
:style="{ background: fillOf(layer) }"
/>
<image
v-else-if="layer.type === 'photo' && photoOf(layer)"
class="tcc-photo"
:src="photoOf(layer)"
mode="aspectFill"
/>
<view
v-else-if="layer.type === 'photo'"
class="tcc-photo-ph"
:style="{ background: primary }"
/>
<text
v-else-if="layer.type === 'text'"
class="tcc-txt"
:style="{ color: textColor(layer), fontSize: fontSize(layer) }"
>
<text
v-for="(run, ri) in textRuns(layer)"
:key="ri"
:style="{ fontFamily: faceOf(layer, run.kind) }"
>{{ run.text }}</text>
</text>
<view
v-else-if="layer.type === 'deco'"
class="tcc-deco"
:class="'tcc-deco--' + (layer.asset || 'gold-corner')"
:style="{ color: toneOf(layer) }"
/>
</view>
</view>
</view>
</template>
<script>
import { getActiveLayout, getActiveSchemes, getActiveVars } from '@/utils/theme'
export default {
name: 'ThemeCanvasCard',
props: {
cover: { type: String, default: '' },
name: { type: String, default: '' },
price: { type: String, default: '' },
gallery: { type: String, default: '' },
scheme: { type: String, default: '' },
caption: { type: String, default: '' },
// 页码:没传 scheme 时读 pages.{page}.card_scheme
page: { type: String, default: '' },
// 列表/搜索原页面没有报价,画布里 bind=price 的图层也要跳过
showPrice: { type: Boolean, default: true },
},
computed: {
vars() {
return getActiveVars()
},
primary() {
return this.vars['--color-primary'] || '#B08D57'
},
schemeLayers() {
const layout = getActiveLayout() || {}
const pages = layout.pages || {}
const pageKey = this.page || ''
const pageScheme = pageKey && pages[pageKey] ? pages[pageKey].card_scheme : ''
const code = this.scheme || pageScheme || layout.card_scheme || ''
const map = getActiveSchemes()
const item = map[code]
return (item && item.layers) || []
},
visibleLayers() {
return this.schemeLayers
.filter((l) => l.visible !== false)
.filter((l) => this.showPrice || l.bind !== 'price')
.slice()
.sort((a, b) => (a.z || 0) - (b.z || 0))
},
shellStyle() {
return {
background: this.vars['--color-bg'] || '#FAF7F2',
}
},
},
methods: {
2026-08-20 19:15:41 +08:00
/**
* 画布坐标按 340×420 设计稿写成百分比,列变窄时图层一起缩小,不会撑破格子。
*/
2026-08-19 08:17:18 +08:00
layerStyle(layer) {
return {
2026-08-20 19:15:41 +08:00
left: (((layer.x || 0) / 340) * 100) + '%',
top: (((layer.y || 0) / 420) * 100) + '%',
width: (((layer.w || 40) / 340) * 100) + '%',
height: (((layer.h || 40) / 420) * 100) + '%',
2026-08-19 08:17:18 +08:00
transform: 'rotate(' + (layer.rotate || 0) + 'deg)',
zIndex: layer.z || 1,
}
},
tokenColor(name) {
const key = '--color-' + String(name || 'primary').replace(/_/g, '-')
return this.vars[key] || this.primary
},
fillOf(layer) {
const fill = layer.fill || 'surface'
if (String(fill).indexOf('#') === 0) return fill
return this.tokenColor(fill)
},
/** 配饰跟主题 token,禁止纯黑兜底 */
toneOf(layer) {
return this.tokenColor(layer.tone || 'primary')
},
photoOf(layer) {
if (layer.bind === 'gallery') return this.gallery || this.cover
if (layer.src) return layer.src
return this.cover
},
textOf(layer) {
if (layer.bind === 'name') return this.name || '型号'
if (layer.bind === 'price') return this.price || ''
if (this.caption && (!layer.bind || layer.bind === 'custom')) return this.caption
return layer.text || ''
},
faceOf(layer, kind) {
const key = kind === 'zh' ? (layer.font_zh || 'sans') : (layer.font_en || 'sans')
const zh = {
sans: 'PingFang SC, Microsoft YaHei, sans-serif',
serif: 'Songti SC, Noto Serif SC, serif',
kai: 'Kaiti SC, STKaiti, serif',
script: 'Kaiti SC, STKaiti, serif',
}
const en = {
sans: '-apple-system, Helvetica, sans-serif',
serif: 'Georgia, Times New Roman, serif',
kai: 'Georgia, serif',
script: 'Snell Roundhand, Segoe Script, cursive',
}
return kind === 'zh' ? (zh[key] || zh.sans) : (en[key] || en.sans)
},
textRuns(layer) {
const raw = this.textOf(layer)
const parts = []
String(raw).split(/([\u3400-\u9fff]+)/).forEach((chunk) => {
if (!chunk) return
parts.push({ text: chunk, kind: /[\u3400-\u9fff]/.test(chunk) ? 'zh' : 'en' })
})
return parts.length ? parts : [{ text: raw, kind: 'en' }]
},
fontSize(layer) {
const map = { title: '32rpx', body: '28rpx', caption: '22rpx', price: '30rpx' }
return map[layer.font] || '28rpx'
},
textColor(layer) {
if (layer.font === 'price') return this.vars['--color-price'] || this.primary
return this.vars['--color-text'] || '#3F2F24'
},
},
}
</script>
<style lang="scss" scoped>
2026-08-20 19:15:41 +08:00
.tcc-fallback {
width: 100%;
}
2026-08-19 08:17:18 +08:00
.tcc {
position: relative;
width: 100%;
2026-08-20 19:15:41 +08:00
height: 0;
padding-bottom: 123.53%;
2026-08-19 08:17:18 +08:00
overflow: hidden;
}
.tcc-stage {
2026-08-20 19:15:41 +08:00
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
2026-08-19 08:17:18 +08:00
}
.tcc-layer {
position: absolute;
box-sizing: border-box;
}
.tcc-fill,
.tcc-photo,
.tcc-photo-ph,
.tcc-txt,
.tcc-deco {
width: 100%;
height: 100%;
}
.tcc-photo {
display: block;
}
.tcc-photo-ph {
opacity: 0.35;
}
.tcc-txt {
overflow: hidden;
line-height: 1.3;
white-space: nowrap;
}
.tcc-deco--paper-edge {
border: 2rpx dashed currentColor;
background: repeating-linear-gradient(-8deg, currentColor 0 2rpx, transparent 2rpx 10rpx);
box-shadow: inset 0 0 0 12rpx currentColor, inset 0 0 24rpx currentColor;
opacity: 0.28;
}
.tcc-deco--pearl-line,
.tcc-deco--folio-line,
.tcc-deco--double-frame {
box-shadow: inset 0 0 0 2rpx currentColor, inset 0 0 0 8rpx transparent, inset 0 0 0 12rpx currentColor;
background: linear-gradient(135deg, currentColor, transparent 42%);
opacity: 0.22;
}
.tcc-deco--walnut-frame {
border: 22rpx solid currentColor;
background:
repeating-linear-gradient(90deg, currentColor 0 5rpx, transparent 5rpx 14rpx),
repeating-linear-gradient(0deg, currentColor 0 2rpx, transparent 2rpx 18rpx);
box-shadow: inset 0 0 0 8rpx currentColor, inset 0 0 28rpx currentColor, 0 6rpx 0 currentColor;
opacity: 0.55;
}
.tcc-deco--gold-foil {
border: 3rpx solid currentColor;
background: linear-gradient(125deg, transparent, currentColor 28%, transparent 48%, currentColor 72%, transparent);
box-shadow: inset 0 0 0 10rpx currentColor, 0 2rpx 0 currentColor;
opacity: 0.52;
}
.tcc-deco--washi-1 {
background: currentColor;
opacity: 0.38;
}
.tcc-deco--botanical {
border-radius: 60% 40% 70% 30%;
background: currentColor;
opacity: 0.3;
}
.tcc-deco--museum-mat {
box-shadow: inset 0 0 0 28rpx currentColor;
opacity: 0.2;
}
.tcc-deco--wax-seal {
border-radius: 999rpx;
background:
radial-gradient(circle at 32% 28%, #fff 0 14%, currentColor 58%),
repeating-conic-gradient(from 16deg, currentColor 0 14deg, transparent 14deg 28deg);
box-shadow: inset 0 0 0 8rpx #fff, 0 8rpx 14rpx currentColor;
opacity: 0.9;
}
.tcc-deco--ribbon {
background: currentColor;
opacity: 0.48;
}
.tcc-deco--newsprint {
opacity: 0.12;
background: repeating-linear-gradient(-12deg, currentColor 0 2rpx, transparent 2rpx 10rpx);
}
.tcc-deco--gold-corner,
.tcc-deco--corner-bracket {
border: 2rpx solid currentColor;
opacity: 0.55;
}
.tcc-deco--handwriting {
border-bottom: 2rpx solid currentColor;
opacity: 0.5;
}
.tcc-deco--arch-mat {
border: 4rpx solid currentColor;
border-radius: 200rpx 200rpx 12rpx 12rpx;
opacity: 0.55;
}
.tcc-deco--inset-shadow {
box-shadow: inset 0 6rpx 20rpx currentColor;
opacity: 0.28;
}
.tcc-deco--torn-edge {
border: 2rpx solid currentColor;
opacity: 0.28;
}
.tcc-deco--film-sprocket {
border-left: 16rpx dotted currentColor;
border-right: 16rpx dotted currentColor;
box-shadow: inset 0 0 0 2rpx currentColor;
opacity: 0.55;
}
.tcc-deco--emboss-plate {
border: 2rpx solid currentColor;
opacity: 0.55;
}
.tcc-deco--circle-crop {
border: 6rpx solid currentColor;
border-radius: 999rpx;
opacity: 0.65;
}
.tcc-deco--vignette {
background: radial-gradient(ellipse at center, transparent 40%, currentColor 100%);
opacity: 0.28;
}
.tcc-deco--folio-number {
border: 3rpx solid currentColor;
box-shadow: inset 0 0 0 6rpx currentColor;
letter-spacing: 2rpx;
opacity: 0.7;
}
.tcc-deco--stamp-ring {
border: 4rpx dashed currentColor;
border-radius: 999rpx;
opacity: 0.5;
}
.tcc-deco--lace-corner {
background:
radial-gradient(circle at 0 0, currentColor 0 16rpx, transparent 17rpx),
radial-gradient(circle at 100% 0, currentColor 0 16rpx, transparent 17rpx),
radial-gradient(circle at 0 100%, currentColor 0 16rpx, transparent 17rpx),
radial-gradient(circle at 100% 100%, currentColor 0 16rpx, transparent 17rpx);
box-shadow: inset 0 0 0 2rpx currentColor, inset 0 0 0 12rpx transparent, inset 0 0 0 16rpx currentColor;
border-radius: 8rpx 32rpx;
opacity: 0.55;
}
.tcc-deco--thin-rule {
background: currentColor;
opacity: 0.65;
}
.tcc-deco--shadow-deck {
background: currentColor;
opacity: 0.16;
}
.tcc-deco--cap-rail {
background: currentColor;
opacity: 0.7;
}
.tcc-deco--gutter-fold {
background: currentColor;
opacity: 0.22;
}
.tcc-deco--pearl-bead {
border: 8rpx dotted currentColor;
border-radius: 24rpx;
opacity: 0.4;
}
.tcc-deco--vert-caption {
border-left: 2rpx solid currentColor;
opacity: 0.5;
}
</style>