topfans/docs/superpowers/specs/2026-06-08-income-curve-default-tooltip-design.md
2026-06-09 00:37:42 +08:00

14 KiB
Raw Blame History

七日收益曲线默认显示最后一条记录的提示 — 设计文档

  • 日期: 2026-06-08
  • 作者: Claude
  • 状态: 已批准,待实施
  • 范围: 前端单组件改动

背景与目标

现状

frontend/pages/dashboard/components/IncomeCurve.vue 是数据看板的"七日收益曲线"组件,使用 qiun-data-charts(基于 u-charts渲染 area 类型渐变面积图。组件已有完整的 tooltipCustom 渲染逻辑、tap 切换、错误态/骨架态。

问题

图表加载完成后默认没有任何 tooltip需要用户触摸/点击图表才会显示。期望进入页面后tooltip 默认显示在最后一个数据点最近一天用户触摸其他点时tooltip 跟随到对应点。

范围

  • 范围内: IncomeCurve.vue 一个文件
  • 范围外: 其他 dashboard 图表、其他业务模块、qiun-data-charts 公共 API

设计

核心机制

u-charts 在每帧渲染完成后会把每个数据点的真实屏幕坐标写入 opts.chartData.calPoints[seriesIndex][dataIndex].x/yqiun-data-charts 组件在 drawCharts 完成后通过 @complete 事件把这个 opts 抛给父组件。

利用这两点:

  1. 父组件监听 @complete,从 e.opts.chartData.calPoints[0] 取最后一点的 {x, y}
  2. 构造一个 fake event { changedTouches: [{ x, y }] }
  3. chartRef.value._showTooltip(fakeE) —— 该方法内部用 getCurrentDataIndex(e) 按 fake 坐标找最近的数据点(在最后一点上调用,最近的就是它自己),再走 u-charts 的 showToolTip 渲染流程,tooltipCustom 自然会用真实 index 渲染正确的文字

方案选择

方案 结论
A. Ref + fake event 调 _showTooltip 采用。只改一个文件;不动 uni_modules改动小
B. 给 qiun-data-charts 加 defaultTooltipIndex prop 拒绝。改 uni_modules 带来升级合并冲突YAGNI
C. 模拟真实 tap 事件 拒绝。跨端兼容差H5/小程序/App 行为不一致)

实现要点

1. IncomeCurve.vue 改动

Template

<qiun-data-charts
  ref="chartRef"
  ...
  @complete="onChartComplete"
  @tap="onChartTap"
/>

Script

import { ref, nextTick } from "vue";

const chartRef = ref(null);
// 防止首次 complete 和后续重复触发叠加
const lastShownLen = ref(0);

const onChartComplete = async (e) => {
  const len = props.points.length;
  if (!len || !chartRef.value) return;
  // 数据没变就不重复触发
  if (lastShownLen.value === len) return;
  await nextTick();
  const series = e?.opts?.chartData?.calPoints?.[0];
  if (!series || !series[len - 1]) return;
  const { x, y } = series[len - 1];
  // 构造 fake event模拟在最后一点"触摸"
  const fakeE = { changedTouches: [{ x, y }] };
  // ⚠️ 调用 qiun-data-charts 的私有方法(下划线开头)
  // 风险与回退见下方"风险与回退"表
  try {
    chartRef.value._showTooltip(fakeE);
  } catch (err) {
    console.warn("[IncomeCurve] show default tooltip failed:", err);
  }
  lastShownLen.value = len;
};

// 清理:原 onChartTap 仅更新 currentIndex死代码删除
// 原 watch + currentIndex 逻辑删除YAGNI

2. 关键边界条件

情况 处理
points.length === 0 组件渲染骨架态,不进入 chart 分支,onChartComplete 不会触发
calPoints 未填充 u-charts 在 process === 1 阶段才填充 calPoints@complete 事件在 drawCharts 之后 emit可信
props 从 0 条变 7 条 lastShownLen 从 0 变 7下次 complete 触发新 tooltip
props 从 7 条变 0 条 进入 skeleton 分支,不会触发;下次有数据时正常触发
多次 complete 事件(重渲染) lastShownLen 判断跳过
_showTooltip 在某些端不可用 chartRef.value?._showTooltip?.(...) 兜底;不可用时静默失败(用户依然能 tap 触发)

3. 删除的死代码

  • const currentIndex = ref(0);
  • 监听 props.points.lengthwatch
  • onChartTap 函数(未做任何事,因为 tooltipCustom 用 u-charts 内部 index
  • 模板上的 @tap="onChartTap" —— u-charts 内部 _tap 仍会处理 tap 事件,父组件不需要再监听

⚠️ 实际上 onChartTap 现在的实现是死代码,因为 tooltipCustom 接收的 index 来自 u-charts 内部不受父组件状态影响。tap 时图表自然会用触摸点的 index 渲染。所以只删父组件的监听和函数qiun-data-charts 内部 tap 行为不变

架构 / 组件 / 数据流

IncomeCurve.vue
  ├─ 模板
  │   └─ <qiun-data-charts @complete="onChartComplete" />
  └─ 脚本
      ├─ import cfu from "uni_modules/.../config-ucharts.js"  — 共享 u-charts config
      ├─ CANVAS_ID = "incomeCurveCanvas"   — 与 template canvasId prop 一致
      ├─ lastShownLen                      — 防止重复触发的游标
      └─ onChartComplete(e)                — 解析 calPoints构造 fake event直接调 cfu.instance[CANVAS_ID].showToolTip

数据流:
  points 变化
    → chartData computed 更新
    → qiun-data-charts 重渲染
    → 内部 u-charts 填充 calPoints
    → emit 'complete' 事件(带 opts
    → onChartComplete 解析 calPoints[0][last]
    → 构造 fakeE (除以 opts.pix 避免双倍缩放)
    → 拿 cfu.instance[CANVAS_ID].showToolTip(fakeE, { index: lastIndex })
    → u-charts 内部用显式 index 渲染 tooltip
    → tooltipCustom(opts, categories, lastIndex) 返回 +收益/日期

错误处理

失败点 行为
chartRef.value 为空 函数开头 if 守卫,静默退出
e.opts.chartData.calPoints[0] 为 undefined 守卫后退出;用户可手动 tap 触发
_showTooltip 抛异常 try/catch 包裹,记录 console.warn 不打断主流程
平台不支持 canvas 内部访问 静默失败极端情况uniapp 内不会出现)

测试

手动验证(必做)

  1. 默认显示

    • 进入 dashboard 页面,等待图表加载完成
    • 验证最后一个数据点(最右)上方出现 tooltip+XXXMM-DD
    • 验证没有 hollow 圆点(因为 tooltipCustom 走的是真实触摸路径u-charts 不会画 active point如有需要可在 extra.area.activeType 控制)
  2. 触摸切换

    • 在图表中间点 tap
    • 验证 tooltip 移动到该点
    • 释放后再 tap 最右点
    • 验证 tooltip 回到最右点(实际是 u-charts 内部逻辑,行为已存在)
  3. 数据切换

    • 切换 star_iddashboard 顶部下拉)
    • 验证新数据加载完成后tooltip 显示在新数据最后一点
  4. 空态/错误态

    • 接口返回空数据 → 不应触发 _showTooltip,不应报错
    • 接口报错 → 错误态显示正常

跨端验证

  • 微信小程序(主战场)
  • H5开发预览
  • App如果有时间

风险与回退

风险 影响 回退方案
_showTooltip 是 qiun-data-charts 私有方法(下划线开头),未在公共 API 中保证 default tooltip 失效,但 tap 仍可用 已用 try/catch + ?.() 兜底;最差情况下回退为 <qiun-data-charts v-if="ready"> 延后首帧渲染(体验略差)
calPoints 在某些情况下不写入 default tooltip 不显示 已加 series[len-1] 守卫;如未触发用户仍可 tap
多次 complete 事件堆积 多次绘制 lastShownLen 游标防抖

改动清单

  • 修改: frontend/pages/dashboard/components/IncomeCurve.vue+~15 行 / -~10 行)
  • 不动: qiun-data-charts 及其依赖、任何后端代码、其他前端组件

实施步骤概要

  1. <qiun-data-charts>ref="chartRef"@complete="onChartComplete"
  2. 实现 onChartComplete 函数(解析 calPoints + fake event + 调 _showTooltip
  3. 删除死代码 currentIndex / watch / onChartTap(可选清理)
  4. 手测三种情况:默认显示 / 触摸切换 / 数据切换
  5. 跨端验证(小程序 + H5

实施过程变更(追加于 2026-06-09

实施过程中发现两个 v1 设计假设不成立,相应做了调整:

变更 1移除 ref="chartRef",改走 cfu.instance[CANVAS_ID]

问题v1 设计用 chartRef.value._showTooltip(fakeE) 触发默认 tooltip。运行时发现

[IncomeCurve] show default tooltip failed: TypeError: chartRef.value._showTooltip is not a function

qiun-data-charts 组件的 _showTooltip 是下划线前缀私有方法,在 Options API + 跨平台编译uni-app vue3 编译器)后,父组件 ref 上拿不到这个方法

修复qiun-data-charts 通过 import cfu from "uni_modules/qiun-data-charts/js_sdk/u-charts/config-ucharts.js" 共享一个 cfu 对象,其中 cfu.instance[canvasId] 直接持有 uCharts 实例。我们在 IncomeCurve.vue 里也 import 同一个 cfu,绕过 qiun-data-charts 组件层,直接调 uCharts 原生的 showToolTip(e, {index})

附带收益

  • 不再依赖 qiun-data-charts 私有 API更稳
  • 可以显式传 option.index,不依赖 getCurrentDataIndex(fakeE) 推断(更准)
  • 移除了 chartRef ref、模板上的 ref="chartRef"QiunDataCharts 显式 import 也保留作为 easycom 兜底

变更 2fake event 坐标除以 opts.pix(像素比双倍缩放修复)

问题v1 设计的 fake event 直接传 calPoints[0][last].x, .y。但 calPoints 已是 canvas 像素u-charts 内部 opts.area = padding * opts.pix),而 getTouchesu-charts.js:505-506)会再乘一次 opts.pix。在 WeChat MP / retina H5opts.pix = 2)上坐标会双倍缩放。

修复:传入 fake event 前先 x / pix, y / pix,让 getTouches 一次乘法抵消,恢复正确坐标。

此变更 v1 设计阶段未识别,由 code review subagent 在 Task 2 review 时发现。修复后 round-trip 验证:((x / pix) * pix) = x,对所有 pix ≥ 1 成立。

变更 3H5 桌面端鼠标移动不切换 tooltip双层 fallback

问题 1renderjs onmouse 默认 undefined。qiun-data-charts mounted() 第 488-490 行:

if(this.inWin === true){
  this.openmouse = this.onmouse;
}

HBuilderX H5 预览iframe 嵌入)下 inWin === falseopenmouse 一直 undefined,传到 cfu.option[cid].onmouserdcharts.mouseMove 第一行 if(onmouse == false) return;undefined == false 为 true 立刻退出。

问题 2uni-app renderjs 不转发 mousemove。即使显式设 onmouse=truerdcharts.mouseMove 仍不会被调用 —— uni-app renderjs 在 HBuilderX 某些版本下不把 mousemove 事件转发到 renderjs 脚本(虽然 tap/click 是转发的)。

修复:放弃 renderjs 路径,直接在 IncomeCurve.vue 的 .chart-canvas 上挂 @mousemove 监听,绕开 renderjs用 cfu.instance 直接调 showToolTip(fakeE, {})

  1. 60ms 节流,避免每次 mousemove 都重绘 canvas
  2. getCurrentInstance().proxy + uni.createSelectorQuery().select('#UC' + canvasId) 拿 chart view 的 boundingClientRect
  3. 转换 e.clientX/Y 为相对坐标,构造 fakeE
  4. 不传 option.index,让 u-charts 内部 getCurrentDataIndex(fakeE) 自动找最近数据点
  5. 边界守卫localX/Y 超出 rect 范围直接 return

变更 4tooltipCustom 是 Object prop 不是 Function prop必须改用 showCategory + cfu.formatter

问题:用户反馈"初始显示的数据需要加上日期" — 我们用 tooltipCustom: (opts, cats, idx) => { return { textList: [...] } } 期望函数被调用,但 tooltip 一直只显示 u-charts 默认的 "收益: 1234",没有日期也没有 + 号。

根因qiun-data-charts 的 tooltipCustom prop 类型声明是 Objecttype: Object, default: undefined),不是 Function。chiart 内部 _showTooltip / renderjs showTooltip 的代码是:

let tc = cfu.option[cid].tooltipCustom
if (tc && tc !== undefined && tc !== null) {
  ...
  cfu.instance[cid].showToolTip(e, {
    index: tc.index,      // 函数没有 .index → undefined
    offset: offset,        // tc.x >= 0 永远 falseundefined >= 0
    textList: tc.textList, // 函数没有 .textList → undefined
    formatter: ...         // 用 chart 默认的 _tooltipDefault
  });
}

函数也是 Object 所以 if (tc && tc !== undefined && tc !== null) 判定通过,但 tc.indextc.textList 都是 undefined,最后 formatter 是默认的 _tooltipDefault,返回 item.name + ': ' + item.data

修复

  1. extra.tooltip.showCategory: trueu-charts 会把 opts.categories[index] 作为 textList 第一行 unshift 进去u-charts.js:2806-2808。我们的 categories 已经是 ["06-02", ..., "06-08"],所以日期会自动出现。
  2. extra.tooltip.fontColor: "#999999"第一行日期color 为 null会 fallback 到 fontColor灰色。
  3. cfu.formatter.incomeFormatter:注册自定义 formatter 返回 +${item.data},让第二行(收入)显示成 +1234
  4. :tooltipFormat="'incomeFormatter'":让 chart 组件的 _showTooltip / renderjs showTooltip 用我们注册的 formatter。
  5. 直接 showToolTip 调用也传 formatter: incomeFormatter:确保默认 tooltiponChartComplete和鼠标移动onCanvasMouseMove也用同一格式。
  6. 删掉原 extra.tooltip.tooltipCustom:彻底无效,留着误导后人。

效果:

  • Line 1: "06-08" — 灰色fontColor
  • Line 2: "+1234" — 蓝色series.color不受 fontColor 影响)
  • 默认显示、点击切换、鼠标移动三种触发方式格式一致

适用范围

  • H5 桌面浏览器: 工作mousemove 事件正常)
  • H5 移动浏览器mousemove 不触发,依赖 chart 内部 touchMove已有 :ontouch=true
  • 微信小程序:依赖 chart 内部 touchMove已有 :ontouch=true
  • App依赖 chart 内部 touchMove已有 :ontouch=true

非 H5 平台 @mousemove 是 no-ophandler 不会被调用。