feat:修改陀螺仪控制为手动控制

This commit is contained in:
zerosaturation 2026-07-06 21:20:24 +08:00
parent 217af65121
commit a1e92448f1
5 changed files with 1418 additions and 11 deletions

View File

@ -323,6 +323,7 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl
materials := assets.Group("/materials") materials := assets.Group("/materials")
materials.POST("/upload", assetCtrl.UploadMaterial) materials.POST("/upload", assetCtrl.UploadMaterial)
assets.POST("/:asset_id/materials", assetCtrl.BindAssetMaterials) assets.POST("/:asset_id/materials", assetCtrl.BindAssetMaterials)
assets.GET("/:asset_id/materials", assetCtrl.GetAssetMaterials) // 获取资产素材列表(修复:原 handler 存在但未注册)
} }
// 分享相关路由(需要认证)— 用 assetCtrl(spec 规定 /api/v1/share/* 前缀) // 分享相关路由(需要认证)— 用 assetCtrl(spec 规定 /api/v1/share/* 前缀)

File diff suppressed because it is too large Load Diff

View 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` emitv-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_SENSORSadb 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` emitv-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/
```

View File

@ -254,8 +254,8 @@ onMounted(() => {
.card-body { .card-body {
position: absolute; position: absolute;
inset: 0; inset: 0;
/* border-radius: 24px; */ border-radius: 24px;
/* overflow: hidden; */ overflow: hidden;
transform-style: preserve-3d; transform-style: preserve-3d;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.55); box-shadow: 0 20px 50px rgba(0, 0, 0, 0.55);
border: 1px solid rgba(255, 255, 255, 0.18); border: 1px solid rgba(255, 255, 255, 0.18);

View File

@ -1,9 +1,8 @@
<template> <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"> <view v-if="showReset" class="tilt-slider-reset" @tap="handleReset">
<text class="tilt-slider-reset-icon"></text> <text class="tilt-slider-reset-icon"></text>
</view> </view>
<text v-if="label" class="tilt-slider-label">{{ label }}</text>
<slider <slider
class="tilt-slider" class="tilt-slider"
:min="min" :min="min"
@ -23,7 +22,6 @@
defineProps({ defineProps({
value: { type: Number, default: 0.5, validator: v => v >= 0 && v <= 1 }, value: { type: Number, default: 0.5, validator: v => v >= 0 && v <= 1 },
disabled: { type: Boolean, default: false }, disabled: { type: Boolean, default: false },
label: { type: String, default: '手动调节倾斜' },
showReset: { type: Boolean, default: true }, showReset: { type: Boolean, default: true },
min: { type: Number, default: 0 }, min: { type: Number, default: 0 },
max: { type: Number, default: 1 }, max: { type: Number, default: 1 },
@ -72,12 +70,6 @@ function handleReset() {
font-size: 28rpx; font-size: 28rpx;
color: #6B7280; color: #6B7280;
} }
.tilt-slider-label {
font-size: 12px;
color: #6B7280;
white-space: nowrap;
flex-shrink: 0;
}
.tilt-slider { .tilt-slider {
flex: 1; flex: 1;
min-width: 0; min-width: 0;