// == 配置 == const API_KEY = "change-me"; const API_HOST = "https://calendar-widget-api.tunpok.com"; const GET_URL = `${API_HOST}/tasks`; const CACHE_FN = "calendar-widget-task-cache.json"; // 本地缓存文件 // == 处理「点击 cell」== if (args.queryParameters?.action === "schedule" && args.queryParameters?.id) { await scheduleTask(args.queryParameters.id); // 小提示:让 Scriptable 返回桌面并刷新 widget Script.complete(); return; } // == 入口:拉取数据 & 渲染 widget == const data = await safeFetchTasks(); // 若接口失败则自动读取缓存 const widget = buildWidget(data); Script.setWidget(widget); Script.complete(); // == 每分钟自动刷新 == widget.refreshAfterDate = new Date(Date.now() + 60 * 1000); // ----------------------------------------------------------------------------- // ⬇️ 工具函数 ⬇️ // ----------------------------------------------------------------------------- /** * 从接口安全拉取任务;失败时返回缓存内容 */ async function safeFetchTasks() { try { const req = new Request(GET_URL); req.method = "GET"; req.headers = { "X-Api-Key": API_KEY }; const json = await req.loadJSON(); cacheJson(json); // 拉取成功即写入缓存 return json; } catch (e) { return readCache(); } } /** * 点击 cell 后调度任务 */ async function scheduleTask(id) { try { const url = `${API_HOST}/tasks/${id}/schedule`; const req = new Request(url); req.method = "POST"; req.headers = { "X-Api-Key": API_KEY }; await req.load(); // 无需关心返回值 } catch (e) { // 失败时静默忽略,保持 widget 不变 } } /** * 构建并返回 ListWidget */ function buildWidget(tasks) { // == Widget 初始化 == const w = new ListWidget(); w.backgroundColor = new Color("#f5f5f9"); w.setPadding(10, 10, 10, 10); const main = w.addStack(); main.layoutVertically(); main.centerAlignContent(); main.spacing = 12; // == 布局判定 == let columns = 4, rows = 1; switch (config.widgetFamily) { case "medium": columns = 2; rows = 2; break; case "large": columns = 4; rows = 2; break; } let idx = 0; const total = tasks.length; for (let r = 0; r < rows; r++) { const row = main.addStack(); row.layoutHorizontally(); row.centerAlignContent(); row.spacing = 10; for (let c = 0; c < columns; c++) { if (idx >= total) break; const item = tasks[idx++]; const cell = row.addStack(); cell.layoutVertically(); cell.backgroundColor = new Color("#f2f2f2"); cell.cornerRadius = 16; cell.setPadding(10, 25, 10, 25); cell.centerAlignContent(); cell.size = new Size(0, 65); // ===== 标题 ===== const title = cell.addText(item.name); title.font = Font.mediumSystemFont(14); title.textColor = Color.black(); title.centerAlignText(); cell.addSpacer(8); // ===== 色条 ===== const bar = cell.addStack(); bar.backgroundColor = colorMap(item.color); bar.cornerRadius = 8; bar.size = new Size(Device.screenSize().width / (columns * 1.8), 10); // ===== 点击跳转 ===== const url = `scriptable:///run?scriptName=${encodeURIComponent(Script.name())}` + `&action=schedule&id=${item.id}`; cell.url = url; // 整个 cell 可点击 } } return w; } /** * 根据接口给出的 color 字段返回 Scriptable 的颜色对象 */ function colorMap(color) { switch ((color || "").toLowerCase()) { case "green": return Color.green(); case "yellow": return Color.yellow(); case "red": return Color.red(); default: return Color.gray(); } } /** * 写缓存 */ function cacheJson(obj) { try { const fm = FileManager.local(); const dir = fm.documentsDirectory(); const path = fm.joinPath(dir, CACHE_FN); fm.writeString(path, JSON.stringify(obj)); } catch (_) {} } /** * 读缓存,若无缓存则返回空数组 */ function readCache() { try { const fm = FileManager.local(); const dir = fm.documentsDirectory(); const path = fm.joinPath(dir, CACHE_FN); if (fm.fileExists(path)) { const str = fm.readString(path); return JSON.parse(str); } } catch (_) {} return []; }