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>
|