feat:修改陀螺仪控制为手动控制
This commit is contained in:
parent
217af65121
commit
a1e92448f1
@ -323,6 +323,7 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl
|
||||
materials := assets.Group("/materials")
|
||||
materials.POST("/upload", assetCtrl.UploadMaterial)
|
||||
assets.POST("/:asset_id/materials", assetCtrl.BindAssetMaterials)
|
||||
assets.GET("/:asset_id/materials", assetCtrl.GetAssetMaterials) // 获取资产素材列表(修复:原 handler 存在但未注册)
|
||||
}
|
||||
|
||||
// 分享相关路由(需要认证)— 用 assetCtrl(spec 规定 /api/v1/share/* 前缀)
|
||||
|
||||
1013
docs/superpowers/plans/2026-07-06-remove-hardware-gyro.md
Normal file
1013
docs/superpowers/plans/2026-07-06-remove-hardware-gyro.md
Normal file
File diff suppressed because it is too large
Load Diff
401
docs/superpowers/specs/2026-07-06-remove-hardware-gyro-design.md
Normal file
401
docs/superpowers/specs/2026-07-06-remove-hardware-gyro-design.md
Normal file
@ -0,0 +1,401 @@
|
||||
# 移除光栅卡硬件陀螺仪 · 设计方案
|
||||
|
||||
> **状态**:待审核 · **创建日期**:2026-07-06 · **目标分支**:feat/actvity
|
||||
>
|
||||
> **本方案替代 2026-06-30 引入的"加速度计 + One-Euro 滤波"方案**(commit `4eab42b`)。旧 spec/plan 保留作为历史演进记录。
|
||||
|
||||
---
|
||||
|
||||
## 方案概述(*必读*)
|
||||
|
||||
### 要解决的问题
|
||||
|
||||
**业务问题**
|
||||
- 铸爱光栅卡(H5 + App)的倾斜预览在 App 端需要硬件陀螺仪/加速度计权限,UX 摩擦大:
|
||||
- iOS 13+ Safari / WKWebView 弹出 `requestPermission`,用户拒绝即不可用
|
||||
- Android 12+ 强制 `BODY_SENSORS` 权限,应用商店审核严格
|
||||
- 部分低端 Android 机陀螺仪硬件缺失,只能降级
|
||||
- 户外/静止场景下用户无法(或不想)倾斜手机预览,缺少替代交互
|
||||
|
||||
**技术问题**
|
||||
- 硬件传感器路径有 3 套(Native.js Android / Native.js iOS / DeviceOrientationEvent),代码 ~28KB(`useLenticularStudioTilt.js`)
|
||||
- 4 天前刚把陀螺仪改为加速度计(`4eab42b`),整套 One-Euro 滤波、跳变拒绝、快慢双通道、预热跳过逻辑投入大、收益不明
|
||||
- 原生插件 `imengyu-UniAndroidGyro`(含 AAR + iOS Framework)成为单点依赖
|
||||
- App 端倾斜效果与触摸拖拽效果**已经并存**(`LenticularCard.vue` 的 `touchstart/move/end` 事件一直在 emit `simulate`),铸爱页(`lenticular-result`)却用 `skipBuiltInTouch=true` 屏蔽触摸、强制走硬件路径
|
||||
|
||||
### 整体实现路径
|
||||
|
||||
| Phase | 内容 | 工作量 |
|
||||
|-------|------|--------|
|
||||
| Phase 1 | 在 `useLenticularCraftTiltPreview` 新增 `simulateTilt / simulateTiltFromNormalized / simulateTiltReset` 公共方法(保留旧路径并行) | 0.5 天 |
|
||||
| Phase 2 | 新增 `<ManualTiltSlider>` 独立组件 | 0.5 天 |
|
||||
| Phase 3 | `<LenticularCard>` / `<HolographicCard>` 加 `showSlider / sliderValue` prop + `update:sliderValue / sliderReset` emit(v-model 配套),引入 `<ManualTiltSlider>` | 0.5 天 |
|
||||
| Phase 4 | `lenticular-result.vue` 切换:改触摸接收器、加滑块、删 3 处 `scheduleTiltStart/stopTiltPreview` 调用 | 0.5 天 |
|
||||
| Phase 5 | 清理收尾:删 `useLenticularStudioTilt.js`、`useOneEuroFilter.js`、原生插件目录、manifest 注册段、权限与隐私描述 | 0.5 天 |
|
||||
| QA | 4.3 验收清单(iOS 真机 / Android 真机 / H5 Chrome) | 1 天 |
|
||||
| **合计** | | **~3.5 天** |
|
||||
|
||||
### 关键决策
|
||||
|
||||
1. **彻底移除硬件传感器代码**(不保留"硬件 / 手动"切换开关)— 决策依据:CLAUDE.md MVP 先行原则,"应急开关"属于 YAGNI;用户已确认手动模拟可以替代硬件方案
|
||||
2. **滑块做成独立组件** `<ManualTiltSlider>`,与卡片解耦 — 便于未来页面级复用
|
||||
3. **触摸拖拽路径保留**(`LenticularCard` / `HolographicCard` 已有),与滑块**不互斥**,数据驱动
|
||||
4. **离散档位逻辑保留**(铸爱页 2 层卡硬切效果不动)— 与硬件源无关
|
||||
5. **删除原生插件目录** `frontend/nativePlugins/imengyu-UniAndroidGyro/` — 这是物理上解除单点依赖
|
||||
6. **iOS `NSMotionUsageDescription` 同步删**(在 `manifest.json`)— 不删会导致应用商店审核仍有"访问运动传感器"声明
|
||||
7. **历史设计文档保留**(2026-06-30 accel-oneeuro design/plan)— 作为设计演进记录,不删
|
||||
|
||||
### 核心架构图
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ 手动输入(双源) │
|
||||
│ ┌────────────────────┐ ┌────────────────────────┐ │
|
||||
│ │ LenticularCard.vue │ │ <ManualTiltSlider/> │ │
|
||||
│ │ (触摸拖拽) │ │ (滑块 0..1) │ │
|
||||
│ └─────────┬──────────┘ └───────────┬────────────┘ │
|
||||
│ │ emit('simulate', x, y) │ change: v │
|
||||
│ │ 归一化 -1..+1 │ 归一化 0..1 │
|
||||
└────────────┼─────────────────────────────┼────────────────┘
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ useLenticularCraftTiltPreview │
|
||||
│ │
|
||||
│ simulateTiltFromNormalized(x, y) // 触摸入口 │
|
||||
│ simulateTilt(dx, dy) // 滑块入口(度) │
|
||||
│ simulateFromSignedDegrees(dx, dy) // 内部(度) │
|
||||
│ simulate(gamma, beta) // 内部(-1..+1) │
|
||||
│ ↓ │
|
||||
│ 离散档位映射 (TILT_ACTIVATE/DEACTIVATE 滞回) │
|
||||
│ ↓ │
|
||||
│ useLenticularPreview 渲染循环 │
|
||||
│ ↓ │
|
||||
│ engine.feedSimulatedTilt() → layerTransforms │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
▼
|
||||
<LenticularCard :transforms />
|
||||
<HolographicCard :viewAngle />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文档说明
|
||||
|
||||
- **适用范围**:前端 uni-app 铸爱光栅卡模块(含全息卡);不影响后端、不影响其他业务
|
||||
- **前置版本/历史**:
|
||||
- `4eab42b` (2026-07-01) 引入加速度计 + One-Euro 滤波方案
|
||||
- `2855cd5` 之前使用陀螺仪
|
||||
- `10168f8` AI 生成镭射卡功能
|
||||
- `889c923` 镭射新链路
|
||||
- **工作量估算**:约 3.5 天(开发 2.5 天 + QA 1 天)
|
||||
- **目标读者**:铸爱光栅卡模块的开发者、iOS/Android 打包配置维护者、QA 验收人
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构与数据流
|
||||
|
||||
参见"核心架构图"小节。
|
||||
|
||||
### 数据流要点
|
||||
|
||||
1. **彻底删除** `useLenticularStudioTilt.js`(28KB / 894 行)+ `useOneEuroFilter.js`(~75 行)
|
||||
2. **API 统一入口**:`useLenticularCraftTiltPreview` 暴露 `simulateTilt(dx, dy)`(度)作为唯一倾斜入口
|
||||
3. **触摸路径**:保留 `LenticularCard.vue` / `HolographicCard.vue` 的触摸事件,把 emit 转发到 `simulateTiltFromNormalized`(在 `useLenticularCraftTiltPreview` 内部做归一化 → 度数转换,转换系数 **45°/单位**)
|
||||
4. **滑块路径**:页面级 `<LenticularCard show-slider v-model:slider-value="tiltValue" @slider-reset="tiltValue = 0.5" />`(卡片内部嵌入 `<ManualTiltSlider>`,所有权归调用方,0.5=中位 → 0°)
|
||||
5. **铸爱页(lenticular-result)**:去掉 `skipBuiltInTouch=true` 改为默认(false),去掉 `scheduleTiltStart` 调用(无传感器要启动了)
|
||||
|
||||
---
|
||||
|
||||
## 2. 文件变更清单
|
||||
|
||||
### 2.1 删除(3 项)
|
||||
|
||||
| 文件 / 目录 | 大小 | 删除理由 |
|
||||
|------|------|---------|
|
||||
| `frontend/composables/useLenticularStudioTilt.js` | 28KB / 894 行 | 整体硬件传感器模块彻底无用 |
|
||||
| `frontend/composables/useOneEuroFilter.js` | ~75 行 | 仅 `StudioTilt` 一处使用(grep 验证:仅 1 处 import) |
|
||||
| `frontend/nativePlugins/imengyu-UniAndroidGyro/` | 含 AAR + iOS Framework + package.json | 原生插件无调用方后即可整体删除 |
|
||||
|
||||
### 2.2 新增(1 项)
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `frontend/components/lenticular/ManualTiltSlider.vue` | uniapp `<slider>` 包装组件,双向绑定 0..1,含复位按钮 |
|
||||
|
||||
### 2.3 修改(8 个文件)
|
||||
|
||||
| 文件 | 关键变更 |
|
||||
|------|---------|
|
||||
| `frontend/composables/useLenticularCraftTiltPreview.js` | ① 删 `useLenticularStudioTilt` 引用和 `scheduleTiltStart/stopTiltPreview` ② 新增公共方法 `simulateTilt(dx, dy)`(度)③ 新增 `simulateTiltFromNormalized(x, y)`(适配触摸)④ `lockPreviewStill` 保留 |
|
||||
| `frontend/composables/useLenticularPreview.js` | 改动最小:注释更新(移除"接受 simulate() 注入"的隐含承诺),内部逻辑不动 |
|
||||
| `frontend/composables/useHolographicPreview.js` | 删 `startGyro/startDeviceOrientation` 等所有 `DeviceOrientationEvent` 路径;保留 `simulate(x, y)` 作为唯一入口;移除 OneEuro 引用 |
|
||||
| `frontend/components/lenticular/LenticularCard.vue` | ① 新增 prop `showSlider: Boolean`,默认 `false` ② 新增 prop `sliderValue: Number`(0..1,双向)③ 模板内 `v-if="showSlider"` 渲染 `<ManualTiltSlider>` ④ 触摸开始/移动 emit `simulate(x, y)` 不变;**触摸结束**改为**不再 emit `(0,0)`**(与滑块数据驱动相容,见 4.1) |
|
||||
| `frontend/components/lenticular/HolographicCard.vue` | 同上,prop 一致;触摸结束同样**不再 emit `(0,0)`** |
|
||||
| `frontend/components/lenticular/CastloveLenticularPreview.vue` | 加 `show-slider` prop 透传 |
|
||||
| `frontend/pages/castlove/lenticular/lenticular-result.vue` | ① 删 `scheduleTiltStart/stopTiltPreview` 调用(3 处)② 删 `:skip-built-in-touch="true"`,改用触摸 emit ③ 加 `show-slider` prop ④ `@simulate` 改为 `(x, y) => simulateTiltFromNormalized(x, y)` |
|
||||
| `frontend/manifest.json` | ① 删 `app-plus.nativePlugins.imengyu-UniAndroidGyro` 段(30-44 行)② 删 `BODY_SENSORS` 权限(69 行)③ 删 `NSMotionUsageDescription` 隐私描述(82 行) |
|
||||
| 构建设置 | 无(iOS Info.plist 由打包机自动生成,修改 `manifest.json` 即可生效) |
|
||||
|
||||
### 2.4 不修改但需回归验证
|
||||
|
||||
- `frontend/pages/castlove/lenticular/lenticular-create.vue`:不挂载 LenticularCard 实际渲染,无需改
|
||||
- `frontend/pages/castlove/lenticular/lenticular-thinking.vue`:同上
|
||||
- `frontend/utils/lenticular-engine.js`:渲染逻辑与输入源无关,无需改
|
||||
|
||||
---
|
||||
|
||||
## 3. API 设计
|
||||
|
||||
### 3.1 `useLenticularCraftTiltPreview(layersRef)`
|
||||
|
||||
**返回对象**:
|
||||
|
||||
| 字段 | 类型 | 状态 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `physics` | `Reactive<PhysicsConfig>` | 保留 | 物理参数 |
|
||||
| `layerTransforms` | `Ref<Object>` | 保留 | 渲染输出(直接喂给 `<LenticularCard :transforms>`) |
|
||||
| `simulate(gamma, beta)` | `(number, number) => void` | 保留 | 内部用,归一化输入 |
|
||||
| `relax(factor)` | `(number=0.85) => void` | 保留 | 平滑归零 |
|
||||
| `lockPreviewStill()` | `() => void` | 保留 | 强制回中(清空离散档位状态 `lastLayerIdx` / `snapCooldownUntil` / `dirSign` / `dirOppositeStreak` + `simulate(0, 0)`)。使用场景:进入页面初始化 / 用户主动点"归位"按钮 / 切换卡类型时。**不再**由 `scheduleTiltStart` 触发(Phase 4 之后该调用一并删除)。 |
|
||||
| `simulateTilt(dx, dy)` | `(number, number) => void` | **新增** | 滑块/外部入口,单位**度** |
|
||||
| `simulateTiltFromNormalized(x, y)` | `(number, number) => void` | **新增** | 触摸入口,-1..+1 转 °(系数 45°) |
|
||||
| `simulateTiltReset()` | `() => void` | **新增** | 滑块点"复位"时调用 |
|
||||
| `gyroSourceLabel` | `Ref<string>` | **删除** | 不再有硬件源 |
|
||||
| `scheduleTiltStart` | `() => void` | **删除** | 不再启动传感器 |
|
||||
| `stopTiltPreview` | `() => void` | **删除** | 同上 |
|
||||
|
||||
**内部状态**:`lastLayerIdx`、`snapCooldownUntil`、`dirSign`、`dirOppositeStreak` 全保留(离散档位逻辑不动)。
|
||||
|
||||
### 3.2 `useHolographicPreview()`
|
||||
|
||||
| 字段 | 类型 | 状态 |
|
||||
|------|------|------|
|
||||
| `physics` | `Reactive` | 保留 |
|
||||
| `viewAngle` | `Reactive{x,y,z}` | 保留 |
|
||||
| `isWebGLReady`, `hasWebGLError`, `fps` | `Ref` | 保留 |
|
||||
| `simulate(x, y)` | `(number, number) => void` | 保留(成为唯一驱动入口) |
|
||||
| `relax(factor)` | 保留 |
|
||||
| `onWebGLReady/onWebGLError/onFPSUpdate` | 保留 |
|
||||
| `startGyro/stopGyro` | **删除**(设备方向监听整套删) |
|
||||
| `gyroSource` | **删除** |
|
||||
| `accelHandler`, `accelBaselineReady`, `accelBaseX/Y` | **删除**(基线追踪是 DeviceOrientationEvent 配套) |
|
||||
| `detectPerformanceTier`, `HOLO_PERFORMANCE_PRESETS` | 保留(独立工具函数) |
|
||||
|
||||
### 3.3 `<LenticularCard>` / `<HolographicCard>` 新增 props
|
||||
|
||||
```ts
|
||||
defineProps({
|
||||
// ... 既有 props ...
|
||||
skipBuiltInTouch: { type: Boolean, default: false }, // 保留
|
||||
showSlider: { type: Boolean, default: false }, // 新增:是否显示内置滑块
|
||||
sliderValue: { type: Number, default: 0.5 }, // 新增:v-model:sliderValue 双向绑定
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'simulate', // 保留:触摸拖拽 emit (x, y) -1..+1
|
||||
'update:sliderValue', // 新增:v-model 配套(v-model:sliderValue 双向绑定)
|
||||
'sliderReset', // 新增:滑块复位按钮 emit ()
|
||||
])
|
||||
```
|
||||
|
||||
**emit 转发逻辑**(卡片内部):
|
||||
```vue
|
||||
<ManualTiltSlider
|
||||
v-if="showSlider"
|
||||
:value="sliderValue"
|
||||
@change="v => emit('update:sliderValue', v)"
|
||||
@reset="emit('sliderReset')"
|
||||
/>
|
||||
```
|
||||
|
||||
- `gyroSource` prop 保留但写死为 `'simulation'`(不删 prop 以避免破坏外部传参,向后兼容,**仅作为显示字段**,不参与业务逻辑)
|
||||
- **滑块所有权归调用方**:调用方使用 `v-model:sliderValue="tiltValue"` 双向绑定,滑块拖动时自动更新 `tiltValue`;调用方再将 `tiltValue` 通过 `simulateTilt` 喂入 composable
|
||||
- `sliderReset` emit 后,调用方负责将 `tiltValue` 重置为 0.5
|
||||
|
||||
### 3.4 `<ManualTiltSlider>` 新组件 API
|
||||
|
||||
```ts
|
||||
// 文件:frontend/components/lenticular/ManualTiltSlider.vue
|
||||
defineProps({
|
||||
value: { type: Number, default: 0.5, validator: v => v >= 0 && v <= 1 },
|
||||
disabled: { type: Boolean, default: false },
|
||||
showReset: { type: Boolean, default: true },
|
||||
min: { type: Number, default: 0 },
|
||||
max: { type: Number, default: 1 },
|
||||
step: { type: Number, default: 0.01 },
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [v: number] // 用户拖动
|
||||
reset: [] // 点复位
|
||||
}>()
|
||||
```
|
||||
|
||||
- 视觉:卡片下方 32rpx 高度的细条 + 居中圆点 thumb + 右侧小图标"↺"
|
||||
- 平台:使用 `<slider>` uniapp 组件(App/H5/小程序都支持,原生渲染,跟手)
|
||||
|
||||
### 3.5 数值约定一致性
|
||||
|
||||
| 上下文 | 范围 | 备注 |
|
||||
|--------|------|------|
|
||||
| 触摸 emit | 实际 [-1.1, +1.1],有效 [-1, +1] | 现有 `LenticularCard.dispatch()` 末尾 `* 1.1` 系数放大(line 183-184),允许轻微超出;模拟路径中由 3.5 公式截断到 [-45°, +45°] |
|
||||
| 滑块 value | 0..1 | 中位 0.5 = 0° |
|
||||
| `simulateTilt(dx, dy)` | -45..+45 度 | 物理量纲,与旧 `simulateFromSignedDegrees` 一致(推导:触摸 x∈[-1,+1]×45° 与滑块 v∈[0,1]×(±45°)) |
|
||||
| `simulate(gamma, beta)` | -1..+1 | 引擎层归一化 |
|
||||
| 滑块 → 度 | 公式 | `dx = (v - 0.5) * 90`(v ∈ [0,1] → dx ∈ [-45°, +45°]) |
|
||||
| 触摸 → 度 | 公式 | `dx = x * 45`(x ∈ [-1, +1] → dx ∈ [-45°, +45°]) |
|
||||
|
||||
---
|
||||
|
||||
## 4. UI + 错误处理 + 测试
|
||||
|
||||
### 4.1 滑块 UI 形态
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ LenticularCard │ ← 触摸区 │
|
||||
│ │ │ (emit simulate) │
|
||||
│ │ │ │
|
||||
│ └──────────────────┘ │
|
||||
│ │
|
||||
│ ↺ [────●──────────] │
|
||||
│ 0 0.5 1 │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 元素 | 规格 |
|
||||
|------|------|
|
||||
| 位置 | 卡片下方 16rpx 间距,水平居中 |
|
||||
| 高度 | 48rpx |
|
||||
| 滑块本体 | `<slider>` uniapp 组件,min=0 / max=1 / step=0.01 |
|
||||
| Thumb | 圆点 24rpx 直径,主题色高亮 |
|
||||
| 复位按钮 | 左侧 `↺` 图标 32rpx,点击 emit `sliderReset` |
|
||||
| 颜色变量 | 与 LenticularCard 主题色一致(透传 prop 或写死 #6E7AFF) |
|
||||
|
||||
**两种输入源的优先级规则**:
|
||||
- 触摸拖拽 + 滑块**不互斥**:都向同一 `simulateTilt*` 入口打,**最后一次操作**生效
|
||||
- 触摸结束 emit `(0, 0)`,会盖掉滑块当前位置 → 这是 bug
|
||||
- **修复方案**:触摸结束 emit 改为不发送 `(0,0)`,改由调用方决定是否 `relax(0.86)`,或 `lockPreviewStill()`
|
||||
- 滑块拖动时也类似:**不**在拖动结束时 emit reset,**完全由数据驱动**(滑块 0.5 = 中位 = 不倾斜)
|
||||
|
||||
### 4.2 错误处理 / 边界场景
|
||||
|
||||
| 场景 | 处理 |
|
||||
|------|------|
|
||||
| `skipBuiltInTouch=true` + `showSlider=true` | 都允许,触摸被屏蔽,滑块生效 |
|
||||
| `skipBuiltInTouch=false` + 触摸与滑块同时操作 | 最后一次事件胜出,无须加锁 |
|
||||
| 触摸事件 `e.touches` 为空(`changedTouches` 也为空) | 现有代码已用 `if (!touch) return` 兜底,保留 |
|
||||
| 滑块 value 超出 0..1(外部 bug) | 组件 prop validator 拦截,console.warn |
|
||||
| `simulateTilt` 收到 `NaN`/`Infinity` | 入口处 `Number.isFinite()` 检查,否则 `console.warn` 丢弃 |
|
||||
| `layersRef` 长度为 0 或 1 | 离散档位逻辑已有 `if (count < 2) return` 兜底,保留 |
|
||||
| App 端 WebView 触摸延迟 | uniapp `<slider>` 原生组件,跟手性 OK;触摸事件延迟是 uniapp 通用问题,沿用现状 |
|
||||
| 旧硬件传感器代码残留 | `useLenticularStudioTilt.js` 整体删除,无残留风险 |
|
||||
| **Phase 5 漏删** `BODY_SENSORS` 权限 | §2.3 已列入修改清单;QA 用附录 grep 命令兜底校验 |
|
||||
| **Phase 5 漏删** iOS `NSMotionUsageDescription` | §2.3 已列入修改清单;QA 用附录 grep 命令兜底校验 |
|
||||
|
||||
### 4.3 测试策略
|
||||
|
||||
**前端测试原则**(来自 memory `share-impl-test-policy.md`):前端不写 Vitest,**走手动验收 + 后端接口测试**。
|
||||
|
||||
| 类型 | 覆盖范围 | 方法 |
|
||||
|------|---------|------|
|
||||
| 后端接口测试 | 不涉及本次(无后端变更) | — |
|
||||
| 前端手动验收 | 所有修改页面(iOS 真机 / Android 真机 / H5 Chrome) | 见下方验收清单 |
|
||||
| 单元验证 | `useLenticularCraftTiltPreview` 的 `simulateTilt` / `simulateTiltFromNormalized` / 离散档位逻辑 | 在 DevTools console 用 `console.log` 直接调用验证(不再单独写测试文件) |
|
||||
| 平台覆盖 | iOS 真机、Android 真机、H5 Chrome | QA 走查 |
|
||||
|
||||
**手动验收清单(每平台各一遍)**:
|
||||
|
||||
1. 打开 `lenticular-result` 页面,确认卡片显示正常
|
||||
2. **触摸拖拽**:手指在卡片上左右拖动,卡片光栅效果跟随;松开手指不强制回中(验证 4.1 修复)
|
||||
3. **滑块拖动**:拖动滑块 0 → 1,卡片光栅从最左切到最右(铸爱页 2 层卡应硬切档位)
|
||||
4. **复位按钮**:点击 ↺,滑块回 0.5,卡片回中间
|
||||
5. **组合**:拖动滑块到 0.3,然后触摸卡片到最右 → 触摸胜出
|
||||
6. `manifest.json` 中 `BODY_SENSORS` / `NSMotionUsageDescription` / `nativePlugins.imengyu-UniAndroidGyro` 已删除(grep 验证)
|
||||
7. 旧 iOS 用户授权提示**不再弹出**(验证 DeviceOrientationEvent 已删干净)
|
||||
8. Android 真机扫描:APP 启动时**不再**申请 BODY_SENSORS(adb logcat | grep SENSORS)
|
||||
9. `lenticular-create` / `lenticular-thinking` 页面**无回归**(这两个页面无 LenticularCard 实际渲染,但要确认不报错)
|
||||
|
||||
---
|
||||
|
||||
## 5. 实施顺序 + 风险 + 回退
|
||||
|
||||
### 5.1 实施顺序(5 个阶段,每阶段独立可回退)
|
||||
|
||||
| Phase | 内容 | 涉及文件 | 风险 | 验证 |
|
||||
|-------|------|---------|------|------|
|
||||
| **1. 底层 API** | 在 `useLenticularCraftTiltPreview` 新增 `simulateTilt` / `simulateTiltFromNormalized` / `simulateTiltReset`,**保留**旧 `scheduleTiltStart` 等方法(先并行不删) | 1 文件 | 极低(纯新增) | 单元调用验证 |
|
||||
| **2. 新组件** | 新增 `ManualTiltSlider.vue`,独立可单测 | 1 新文件 | 极低 | H5 Chrome 单独挂载测 |
|
||||
| **3. 卡片接入** | `LenticularCard` / `HolographicCard` 加 `showSlider/sliderValue` prop + `update:sliderValue/sliderReset` emit(v-model 配套),引入 `<ManualTiltSlider>` | 2 文件 | 中(prop 增减可能影响外部调用方) | lenticular-result 页面挂载验证 |
|
||||
| **4. 页面切换** | `lenticular-result.vue`:① 改 `@simulate` 接收器 ② 加 `show-slider` ③ 删 `scheduleTiltStart` 等 3 处调用 | 1 文件 | 中 | 触摸 + 滑块组合验收 |
|
||||
| **5. 清理收尾** | ① 删 `useLenticularStudioTilt.js` ② 删 `useOneEuroFilter.js` ③ 删原生插件目录 `frontend/nativePlugins/imengyu-UniAndroidGyro/` ④ 删 `manifest.json` 中 `nativePlugins` 注册段、`BODY_SENSORS`、`NSMotionUsageDescription` ⑤ PR 描述提示 | 3 文件 + 1 目录 | 中(删除 28KB + 改 manifest) | grep 残留 + 真机无传感器调用 |
|
||||
|
||||
**关键不变量**:Phase 1-4 完成后**仍然可回退**到当前状态(旧 `scheduleTiltStart` 路径保留运行);只有 Phase 5 是真正不可逆。
|
||||
|
||||
### 5.2 风险点
|
||||
|
||||
| 风险 | 等级 | 缓解措施 |
|
||||
|------|------|---------|
|
||||
| `useLenticularStudioTilt.js` 删除后存在隐藏引用 | **高** | Phase 5 之前全局 grep `useLenticularStudioTilt` / `useOneEuroFilter` / `BODY_SENSORS` / `NSMotionUsageDescription` / `imengyu-UniAndroidGyro` 必须 0 命中;CI 加 grep 校验 |
|
||||
| 触摸结束 emit `(0,0)` 导致与滑块状态打架 | **中** | 已在 4.1 给出修复方案(触摸结束不强制发 0,0,由数据驱动) |
|
||||
| `HolographicCard.vue` 当前无页面引用 | 低 | 按"全套都改"原则同步清理,**不单独删除**该组件(可能未来用) |
|
||||
| 4eab42b 新增的两个 accel-oneeuro 文档变成历史 | 低 | **保留**不删(作为设计演进记录),但在新 spec 开头加"本方案替代 2026-06-30 accel-oneeuro 方案"说明 |
|
||||
| iOS `NSMotionUsageDescription` 不在仓库 | 中 | PR 描述 / iOS 开发者文档显式提示 |
|
||||
| Android `BODY_SENSORS` 权限是 4eab42b 才加的 | 低 | manifest 改动是单点,git blame 清晰 |
|
||||
| 触屏 + 滑块**同时**操作的边界 | 中 | 设计上接受"最后一次胜出",无锁;4.2 已说明 |
|
||||
| 真机回归范围 | 中 | iOS/Android/H5 三平台均需过 4.3 验收清单 |
|
||||
| 设计师未确认滑块视觉 | 低 | 4.1 已给出 ASCII mock + 规格,UI 沿用主题色即可 |
|
||||
| 滑块 emit 命名(`update:sliderValue`)与 `simulate` 风格不同 | 极低 | 滑块用 Vue 3 标准 v-model 模式(`update:sliderValue`),与现有 `simulate(x, y)` 风格不同但符合 Vue 约定;§3.3 命名表已标注 |
|
||||
|
||||
### 5.3 兼容回退方案
|
||||
|
||||
| 阶段 | 回退方式 | 粒度 |
|
||||
|------|---------|------|
|
||||
| Phase 1 出问题 | revert 单 commit | 1 文件 |
|
||||
| Phase 2 出问题 | 删除新文件 | 1 文件 |
|
||||
| Phase 3 出问题 | revert prop 改动 | 2 文件 |
|
||||
| Phase 4 出问题 | revert 页面改动 | 1 文件 |
|
||||
| Phase 5 出问题 | revert 删除 + manifest | 3 文件 + 需回补权限字符串 + 恢复插件目录 |
|
||||
| **整体回退** | `git revert <merge-commit>` | 一次到位 |
|
||||
|
||||
**应急开关**(可选,**不实施**除非 QA 需要):
|
||||
- 保留一个 `LENTICULAR_USE_HARDWARE_GYRO` 全局开关,默认 `false`
|
||||
- 如果线上发现严重问题,远程打开临时回退到旧路径
|
||||
- 决定:**不实现**。理由:CLAUDE.md MVP 先行原则 + 28KB 代码就为"应急"留着属于 YAGNI。Phase 1-4 并行保留旧路径已经足够
|
||||
|
||||
### 5.4 范围确认
|
||||
|
||||
| 维度 | 范围 |
|
||||
|------|------|
|
||||
| **包含** | 5 个 composables(删 2 改 3:删 useLenticularStudioTilt + useOneEuroFilter,改 useLenticularCraftTiltPreview + useLenticularPreview + useHolographicPreview)+ 1 新组件 + 1 原生插件目录(删)+ 2 卡片组件(LenticularCard + HolographicCard)+ 1 包装预览组件(CastloveLenticularPreview)+ 1 页面 + manifest |
|
||||
| **包含** | 1 个新 spec 文档 `docs/superpowers/specs/2026-07-06-remove-hardware-gyro-design.md`(本文件) |
|
||||
| **不包含** | 后端代码(无后端变更) |
|
||||
| **不包含** | `lenticular-create.vue` / `lenticular-thinking.vue`(无 LenticularCard 实际渲染) |
|
||||
| **不包含** | 删除 4eab42b 的 accel-oneeuro 设计/实施文档(保留为历史记录) |
|
||||
| **不包含** | 重构 LenticularEngine 渲染逻辑(本次不改) |
|
||||
| **不包含** | "未来接回硬件"的接口预留(YAGNI) |
|
||||
|
||||
### 5.5 文档同步
|
||||
|
||||
- **新增**:`docs/superpowers/specs/2026-07-06-remove-hardware-gyro-design.md`(本文件)
|
||||
- **保留**:`docs/superpowers/specs/2026-06-30-lenticular-android-accel-oneeuro-design.md`(历史)
|
||||
- **保留**:`docs/superpowers/plans/2026-06-30-android-accel-oneeuro-implementation.md`(历史)
|
||||
- **保留**:`docs/superpowers/specs/2026-06-26-lenticular-webgl-engine-design.md`(WebGL 引擎与倾斜源无关)
|
||||
- **保留**:`docs/superpowers/specs/2026-05-22-lenticular-gyro-optimization-design.md`(原始陀螺仪方案)
|
||||
|
||||
---
|
||||
|
||||
## 附:grep 校验清单(Phase 5 前必跑)
|
||||
|
||||
```bash
|
||||
# 应全部 0 命中
|
||||
grep -rn "useLenticularStudioTilt" frontend/
|
||||
grep -rn "useOneEuroFilter" frontend/
|
||||
grep -rn "imengyu-UniAndroidGyro" frontend/
|
||||
grep -rn "BODY_SENSORS" frontend/
|
||||
grep -rn "NSMotionUsageDescription" frontend/
|
||||
grep -rn "imengyu" frontend/
|
||||
```
|
||||
@ -254,8 +254,8 @@ onMounted(() => {
|
||||
.card-body {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
/* border-radius: 24px; */
|
||||
/* overflow: hidden; */
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
transform-style: preserve-3d;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.55);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<view v-if="showReset || label" class="tilt-slider-row">
|
||||
<view v-if="showReset" class="tilt-slider-row">
|
||||
<view v-if="showReset" class="tilt-slider-reset" @tap="handleReset">
|
||||
<text class="tilt-slider-reset-icon">↺</text>
|
||||
</view>
|
||||
<text v-if="label" class="tilt-slider-label">{{ label }}</text>
|
||||
<slider
|
||||
class="tilt-slider"
|
||||
:min="min"
|
||||
@ -23,7 +22,6 @@
|
||||
defineProps({
|
||||
value: { type: Number, default: 0.5, validator: v => v >= 0 && v <= 1 },
|
||||
disabled: { type: Boolean, default: false },
|
||||
label: { type: String, default: '手动调节倾斜' },
|
||||
showReset: { type: Boolean, default: true },
|
||||
min: { type: Number, default: 0 },
|
||||
max: { type: Number, default: 1 },
|
||||
@ -72,12 +70,6 @@ function handleReset() {
|
||||
font-size: 28rpx;
|
||||
color: #6B7280;
|
||||
}
|
||||
.tilt-slider-label {
|
||||
font-size: 12px;
|
||||
color: #6B7280;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tilt-slider {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user