Files
lucky_shop/pages_tool/card/style.vue
2026-10-19 16:44:02 +08:00

820 lines
26 KiB
Vue
Raw 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="card-detail" :style="[previewVars, { fontFamily: previewDs.fontFamily }]" :class="{ 'qs-square': previewDs.quickStyle === 'square' }">
<view class="page-layer">
<view class="ps-disabled" v-if="relation && !enabled">商家未开启员工端样式自选,暂不可选样式</view>
<!-- 实时预览卡: 与详情页同款 design 渲染(展示当前所选预设的实际落库效果) -->
<view class="card-wrap" v-if="relation">
<view class="card-wrap-bg" :style="previewHeaderBg"></view>
<view class="detail-card" :style="{ backgroundColor: previewDs.bgColor, borderRadius: previewDs.cardRadius + 'px' }"
:class="['layout-' + previewDs.layout, previewDs.cover && previewDs.layout !== 'classic' ? 'has-cover' : '']">
<view v-if="previewDs.cover && previewDs.layout !== 'classic'" class="dc-cover"
:style="{ borderTopLeftRadius: previewDs.cardRadius + 'px', borderTopRightRadius: previewDs.cardRadius + 'px' }"
:class="previewDs.layout === 'compact' ? 'dc-cover-compact' : 'dc-cover-hero'">
<image class="dc-cover-img" :src="$util.img(previewDs.cover)" mode="aspectFill"></image>
</view>
<view class="dc-name" :style="{ color: previewDs.textColor }">{{ relation.name || '未命名名片' }}</view>
<view class="dc-job" v-if="relation.job_title" :style="{ color: previewDs.textSoft, fontSize: (28 * previewDs.fontScaleNum) + 'rpx' }">{{ relation.job_title }}</view>
<view class="dc-company" v-if="relation.company" :style="{ color: previewDs.textSoft, fontSize: (26 * previewDs.fontScaleNum) + 'rpx' }">{{ relation.company }}</view>
<view class="dc-rows" v-if="fieldRows.length > 0">
<view v-for="(row, index) in fieldRows" :key="index" class="field-row"
:class="{ 'field-row-multi': row.multiline }">
<text v-if="row.icon" class="field-icon iconfont" :class="row.icon" :style="{ color: previewDs.textSoft }"></text>
<text v-else class="field-tag" :style="{ color: previewDs.textSoft, fontSize: (26 * previewDs.fontScaleNum) + 'rpx' }">{{ row.label }}</text>
<view class="field-value" :class="{ 'field-value-multi': row.multiline }" :style="{ color: previewDs.textColor, fontSize: (28 * previewDs.fontScaleNum) + 'rpx' }">{{ row.value }}</view>
</view>
</view>
</view>
</view>
<view class="detail-loading" v-if="isLoading">加载中…</view>
<ns-empty v-if="!isLoading && !relation" :isIndex="false"
:text="errorMsg || '卡片不存在或已被删除'"></ns-empty>
<!-- 预设网格: 每个单元格 = 该预设的迷你预览(同源后台 CardTemplatePresets) -->
<view class="preset-section" v-if="relation && enabled">
<text class="ps-title">选择样式<text class="ps-count">(共 {{ presets.length }} 个)</text></text>
<view class="ps-canvas-hint" v-if="isCanvasCard">
当前名片为商家自定义画布设计,保存将不被允许
</view>
<view class="preset-grid">
<view class="preset-cell" v-for="(p, idx) in presets" :key="p.key"
:class="{ active: p.key === selectedKey }" :style="cellActiveStyle(p)"
@tap="selectPreset(p.key)">
<text class="preset-tag" v-if="p.source === 'canvas'">画布式</text>
<!-- 缩略图: 彩色底 + 内嵌白卡。
底色改由样式表 .pt-N(N=格序) 给出: 微信小程序会丢弃内联 background,
只用内联会导致 14 个格子全部回落成同一个兜底蓝。 -->
<view class="preset-thumb" :class="'pt-' + (idx % 14)" :style="cellVars(p)">
<view class="pt-header"></view>
<view class="pt-body" :style="cellBody(p)">
<view class="pt-name" :style="cellText(p)"></view>
<view class="pt-line" :style="cellSoft(p)"></view>
<view class="pt-line pt-short" :style="cellSoft(p)"></view>
</view>
<view v-if="p.key === selectedKey" class="pt-check">
<view class="pt-check-mark"></view>
</view>
</view>
<text class="preset-name">{{ p.name }}</text>
<text class="preset-desc">{{ p.desc }}</text>
<text v-if="p.key === currentKey" class="preset-current">当前</text>
</view>
</view>
<view v-if="!isLoading && presets.length === 0" class="ps-empty">
预设列表为空(后端 design/presets 未返回)
</view>
</view>
</view>
<!-- 底部保存 -->
<view class="ps-footer" v-if="relation && enabled">
<view class="ps-save" :class="{ disabled: !selectedKey || saving || isCanvasCard }" @tap="save">
{{ saving ? '保存中…' : (isCanvasCard ? '不支持修改' : '保存') }}
</view>
</view>
</view>
</template>
<script>
/**
* 名片样式(可选样式页)· 后端 v2 对接
* GET /card/api/design/presets → 与商家后台 CardTemplatePresets 同源的预设列表
* POST /card/api/design/save → 按 preset_key 应用预设并保存到本卡
* 关键点(v2):
* - 预设列表由后端下发, 前端不自建; 数量不写死(当前 14 个, 直接渲染 list.length)
* - save 只写入 10 个字段(theme_color/text_color/bg_color/font_family/font_scale/
* card_radius/header_gradient_end/header_gradient_angle/quick_style/template_id),
* 不覆盖 layout / cover; 预览据此合并(本卡 layout/cover + 预设 10 字段)
* - source==='canvas' 的预设仅改配色; 本卡自身为 canvas 设计时 save 会被拒
* - data.enabled 控制入口显隐(detail.vue 已据此隐藏按钮)
*/
function mixHex(a, b, t) {
const toRgb = h => {
h = (h || '').replace('#', '');
if (h.length === 3) h = h.split('').map(c => c + c).join('');
const n = parseInt(h, 16);
if (isNaN(n)) return [0, 0, 0];
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
};
const [r1, g1, b1] = toRgb(a);
const [r2, g2, b2] = toRgb(b);
const r = Math.round(r1 + (r2 - r1) * t);
const g = Math.round(g1 + (g2 - g1) * t);
const bl = Math.round(b1 + (b2 - b1) * t);
return '#' + [r, g, bl].map(x => x.toString(16).padStart(2, '0')).join('');
}
// save 实际写入的字段(与后端 v2 一致); 预览只叠加这些, 其余沿用本卡既有值
const SAVE_WRITTEN_FIELDS = [
'theme_color', 'text_color', 'bg_color', 'font_family', 'font_scale',
'card_radius', 'header_gradient_end', 'header_gradient_angle', 'quick_style', 'template_id'
];
// 归一化 design(字段缺失/非法回落文档默认值); d 可为 null(旧后端)
function normalizeDesign(d) {
const dd = d || {};
const FONTS = {
system: '-apple-system,BlinkMacSystemFont,"PingFang SC","Helvetica Neue",Arial,sans-serif',
sans: '"Source Han Sans SC","Noto Sans SC","PingFang SC",Helvetica,Arial,sans-serif',
serif: '"Source Han Serif SC","Songti SC",SimSun,Georgia,serif',
rounded: '"Yuanti SC","YouYuan","Quicksand",-apple-system,sans-serif'
};
const isHex = v => /^#[0-9a-fA-F]{3,8}$/.test(v || '');
// 兼容后端可能的多种命名: snake_case / camelCase / 简写
// (后端若下发 camelCase 而这里只读 snake_case, 会全部回落默认蓝 → 14 个格子同色)
const pick = (...keys) => {
for (let i = 0; i < keys.length; i++) {
const v = dd[keys[i]];
if (v !== undefined && v !== null && v !== '') return v;
}
return undefined;
};
const themeRaw = pick('theme_color', 'themeColor', 'theme', 'primary_color', 'primaryColor', 'primary', 'color');
const textRaw = pick('text_color', 'textColor', 'text', 'text_primary', 'textPrimary');
const bgRaw = pick('bg_color', 'bgColor', 'background_color', 'backgroundColor', 'background', 'bg');
const radiusRaw = pick('card_radius', 'cardRadius', 'radius');
const layoutRaw = pick('layout', 'card_layout', 'cardLayout');
const fontRaw = pick('font_family', 'fontFamily', 'font');
const scaleRaw = pick('font_scale', 'fontScale');
const headerEndRaw = pick('header_gradient_end', 'headerGradientEnd', 'gradient_end', 'gradientEnd');
const angleRaw = pick('header_gradient_angle', 'headerGradientAngle', 'gradient_angle', 'gradientAngle');
const theme = isHex(themeRaw) ? themeRaw : '#119dff';
const text = isHex(textRaw) ? textRaw : '#1f2329';
const bg = isHex(bgRaw) ? bgRaw : '#ffffff';
const fontKey = FONTS[fontRaw] ? fontRaw : 'system';
const fontScale = Math.min(160, Math.max(60, Number(scaleRaw) || 100));
const cardRadius = Number(radiusRaw) || 16;
const layout = ['hero', 'compact', 'classic'].includes(layoutRaw) ? layoutRaw : 'classic';
const headerEnd = typeof headerEndRaw === 'string' ? headerEndRaw : '';
const headerAngle = Number(angleRaw) || 150;
return {
themeColor: theme,
textColor: text,
bgColor: bg,
layout: layout,
cover: pick('cover', 'cover_url', 'coverUrl') || '',
fontKey: fontKey,
fontFamily: FONTS[fontKey],
fontScale: fontScale,
cardRadius: cardRadius,
headerGradientEnd: headerEnd,
headerGradientAngle: headerAngle,
quickStyle: pick('quick_style', 'quickStyle') || '',
templateId: pick('template_id', 'templateId', 'template') || '',
source: pick('source') || 'none',
textSoft: mixHex(text, bg, 0.55),
themeSoft: mixHex(theme, '#ffffff', 0.85),
fontScaleNum: fontScale / 100
};
}
/**
* 预设配色兜底: 后端 design 未带合法 theme_color 时(字段名不一致 / 未下发),
* 按预设 key/name/desc 关键词匹配一组与后台预设一致的色。
* 仅用于缩略图观感, 不影响保存(save 仍按 preset_key 走后端)。
*/
const PRESET_FALLBACK = [
{ re: /(极简|留白|minimal|white|blank)/, theme: '#1f2329' },
{ re: /(霓虹|深色|黑|墨|neon|dark|night)/, theme: '#111827' },
{ re: /(紫|purple|violet)/, theme: '#7b61ff' },
{ re: /(绿|green)/, theme: '#12b886' },
{ re: /(橙|暖|orange|warm)/, theme: '#f59f3c' },
{ re: /(红|crimson|red)/, theme: '#ef4444' },
{ re: /(粉|pink|rose)/, theme: '#ec4899' },
{ re: /(金|gold|amber)/, theme: '#d4a017' },
{ re: /(青|teal|cyan)/, theme: '#06b6d4' },
{ re: /(灰|gray|grey)/, theme: '#6b7280' },
{ re: /(渐|gradient)/, theme: '#5b6bff' },
{ re: /(大图|门面|封面|cover)/, theme: '#0b5cff' },
{ re: /(经典|商务|classic|business)/, theme: '#0b5cff' },
{ re: /(蓝|blue|flow|origin|原生)/, theme: '#119dff' }
];
function fallbackTheme(p) {
if (!p) return '';
const hay = String(p.key || '') + ' ' + String(p.name || '') + ' '
+ String(p.title || '') + ' ' + String(p.label || '') + ' ' + String(p.desc || '');
const low = hay.toLowerCase();
for (let i = 0; i < PRESET_FALLBACK.length; i++) {
if (PRESET_FALLBACK[i].re.test(low)) return PRESET_FALLBACK[i].theme;
}
return '';
}
function designVarsFor(s) {
return {
'--card-theme': s.themeColor,
'--card-theme-end': s.headerGradientEnd || s.themeColor,
'--card-text': s.textColor,
'--card-text-soft': s.textSoft,
'--card-bg': s.bgColor,
'--card-radius': s.cardRadius + 'px',
'--card-font': s.fontFamily,
'--card-font-scale': s.fontScaleNum
};
}
function headerBgFor(s) {
if (s.headerGradientEnd) {
return 'linear-gradient(' + s.headerGradientAngle + 'deg, ' + s.themeColor + ', ' + s.headerGradientEnd + ')';
}
return 'linear-gradient(150deg, ' + s.themeColor + ' 0%, ' + s.themeColor + ' 62%, rgba(0,0,0,0.28) 160%)';
}
export default {
data() {
return {
cardId: '',
relation: null,
// 名片设计摘要(后端 /card/api/card/detail 顶层 design, 旧后端无此键时为 null)
design: null,
// 后端下发的预设列表(同源后台 CardTemplatePresets, 数量不写死)
presets: [],
// 商家是否开启「员工端样式自选」(presets 响应 enabled)
enabled: true,
// 当前选中的预设 key(初始 = 本卡当前 template_id)
selectedKey: '',
isLoading: true,
errorMsg: '',
saving: false
};
},
computed: {
fieldRows() {
const r = this.relation;
if (!r) return [];
const rows = [];
const push = (label, value, action, icon) => {
if (value !== undefined && value !== null && String(value).trim() !== '') {
rows.push({ label, value: String(value), action: action || null, icon: icon || '' });
}
};
push('手机', r.mobile, { type: 'call', text: '拨打' }, 'icon-dianhua');
push('电话', r.phone, { type: 'call', text: '拨打' }, 'icon-dianhua');
push('邮箱', r.email, { type: 'copy', text: '复制' });
push('微信', r.wechat, { type: 'copy', text: '复制' }, 'icon-weixin');
push('网站', r.website);
push('地址', [r.country, r.province, r.city, r.address].filter(Boolean).join(' '), null, 'icon-dizhi');
push('邮编', r.postal_code);
(r.mobiles_list || []).forEach((m, i) => {
push('手机' + (i + 2), m, { type: 'call', text: '拨打' }, 'icon-dianhua');
});
(r.socials_list || []).forEach(s => {
push(s.label || s.type, s.value, { type: 'copy', text: '复制' });
});
(r.custom_list || []).forEach(c => {
push(c.label, c.value);
});
if (r.bio && String(r.bio).trim() !== '') {
rows.push({ label: '简介', value: String(r.bio), action: null, multiline: true });
}
return rows;
},
// 本卡当前 design 的 template_id(用于标记"当前")
currentKey() {
return (this.design && this.design.template_id) || '';
},
// 本卡自身为画布设计时, save 会被后端拒
isCanvasCard() {
return !!(this.design && this.design.source === 'canvas');
},
activePreset() {
return this.presets.find(p => p.key === this.selectedKey) || null;
},
// 实时预览用的 design: 以本卡既有 design 为底, 仅叠加 save 实际写入的 10 个字段
// (layout/cover 不被 save 覆盖, 预览须与落库一致, 避免误导)
previewDesign() {
const base = this.design || {};
const p = this.activePreset ? this.activePreset.design : null;
if (!p) return base;
const merged = Object.assign({}, base);
SAVE_WRITTEN_FIELDS.forEach(k => {
if (p[k] !== undefined) merged[k] = p[k];
});
return merged;
},
previewDs() {
return normalizeDesign(this.previewDesign);
},
previewVars() {
return designVarsFor(this.previewDs);
},
previewHeaderBg() {
// 同 cellVars: 小程序内联 `background` 渐变不生效, 用 backgroundColor 纯色兜住
return { backgroundColor: this.previewDs.themeColor };
}
},
methods: {
/**
* 预设缩略图的配色。
*
* 为什么不用 CSS 变量: 微信小程序的渲染层不支持在内联 style 里定义自定义属性
* (`style="--card-bg:#fff"`), 整条 style 会被丢弃 → var(--card-bg) 全部回落默认值,
* 表现就是「小程序端样式缩略图全是白底黑条灰线, 没有颜色」(H5 正常)。
* 这里改为直接输出具体属性 + 具体色值, 三端一致。
*/
cellDs(p) {
let raw = p && p.design;
// design 可能是 JSON 字符串(部分后端把设计摘要存 JSON 列)
if (typeof raw === 'string') {
try { raw = JSON.parse(raw); } catch (e) { raw = null; }
}
// 合并: 预设项顶层字段(扁平下发) + design 子对象(优先), 两种结构都能读到色值
raw = Object.assign({}, p || {}, (raw && typeof raw === 'object') ? raw : {});
const s = normalizeDesign(raw);
// 缩略图颜色以「预设名称语义」为准(与后台/H5 的预设一致)。
// 后端可能给所有预设下发同一个默认 theme_color(合法但是同一种蓝),
// 若只在"没有合法色值"时才兜底, 就会 14 个格子全蓝 —— 所以这里名称命中即覆盖。
const fb = fallbackTheme(p);
if (fb) {
s.themeColor = fb;
s.headerGradientEnd = ''; // 纯色, 避免后端残留的渐变把兜底色盖掉
s.themeSoft = mixHex(fb, '#ffffff', 0.85);
}
return s;
},
cellVars(p) {
const s = this.cellDs(p);
// 底色不再走内联(小程序端实测内联 background/backgroundColor 均被丢,
// 导致 14 格回落同一个 CSS 兜底蓝), 改由样式表按格序上色(见 .preset-cell:nth-child)。
// 这里只保留圆角这类无颜色语义的属性。
const radius = Math.min(Number(s.cardRadius) || 16, 18);
return { borderRadius: radius + 'px' };
},
// 内嵌白卡: 底色取预设 bg_color, 在彩色底上形成"浮起的名片"层次
cellBody(p) {
const s = this.cellDs(p);
return { backgroundColor: s.bgColor };
},
cellText(p) {
return { backgroundColor: this.cellDs(p).textColor };
},
cellSoft(p) {
return { backgroundColor: this.cellDs(p).textSoft };
},
// 选中描边同样交给样式表(按格序), 避免内联失效
cellActiveStyle() {
return null;
},
onLoad(options) {
this.cardId = decodeURIComponent(options.card_id || '');
this.loadDetail();
this.loadPresets();
},
onReady() {
// H5 uni-page-head 挂载早于路由 meta 就绪会回落默认标题 "title", onReady 再兜底一次
uni.setNavigationBarTitle({ title: '名片' });
// #ifdef H5
document.title = '名片';
// #endif
},
loadDetail() {
if (!this.cardId) {
this.isLoading = false;
this.errorMsg = '缺少名片编号';
return;
}
this.$cardApi.cardDetail({
data: { card_id: this.cardId },
success: res => {
const data = res.data || {};
if (res.code == 0) {
this.relation = data.relation || null;
this.design = data.design || null;
this.initSelected();
} else {
this.errorMsg = res.message || '卡片不存在或已被删除';
this.relation = null;
}
this.isLoading = false;
},
fail: () => {
this.isLoading = false;
this.errorMsg = '网络异常,请稍后重试';
}
});
},
loadPresets() {
this.$cardApi.cardDesignPresets({
success: res => {
const data = res.data || {};
// 商家未开启样式自选时前端隐藏入口(默认 true, 失败开放)
this.enabled = data.enabled !== false;
this.presets = Array.isArray(data.list) ? data.list : [];
this.initSelected();
},
fail: () => {
this.enabled = true;
this.presets = [];
}
});
},
// 两路加载都到位后, 若本卡 template_id 命中某个预设, 默认选中它
initSelected() {
if (this.selectedKey || !this.currentKey || this.presets.length === 0) return;
const hit = this.presets.find(p => p.key === this.currentKey);
if (hit) this.selectedKey = hit.key;
},
selectPreset(key) {
this.selectedKey = key;
},
save() {
if (!this.selectedKey || this.saving) return;
if (this.isCanvasCard) {
uni.showToast({ title: '该名片使用商家自定义画布设计,暂不支持员工端修改', icon: 'none' });
return;
}
this.saving = true;
this.$cardApi.cardDesignSave({
data: { card_id: this.cardId, preset_key: this.selectedKey },
success: res => {
this.saving = false;
if (res.code == 0) {
uni.showToast({ title: '已保存', icon: 'success' });
// 返回详情页, 其 onShow 会重新拉取 design 并刷新生效
setTimeout(() => uni.navigateBack(), 600);
} else if (res.message) {
uni.showToast({ title: res.message, icon: 'none' });
}
},
fail: () => {
this.saving = false;
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
}
});
}
}
};
</script>
<style lang="scss" scoped>
.card-detail {
min-height: 100vh;
background-color: #ffffff;
// 底部预留高度须 ≥ 固定底栏(按钮 88 + 上下 padding + 安全区), 否则滚到底最后一行会被遮住
padding: 0 0 240rpx;
box-sizing: border-box;
font-family: var(--card-font, -apple-system, "PingFang SC", "Helvetica Neue", Arial, sans-serif);
}
.page-layer {
position: relative;
}
.ps-disabled {
margin: 24rpx;
padding: 28rpx;
text-align: center;
font-size: 26rpx;
color: #8a94a6;
background-color: #f5f7fa;
border-radius: 12rpx;
}
// 预览卡(与详情页同款 design 渲染)
.card-wrap {
position: relative;
padding-top: 24rpx;
}
.card-wrap-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 48rpx;
z-index: 0;
background-color: var(--card-theme, #2aa5ff);
border-bottom-left-radius: 240rpx 44rpx;
border-bottom-right-radius: 240rpx 44rpx;
pointer-events: none;
}
.detail-card {
position: relative;
z-index: 1;
margin: 0 24rpx;
background-color: var(--card-bg, #ffffff);
border-radius: var(--card-radius, 20rpx);
padding: 40rpx 36rpx 32rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 30, 80, 0.06);
.dc-cover {
margin: -40rpx -36rpx 24rpx;
border-top-left-radius: var(--card-radius, 20rpx);
border-top-right-radius: var(--card-radius, 20rpx);
overflow: hidden;
.dc-cover-img {
display: block;
width: 100%;
}
}
.dc-cover-hero .dc-cover-img { height: 200rpx; }
.dc-cover-compact .dc-cover-img { height: 90rpx; }
.dc-name {
font-size: 44rpx;
font-weight: 700;
color: var(--card-text, #1a1a1a);
line-height: 1.3;
}
.dc-job {
margin-top: 6rpx;
font-size: calc(28rpx * var(--card-font-scale, 1));
line-height: 1.3;
color: var(--card-text-soft, #4a4a4a);
}
.dc-company {
margin-top: 2rpx;
font-size: calc(26rpx * var(--card-font-scale, 1));
line-height: 1.3;
color: var(--card-text-soft, #9aa4b2);
}
.dc-rows {
margin-top: 56rpx;
}
&.layout-compact {
padding-top: 28rpx;
.dc-rows { margin-top: 28rpx; }
.field-row { padding: 8rpx 0; }
.dc-name { font-size: 40rpx; }
}
.field-row {
display: flex;
align-items: center;
line-height: 1.2;
padding: 12rpx 0;
&.field-row-multi {
align-items: flex-start;
}
.field-icon {
width: 40rpx;
margin-right: 18rpx;
font-size: 32rpx;
line-height: 1;
color: var(--card-text-soft, #8a94a6);
text-align: center;
flex-shrink: 0;
}
.field-tag {
min-width: 40rpx;
margin-right: 18rpx;
font-size: calc(26rpx * var(--card-font-scale, 1));
line-height: 1.2;
color: var(--card-text-soft, #9aa4b2);
flex-shrink: 0;
}
.field-value {
flex: 1;
min-width: 0;
font-size: calc(28rpx * var(--card-font-scale, 1));
line-height: 1.25;
color: var(--card-text, #333333);
word-break: break-all;
}
.field-value-multi {
line-height: 1.4;
white-space: pre-wrap;
}
}
}
/* 预设网格(3 列自适应, 14 个不再过长) */
.preset-section {
margin: 32rpx 24rpx 0;
.ps-title {
display: block;
margin-bottom: 16rpx;
font-size: 30rpx;
font-weight: 600;
color: #1f2329;
}
.ps-count {
font-size: 24rpx;
font-weight: 400;
color: #8a94a6;
}
.ps-canvas-hint {
margin-bottom: 16rpx;
font-size: 24rpx;
color: #f5a623;
}
.preset-grid {
display: flex;
flex-wrap: wrap;
}
.preset-cell {
position: relative;
// 自建堆叠上下文: 把格子内的绝对定位元素(角标/选中钩)限制在本格内,
// 避免其 z-index 逃逸到固定底栏之上
z-index: 0;
width: 32%;
margin: 0 2% 16rpx 0;
background-color: #ffffff;
border: 2rpx solid #e8ebee;
border-radius: 16rpx;
padding: 14rpx;
box-sizing: border-box;
&:nth-child(3n) {
margin-right: 0;
}
&.active {
border-color: var(--card-theme, #2aa5ff);
box-shadow: 0 6rpx 18rpx rgba(42, 165, 255, 0.18);
}
.preset-tag {
position: absolute;
top: 8rpx;
left: 8rpx;
z-index: 2;
padding: 2rpx 10rpx;
font-size: 20rpx;
color: #ffffff;
background-color: rgba(0, 0, 0, 0.45);
border-radius: 8rpx;
}
.preset-thumb {
position: relative;
height: 160rpx;
// 底色由 .pt-N 提供(见下方); 这里不能设兜底色, 否则会盖掉 .pt-N
border-radius: 12rpx;
overflow: hidden;
// 顶部彩色露出高度(底色由 cellVars 的渐变铺满整块)
.pt-header {
height: 24rpx;
}
// 内嵌白卡: 浮在主题色底上, 左右下留边露出彩色(对齐 H5 缩略图层次)
.pt-body {
margin: 0 12rpx 18rpx;
padding: 12rpx 14rpx;
background-color: #ffffff;
border-radius: 8rpx;
box-sizing: border-box;
}
.pt-name {
height: 14rpx;
width: 56%;
border-radius: 7rpx;
background-color: var(--card-text, #1a1a1a);
}
.pt-line {
height: 7rpx;
width: 80%;
margin-top: 12rpx;
border-radius: 4rpx;
background-color: var(--card-text-soft, #9aa4b2);
}
.pt-short {
width: 50%;
}
.pt-check {
position: absolute;
right: 10rpx;
bottom: 10rpx;
width: 34rpx;
height: 34rpx;
border-radius: 50%;
background-color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
.pt-check-mark {
width: 13rpx;
height: 6rpx;
margin-top: -4rpx;
border-left: 3rpx solid #2aa5ff;
border-bottom: 3rpx solid #2aa5ff;
transform: rotate(-45deg);
}
}
}
.preset-name {
display: block;
margin-top: 12rpx;
font-size: 24rpx;
font-weight: 600;
color: #1f2329;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.preset-desc {
display: block;
margin-top: 4rpx;
font-size: 20rpx;
color: #8a94a6;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.preset-current {
position: absolute;
top: 8rpx;
right: 8rpx;
padding: 2rpx 10rpx;
font-size: 20rpx;
color: #ffffff;
background-color: rgba(0, 0, 0, 0.35);
border-radius: 8rpx;
}
}
/* ===== 缩略图底色 =====
* 按后台 14 个预设的固定顺序写死。
* 为什么不用内联/渐变: 微信小程序会丢弃内联 background(渐变/纯色均可能失效),
* 之前 14 个格子全部回落成同一个蓝就是因此。样式表 class 是唯一稳定的方式。
* 后台预设顺序/配色调整时, 同步修改这里即可。
*/
.pt-0 { background-color: #119dff; } /* 原生样式 */
.pt-1 { background-color: #12b886; } /* 商务绿 */
.pt-2 { background-color: #f59f3c; } /* 暖橙 */
.pt-3 { background-color: #7b61ff; } /* 紫罗兰 */
.pt-4 { background-color: #0b5cff; } /* 经典商务 */
.pt-5 { background-color: #1f2329; } /* 极简留白 */
.pt-6 { background-color: #111827; } /* 深色霓虹 */
.pt-7 { background-color: #f59f3c; } /* 暖调名片 */
.pt-8 { background-color: #0b5cff; } /* 大图门面 */
.pt-9 { background-color: #7aa7c7; }
.pt-10 { background-color: #6cc24a; }
.pt-11 { background-color: #111827; }
.pt-12 { background-color: #d4a017; } /* 黑金质感: 金 */
.pt-13 { background-color: #ef4444; } /* 朱砂活力: 红 */
.ps-empty {
margin-top: 20rpx;
font-size: 24rpx;
color: #f5a623;
}
}
.detail-loading {
text-align: center;
color: #999999;
font-size: 26rpx;
padding: 120rpx 0;
}
/* 底部保存条 */
.ps-footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
// 必须高于内容层: 否则预设格子里的绝对定位元素(如 .preset-tag z-index:2)
// 会"浮"到固定底栏之上, 只露出角标而卡片本体被遮住
z-index: 100;
padding: 20rpx 24rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: #ffffff;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.04);
.ps-save {
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 44rpx;
background-color: #2aa5ff;
color: #ffffff;
font-size: 32rpx;
&.disabled {
background-color: #c8d6e5;
}
}
}
</style>