Просмотр исходного кода

docs(signature): v2 实现方式重写任务书+需求变更(竖屏直签+canvas type=2d,取代 11-08 横屏适配 a7edf5698)

Agnes Agent 4 дней назад
Родитель
Сommit
c6223975b8

+ 113 - 0
docs/superpowers/specs/2026-11-09-signature-canvas2d-rewrite.md

@@ -0,0 +1,113 @@
+# 手签名实现方式重写(竖屏直签 + canvas 2d)任务书
+
+- 需求编号:DOC-2026-11-09-signature-canvas2d-rewrite
+- 需求来源:主理人反馈(小程序端)
+- 状态:待开发
+- **取代**:`docs/superpowers/specs/2026-11-08-signature-landscape-fix.md`(commit `a7edf5698`,横屏适配方案,**整份作废**,勿再按旧任务书实施)
+- 关联需求变更:`docs/需求分析/2026-11-09-手签名实现方式重写需求变更.md`(取代 2026-11-08-手签名横屏适配需求变更)
+
+---
+
+## 一、需求原话(主理人 2026-11-09 更新)
+
+> 换一种手签名的实现方式,目前的方式字迹显示不出来,部分手机还实现不了自动横屏
+
+(注:此前"横屏适配"方向已被主理人推翻——不再依赖横屏;核心诉求变为「**不依赖横屏、无需特殊机型能力,任何手机都能正常手写并看到笔迹**」。)
+
+## 二、现状核查(已确认根因)
+
+涉改文件(均基于 `origin/cfclub` 最新,共 2 个前端文件):
+
+1. `cfc-frontend/pages/signature/signature.vue`(311 行,签名页,`navigationStyle:custom`)
+2. `cfc-frontend/components/hand-signature/hand-signature.vue`(367 行,通用手签名组件,**当前实际仅 signature.vue 渲染**;checkout.vue 仅跳转不内嵌,改动零波及)
+
+### 根因 R1:部分手机无法自动横屏(机型能力差异)
+
+- 现实现依赖 `wx.setPageOrientation({orientation:'landscape'})` 强制横屏(H5 还依赖 `screen.orientation.lock`)。
+- **结论**:`setPageOrientation` 在部分安卓机型/基础库版本下不生效或需手动能力校验,属于**不可靠的平台能力**;「必须横屏才能签名」本身是错误的交互前提,应整体放弃横屏依赖。
+
+### 根因 R2:字迹显示不出来(旧式 canvas API 兼容性差)
+
+- 手签名组件是**全前端唯一仍用旧式 `uni.createCanvasContext`(canvas-id + `ctx.draw()`)的 canvas 组件**;项目内其它 canvas 组件(`RadarChart.vue`、`TrendChart.vue`、`FamilyRelationGraph.vue`、`wuxing-sandbox.vue`、`mbti-result.vue`、`AbilityDiagram.vue` 等 10+ 处)**已全部采用新版 `type="2d"` + `canvas.getContext('2d')` + `fields({node:true})` 接口**。
+- 旧式 `uni.createCanvasContext` 在新基础库上有两个已知不稳定点:
+  1. 触摸坐标依赖手动缓存 `rect`(`loadRect` 只在 mounted 取一次,页面/布局变化后坐标偏移 → 笔迹画到画布外,**用户感知"字迹显示不出来"**);新版 2d 触摸事件直接提供相对画布的 `touches[0].x/y`,不依赖 rect 缓存。
+  2. `ctx.draw(true/false)` 异步提交在部分机型存在丢帧/不渲染问题;新版 `ctx.stroke()` 为同步绘制,笔迹实时可见。
+- **对照标准实现(开发必读参考)**:`cfc-frontend/components/RadarChart.vue`——
+  - 初始化:`uni.createSelectorQuery().in(this).select('#'+this.canvasId).fields({node:true,size:true,rect:true}).exec(...)`(注意 `.in(this)` 限定组件作用域;`canvasId` 用 `'xxx_' + (this._uid||0)` 保证多实例唯一);
+  - `fields size` 返回 0(flex 塌陷)时用 `uni.getWindowInfo().windowWidth` 按 rpx 换算兜底;
+  - `canvas.width = width * dpr; canvas.height = height * dpr; ctx.scale(dpr, dpr)`(dpr 取 `uni.getWindowInfo().pixelRatio`);
+  - `getContext('2d')` 返回 null 时走失败回调并重试(不要静默)。
+
+### 波及面确认(改动安全):
+
+- `hand-signature` 组件引用方:grep 全前端仅 `signature.vue` 实际渲染(checkout.vue 只有 `hasHandSignatureField/purchase-signature-block` 字样,不 import 不渲染)→ **组件改动零波及**。
+- 调用方 health-plan-summary.vue / checkout.vue / consignee-edit.vue 均为 navigateTo 跳转 + eventChannel 回传,**不改**。
+- 组件对外契约(props:value/strokeColor/strokeWidth/height/requireAgree/agreeText;emit:confirm/clear)**保持不变**,三调用方无感。
+- 无后端改动。
+
+## 三、需求变更定义
+
+### 变更 S4-1:弃用强制横屏,改竖屏直签(signature.vue)
+
+- 删除 `enterLandscape()` / `restorePortrait()` 两个方法及其在 `onLoad`/`onUnload` 中的调用(连同 H5 降级提示 `h5DegradeHint`、`.rotate-hint` 模板块一并删除)。
+- 页面保持默认竖屏布局(`navigationStyle:custom` 保留),**不再调用 `wx.setPageOrientation` / `screen.orientation.lock`**。
+- `computeCanvasHeight()` 保留竖屏计算:`uni.getWindowInfo().windowHeight`(竖屏=长边,如 812/844)− reserved(requireAgree 时 214,否则 174)→ 画布高度充足(≈600px 级),签名区域大。
+- 页面标题/底部栏/同意声明布局与样式保持(竖屏下 rpx 尺寸正常,无需 px 化)。
+- **删除旧横屏任务书的 S1/S2/S3 全部内容**(窗口尺寸监听重算、rpx→px、refreshRect),不再实施。
+
+### 变更 S4-2:canvas 重写为新版 2d 接口(hand-signature.vue)
+
+模板:
+- `<canvas :id="canvasId" type="2d" class="signature-canvas" :style="'width:100%;height:'+height+'px;'" @touchstart.stop.prevent="onTouchStart" @touchmove.stop.prevent="onTouchMove" @touchend.stop.prevent="onTouchEnd" />`(canvas-id 换成 `:id` + `type="2d"`,参照 RadarChart)。
+
+data 新增:
+- `canvasId: 'hsCanvas_' + (this._uid || 0)`
+- `_canvasNode: null`(2d 节点,导出用)
+- `canvasWidth / canvasHeight / dpr`(绘制与清空用)
+- `_destroyed: false`、`_initTimer: null`(防销毁后继续查询刷错;参照 RadarChart)
+
+初始化(替换现 `loadRect` 逻辑):
+- `mounted`:按 RadarChart 标准执行 `createSelectorQuery().in(this).select('#'+canvasId).fields({node:true,size:true,rect:true}).exec()`;
+- `size` 为 0 且 `rect` 有效时用 `rect.width/rect.height`;两者皆无 → 用 `windowWidth/750` 按 height prop 换算兜底;
+- `canvas.width=width*dpr; canvas.height=height*dpr; ctx=canvas.getContext('2d'); ctx.scale(dpr,dpr)`;
+- 失败路径 console.log + 定时器重试一次(mounted 延迟初始化),`_destroyed` 时停止。
+
+触摸坐标(核心修复,替代旧 `getPos`):
+- **优先** `var t = e.touches && e.touches[0]; var x = (t && t.x) || 0; var y = (t && t.y) || 0`(type=2d 时微信提供相对画布坐标,**不再依赖缓存 rect**);
+- 兜底:`t.x === undefined` 时用 `t.clientX - rect.left / t.clientY - rect.top`(rect 用 fields 返回的 `info.left/top` 存为 `_rectLeft/_rectTop`,比旧实现实时、准确)。
+
+绘制:
+- `onTouchMove`:`ctx.beginPath(); ctx.moveTo(lastX,lastY); ctx.lineTo(x,y); ctx.strokeStyle=this.strokeColor; ctx.lineWidth=this.strokeWidth; ctx.lineCap='round'; ctx.lineJoin='round'; ctx.stroke()`(**同步绘制,无需 `ctx.draw()`**);更新 lastX/lastY、`hasContent=true; hasDrawn=true`。
+
+导出(handleConfirm 改造):
+- 用 `uni.canvasToTempFilePath({ canvas: this._canvasNode, x:0, y:0, width: this.canvasWidth, height: this.canvasHeight, destWidth: this.canvasWidth, destHeight: this.canvasHeight, fileType:'png', quality:1, success..., fail... }, this)`——**2d 接口传 `canvas` 节点**(不再传 canvasId);success 后 H5 直接 pass dataURL、小程序 `fs.readFile` 转 base64 的既有链路**保持不变**。
+- 既有分支:未绘制但有 `currentDataUrl` 预填 → 直接 emit 原签名(保留)。
+
+清空(handleClear 改造):
+- `ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)`(同步,无需 `ctx.draw`);保留 `hasContent/hasDrawn/currentDataUrl` 复位与 `$emit('clear')`。
+
+prefill 回显:
+- **保持现有 image 层叠加方案**(`currentDataUrl && !hasContent` 时 `<image mode="aspectFit">` 覆盖展示、pointer-events-none),不把预填画进 2d canvas;`hasDrawn` 语义沿用(已手绘则盖住预填)。
+
+### 不修改:
+
+- 不改 `signature-bus.js`、`pages.json`、后端;
+- 不改三个调用方跳转回传链路;
+- 不引入任何第三方签名库(保持自研组件 + 项目内标准 2d 写法);
+- 不做横屏适配(S4-1 已弃用)。
+
+## 四、TDD 入池建议
+
+- 能力键:`signature.canvas2d.rewrite`(前端 UI 纵切,无独立后端接口)
+- 验收(开发自验全部通过才可提交):
+  1. 任意机型(含无法自动横屏的)进入签名页即**竖屏可签**,无任何横屏依赖;
+  2. 画布竖屏全宽、高度 ≈ windowHeight − 固定栏(≥400px),签名区域充足;
+  3. **书写笔迹实时可见**(2d 同步绘制),不再出现"画了看不到";
+  4. 确认导出:小程序转 base64、H5 dataURL,提交后回显正常;
+  5. 重写/确认/返回按钮行为、同意声明勾选、prefill 回显均不回归;
+  6. 项目内既有 2d 组件(雷达图/沙盘/趋势图/mbti)不受影响(未触碰)。
+
+## 五、风险与关联记录
+
+- 风险:`type="2d"` 需要微信基础库 ≥2.9.0——项目内 10+ 组件已在用,**无新增兼容风险**;H5 端 `fields node` 与 RadarChart 同路径处理(主目标平台为微信小程序)。
+- 关联:取代 `2026-11-08-signature-landscape-fix.md`(`a7edf5698`);需求变更 `2026-11-09-手签名实现方式重写需求变更.md` 取代 11-08 横屏适配变更。无其它任务书依赖。

+ 45 - 0
docs/需求分析/2026-11-09-手签名实现方式重写需求变更.md

@@ -0,0 +1,45 @@
+# 手签名实现方式重写需求变更
+
+> 对齐模板:2026-11-06-成就成长打卡需求变更.md
+> 变更编号:SIG-2026-11-09-V2 | 变更日期:2026-11-09 | 版本:v2.0(**取代** SIG-2026-11-08-L1「手签名横屏适配」v1.0,原横屏适配方向作废)
+
+## 一、需求原话
+
+> 换一种手签名的实现方式,目前的方式字迹显示不出来,部分手机还实现不了自动横屏
+
+(注:横屏适配方向被推翻——不再依赖横屏;核心诉求为「不依赖横屏、无需特殊机型能力,任何手机都能正常手写并看到笔迹」。)
+
+## 二、现状核查
+
+- 手签名页 `pages/signature/signature.vue`(navigationStyle:custom)复用通用组件 `components/hand-signature/hand-signature.vue`;经 `utils/signature-bus.js` 传 prefill、eventChannel 回传 dataUrl;被 health-plan-summary / checkout / consignee-edit 三处 navigateTo 调用。
+- 两个真机问题同源:
+  1. **部分手机无法自动横屏**:现实现强依赖 `wx.setPageOrientation({orientation:'landscape'})`(H5 还依赖 `screen.orientation.lock`),`setPageOrientation` 在部分安卓机型/基础库版本下不生效——「必须横屏才能签」本身是错误的交互前提,应整体放弃。
+  2. **字迹显示不出来**:手签名组件是**前端唯一仍用旧式 `uni.createCanvasContext`(canvas-id + `ctx.draw()`)的 canvas 组件**;其它 canvas 组件(`RadarChart`/`TrendChart`/`FamilyRelationGraph`/`wuxing-sandbox`/`mbti-result`/`AbilityDiagram` 等 10+ 处)**已全部迁移到新版 `type="2d"` + `canvas.getContext('2d')` + `fields({node:true})`**。旧接口两个已知不稳定点:
+     - 触摸坐标依赖缓存 `rect`(仅 mounted 取一次),布局变化后偏移 → 笔迹画到画布外,用户感知"看不到字迹";新版 2d 直接提供相对画布的 `touches[0].x/y`,无 rect 缓存。
+     - `ctx.draw(true/false)` 异步提交在部分机型存在丢帧/不渲染;新版 `ctx.stroke()` 同步绘制,笔迹实时可见。
+
+## 三、需求变更定义
+
+| 变更 | 文件 | 内容 |
+|---|---|---|
+| S4-1 | pages/signature/signature.vue | **弃用强制横屏**:删 `enterLandscape` / `restorePortrait` 两方法及 onLoad/onUnload 调用;删 H5 降级提示 `h5DegradeHint` 与 `.rotate-hint` 模板;`computeCanvasHeight` 保留竖屏计算(windowHeight 长边 − reserved);页面保持默认竖屏布局 |
+| S4-2 | components/hand-signature/hand-signature.vue | **canvas 重写为新版 2d 接口**:模板 `:id="canvasId"` + `type="2d"`;初始化走 `createSelectorQuery().in(this).select('#'+canvasId).fields({node:true,size:true,rect:true}).exec()`(canvasId=`'hsCanvas_'+(this._uid||0)` 唯一化);`canvas.width=width*dpr; canvas.height=height*dpr; ctx=canvas.getContext('2d'); ctx.scale(dpr,dpr)`;触摸坐标优先 `touches[0].x/y`(相对画布),兜底 clientX−rect;`ctx.stroke()` 同步绘制(无 `ctx.draw()`);导出改 `uni.canvasToTempFilePath({canvas: _canvasNode,...}, this)`(传节点不传 canvasId);清空 `ctx.clearRect(0,0,w,h)` 同步;**prefill 回显保留 image 层叠加方案** |
+| S4-3 | 两个文件 | **旧横屏适配方案 S1/S2/S3 全部作废**:不再需要窗口尺寸监听、rpx→px 化、refreshRect(竖屏后 rpx 正常,布局不漂移) |
+
+**参考实现(开发必读)**:`cfc-frontend/components/RadarChart.vue` ——项目内已在生产验证的标准 2d 写法,含多实例唯一 canvasId、fields size=0 时 windowWidth 换算兜底、dpr 处理、失败重试、`_destroyed` 防销毁竞态。手签名组件照此模式实现。
+
+## 四、TDD 入池
+
+- 建议能力键:`signature.canvas2d.rewrite`
+- 验收:竖屏可签(无横屏依赖)、画布全宽且高≈windowHeight−固定栏(≥400px)、书写笔迹实时可见(2d 同步绘制)、小程序转 base64 / H5 dataURL 导出正常、按钮与 prefill 回显不回归、项目内既有 2d 组件(雷达图/沙盘/趋势图/mbti)未受影响。
+
+## 五、风险
+
+- `type="2d"` 需基础库 ≥2.9.0:项目内 10+ 组件已在用,无新增兼容风险;H5 端 `fields node` 与 RadarChart 同路径处理(主目标平台为微信小程序)。
+- 触摸事件 `touches[0].x/y` 字段由基础库 2.9+ 提供;仍保留 clientX−rect 兜底路径。
+
+## 六、关联记录
+
+- 任务书:`docs/superpowers/specs/2026-11-09-signature-canvas2d-rewrite.md`(v2 正本)
+- 取代:`docs/需求分析/2026-11-08-手签名横屏适配需求变更.md` 与 `docs/superpowers/specs/2026-11-08-signature-landscape-fix.md`(a7edf5698,**作废**,勿按旧方案实施)
+- 需求分析台账新增本章;无既有变更覆盖。