TimeLens 第三方小组件开发指南

本文档介绍如何为 TimeLens v1.1.0 开发第三方 JavaScript 小组件。通过简单的 manifest.json + JS 入口文件,即可创建功能丰富的桌面悬浮组件。


功能范围(v1.1.0)

当前支持:

尚未支持:

目录结构

每个第三方小组件在应用数据目录的 widgets 文件夹下拥有独立目录:

%APPDATA%\com.timelens.app\widgets\
  my-widget/
    manifest.json       ← 小组件描述文件
    index.js            ← 入口 JS(ESM 模块)
    assets/
      icon.png          ← 可选:小组件图标
      styles.css        ← 可选:样式文件

manifest.json 详解

清单文件向 TimeLens 注册表描述你的小组件。以下是完整示例:

{
  "widget_type": "my_awesome_widget",
  "name": "我的超棒小组件",
  "description": "这是一个演示用途的第三方小组件",
  "version": "1.0.0",
  "author": "你的名字 ",
  "entry": "index.js",
  "icon": "assets/icon.png",
  "default_size": {
    "width": 360,
    "height": 240
  },
  "permissions": [
    "screen-time:read",
    "active-window:subscribe",
    "goals:read"
  ]
}

必填字段

字段类型说明
widget_typestring唯一标识符,仅允许 a-zA-Z0-9_-
namestring在小组件中心显示的名称
entrystring入口 JS 文件路径(相对于 manifest 所在目录)

可选字段

字段类型说明
descriptionstring简短描述
versionstring语义化版本号
authorstring作者名或邮箱
iconstring图标路径(相对路径)
default_size.widthnumber默认窗口宽度(像素)
default_size.heightnumber默认窗口高度(像素)
permissionsstring[]所需权限列表

权限系统

第三方小组件在沙箱 iframe 中运行,必须在 manifest 中声明所需权限。用户首次创建小组件时会被提示授权。

权限访问范围
screen-time:read读取屏幕时间统计数据(今日、小时分布、趋势)
active-window:subscribe订阅前台窗口变更事件
goals:read读取使用目标和进度
focus:read读取专注模式状态
widgets:read读取小组件注册表信息

渲染接口契约

TimeLens 将入口文件作为 ESM 模块加载。支持两种导出模式:

模式 A — createWidget() 工厂函数

适合需要在 mount/unmount 之间保持状态的小组件。

export function createWidget() {
  let root;
  let intervalId;

  return {
    mount(container, context) {
      root = document.createElement("div");
      root.style.cssText = `
        padding: 16px;
        color: #e6edf3;
        font-family: Inter, system-ui, sans-serif;
      `;
      root.innerHTML = `

${context.displayName}

`; container.appendChild(root); intervalId = setInterval(() => { root.querySelector("h3").textContent = new Date().toLocaleTimeString(); }, 1000); }, unmount() { clearInterval(intervalId); if (root && root.parentNode) { root.parentNode.removeChild(root); } }, }; }

模式 B — 直接导出 mount()

适合简单的一次性渲染组件。

let root;

export function mount(container, context) {
  root = document.createElement("div");
  root.innerHTML = `
    
    

${context.displayName}

类型: ${context.widgetType}

ID: ${context.widgetId}

`; container.appendChild(root); } export function unmount() { if (root && root.parentNode) { root.parentNode.removeChild(root); } }

Context 上下文对象

属性类型说明
widgetTypestring小组件类型标识符(同 manifest 中的 widget_type)
displayNamestring显示名称(同 manifest 中的 name)
widgetIdstring唯一实例 ID
permissionsstring[]用户已授予的权限列表

数据通道 API

小组件通过 window.parent.postMessage 与 TimeLens 核心通信。父窗口提供了请求/响应桥接。

请求格式

window.parent.postMessage({
  type: "timelens:request",
  id: "req-" + Date.now(),      // 唯一请求 ID
  method: "getTodayAppTotals",   // 调用的 API 方法名
  params: {}                     // 参数对象
}, "*");

响应监听

window.addEventListener("message", (e) => {
  if (e.data.type === "timelens:response") {
    if (e.data.error) {
      console.error("API 错误:", e.data.error);
    } else {
      console.log("API 结果:", e.data.result);
    }
  }
});

常用数据方法

所有来自 API 参考 的只读方法都可通过数据通道调用:

样式设计规范

小组件在透明背景的 iframe 中运行。为保证与 TimeLens 视觉风格一致,建议遵循以下规范:

属性推荐值
背景色rgba(22, 27, 34, 0.75) + backdrop-filter: blur(12px)
主文字#e6edf3
次级文字#8b949e
强调色#58a6ff
边框1px solid rgba(48, 54, 61, 0.6)
圆角12px8px
字体Inter, system-ui, -apple-system, sans-serif
阴影0 4px 12px rgba(0, 0, 0, 0.3)

完整示例:今日应用排行小组件

// index.js
export function createWidget() {
  let root;
  let interval;
  let unlisten;

  function requestData(method, params = {}) {
    return new Promise((resolve, reject) => {
      const reqId = "req-" + Date.now() + "-" + Math.random();
      const handler = (e) => {
        if (e.data.type === "timelens:response" && e.data.id === reqId) {
          window.removeEventListener("message", handler);
          if (e.data.error) reject(e.data.error);
          else resolve(e.data.result);
        }
      };
      window.addEventListener("message", handler);
      window.parent.postMessage({
        type: "timelens:request",
        id: reqId,
        method,
        params
      }, "*");
    });
  }

  async function renderStats() {
    try {
      const stats = await requestData("getTodayAppTotals");
      const top5 = stats.slice(0, 5);
      const html = top5.map(s => {
        const mins = Math.round(s.total_seconds / 60);
        return `
${s.app_name} ${mins} 分钟
`; }).join(""); const el = root.querySelector("#content"); if (el) el.innerHTML = html || "

暂无数据

"; } catch (err) { console.error("获取数据失败:", err); } } return { async mount(container, context) { root = document.createElement("div"); root.style.cssText = ` padding: 16px; color: #e6edf3; font-family: Inter, system-ui, sans-serif; background: rgba(22, 27, 34, 0.8); backdrop-filter: blur(12px); border-radius: 12px; border: 1px solid rgba(48, 54, 61, 0.5); height: 100%; box-sizing: border-box; `; root.innerHTML = `

${context.displayName}

加载中...
`; container.appendChild(root); await renderStats(); interval = setInterval(renderStats, 60000); }, unmount() { clearInterval(interval); if (root && root.parentNode) { root.parentNode.removeChild(root); } } }; }

调试技巧

官方模板

TimeLens 仓库提供了官方示例模板:

examples/third-party-widget-template/
  ├── manifest.json
  ├── index.js
  └── README.md

建议以此模板为基础开始你的小组件开发。


最后更新:2025-05-03 · TimeLens v1.1.0