反馈组件 · 接入

让包里的反馈直接落进「对应项目 · 对应版本」的清单 —— 测试者不用截图发群里,开发不用猜这是哪一版。 一行 script,所有端(iOS / Android / 鸿蒙 / Web / 桌面)通用,因为它跑在 WebView 里。

1先试一下

右边是真的在跑的组件,不是效果图。在左边假装是「桐桐画坊」的窗口里画两笔, 然后拖动右下角的「反馈」按钮、点它 —— 看它抓走了什么。

拖动:按住「反馈」拖到任意位置,松手会自动吸到最近的边缘, 位置存在本机,下次打开还在原地。
连击 5 下:切到正式版后按钮消失,在页面任意位置 1.6 秒内单指连点 5 下即可唤出。

2把这一行贴进你的项目

贴在 </body> 之前就行。它会自动把现场采集、表单、样式一起拉起来,你不需要额外引任何东西。 凭证(data-zy-key)不用申请:后台 → 建项目/版本 → 版本详情 →「包内反馈入口」一键生成(一版一个,稳定不变);上面第 1 节选好版本,这里的代码会自动填上真凭证。

包内反馈组件


  
属性作用取值
data-zy-key这一版的反馈凭证。系统凭它把反馈归到对应的项目和版本上 FK-XXXXX-XXXXX(后台版本详情里生成)
data-zy-mode哪一种触发姿势 test 浮动按钮 · release 连击 5 下 · 不加则按有没有 key 自动定
data-zy-endpoint上报地址。默认回到「组件从哪来,就往哪报」 只有把组件文件搬到别处托管时才需要写
data-zy-label按钮文案默认「反馈」
data-zy-snapshotoff 可以关掉自动页面快照默认开

3两种触发,对应两种版本

版本入口为什么
测试版
data-zy-mode="test"
可移动浮动按钮常显 测试者要能随时报。找不到入口这件事本身就是最大的障碍 —— 他会攒到测试结束再说,那时细节已经忘了一半
正式版
data-zy-mode="release"
单指连击 5 下 线上用户不该看见调试入口。但真出事时,他得有办法报上来 —— 连击 5 下不会被误触,也不需要他会描述「请把日志发给我」

连击判定:1.6 秒内 5 次,每一下都要落在起点 26px 以内 —— 边走边点不会误触发。桌面浏览器用鼠标连点 5 下等效。

4点下去的那一刻,系统自动带走了什么

内容怎么来的可靠性
页面快照抓当前页面渲染结果,缩到 1280 宽、JPEG 0.72 尽力而为。跨域图片会变灰块,iframe 内拿不到
屏幕上的文字可见文本前 400 字基本不会失败
控制台错误组件装载时就挂钩子,最多 20 条只捞装载之后抛的错
失败的网络请求4xx / 5xx / 断网,最多 10 条记录在载入组件之后
环境机型、系统、屏幕、DPR、语言、当前网址、触发方式必定带上
你写的复现方法 / 错误点必填,前后端各挡一道提交的硬门槛
关于「自动截屏」需要说清楚的三件事:
  • Web 没有原生截屏 API。抓的是这个页面,不是用户的整个屏幕 —— 浏览器界面、系统通知、别的 App 都不在画面里。
  • 抓取发生在面板出现之前,所以不会把反馈面板自己拍进去。
  • 自动附上的东西全部可见、可删。缩略图上点 × 就删掉了。 悄悄上传一张用户没看过的截图,比不自动截屏更坏。

5原生壳想要「真·全屏截图」

页面快照只能拿到 WebView 里的内容。如果你想要一张连原生界面一起的全屏截图, 让原生侧接一根桥即可 —— 协议只有一句话:我们喊一声,你把图回灌给我们。

iOS · WKWebView(Swift)
// 1. 注册桥。网页侧会 postMessage {cmd:"shot"}
let script = """
window.addEventListener('DOMContentLoaded', function () {
  window.webkit.messageHandlers.zyfb && null;
});
"""
let ucc = WKUserContentController()
ucc.add(self, name: "zyfb")          // 网页喊的就是这个名字
cfg.userContentController = ucc

// 2. 收到喊话 → 截图 → 回灌
func userContentController(_ uc: WKUserContentController,
        didReceive message: WKScriptMessage) {
  guard (message.body as? [String: Any])?["cmd"] as? String == "shot" else { return }
  let cfg = WKSnapshotConfiguration()
  webView.takeSnapshot(with: cfg) { image, _ in
    guard let d = image?.jpegData(compressionQuality: 0.72) else { return }
    let b64 = "data:image/jpeg;base64," + d.base64EncodedString()
    webView.evaluateJavaScript(
      "window.ZYSnapshot && window.ZYSnapshot.attach('\(b64)')")
  }
}
Android · WebView(Kotlin)
// 1. 暴露桥对象
webView.addJavascriptInterface(object {
  // 组件会调用这个方法
  @JavascriptInterface fun shot() {
    webView.post {
      val bmp = Bitmap.createBitmap(webView.width, webView.height, Bitmap.Config.ARGB_8888)
      webView.draw(Canvas(bmp))                    // 含 WebView 内的全部渲染结果
      val bos = ByteArrayOutputStream()
      bmp.compress(Bitmap.CompressFormat.JPEG, 72, bos)
      val b64 = Base64.encodeToString(bos.toByteArray(), Base64.NO_WRAP)
      webView.post {
        webView.evaluateJavascript(
          "window.ZYSnapshot \&\& window.ZYSnapshot.attach('data:image/jpeg;base64,$b64')", null)
      }
    }
  }
}, "ZYFeedbackAndroid")

// 2. 想要连状态栏一起拍,用 MediaProjection;只在 WebView 内就够的话上面这段已经是了

接上桥之后,组件会优先用原生给的那张图。900 毫秒内没等到就自动退回页面快照 —— 原生侧不该让用户卡在提交页上。

整个页面都是纯原生(没有 WebView)?不走组件,直接打 POST /api/v1/reports(对任意来源开放、匿名)。字段与组件提交的完全一致,截图原生侧自己截 (SwiftUI ImageRenderer / Compose View.draw),转 data URL 塞进 media; 摇一摇等原生手势唤面板用 evaluateJavascript("window.ZYFeedback.open()")。

纯原生直连(任意语言的 HTTP 客户端)
POST /api/v1/reports                  Content-Type: application/json
{ "feedbackKey": "FK-…",          // 后台版本详情里生成的凭证,决定反馈挂到哪一版
  "steps": "1. … 2. …",           // 复现方法(带凭证时 steps / actual 至少一个要有内容)
  "actual": "哪不对",
  "version": "1.4.0-beta.3",
  "reporter": { "channel": "native-ios" },
  "env": { "model": "iPhone16,2", "os": "iOS 20.0" },
  "media": [ { "kind": "shot", "mime": "image/jpeg", "name": "01-autoshot.jpg",
               "data": "data:image/jpeg;base64,…" } ] }   // ≤12 张、每张 ≤8MB

6接上了没?一行自检

在你的项目页面控制台里粘贴
fetch('/api/v1/onsite/check?key=FK-XXXXX-XXXXX')
  .then(r => r.json()).then(console.log)
返回里 found: true 说明这个凭证认识这一版;release 里能看到它挂到哪个项目哪一版。 返回 found: false 也别慌 —— 反馈照样能提交,只是会进总池、不挂具体版本。
跨域已经放行:POST /api/v1/reports 与 GET /api/v1/feedback-key/:key 允许任何来源直接调用(匿名上报,不带 Cookie)。组件文件托管在别的域也没关系。