Files
qitongxue-wx/src/components/card/CardTile.vue
2026-09-29 10:57:55 +08:00

758 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.
<script setup lang="ts">
/**
* 首页**唯一**卡片渲染组件(P3 瓷片区重做)。
*
* 设计:**版式(template)与皮肤(variant)正交**
* - 版式:iconTop 图标墙 / iconLeft 行卡 / lead 通栏主卡 / duo 双列渐变卡 / group 分组容器卡;
* - 皮肤:porcelain / tint / glass / solid / line(沿用 P2,已验过);
* - 色系:gold / blue / mint / rose / violet(tone,走 CSS 变量注入)。
* 根元素同时带 `.ct--<template>` 与 `.ct--skin-<variant>` 两个 class,二者互不耦合。
*
* `group` 版式用默认插槽承载子卡网格;其余版式为单卡自渲染。
* 本组件不涉及浮层,**禁止写裸 z-index**(S15)。
*/
import { computed } from 'vue'
import Sticker from '@/components/Sticker.vue'
import { CARD_TONES } from '@/config/card-styles'
const props = withDefaults(defineProps<{
/** 版式(见 CARD_TEMPLATES) */
template?: string
/** 皮肤(见 CARD_VARIANTS) */
variant?: string
/** 色系(见 CARD_TONES) */
tone?: string
/** wot 图标名 */
icon?: string
/** 标题 */
title?: string
/** 描述 */
desc?: string
/** 右上角角标文案(locked 时被「未解锁」覆盖) */
badge?: string
/** 尺寸 */
size?: 'sm' | 'md' | 'lg'
/** 是否未解锁(灰化 + 角标「未解锁」) */
locked?: boolean
/** 插画 / 贴纸 key(见 Sticker.vue 的 SVGS;用于 lead/duo/group/iconTop 版式) */
illustration?: string
/** 行卡(iconLeft)右端是否显示箭头(用于「我的」菜单行) */
arrow?: boolean
/** 分组容器卡(group)子卡网格列数(默认 2;工具楼层用 4,纵向列表用 1) */
columns?: number
/**
* group 版式是否由本组件自带子卡网格(默认 true)。
* 置 false 时渲染裸 slot,由**调用方用自己的 scoped 网格**承接 —— P5 实证:
* 同样的子卡在工作台自管网格(.wb-grid)里横排正常,在组件内联 style 的 .ct__grid 里
* 却逐字竖排(MP 环境差异),故提供逃生口让调用方复用「已被真机验证」的网格写法。
*/
grid?: boolean
// ===== v2 版式原子(全部**可选**,缺省 = 现状,零回归)=====
/** 角标位置:`tr` 右上贴边(默认,除 tile 外)/ `tl` 左上小 pill(tile 默认,参考图 B) */
badgePos?: 'tr' | 'tl'
/** 图标块形状:`square` 圆角方(默认)/ `circle` 圆形(参考图 C1) */
iconShape?: 'square' | 'circle'
/** 插画位置:`top` 顶部(默认,除 duo/tile 外)/ `rightBottom` 右下大插画(duo/tile 默认,参考图 C2/B) */
artPos?: 'top' | 'rightBottom'
/** 胶囊 CTA 文案(非空时在标题下方渲染 `.ct__cta`;默认空串不渲染,参考图 C4) */
cta?: string
/** group 版式子卡网格是否用 mosaic(2 列 + 通栏行;默认 false = 按 `columns`)。参考图 A */
mosaic?: boolean
/** 本卡作为容器内「通栏行」渲染(根元素加 `.ct__row`,跨整行;默认 false) */
row?: boolean
/** group 版式右上胶囊文案(非空时渲染 `.ct__pill`;默认空串不渲染) */
pill?: string
}>(), {
template: 'iconTop',
variant: 'porcelain',
tone: 'gold',
icon: '',
title: '',
desc: '',
badge: '',
size: 'md',
locked: false,
illustration: '',
arrow: false,
columns: 2,
grid: true,
})
/** 色系元数据(未知 tone 回落首个,绝不抛错) */
const toneMeta = computed(() => CARD_TONES.find(t => t.id === props.tone) || CARD_TONES[0])
/** 色系 → CSS 变量(供各 skin / 版式 class 消费) */
const toneVars = computed<Record<string, string>>(() => ({
'--ct-from': toneMeta.value.from,
'--ct-to': toneMeta.value.to,
'--ct-accent': toneMeta.value.accent,
'--ct-tint': toneMeta.value.tintBg,
'--ct-icon-bg': toneMeta.value.iconBg,
'--ct-line': toneMeta.value.lineBorder,
// 强渐变(solid 皮肤 / duo 版式专用):饱和端点,避免用 accent(文字深色)起笔而发暗
'--ct-grad-from': toneMeta.value.gradFrom,
'--ct-grad-to': toneMeta.value.gradTo,
// 角标:随色系派生(高对比),勿用固定金色(会与 gold 卡同色不可见)
'--ct-badge-bg': toneMeta.value.badgeBg,
'--ct-badge-fg': toneMeta.value.badgeFg,
// 彩色环境投影(参考图质感关键:随本卡色相的低透明色,替代灰黑阴影)
'--ct-shadow': toneMeta.value.shadow,
// 层叠卡(stack)两层厚度色
'--ct-layer': toneMeta.value.layer,
'--ct-layer-soft': toneMeta.value.layerSoft,
}))
/** 深色皮肤/强渐变版式用白图标 */
const darkSkin = computed(() => props.variant === 'solid' || props.template === 'duo')
const iconColor = computed(() => {
if (props.locked)
return '#BDA794'
return darkSkin.value ? '#FFFFFF' : toneMeta.value.accent
})
/** 插画尺寸随 size 放大(lg ≥ 128rpx) */
const artSize = computed(() => (props.size === 'lg' ? 128 : props.size === 'sm' ? 72 : 96))
/** 图标墙(iconTop)插画尺寸(rpx;与放大的图标块同档,参考图插画占比更大) */
const iconArtPx = computed(() => (props.size === 'lg' ? 120 : props.size === 'sm' ? 68 : 96))
/** 角标文案:未解锁优先 */
const badgeText = computed(() => (props.locked ? '未解锁' : props.badge))
/**
* v2 版式原子的「生效值」:显式 prop 优先,否则按版式给默认(缺省即现状)。
* - 角标:tile 默认左上小 pill(参考图 B),其余右上贴边(现状);
* - 插画:duo(参考图 C2)与 tile(参考图 B)默认右下大插画,其余顶部(现状);
* - 图标形状:默认圆角方(现状)。
*/
const effBadgePos = computed<'tr' | 'tl'>(() => props.badgePos || (props.template === 'tile' ? 'tl' : 'tr'))
const effIconShape = computed<'square' | 'circle'>(() => props.iconShape || 'square')
const effArtPos = computed<'top' | 'rightBottom'>(() =>
props.artPos || ((props.template === 'duo' || props.template === 'tile') ? 'rightBottom' : 'top'),
)
</script>
<template>
<view
class="ct"
:class="[`ct--${template}`, `ct--skin-${variant}`, `ct--${size}`, { 'ct--lock': locked, 'ct__row': row }]"
:style="toneVars"
>
<!-- ===== 分组容器卡:白底 + 标题 + 右上角标 + 子卡插槽 ===== -->
<template v-if="template === 'group'">
<view v-if="title || badgeText || pill" class="ct__head">
<view class="ct__headlead">
<Sticker v-if="illustration" :type="illustration" :size="52" float />
<text class="ct__headtitle">
{{ title }}
</text>
</view>
<!-- 通栏标题行右胶囊(参考图 D;非空才渲染,默认不渲染 → 零回归) -->
<text v-if="pill" class="ct__pill">
{{ pill }}
</text>
<text v-if="badgeText" class="ct__headmore">
{{ badgeText }}
</text>
</view>
<!-- grid=false 时渲染裸 slot:由调用方用自己的 scoped 网格承接(P5 实证 MP 对内联 style 的 repeat() 网格支持不可靠,scoped class 已被工作台真机验证可用)。
网格铁律:`grid-template-columns` 只允许出现在 scoped class(`.ct__grid--c<N>` / `.ct__grid--mosaic`),**禁止内联 `repeat()`**(MP 不生效,P5 真事故)。 -->
<view v-if="grid" class="ct__grid" :class="mosaic ? 'ct__grid--mosaic' : `ct__grid--c${columns}`">
<slot />
</view>
<slot v-else />
</template>
<!-- ===== 通栏主卡:左文案 + 右大插画 ===== -->
<template v-else-if="template === 'lead'">
<view class="ct__lead-main">
<text class="ct__title">
{{ title }}
</text>
<text v-if="desc" class="ct__desc">
{{ desc }}
</text>
<view class="ct__extra">
<slot />
</view>
</view>
<view v-if="illustration || icon" class="ct__art ct__art--lead">
<Sticker v-if="illustration" :type="illustration" :size="artSize" float />
<view v-else class="ct__icon ct__icon--art">
<wd-icon :name="icon" :size="`${artSize * 0.5}rpx`" :color="iconColor" />
</view>
</view>
<text v-if="badgeText" class="ct__badge">
{{ badgeText }}
</text>
</template>
<!-- ===== 双列渐变卡:强渐变底 + 右下插画(参考图 C2)+ 标题 + 描述 ===== -->
<template v-else-if="template === 'duo'">
<view class="ct__art" :class="effArtPos === 'rightBottom' ? 'ct__art--br' : 'ct__art--duo'">
<Sticker v-if="illustration" :type="illustration" :size="artSize" />
<view v-else-if="icon" class="ct__icon ct__icon--art" :class="{ 'ct__icon--circle': effIconShape === 'circle' }">
<wd-icon :name="icon" :size="`${artSize * 0.5}rpx`" :color="iconColor" />
</view>
</view>
<text class="ct__title">
{{ title }}
</text>
<text v-if="desc" class="ct__desc">
{{ desc }}
</text>
<view v-if="cta" class="ct__cta">
{{ cta }}
</view>
<text v-if="badgeText" class="ct__badge" :class="{ 'ct__badge--tl': effBadgePos === 'tl' }">
{{ badgeText }}
</text>
</template>
<!-- ===== 行卡:图标左置 + 文案在右 ===== -->
<template v-else-if="template === 'iconLeft'">
<view class="ct__icon">
<wd-icon v-if="icon" :name="icon" size="44rpx" :color="iconColor" />
</view>
<view class="ct__body">
<text class="ct__title">
{{ title }}
</text>
<text v-if="desc" class="ct__desc">
{{ desc }}
</text>
</view>
<text v-if="badgeText" class="ct__badge ct__badge--inline">
{{ badgeText }}
</text>
<wd-icon v-if="arrow" name="arrow-right" size="30rpx" color="#D9C3B2" class="ct__go" />
</template>
<!-- ===== 图标墙(默认,含 tile 瓷片):插画/图标在上 + 标题 + 描述 ===== -->
<template v-else>
<view v-if="illustration" class="ct__art" :class="effArtPos === 'rightBottom' ? 'ct__art--br' : 'ct__art--top'">
<Sticker :type="illustration" :size="iconArtPx" float />
</view>
<view v-else class="ct__icon" :class="{ 'ct__icon--circle': effIconShape === 'circle' }">
<wd-icon v-if="icon" :name="icon" size="46rpx" :color="iconColor" />
</view>
<text class="ct__title">
{{ title }}
</text>
<text v-if="desc" class="ct__desc">
{{ desc }}
</text>
<view v-if="cta" class="ct__cta">
{{ cta }}
</view>
<text v-if="badgeText" class="ct__badge" :class="{ 'ct__badge--tl': effBadgePos === 'tl' }">
{{ badgeText }}
</text>
</template>
</view>
</template>
<!-- ⚠️ :host 必须放在【非 scoped】style 块:uni-app 的 scoped 编译会把它改写成
`.data-v-<id>:host`(scope 后缀加在 :host 之前),而宿主 <card-tile> 上的 class
是【引用方】的 scope id(如 data-v-dde6cf0f),永远不带自己的 data-v-<id>
⇒ 该规则匹配不到任何节点、静默失效(2026-09-24 用户真机实测确证:
`.cttap` 满格 159px 但卡片没撑满它)。非 scoped 的规则不加后缀,:host 才能命中宿主。 -->
<style>
/* ============================== 缺陷 A:MP 宿主撑满(唯一落点) ==============================
MP 下自定义组件宿主 <card-tile> 是 `.cttap`(flex 容器)的 flex 项,不显式撑满会
退化为 max-content → 卡片塌成内容宽(≈4 个中文字)。用 `:host` 匹配宿主
(微信自定义组件 wxss 原生支持,基础库 1.7.2+),**一处规则覆盖全部调用点**,不依赖标签名。
⚠️ 三条禁令:
① 不能在页面/全局侧写 `.cttap > *` 兜底 —— WXSS 不支持通配符 `*`,
微信开发者工具报 `error at token '*'`,整个 app.wxss 编译失败;
② 本规则不能放进 <style scoped>(会被改写成 .data-v-<id>:host 而静默失效,见上);
③ 产物验收必须断言「纯 :host{」(前面不得是 .data-v- 前缀),见 r7-assert-mp-v2.py A4 v4。
H5 端 :host 不匹配任何元素(非 shadow DOM),撑满由 App.vue 的 `.cttap > .ct` 承担。 */
:host {
display: block;
flex: 1 1 auto;
min-width: 0;
width: 100%;
}
</style>
<style scoped>
/* ============================== 骨架 ============================== */
.ct {
position: relative;
box-sizing: border-box;
overflow: hidden;
/* 缺陷 A:撑满宿主槽位。MP 下自定义组件宿主包裹本节点,宿主侧由上方 `:host` 规则撑满,
宿主内部的 `.ct` 仍需自身显式 100%。
height:100% 用于「同排等高」:宿主为 flex-stretch 项时高度确定 → 生效;
父高为 auto 时百分比高度按 CSS 规范退化为 auto → 安全(不会塌高)。 */
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10rpx;
/* 尺寸基准走 CSS 变量(默认 = 基准值):尺寸档 / 容器档只改变量,不改直接值,
避免 `.ct.ct--group`(0,2,0) 吞掉 `.ct--sm`/`.ct--lg`(0,1,0) 的 padding/圆角(R-2 特异度连锁)。 */
--ct-radius: 32rpx;
--ct-pad: 26rpx 26rpx 24rpx;
border-radius: var(--ct-radius);
padding: var(--ct-pad);
/* 双色渐变底 + 内高光 + 外层柔和投影(默认皮肤 porcelain 在此覆写底色/描边) */
}
.ct__title {
font-size: 28rpx;
font-weight: 800;
line-height: 1.25;
color: #3d2a1e;
}
.ct__desc {
font-size: 22rpx;
line-height: 1.45;
color: #6b4f3a;
max-width: 100%;
}
.ct--iconTop .ct__desc,
.ct--duo .ct__desc {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ct__body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
}
/* ===== 图标块(视觉精致化):渐变底 + 内高光 + 多层柔和投影 ===== */
.ct__icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 96rpx;
height: 96rpx;
border-radius: 28rpx;
margin-bottom: 6rpx;
background: linear-gradient(160deg, rgba(255, 255, 255, 0.98), var(--ct-icon-bg));
box-shadow:
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.95),
inset 0 -6rpx 12rpx rgba(255, 255, 255, 0.6),
0 8rpx 18rpx var(--ct-shadow),
0 2rpx 4rpx rgba(107, 47, 15, 0.08);
}
.ct__icon--art {
width: auto;
height: auto;
padding: 16rpx;
border-radius: 28rpx;
margin-bottom: 0;
}
/* 宫格①·圆形图标(参考图 C1):圆形描边质感 + 内高光 + 彩色投影 */
.ct__icon--circle {
border-radius: 50%;
box-shadow:
inset 0 2rpx 6rpx #fff,
0 8rpx 18rpx var(--ct-shadow);
}
/* ===== 插画位 ===== */
.ct__art {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* 图标墙(iconTop)插画:大号彩色插画(参考图那组卡的视觉主体) */
.ct__art--top {
margin-bottom: 8rpx;
}
/* 宫格②/③ 右下大插画(参考图 C2/C3):贴右下角、轻微旋转,饱和渐变卡专用 */
.ct__art--br {
position: absolute;
right: 16rpx;
bottom: 10rpx;
opacity: 0.96;
transform: rotate(-6deg);
}
/* ===== 角标(随色系派生:白底 + 同色系深字 + 轻投,任意卡底色上都高对比)===== */
.ct__badge {
position: absolute;
top: 0;
right: 0;
padding: 6rpx 18rpx;
font-size: 18rpx;
font-weight: 700;
border-radius: 0 32rpx 0 22rpx;
background: var(--ct-badge-bg);
color: var(--ct-badge-fg);
box-shadow: 0 4rpx 10rpx rgba(107, 47, 15, 0.22);
}
.ct__badge--inline {
position: static;
align-self: center;
border-radius: 999rpx;
flex-shrink: 0;
}
/* 子卡(tile)左上小 pill 角标(参考图 B):与右上贴边的 `.ct__badge` 区分开 */
.ct__badge--tl {
top: 12rpx;
left: 12rpx;
right: auto;
border-radius: 999rpx;
padding: 4rpx 14rpx;
font-size: 18rpx;
box-shadow: 0 4rpx 10rpx var(--ct-shadow);
}
/* 胶囊 CTA 按钮(参考图 C4):白底 + 同色系文字,浮在卡片上 */
.ct__cta {
display: inline-flex;
align-items: center;
gap: 6rpx;
margin-top: 10rpx;
padding: 10rpx 26rpx;
border-radius: 999rpx;
font-size: 22rpx;
font-weight: 800;
background: rgba(255, 255, 255, 0.92);
color: var(--ct-accent);
box-shadow: 0 6rpx 14rpx var(--ct-shadow);
}
/* 行卡右端箭头 */
.ct__go {
flex-shrink: 0;
margin-left: 6rpx;
}
/* ============================== 版式 1 · iconTop 图标墙 ============================== */
.ct--iconTop {
align-items: flex-start;
}
/* ============================== 版式 2 · iconLeft 行卡 ============================== */
.ct--iconLeft {
flex-direction: row;
align-items: center;
gap: 20rpx;
}
.ct--iconLeft .ct__icon {
margin-bottom: 0;
}
/* ============================== 版式 3 · lead 通栏主卡 ============================== */
.ct--lead {
flex-direction: row;
align-items: center;
gap: 16rpx;
padding: 30rpx 26rpx;
}
.ct__lead-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.ct--lead .ct__title {
font-size: 34rpx;
/* 缺陷 D:主卡文案不可被右端插画位挤成逐字换行 → 单行 + 省略 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.ct__extra:empty {
display: none;
}
.ct__art--lead {
/* 缺陷 D:原为刚性 width:168rpx,窄屏/长名会把左侧文案挤到逐字换行。
改为可收缩(flex-basis auto + shrink 1)并保留 168rpx 上限,仅在宽裕时占满。 */
flex: 0 1 auto;
min-width: 0;
max-width: 168rpx;
justify-content: flex-end;
}
/* ============================== 版式 4 · duo 双列渐变卡 ==============================
强渐变底(色系饱和端点)+ 白字;皮肤仍叠加描边/阴影细微差异。
特异性 0,2,0(.ct.ct--duo)> 皮肤 0,1,0 → 版式底色不被主题皮肤覆盖。 */
.ct.ct--duo {
background:
radial-gradient(120% 92% at 8% -14%, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0) 60%),
linear-gradient(150deg, var(--ct-grad-from), var(--ct-grad-to));
align-items: flex-start;
padding: 28rpx 24rpx 26rpx;
}
.ct--duo .ct__title {
color: #ffffff;
/* 插画移到右下(参考图 C2)→ 文本右侧留白避让,防压字。
⚠️ 落到**真实存在的**文本节点(duo 分支刻意没有 .ct__body 包裹),别写永不匹配的选择器。 */
padding-right: 88rpx;
}
.ct--duo .ct__desc {
color: rgba(255, 255, 255, 0.88);
padding-right: 88rpx;
}
.ct__art--duo {
margin-bottom: 8rpx;
}
/* ============================== 版式 5 · group 分组容器卡 ==============================
白底容器 + 区块标题 + 右上「更多」角标 + 子卡网格(子卡由默认插槽渲染)。
特异性 0,2,0(.ct.ct--group)> 皮肤 0,1,0 → 恒为白底,不被主题皮肤染色。 */
.ct.ct--group {
background: #ffffff;
border: 1rpx solid rgba(255, 195, 155, 0.55);
/* 容器尺寸基准也走变量:与尺寸档同特异度时靠**源序**(尺寸档在后)让窄列尺寸档胜出(R-2) */
--ct-radius: 34rpx;
--ct-pad: 26rpx 24rpx 28rpx;
box-shadow: 0 3rpx 10rpx rgba(107, 47, 15, 0.05), 0 16rpx 38rpx rgba(107, 47, 15, 0.1);
gap: 20rpx;
align-items: stretch;
}
/* tinted 变体(可选):容器非纯白时用浅色 tint 底(参考图 A「容器通栏」的柔和变体) */
.ct.ct--group--tinted {
background:
radial-gradient(120% 92% at 8% -14%, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0) 62%),
var(--ct-tint);
border-color: var(--ct-line);
}
.ct__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
}
/* 悬浮胶囊标题(参考图 A):半透明白胶囊 + 轻模糊,浮在容器底色之上 */
.ct__headlead {
display: flex;
align-items: center;
gap: 12rpx;
min-width: 0;
background: rgba(255, 255, 255, 0.86);
border-radius: 999rpx;
padding: 4rpx 16rpx;
backdrop-filter: blur(6rpx);
}
.ct__headtitle {
font-size: 32rpx;
font-weight: 800;
color: #3d2a1e;
/* 缺陷 B 防御性加固:group 版式标题长期没有断行策略 → 窄列里会逐字竖排。
统一 nowrap + 省略号,即使将来再有人把窄列配成 group 也不会竖排。 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.ct__headmore {
font-size: 24rpx;
font-weight: 700;
color: #ff6b35;
padding: 4rpx 18rpx;
border-radius: 999rpx;
background: #fff4ec;
border: 1rpx solid #ffd9c4;
}
/* 通栏标题行右胶囊(参考图 D,`.ct__headmore` 的泛化版):橙底白字,醒目 */
.ct__pill {
display: inline-flex;
align-items: center;
gap: 6rpx;
padding: 8rpx 22rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #ff9a4d, #fb4e14);
color: #ffffff;
font-weight: 700;
}
.ct__grid {
display: grid;
/* 铁律:`grid-template-columns` 只允许出现在 scoped class(禁止内联 repeat(),MP 不生效)。 */
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16rpx;
}
/* 列数档:由调用方传入的 columns 决定(模板用 `:class="ct__grid--c<N>"`),彻底避免内联 repeat() */
.ct__grid--c1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.ct__grid--c2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ct__grid--c3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.ct__grid--c4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* mosaic(参考图 A 容器内网格):2 列子卡 + 通栏行 `.ct__row`(跨整行) */
.ct__grid--mosaic {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
}
.ct__grid--mosaic > .ct__row {
grid-column: 1 / -1;
}
/* 通栏行子卡(参考图 A:mosaic 内跨整行的横条) */
.ct__row {
display: flex;
align-items: center;
gap: 16rpx;
border-radius: 24rpx;
padding: 18rpx 20rpx;
background: var(--ct-tint);
}
/* ============================== 皮肤(skin)============================== */
/* porcelain:釉面——左上白色高光叠在双色渐变上 + 彩色环境投影 + 顶部内高光 */
.ct--skin-porcelain {
background:
radial-gradient(120% 92% at 8% -14%, rgba(255, 255, 255, 0.62), rgba(255, 255, 255, 0) 62%),
linear-gradient(140deg, var(--ct-from), var(--ct-to));
box-shadow:
inset 0 2rpx 8rpx rgba(255, 255, 255, 0.75),
inset 0 -10rpx 20rpx rgba(255, 255, 255, 0.32),
inset 0 0 0 1rpx rgba(255, 255, 255, 0.38),
0 4rpx 10rpx rgba(107, 47, 15, 0.06),
0 14rpx 30rpx var(--ct-shadow);
}
/* tint:淡彩平铺 + 同款釉面高光 + 彩色投影 */
.ct--skin-tint {
background:
radial-gradient(120% 92% at 8% -14%, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0) 62%),
var(--ct-tint);
box-shadow: 0 10rpx 24rpx var(--ct-shadow);
}
/* glass:半透明 + 1rpx 白描边 + 内高光 */
.ct--skin-glass {
background: rgba(255, 255, 255, 0.55);
border: 1rpx solid rgba(255, 255, 255, 0.9);
box-shadow:
inset 0 2rpx 8rpx rgba(255, 255, 255, 0.85),
0 8rpx 22rpx rgba(107, 47, 15, 0.1);
backdrop-filter: blur(10rpx);
}
/* solid:饱和渐变 + 釉面高光 + 彩色投影(不是灰黑) */
.ct--skin-solid {
background:
radial-gradient(120% 92% at 8% -14%, rgba(255, 255, 255, 0.35), rgba(255, 255, 255, 0) 60%),
linear-gradient(140deg, var(--ct-grad-from), var(--ct-grad-to));
box-shadow: 0 6rpx 14rpx rgba(107, 47, 15, 0.1), 0 16rpx 32rpx var(--ct-shadow);
}
.ct--skin-solid .ct__title {
color: #ffffff;
}
.ct--skin-solid .ct__desc {
color: rgba(255, 255, 255, 0.85);
}
.ct--skin-solid .ct__icon {
background: rgba(255, 255, 255, 0.26);
box-shadow: inset 0 2rpx 6rpx rgba(255, 255, 255, 0.4);
}
/* line:极简描边无底 */
.ct--skin-line {
background: transparent;
border: 2rpx solid var(--ct-line);
box-shadow: none;
}
.ct--skin-line .ct__icon {
background: var(--ct-tint);
}
/* ============================== 尺寸 ============================== */
/* 尺寸档提特异度为 `.ct.ct--sm`/`.ct.ct--lg`(0,2,0),与容器档 `.ct.ct--group`(0,2,0) 同级;
源序在后 → 窄列尺寸档胜出;圆角/内边距统一走 --ct-* 变量,彻底避免互相吞并(R-2 特异度连锁)。 */
.ct.ct--sm {
--ct-radius: 26rpx;
/* 左右内边距对齐 P3 前基线(.floor__tile 的 8rpx):4 列窄卡内容宽须容得下 5 个中文字 */
--ct-pad: 18rpx 8rpx 16rpx;
gap: 8rpx;
}
.ct--sm .ct__icon {
width: 68rpx;
height: 68rpx;
border-radius: 22rpx;
}
.ct--sm .ct__title {
font-size: 24rpx;
/* 窄列防竖排:基线 .floor__tile-name 同款策略(nowrap + 省略号),中文不再逐字断行 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.ct--sm .ct__desc {
font-size: 20rpx;
}
.ct.ct--lg {
--ct-radius: 36rpx;
--ct-pad: 32rpx 30rpx 30rpx;
gap: 12rpx;
}
.ct--lg .ct__icon {
width: 120rpx;
height: 120rpx;
border-radius: 32rpx;
}
.ct--lg .ct__title {
font-size: 32rpx;
}
.ct--lg .ct__desc {
font-size: 24rpx;
}
/* ============================== 异形版式(只改形状/质感,内容走 iconTop 骨架)==============================
统一用 .ct.ct--* 双类(0,2,0),确保赢过 .ct 基础圆角与 .ct--sm/.ct--lg 的圆角。 */
/* 缺角卡:左上直角,其余大圆角 */
.ct.ct--notch {
border-radius: 0 44rpx 44rpx 44rpx;
}
/* 拱形卡:顶部高拱 */
.ct.ct--arch {
border-radius: 64rpx 64rpx 36rpx 36rpx / 96rpx 96rpx 36rpx 36rpx;
}
/* 叶形卡:对角异形 */
.ct.ct--leaf {
border-radius: 48rpx 10rpx 48rpx 10rpx;
}
/* 层叠卡:多层位移投影做出堆叠厚度(不用 z-index / 伪元素,规避小程序层叠风险) */
.ct.ct--stack {
border-radius: 36rpx;
box-shadow:
0 10rpx 0 -2rpx var(--ct-layer),
0 22rpx 0 -8rpx var(--ct-layer-soft),
0 30rpx 40rpx var(--ct-shadow);
}
/* ============================== 版式 6 · tile 瓷片子卡 ==============================
规格硬约束(§2 :99):**不新增渲染分支**,tile 走「else 分支的 iconTop 骨架 + 本修饰 class」。
角标左上 pill / 插画右下,均由模板按 effBadgePos / effArtPos 挂 `.ct__badge--tl` / `.ct__art--br`,
本类只负责「文案左对齐」等骨架差异。 */
.ct--tile {
align-items: flex-start;
}
.ct--tile .ct__title,
.ct--tile .ct__desc {
text-align: left;
max-width: 100%;
}
/* ============================== 未解锁 ============================== */
.ct--lock {
/* 强渐变底色本身饱和,0.72 几乎看不出灰化 → 调狠并略提亮,明显「未解锁」 */
filter: saturate(0.35) brightness(1.04);
}
.ct--lock .ct__title {
color: #bda794;
}
.ct--lock .ct__desc {
color: #c6b3a4;
}
.ct--lock .ct__icon {
background: #f3eee9;
box-shadow: none;
}
.ct--lock .ct__badge {
background: #f3eee9;
color: #bda794;
box-shadow: none;
}
</style>