Add AssetCardPullExpand component for locked-state display and wire up/down gesture symmetry on the asset detail page. Down-pull at top of list: - card scales 1.0 -> 1.6 with translateY follow, sibling modules fade out, hint text '下拉查看大图' -> '松手查看大图' past 80rpx - on release past threshold, viewMode flips to expanded Up-pull in expanded state: - card shrinks 1.6 -> 1.0 with translateY follow, sibling modules fade back in, hint text '上拉收起' -> '松手收起' past 80rpx - on release past threshold, viewMode flips back to normal - the locked scale(1.6) lives on the wrapper so the release animation stays single-jump, not dual-jump Implementation: - AssetCardPullExpand renders the card at scale(1) and exposes isExpanded for the parent's locked-state toggle (showMask prop gates the legacy full-screen tap-to-collapse overlay) - both gesture surfaces use lazy startY capture in onMove to avoid tap-induced jumps; no CSS transition on the wrapper during active pull - 0.15s transform transition smooths the release interpolation only - header bar and report/share buttons stay visible in both states; v-if/v-else-if/v-else chain (loading -> error -> expanded -> scroll-view) preserved; existing 8 card class names and parent CSS untouched Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
28 KiB
28 KiB
藏品详情页下拉放大卡片设计
★ MVP 优先:本次只引入一个独立放大组件承载下拉手势与放大态,不修改
LenticularCard/ShareReportButtons等已有组件,不引入状态管理库。
方案概述(必读)
要解决的问题
业务问题
- 用户在藏品详情页希望“快速预览大图”,但不希望跳转到独立大图页后丢失返回上下文。
- 现有
asset-detail.vue卡片是固定尺寸 352×552rpx,无法满足“看清细节”的诉求。
技术问题
asset-detail.vue已承担卡片、光栅、贴纸、点赞、链上数据、举报/分享弹窗等职责,行数接近 1700。- 直接在同一文件内继续堆触摸手势、放大态、CSS 状态类会让单文件接近 2000 行。
- App 端
scroll-view顶部回弹事件不稳定,负scrollTop不可靠,无法仅凭scrollTop识别下拉意图。 - 现有
LenticularCard通过陀螺仪/模拟输入控制光栅切换,不希望被外层 transform 重新渲染打乱。
整体实现路径
- 新建独立组件
frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue,承担:- 触摸手势(
touchstart/touchmove/touchend/touchcancel)。 - 放大态状态机(
isPullActive/isExpanded)。 - 提示文案与点击背景收起。
- 触摸手势(
- 在
asset-detail.vue中:- 沿用顶层
v-if / v-else-if / scroll-view v-else链路,新增v-else-if="viewMode === 'expanded'"分支挂<AssetCardPullExpand>。 - 正常态分支里的
<view class="card-wrapper">一字不改,性能与手感零回归。 - 引入
viewMode: ref('normal' | 'expanded'),由AssetCardPullExpand发出pull-expand-change事件后切换。 - 顶部 Header(返回键、
ShareReportButtons)始终保留,不受viewMode影响。
- 沿用顶层
- H5 端降级:组件检测非
APP-PLUS时整体不进入下拉态,仅做静态卡片展示。
预计工作量:1~1.5 小时(新建组件 + asset-detail 集成 + 手工联调)。
关键决策
- 抽出独立组件:避免
asset-detail.vue进一步膨胀;同时让组件可复用(在星册页、铸造预览页等场景后续可直接挂上)。 - 数据全部由父级以 props 传入:组件内部不调用任何
utils/api.js/utils/task-api.js/utils/assetImageHelper.js,不读uni.getStorageSync/getStoredUser(),不直连 Vuex;不持有“自己的”藏品数据/光栅贴纸配置。卡片内容、角标、贴纸、LenticularCard所需的lenticularLayers/layerTransforms/simulate/tiltHintText/shimmerMidOpacity/coverUrl/gradeBadgeUrl等全部从asset-detail.vue(以及后续复用方)以 props 注入。这样保证AssetCardPullExpand是纯展示 + 手势容器,可在任何页面复用。 - 组件 DOM 与 class 必须与现有
.card-wrapper区块一摸一样:模板中原card-wrapper区域的 class 名(card-wrapper/card-wrapper--lenticular/card-frame/card-image/card-badge/card-sticker/detail-lenticular-slot/detail-lenticular-card)、DOM 嵌套顺序、属性名(:src/:layers/:transforms/gyro-source/tilt-hint-text/:shimmer-mid-opacity/:simulate-tilt-from-normalized/@mode-change)都保持原样不动;AssetCardPullExpand.vue只是在这个原结构外面再包一层“手势容器 + 放大态容器”,内层卡片原封不动。禁止改 class 名前缀(如把card-改成pull-),禁止删/调 DOM 顺序,禁止重命名 props。这样asset-detail.vue现有的.card-wrapper/.card-frame等样式无需修改即可继续生效。 .card-wrapper的尺寸由组件本地显式声明:虽然视觉样式不重写,但.card-wrapper的width: 352rpx; height: 552rpx; transform-origin: center center;这三条尺寸/变换相关属性必须出现在AssetCardPullExpand.vue的<style scoped>里(不带scoped穿透选择器),因为组件是.card-wrapper的“拥有者”,不能假设父级一定把这些属性传过来。其它一切样式(背景、边框、阴影、动画 keyframes 等)继续由asset-detail.vue提供。组件内的.card-wrapper样式块只能写尺寸/transform 相关三行,不得添加任何其它属性。- 沿用
<scroll-view v-else>思路切换两种态,性能最稳:asset-detail.vue现在的模板顶层是<view v-if="loading">… / <view v-else-if="loadError && !craftConfirmMode">… / <scroll-view v-else>…</scroll-view>,我们要沿用并扩展这条 v-if/v-else 链。父级新增一个viewMode: ref('normal' | 'expanded'),与loading/loadError形成互斥;在expanded态下用<view v-else-if="viewMode === 'expanded'">…</view>替代<scroll-view v-else>,把<AssetCardPullExpand>放在这个新 view 下,完全脱离scroll-view渲染树。这样:- 正常态:沿用现有
<scroll-view>,列表滚动 + 卡片展示无任何回归。 - 放大态:不再创建
scroll-view节点,父级没有滚动容器,组件手势不会被父级滚动抢占,卡顿与冲突显著减少。 - 切换走 Vue 的 v-if/v-else diff,节点销毁/创建一次性完成,过渡丝滑。
- 不引入 fixed 全屏遮罩层、也不在
scroll-view上做 touch 事件 hack,实现最轻、性能最稳。
- 正常态:沿用现有
- 复用
LenticularCard:组件内部直接渲染<LenticularCard>子组件,props 全部由父级透传;放大态完全由外层transform: scale()驱动,光栅状态不重置。 - 触摸事件而非
scrollTop:仅在scrollTop === 0时进入激活态,避免与列表滚动冲突。 - 松手后保持放大:松手后通过
isExpanded锁态保留放大视图,用户点击背景遮罩或返回键退出;返回键走原有navigateBack/reLaunch流程。 - Header 始终保留:返回、分享、举报作为放大态下的退出/分享入口。
- H5 不启用:避免 H5 端
scroll-view行为差异引入新 bug。
核心架构图(TL;DR)
asset-detail.vue (顶层 v-if / v-else-if / v-else 链)
├── <view v-if="loading"> // 加载态
├── <view v-else-if="loadError && !craft…"> // 错误态
├── <view v-else-if="viewMode === 'expanded'"> // ★ 放大态(无 scroll-view)
│ └── <AssetCardPullExpand> // 新增独立组件
│ ├── touchstart/move/end/cancel
│ ├── 放大态锁 isExpanded
│ └── @pull-expand-change → viewMode
└── <scroll-view v-else> // ★ 正常态(原卡片+其它模块)
└── .card-wrapper / .info-row / .creator-section / .chain-section …
Header(返回 + ShareReportButtons) 在 detail-container 下,两种 viewMode 都渲染
文档说明
- 适用范围:
frontend/pages/asset-detail/asset-detail.vue详情页卡片放大交互。 - 不包含:光栅组件改造、ShareReportButtons 改造、独立大图页、新的状态管理库、H5 端手势支持。
- 前置版本:
LenticularCard已支持simulate/layerTransformsprops(见2026-05-22-lenticular-gyro-optimization-design.md、2026-06-30-lenticular-android-accel-oneeuro-design.md)。 - 历史依据:用户在使用
asset-detail.vue时反馈“卡片太小看不清”,但又不愿离开详情页。 - 目标读者:前端开发、联调 QA、产品。
1. 新增组件 AssetCardPullExpand
1.1 目录与文件
frontend/components/AssetCardPullExpand/
├── AssetCardPullExpand.vue # 主组件
└── useCardPullGesture.js # 可选:抽出下拉手势 hook(优先放进同一文件,避免过度拆分)
决策:先在一个
.vue文件内完成;如果后续asset-detail.vue/castlove/success.vue/ 星册页都复用,再拆 hook。
1.2 Props(全部由父级注入,组件内不自行获取)
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
coverUrl |
String | '' |
非光栅卡片的封面图 URL(由父级 asset-detail.vue 解析 OSS 签名后传入) |
isLenticular |
Boolean | false |
是否为光栅卡片(由父级根据 assetData.tags 计算后传入) |
lenticularLayers |
Array | [] |
透传给 LenticularCard 的层数据(由父级 buildLenticularLayers* 构造) |
layerTransforms |
Object | {} |
透传给 LenticularCard 的变换配置(由父级 useLenticularCraftTiltPreview 提供) |
simulate |
Object | null |
透传给 LenticularCard 的模拟输入(同上) |
tiltHintText |
String | '倾斜手机查看光栅效果' |
透传 LenticularCard 提示文案 |
shimmerMidOpacity |
Number | 0.16 |
透传 LenticularCard |
gradeBadgeUrl |
String | '' |
角标 URL(由父级 gradeBadgeUrl computed 传入) |
stickers |
Array | [] |
贴纸数组,字段同父级 getStickerStyle(由父级 activeStickers 直接传入) |
cardAnimClass |
String | '' |
父级传入的额外动画 class(如 card-wrapper--auto-flip),与 auto/manual 模式联动 |
expandThreshold |
Number | 80 |
松手放大的临界下拉值(rpx) |
maxScale |
Number | 1.6 |
最大放大倍数 |
maxPullDistance |
Number | 240 |
最大下拉距离(rpx) |
disabled |
Boolean | false |
加载中 / 错误态时禁用触摸 |
禁止行为(组件内不要写):
- 不要 import
utils/api.js/utils/task-api.js/utils/assetImageHelper.js。 - 不要调用
uni.getStorageSync/getStoredUser()/getPreloadApi()。 - 不要访问 Vuex(
uni.$emit/uni.$on也不要)。 - 不要内部再起一个
loading/loadError状态——这些状态由父级控制,通过disabled透传。 - 不要维护自己的
coverUrl/lenticularLayersref;所有展示数据来源都必须是 props。 - 不要修改卡片内层 DOM 的 class 名 / 嵌套顺序 / 属性名;原
.card-wrapper区块的 8 个 class(card-wrapper/card-wrapper--lenticular/card-frame/detail-lenticular-slot/detail-lenticular-card/card-image/card-badge/card-sticker)原封保留,@mode-change事件、gyro-source/tilt-hint-text/:shimmer-mid-opacity/:simulate-tilt-from-normalized等属性原样照写,不要在组件里把它们改写成pull-*或asset-card-*之类。 - 不要在组件
<style scoped>里覆盖.card-wrapper/.card-frame/.card-image/.card-badge/.card-sticker/.detail-lenticular-slot/.detail-lenticular-card的样式;这些样式继续由asset-detail.vue的现有<style scoped>提供。组件只负责手势/放大态所需的额外样式(如.pull-expand-root/.pull-card-touch/.pull-mask/.pull-hint),与卡片原样式不冲突。
1.3 Events
@pull-expand-change(payload: { state: 'normal' | 'expanded' }):状态变化时通知父级。@collapse():父级可主动调起关闭放大态,组件内部isExpanded = false并 emit 一次 change。
1.4 内部状态
pullStartY = ref<number | null>(null)pullDistance = ref(0)isPullActive = ref(false)isExpanded = ref(false)
1.5 Computed
cardTransform:根据isPullActive与pullDistance计算transform: scale(s) translateY(d)。- 激活态:
s = 1 + clamp(pullDistance / maxPullDistance, 0, 1) * (maxScale - 1)。 - 锁定态:
s = maxScale,d = 0,过渡曲线cubic-bezier(.22,.61,.36,1) .25s。
- 激活态:
hintText:根据pullDistance切换''/'下拉查看大图'/'松手查看大图'。shouldHandleGesture:disabled为真或非APP-PLUS时为false;false时所有触摸回调直接 return。
1.6 事件回调(在组件内定义)
function onTouchStart(e) {
if (!shouldHandleGesture.value) return;
if (scrollTopRef.value !== 0) return; // 仅在顶部进入
pullStartY.value = e.touches[0].pageY;
}
function onTouchMove(e) {
if (pullStartY.value == null) return;
const delta = e.touches[0].pageY - pullStartY.value;
if (delta <= 0) return;
pullDistance.value = Math.min(delta * 0.6, props.maxPullDistance);
isPullActive.value = pullDistance.value > 0;
}
function onTouchEnd() {
if (!isPullActive.value) {
pullStartY.value = null;
return;
}
isPullActive.value = false;
if (pullDistance.value >= props.expandThreshold) {
isExpanded.value = true;
emit('pull-expand-change', { state: 'expanded' });
}
pullDistance.value = 0;
pullStartY.value = null;
}
function onMaskTap() {
if (!isExpanded.value) return;
isExpanded.value = false;
emit('pull-expand-change', { state: 'normal' });
}
scrollTopRef:组件 mounted 时通过uni.createSelectorQuery选中父级scroll-view拿scrollTop。若未传 ref,使用默认值 0(只允许在顶部下拉,作为安全默认)。
1.7 模板(内层卡片与原 .card-wrapper 完全一致)
<template>
<view class="pull-expand-root" :class="{ 'is-expanded': isExpanded }">
<!-- 手势 / 放大容器:仅负责 transform 与触摸事件 -->
<view
class="pull-card-touch"
:style="{ transform: cardTransform }"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
@touchcancel="onTouchEnd"
>
<!-- ↓↓↓ 以下区块与原 .card-wrapper 一摸一样,class / DOM / 属性都不要改 ↓↓↓ -->
<view class="card-wrapper" :class="[
{ 'card-wrapper--lenticular': isLenticular },
cardAnimClass,
]">
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill"></image>
<view v-if="isLenticular" class="detail-lenticular-slot">
<LenticularCard
class="detail-lenticular-card"
:layers="lenticularLayers"
:transforms="layerTransforms"
gyro-source="simulation"
:tilt-hint-text="tiltHintText"
:shimmer-mid-opacity="shimmerMidOpacity"
:simulate-tilt-from-normalized="simulate"
@mode-change="onCardModeChange"
/>
</view>
<image v-else class="card-image" :src="coverUrl" mode="aspectFill"></image>
<image class="card-badge" :src="gradeBadgeUrl" mode="aspectFit"></image>
<image
v-for="sticker in stickers"
:key="sticker.id"
class="card-sticker"
:src="sticker.src"
mode="aspectFit"
:style="getStickerStyle(sticker)"
/>
</view>
<!-- ↑↑↑ 区块结束 ↑↑↑ -->
</view>
<text v-if="isPullActive && hintText" class="pull-hint">{{ hintText }}</text>
<view v-if="isExpanded" class="pull-mask" @tap="onMaskTap" />
</view>
</template>
getStickerStyle:组件内私有函数,逻辑与父级asset-detail.vue中的getStickerStyle一致(本组件允许保留一个本地实现,因为sticker是 props 传入的纯数据,无副作用)。如果后续要彻底统一,可挪到utils/stickerStyle.js;本次先在组件内复制一份。
1.8 样式要点
.pull-card-touch:transform-origin: center center; transition: transform .12s linear(激活) /.25s cubic-bezier(.22,.61,.36,1)(锁定)。不重写.card-wrapper/.card-frame/.card-image/.card-badge/.card-sticker/.detail-lenticular-slot/.detail-lenticular-card的样式;这些 class 由父级asset-detail.vue的现有<style scoped>继续提供。.pull-expand-root > .card-wrapper:只允许在组件内显式声明尺寸:width: 352rpx; height: 552rpx; transform-origin: center center;。这三行是组件必须写的,不允许超出此范围(不允许补 padding / margin / background / border / box-shadow / animation 等)。光栅卡片的尺寸差异(若未来需要 520×680)通过 props 传入,而不是在组件内改。.pull-mask:position: fixed; inset: 0; z-index: 90; background: transparent;用于点击空白处收起,不阻挡 Header(Headerz-index: 100)。.pull-hint:顶部固定位置,放大态隐藏。// #ifdef APP-PLUS包裹触摸事件相关样式注释(不影响逻辑,只用于标注)。
2. asset-detail.vue 集成
2.1 import
import AssetCardPullExpand from '@/components/AssetCardPullExpand/AssetCardPullExpand.vue';
2.2 顶层 v-if/v-else 链(沿用现有写法,新增 viewMode === 'expanded' 分支)
原结构:
<view v-if="loading">...</view>
<view v-else-if="loadError && !craftConfirmMode">...</view>
<scroll-view v-else scroll-y class="content-scroll" :show-scrollbar="false">
<view class="content-wrapper">
<view class="card-section">
<view class="card-wrapper" :class="[...]">...原卡片...</view>
<view class="card-meta-row">...</view>
</view>
<view class="info-row">...</view>
<view v-if="likeCount > 0" class="creator-section">...</view>
<view class="chain-section">...</view>
</view>
</scroll-view>
调整为:
<view v-if="loading">...</view>
<view v-else-if="loadError && !craftConfirmMode">...</view>
<!-- 放大态:完全脱离 scroll-view,父级无滚动,组件手势不被抢占 -->
<view v-else-if="viewMode === 'expanded'" class="pull-expanded-view">
<AssetCardPullExpand
:cover-url="coverUrl"
:is-lenticular="isLenticularAsset"
:lenticular-layers="lenticularLayers"
:layer-transforms="layerTransforms"
:simulate="simulate"
:grade-badge-url="gradeBadgeUrl"
:stickers="activeStickers"
:tilt-hint-text="'倾斜手机查看光栅效果'"
:shimmer-mid-opacity="0.16"
:card-anim-class="cardWrapperAnimClass"
@pull-expand-change="onPullExpandChange"
/>
</view>
<!-- 正常态:沿用 v-else + scroll-view,卡片+其它模块一切如旧 -->
<scroll-view v-else scroll-y class="content-scroll" :show-scrollbar="false">
<view class="content-wrapper">
<view class="card-section">
<!-- 原 .card-wrapper 一字不改,组件不参与正常态 -->
<view class="card-wrapper" :class="[...]">...原卡片...</view>
<view class="card-meta-row">...</view>
</view>
<view class="info-row">...</view>
<view v-if="likeCount > 0" class="creator-section">...</view>
<view class="chain-section">...</view>
</view>
</scroll-view>
要点:
- 正常态分支完全不动:
.card-wrapper原段继续写在<scroll-view v-else>里,与现在一模一样,性能与手感零回归。 - 放大态分支是新增的
v-else-if分支,内部不创建scroll-view,只挂<AssetCardPullExpand>,父级无滚动容器,组件内部的touchmove不会被外层滚动抢占。 - v-if/v-else 切换走 Vue 的 diff:正常态与放大态是“节点销毁/创建”级别,一次性完成,过渡丝滑。
- 不引入 fixed 全屏遮罩层、不在
scroll-view上做 touch 事件 hack,实现最轻、性能最稳。
2.3 状态切换
const viewMode = ref('normal'); // 'normal' | 'expanded'
function onPullExpandChange({ state }) {
viewMode.value = state;
}
顶部 Header(返回键 + ShareReportButtons)、LikeUsersModal 弹窗、ReportModal 弹窗、离屏 shareCanvas、链上哈希遮罩 txhash-mask 都继续挂在 <view class="detail-container"> 下,不被 viewMode 影响。这样:
- 放大态下用户仍可点分享/举报/返回/弹出 txhash 详情;
- 弹窗本身用
v-if控制,与放大态互不干扰。
2.4 .card-wrapper 段处理
- 正常态:原
<view class="card-wrapper" :class="[...]">...</view>完全不动,继续写在<scroll-view v-else>分支里。 - 放大态:不写第二份
.card-wrapper,而是把<AssetCardPullExpand>放进新增的v-else-if分支;组件内部再嵌一份与正常态完全相同的.card-wrapper区块(只为手势/放大态服务)。 - 组件不参与正常态渲染;正常态的
.card-wrapper还是asset-detail.vue自己的那一份。
2.5 样式
- 不删除
asset-detail.vue中.card-wrapper/.card-frame/.card-image/.card-badge/.card-sticker/.detail-lenticular-slot/.detail-lenticular-card的样式块——正常态还要用。 - 放大态新增
.pull-expanded-view容器:position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 50;(asset-detail.vue的<style scoped>内),让卡片居中显示,完全脱离scroll-view高度。 <AssetCardPullExpand>内部.card-wrapper显式声明三行尺寸:width: 352rpx; height: 552rpx; transform-origin: center center;(其它样式由父级asset-detail.vue现有 CSS 提供)。
2.6 行为契约
- 顶部 Header(返回 + ShareReportButtons)在
normal/expanded两种viewMode下都渲染,且z-index始终高于.pull-expanded-view,可正常点击。 - 加载中、错误态时,
viewMode === 'normal',组件不会被挂载。 - 返回键不强制清除放大态:Android/iOS 物理返回 →
handleBack()走既有navigateBack/reLaunch;若想顺带收起放大态,可在handleBack顶部加if (viewMode.value === 'expanded') { viewMode.value = 'normal'; return; },但默认不强制。 - 组件内
disabled在加载/错误态被父级设为true,但因为这些态下viewMode === 'normal'、组件根本不被挂载,实际上不会触发该路径——disabled主要用于craftConfirmMode等其他复用场景。
3. 数据流
用户手指向下 → touchstart 记录 startY
→ touchmove 累加 pullDistance
→ touchend:
pullDistance >= threshold → isExpanded = true → emit('expanded')
否则 → pullDistance = 0
父级 onPullExpandChange → viewMode = 'expanded' → 整棵 scroll-view 销毁、放大态节点创建
点击 pull-mask → isExpanded = false → emit('normal') → viewMode = 'normal'
→ scroll-view 重新创建,卡片与其它模块恢复
4. 错误处理
- 加载中/错误态:
viewMode === 'normal',组件不会被挂载,触摸逻辑不会触发。 - H5 端:
shouldHandleGesture通过// #ifdef APP-PLUS检测,非 App 直接返回 false,组件退化为静态卡片。 touchcancel:复用onTouchEnd的回弹分支。- 页面销毁/切后台:
onUnload/onHide中清空pullStartY、pullDistance重置为 0、isExpanded = false。 scrollTop异常:若未传scrollTopRef,组件假定当前在顶部,只在touchstart时记录起点;touchmove一旦delta <= 0直接 return,不会与正常列表滚动冲突。
5. 验证与回归
5.1 自检
- 详情页加载完成后,在列表顶部下拉 → 卡片跟随手指放大。
- 下拉到约 80rpx 后松手 →
viewMode切到expanded,<scroll-view>节点销毁,<AssetCardPullExpand>节点创建,卡片保持 1.6 倍放大。 - 放大态下点击背景 → 卡片恢复原大小,
viewMode切回normal,<scroll-view>节点重新创建,其它模块恢复。 - 列表中段下拉 → 不进入激活态,列表正常滚动。
- 光栅卡片放大 →
LenticularCard状态保留,贴纸/角标同步放大。 - Header 在两种状态下都能点击(返回、分享、举报)。
- 加载中、错误态下,下拉无响应(组件未被挂载)。
- H5 端打开,组件退化为静态卡片,无下拉手势。
- DOM/class 一致性核对:
AssetCardPullExpand.vue内层 DOM 节点的 class 名、嵌套顺序、属性与asset-detail.vue第 45-63 行原.card-wrapper区块逐字相同;card-wrapper/card-wrapper--lenticular/card-frame/card-image/card-badge/card-sticker/detail-lenticular-slot/detail-lenticular-card八个 class 一个都不能少一个都不能改;@mode-change/gyro-source/tilt-hint-text/:shimmer-mid-opacity/:simulate-tilt-from-normalized等属性原样照写。 - 样式不重复声明:组件
<style scoped>中没有.card-wrapper/.card-frame/.card-image/.card-badge/.card-sticker/.detail-lenticular-slot/.detail-lenticular-card的样式块(全靠父级asset-detail.vue现有样式生效)。 - 尺寸/transform 显式声明:组件
<style scoped>中允许出现.card-wrapper的尺寸/transform 三行(width: 352rpx; height: 552rpx; transform-origin: center center;),其它任何属性都不允许写在该块内。
5.2 回归检查
- 改动文件:
frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue(新增);frontend/pages/asset-detail/asset-detail.vue(模板/脚本/样式 局部)。 - 未改动文件:
LenticularCard.vue、ShareReportButtons.vue、composables/useLenticularCraftTiltPreview.js、utils/sticker-compositor.js、utils/castloveMintForm.js。 - 相邻模块:
castlove/success.vue/castlove/mall.vue/pages/square/*等其它使用LenticularCard的页面不受影响(它们没有引入放大态)。 - 样式来源:内层卡片所有视觉(角标、贴纸、光栅槽位、图片裁切)继续由
asset-detail.vue现有<style scoped>下的.card-wrapper/.card-frame/.card-image/.card-badge/.card-sticker/.detail-lenticular-slot/.detail-lenticular-card提供;这些样式不删除、不修改,组件不重新声明(仅尺寸三行)。 - API:不修改后端。
- 数据库:不修改 schema。
5.3 风险与回退
- 风险:
pullStartY监听挂在卡片容器,若LenticularCard子组件内catch了touchmove阻止冒泡,可能导致外层拿不到事件。LenticularCard当前未阻止冒泡,如有需要在抽组件时通过e.cancelBubble兼容。 - 风险:组件内层 DOM 与
asset-detail.vue现有.card-wrapper区块完全一致;若实施时偷懒改了某个 class(如card-frame→pull-card-frame),会出现.card-frame的position: absolute; top: 0; left: 0;等样式失效、卡片视觉崩坏的回归。spec 自检阶段需明确核对每个 class。 - 风险:v-if/v-else 切换会销毁/重建
<scroll-view>,对内部的链上数据 / 点赞用户列表组件状态会有影响;如果状态在父级用 ref/computed 维护,则无影响;如挂在 scroll-view 内部,切换会重置。本次状态都在父级asset-detail.vue用 ref 维护,所以无回归。 - 回退:将
AssetCardPullExpand拆下,父级恢复card-wrapper原状,即可回到当前行为。
6. 实施步骤(实现时按序)
- 新建
frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue,按 §1.x 实现 props / events / 模板 / 样式。 - 在
asset-detail.vue中 import 新组件。 - 在顶层
v-if / v-else-if / scroll-view v-else链中插入v-else-if="viewMode === 'expanded'"分支,引入viewModeref 与onPullExpandChange回调。 - 在
asset-detail.vue的<style scoped>内追加.pull-expanded-view容器样式。 - 不要删除
asset-detail.vue中.card-wrapper/.card-frame/.card-image/.card-badge/.card-sticker/.detail-lenticular-slot/.detail-lenticular-card的样式块——正常态还要用。 - 手动在真机(App-Plus)验证:下拉 / 锁定 / 点击背景收起 / 列表滚动 / 光栅卡片 / Header 按钮。
- 在 H5 端确认组件退化为静态展示。
7. 未做(显式列出)
- H5 / 小程序端的下拉手势支持。
- 大图独立页 / 缩放查看 / 多指捏合。
- 阈值 80rpx、最大 240rpx、放大 1.6 倍的可视化配置(暂走 props 默认值,后续按手感调)。
- 贴纸层在放大态下做单独动画(目前随卡片一起 scale)。
- 与
useLenticularCraftTiltPreview的进一步解耦(目前直接传simulate/layerTransformsprops)。