813 lines
26 KiB
Vue
813 lines
26 KiB
Vue
<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();
|
||
},
|
||
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>
|