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>
25 KiB
藏品详情页下拉放大卡片实现计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 在 frontend/pages/asset-detail/asset-detail.vue 实现“下拉放大预览大图”交互:下拉过程中只保留卡片、顶部返回键、分享和举报按钮,其它模块隐藏,松手后保持 1.6 倍放大态,点击背景收起。
Architecture: 抽出独立组件 frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue 承担触摸手势与放大态;asset-detail.vue 沿用顶层 v-if / v-else-if / scroll-view v-else 链,新增 v-else-if="viewMode === 'expanded'" 分支挂放大态组件,正常态一字不改。
Tech Stack: uni-app 3.x + Vue 3 Composition API + <script setup> + 移动端原生 touch 事件。
全局约束
- Vue 3 组合式 API:
AssetCardPullExpand.vue必须使用<script setup>,不写this。 - 条件编译:所有与原生 API / 平台差异相关代码用
// #ifdef APP-PLUS … // #endif;禁止if (typeof plus !== 'undefined')兜底。 - DOM 一致性硬约束:
AssetCardPullExpand.vue内层.card-wrapper区块的 class 名(card-wrapper/card-wrapper--lenticular/card-frame/card-image/card-badge/card-sticker/detail-lenticular-slot/detail-lenticular-card)、嵌套顺序、属性名(@mode-change/gyro-source/tilt-hint-text/:shimmer-mid-opacity/:simulate-tilt-from-normalized/:layers/:transforms)必须与asset-detail.vue第 45-63 行逐字一致。 - 组件 props-only:组件内部不调用
utils/api.js/utils/task-api.js/utils/assetImageHelper.js,不读uni.getStorageSync/getStoredUser(),不访问 Vuex;卡片所有数据由父级以 props 传入。 - 样式不重复声明:组件
<style scoped>中不允许出现.card-wrapper/.card-frame/.card-image/.card-badge/.card-sticker/.detail-lenticular-slot/.detail-lenticular-card的样式块;这些继续由asset-detail.vue现有<style scoped>提供。唯一例外:.card-wrapper的尺寸/transform 三行(width: 352rpx; height: 552rpx; transform-origin: center center;)写在组件内,三行之外不得补任何属性。 - v-if/v-else 链顺序:
asset-detail.vue顶层必须按v-if="loading"→v-else-if="loadError && !craftConfirmMode"→v-else-if="viewMode === 'expanded'"→scroll-view v-else排列。 - Header 始终可见:顶部返回键、
ShareReportButtons、弹窗、离屏 canvas 都挂在<view class="detail-container">下,不被viewMode影响。 - 不在
unpackage/dist/改动(frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue是源码,不影响)。
文件结构
frontend/
├── components/
│ └── AssetCardPullExpand/
│ └── AssetCardPullExpand.vue # 新增:独立放大组件
└── pages/
└── asset-detail/
└── asset-detail.vue # 修改:加 v-else-if 视图分支 + viewMode ref + .pull-expanded-view 容器
不修改:
frontend/components/lenticular/LenticularCard.vuefrontend/pages/components/ShareReportButtons.vuefrontend/pages/components/ReportModal.vuefrontend/pages/components/LikeUsersModal.vuefrontend/composables/useLenticularCraftTiltPreview.jsfrontend/utils/sticker-compositor.jsfrontend/utils/castloveMintForm.jsfrontend/utils/getStoredUser.js- 后端 Go 代码、数据库 schema、API 文档
任务列表
Task 1: 新建 AssetCardPullExpand.vue 骨架与 props
Files:
- Create:
frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue
Interfaces:
- Consumes: 无(独立文件)
- Produces: 一个 Vue 3 SFC 组件,导出 props / events / 内层 DOM 结构(与
asset-detail.vue第 45-63 行.card-wrapper区块一致)
Step 1: 创建组件目录与文件骨架
创建文件 frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue,写入以下内容(<script setup> + props + template + style 三行尺寸):
<template>
<view class="pull-expand-root" :class="{ 'is-expanded': isExpanded }">
<view
class="pull-card-touch"
:style="{ transform: cardTransform }"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
@touchcancel="onTouchEnd"
>
<!-- 内层卡片:与 asset-detail.vue 第 45-63 行原 .card-wrapper 区块逐字一致 -->
<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>
<script setup>
import { ref, computed, watch } from 'vue';
import LenticularCard from '@/components/lenticular/LenticularCard.vue';
// === 内部状态 ===
const pullStartY = ref(null);
const pullDistance = ref(0);
const isPullActive = ref(false);
const isExpanded = ref(false);
// === Props(全部由父级注入) ===
const props = defineProps({
coverUrl: { type: String, default: '' },
isLenticular: { type: Boolean, default: false },
lenticularLayers: { type: Array, default: () => [] },
layerTransforms: { type: Object, default: () => ({}) },
simulate: { type: Object, default: null },
tiltHintText: { type: String, default: '倾斜手机查看光栅效果' },
shimmerMidOpacity: { type: Number, default: 0.16 },
gradeBadgeUrl: { type: String, default: '' },
stickers: { type: Array, default: () => [] },
cardAnimClass: { type: String, default: '' },
expandThreshold: { type: Number, default: 80 },
maxScale: { type: Number, default: 1.6 },
maxPullDistance: { type: Number, default: 240 },
disabled: { type: Boolean, default: false },
});
// === Events ===
const emit = defineEmits(['pull-expand-change']);
// === Computed ===
// #ifdef APP-PLUS
const isAppPlus = true;
// #else
const isAppPlus = false;
// #endif
const shouldHandleGesture = computed(() => !props.disabled && isAppPlus);
const cardTransform = computed(() => {
if (isExpanded.value) {
return `scale(${props.maxScale})`;
}
if (isPullActive.value) {
const t = Math.min(props.maxPullDistance, Math.max(0, pullDistance.value)) / props.maxPullDistance;
const s = 1 + t * (props.maxScale - 1);
return `scale(${s.toFixed(3)})`;
}
return 'scale(1)';
});
const hintText = computed(() => {
if (isExpanded.value) return '';
if (pullDistance.value >= props.expandThreshold) return '松手查看大图';
if (pullDistance.value > 20) return '下拉查看大图';
return '';
});
// === 内部方法(本任务先占位,Task 2 完善手势) ===
function onTouchStart() {}
function onTouchMove() {}
function onTouchEnd() {}
function onMaskTap() {
if (!isExpanded.value) return;
isExpanded.value = false;
emit('pull-expand-change', { state: 'normal' });
}
function onCardModeChange() {}
// === 贴纸样式(本任务先占位,Task 3 完善) ===
function getStickerStyle(sticker) {
return {
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)',
};
}
// === 监听外部 disabled,重置内部状态 ===
watch(() => props.disabled, (v) => {
if (v) {
isPullActive.value = false;
isExpanded.value = false;
pullDistance.value = 0;
pullStartY.value = null;
emit('pull-expand-change', { state: 'normal' });
}
});
</script>
<style scoped>
/* 组件 .card-wrapper 必须显式声明尺寸三行(其它视觉样式由父级 asset-detail.vue 提供) */
.card-wrapper {
width: 352rpx;
height: 552rpx;
transform-origin: center center;
}
/* 手势容器(只负责 transform 与触摸) */
.pull-card-touch {
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.12s linear;
will-change: transform;
}
.pull-card-touch:deep(.card-wrapper),
.pull-expand-root:deep(.card-wrapper) {
/* 深选择:不重复视觉,只确保 transform-origin 生效(已被 .card-wrapper 块覆盖) */
}
/* 放大态根容器 */
.pull-expand-root {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
/* 锁定态过渡 */
.pull-expand-root.is-expanded .pull-card-touch {
transition: transform 0.25s cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* 点击背景收起 */
.pull-mask {
position: fixed;
inset: 0;
z-index: 90;
background: transparent;
}
/* 提示文案 */
.pull-hint {
position: fixed;
top: 220rpx;
left: 50%;
transform: translateX(-50%);
z-index: 95;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.85);
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.6);
pointer-events: none;
}
</style>
Step 2: 校验文件路径与命名
ls -la /Users/liulujian/Documents/code/TopFansByGithub/frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue
Expected: 文件存在,无错误。
Step 3: 提交(按 CLAUDE.md 规范需用户明确指示,本计划仅列命令)
git add frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue
git commit -m "feat(asset-detail): scaffold AssetCardPullExpand component with props and inner .card-wrapper block"
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Task 2: 完善触摸手势实现
Files:
- Modify:
frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue(仅替换<script setup>内的onTouchStart/onTouchMove/onTouchEnd函数体)
Interfaces:
- Consumes:
props.expandThreshold/props.maxPullDistance/props.disabled、内部 refpullStartY/pullDistance/isPullActive/isExpanded - Produces: 在
isPullActive过程中根据pullDistance同步cardTransform;onTouchEnd在达到阈值时设isExpanded = true并 emitpull-expand-change({ state: 'expanded' }),否则pullDistance = 0回弹
Step 1: 替换 onTouchStart 函数体
把 Task 1 占位的 function onTouchStart() {} 替换为:
function onTouchStart(e) {
if (!shouldHandleGesture.value) return;
// 放大态下不重新捕获(避免误触)
if (isExpanded.value) return;
if (e.touches.length !== 1) return;
pullStartY.value = e.touches[0].pageY;
}
Step 2: 替换 onTouchMove 函数体
把占位的 function onTouchMove() {} 替换为:
function onTouchMove(e) {
if (!shouldHandleGesture.value) return;
if (pullStartY.value == null) return;
if (e.touches.length !== 1) return;
const delta = e.touches[0].pageY - pullStartY.value;
if (delta <= 0) {
// 向上滑/未真正下拉:不进入激活态
return;
}
const next = Math.min(delta * 0.6, props.maxPullDistance);
pullDistance.value = next;
if (next > 0) {
isPullActive.value = true;
// 阻止默认滚动(放大态无 scroll-view,正常态父级控制)
if (typeof e.preventDefault === 'function') {
try { e.preventDefault(); } catch (_) {}
}
}
}
Step 3: 替换 onTouchEnd 函数体
把占位的 function onTouchEnd() {} 替换为:
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;
}
Step 4: 手动语法检查
# 仅作语法级别的快速验证(无 linter 工具时)
node -e "const fs=require('fs');const src=fs.readFileSync('/Users/liulujian/Documents/code/TopFansByGithub/frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue','utf8');const m=src.match(/<script setup>([\\s\\S]*?)<\\/script>/);if(!m){console.error('no script');process.exit(1);}try{new Function(m[1].replace(/import [^;]+;/g,'').replace(/defineProps|defineEmits|watch/g,'(function(){})'));console.log('syntax ok');}catch(e){console.error('syntax error',e.message);process.exit(1);}"
Expected: syntax ok
Step 5: 提交
git add frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue
git commit -m "feat(asset-detail): add pull gesture handlers with APP-PLUS gating"
Task 3: 完善 getStickerStyle 函数
Files:
- Modify:
frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue(替换 Task 1 占位的getStickerStyle函数体)
Interfaces:
- Consumes:
sticker对象(来自 propsstickers,字段id/src/pos_x/pos_y/rotation/scale_x/scale_y/opacity) - Produces: 与
asset-detail.vue中getStickerStyle行为一致的对象,字段与image的 inlinestyle兼容
Step 1: 替换 getStickerStyle 函数体
把 Task 1 占位的 getStickerStyle 替换为:
function getStickerStyle(sticker) {
const ox = (sticker.pos_x != null ? sticker.pos_x : 0.5) * 100;
const oy = (sticker.pos_y != null ? sticker.pos_y : 0.5) * 100;
const rot = sticker.rotation != null ? sticker.rotation : 0;
const scX = sticker.scale_x != null ? sticker.scale_x : 1;
const scY = sticker.scale_y != null ? sticker.scale_y : 1;
const op = sticker.opacity != null ? sticker.opacity : 1;
return {
left: `${ox}%`,
top: `${oy}%`,
transform: `translate(-50%, -50%) translateZ(0) rotate(${rot}deg) scale(${scX}, ${scY})`,
opacity: op,
};
}
Step 2: 与父级逻辑一致性核对
打开 frontend/pages/asset-detail/asset-detail.vue,搜索 function getStickerStyle,把两个函数体逐字对照,确保字段映射一致(pos_x / pos_y / rotation / scale_x / scale_y / opacity 与 transform 字符串模板)。
Expected: 函数体字段名 / 默认值与父级一致。
Step 3: 提交
git add frontend/components/AssetCardPullExpand/AssetCardPullExpand.vue
git commit -m "feat(asset-detail): mirror getStickerStyle in AssetCardPullExpand"
Task 4: 在 asset-detail.vue 引入 viewMode 与 AssetCardPullExpand
Files:
- Modify:
frontend/pages/asset-detail/asset-detail.vue
Interfaces:
- Consumes: 组件
<AssetCardPullExpand>暴露的 props / events - Produces:
- 顶层
v-else-if="viewMode === 'expanded'"分支挂组件 viewMode: ref('normal')状态onPullExpandChange({ state })回调.pull-expanded-view容器样式
- 顶层
Step 1: 添加 import 与 ref
在 asset-detail.vue 的 <script setup> 顶部 import 区域(现有 import LenticularCard from '@/components/lenticular/LenticularCard.vue'; 之后)插入:
import AssetCardPullExpand from '@/components/AssetCardPullExpand/AssetCardPullExpand.vue';
在 // 倒计时 注释之前的合适位置(refs 区域)添加:
// 视图模式: 'normal' = 列表滚动卡片 / 'expanded' = 全屏放大态
const viewMode = ref('normal');
function onPullExpandChange({ state }) {
viewMode.value = state;
}
Step 2: 在顶层 v-if 链中插入放大态分支
把现有:
<view v-if="loading" class="loading-wrapper">
...
</view>
<!-- 错误状态 -->
<view v-else-if="loadError && !craftConfirmMode" class="error-wrapper">
...
</view>
<!-- 详情内容 -->
<scroll-view v-else scroll-y class="content-scroll" :show-scrollbar="false">
...
</scroll-view>
调整为(在错误态 <view> 与 <scroll-view> 之间插入新的 <view v-else-if>):
<view v-if="loading" class="loading-wrapper">
...
</view>
<!-- 错误状态 -->
<view v-else-if="loadError && !craftConfirmMode" class="error-wrapper">
...
</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>
<!-- 详情内容 -->
<scroll-view v-else scroll-y class="content-scroll" :show-scrollbar="false">
...
</scroll-view>
Step 3: 添加 .pull-expanded-view 容器样式
在 asset-detail.vue 的 <style scoped> 末尾(.share-canvas 之前)追加:
/* 放大态容器:全屏居中卡片,完全脱离 scroll-view */
.pull-expanded-view {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
/* 背景:复用 detail-container 的 .background-image(不重复声明) */
}
Step 4: 检查 v-if 链顺序
确认模板中四条分支的顺序为:
<view v-if="loading" class="loading-wrapper"><view v-else-if="loadError && !craftConfirmMode" class="error-wrapper"><view v-else-if="viewMode === 'expanded'" class="pull-expanded-view">← 新增<scroll-view v-else scroll-y class="content-scroll" :show-scrollbar="false">
Step 5: 提交
git add frontend/pages/asset-detail/asset-detail.vue
git commit -m "feat(asset-detail): wire viewMode and AssetCardPullExpand via v-else-if branch"
Task 5: 验证 <scroll-view v-else> 分支未改动
Files:
- Modify: 无(纯验证)
- Read:
frontend/pages/asset-detail/asset-detail.vue
Interfaces:
- Consumes: 无
- Produces: 确认正常态分支与
card-wrapper区块未变
Step 1: 确认 <scroll-view v-else> 分支与 card-wrapper 段未变
git diff --stat HEAD~1 -- frontend/pages/asset-detail/asset-detail.vue
Expected: 仅显示新增的 view v-else-if / import / viewMode / onPullExpandChange / .pull-expanded-view 样式;不显示 <scroll-view> 内部或 <view class="card-wrapper"> 区块的改动。
Step 2: 手动核对 card-wrapper 区块
打开 frontend/pages/asset-detail/asset-detail.vue,在 <scroll-view v-else> 分支中找到 <view class="card-wrapper" :class="[…">,确认其下 card-frame / detail-lenticular-slot / LenticularCard / card-image / card-badge / card-sticker 六个子节点与组件 1.7 模板一致。
Expected: 与 Task 1 的 AssetCardPullExpand.vue 模板内层 DOM 字符串完全一致(顺序 / class / 属性名 / 标签名)。
Step 3: 确认 asset-detail.vue 现有卡片样式未删除
git diff HEAD~1 -- frontend/pages/asset-detail/asset-detail.vue | grep -E "^-.*\\.card-(frame|image|badge|sticker)|^-.*\\.detail-lenticular-(slot|card)|^-.*\\.card-wrapper"
Expected: 无 - 开头的删除行(说明 card-wrapper 等样式块未被删除)。
Step 4: 提交(如无改动则跳过)
如发现非预期改动,立即回滚:
git checkout -- frontend/pages/asset-detail/asset-detail.vue
然后回到 Task 4 重新执行。
Task 6: 手工联调 — 真机 (App-Plus) 验证
Files:
- Modify: 无
- Run:
frontend/项目 App 构建(命令视项目 README 而定;UniApp 标准命令npm run build:app-plus或 HBuilderX 运行到真机)
Interfaces:
- Consumes: 真机 App 环境
- Produces: 在真机上观察到的行为记录(写到 commit message 或 PR 描述)
Step 1: 准备运行环境
- 在 HBuilderX 中打开
frontend/根目录。 - 选择“运行 → 运行到手机或模拟器 → Android App-基座 / iOS App-基座”。
- 打开任意一条藏品详情(
pages/asset-detail/asset-detail.vue)。
Step 2: 验证清单
逐条对照,记录通过/未通过:
- 正常态加载:详情页正常加载,卡片、点赞、信息行、创作者、链上数据全部显示,无回归。
- 顶部下拉:在列表顶部(
scrollTop === 0)下拉 → 卡片跟随手指放大,显示“下拉查看大图”提示。 - 下拉阈值:继续下拉到约 80rpx → 提示文案切换为“松手查看大图”。
- 松手放大:松手后
viewMode === 'expanded',<scroll-view>销毁,卡片保持 1.6 倍放大,其它模块消失。 - Header 保留:返回键、分享按钮、举报按钮在放大态下仍可见可点。
- 点击背景:在放大态下点击卡片外空白处 → 卡片回弹到 1.0 倍,
viewMode === 'normal',<scroll-view>重新创建,其它模块恢复。 - 列表中段下拉:在
scrollTop > 0时下拉 → 列表正常滚动,卡片无放大。 - 光栅卡片:进入光栅藏品详情 → 下拉后
LenticularCard状态保留(贴纸/角标同步放大),无重渲染。 - 加载/错误态:模拟
loadError(在loadData抛错)→ 错误态显示,组件未挂载,无下拉。 - 返回键:在放大态下按 Android 物理返回 / iOS 左滑 → 走
handleBack()既有流程,无 JS 报错。
Step 3: 记录未通过项
任何未通过的项都对应到本计划的某个 Task,回到对应 Task 修复后重新跑本验证,直到全部通过。
Step 4: 提交(如有调整)
git add -A
git commit -m "fix(asset-detail): address QA findings from pull-expand manual test"
Task 7: H5 端降级验证
Files:
- Modify: 无
- Run: H5 端构建/运行(命令:
npm run build:h5+ 浏览器打开)
Interfaces:
- Consumes: H5 端运行环境
- Produces: 确认
// #ifdef APP-PLUS路径不会在 H5 启用
Step 1: 启动 H5
cd /Users/liulujian/Documents/code/TopFansByGithub/frontend
npm run dev:h5
或 HBuilderX 选择“运行 → 运行到浏览器 → Chrome”。
Step 2: 验证清单
- 详情页加载正常,卡片显示无异常。
- 鼠标在卡片上按住往下拖 → 卡片无放大(因为
isAppPlus === false,shouldHandleGesture始终 false,触摸回调直接 return)。 - 列表滚动正常,无 JS 报错。
Step 3: 浏览器控制台无报错
打开 DevTools Console,确认无以下报错:
Cannot read property 'touches' of undefinedshouldHandleGesture is not definedpullStartY is not a function
Step 4: 提交(如有 H5 端调整)
git add -A
git commit -m "fix(asset-detail): ensure pull-expand is no-op on H5"
Task 8: 全局回归与 spec 自检对照
Files:
- Modify: 无
- Read:
docs/superpowers/specs/2026-07-27-asset-detail-pull-expand-design.md
Interfaces:
- Consumes: 已完成的实现 + spec
- Produces: 自检对照表(记录到 PR 描述)
Step 1: spec §5.1 自检逐条核对
| # | 自检项 | 任务映射 | 状态 |
|---|---|---|---|
| 1 | 列表顶部下拉 → 卡片跟随手指放大 | Task 6 | ☐ |
| 2 | 下拉到 80rpx 后松手 → viewMode 切到 expanded, scroll-view 销毁 | Task 6 | ☐ |
| 3 | 点击背景 → viewMode 切回 normal, scroll-view 重建 | Task 6 | ☐ |
| 4 | 列表中段下拉 → 不进入激活态 | Task 6 | ☐ |
| 5 | 光栅卡片放大 → LenticularCard 状态保留 | Task 6 | ☐ |
| 6 | Header 两种状态下都可点 | Task 6 | ☐ |
| 7 | 加载/错误态下无下拉 | Task 6 | ☐ |
| 8 | H5 端退化为静态 | Task 7 | ☐ |
| 9 | DOM/class 一致性(8 个 class + 5 个属性) | Task 1 + Task 5 | ☐ |
| 10 | 样式不重复声明(组件 .card-wrapper 仅 3 行) | Task 1 + Task 5 | ☐ |
Step 2: spec §5.2 回归检查逐条核对
- 改动文件只有
AssetCardPullExpand.vue(新增)和asset-detail.vue(局部)。 - 未改动
LenticularCard.vue/ShareReportButtons.vue/composables/useLenticularCraftTiltPreview.js/utils/sticker-compositor.js/utils/castloveMintForm.js。 - 不修改后端 API,不改数据库 schema。
Step 3: 风险与回退
- 风险 1 (
touchmove冒泡): 已在 Task 2onTouchMove用e.preventDefault()兜底。 - 风险 2 (DOM 偷改): Task 5 步骤 1-3 已验证。
- 风险 3 (scroll-view 销毁重建): 父级
viewMode是 ref,所有数据(coverUrl/lenticularLayers/activeStickers等)在父级维护,不受影响。
Step 4: 把对照表写入 PR 描述
(若此计划经 subagent-driven-development 执行,最终 PR 描述需包含本表。)
完成标准
- 所有 Task 1-7 步骤勾选完毕。
- Task 8 自检表全部勾选完毕。
- 真机 (App-Plus) 与 H5 端均无 JS 报错、视觉与功能符合 spec。
git log显示每步独立 commit,commit message 符合Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>规范。- 不修改
unpackage/dist/、不修改后端代码、不修改数据库 schema。