// ParkingRecorder 小组件(严格逻辑 + 性能优化版) // 数据源: // 未停车 → {"key":"ParkingRecorder","val":"null"} // 已停车 → {"key":"ParkingRecorder","val":"YYYY-MM-DD HH:mm:ss"} const BOXJS_URL = "https://boxjs.com/query/data/ParkingRecorder"; const REFRESH_INTERVAL_MINUTES = 5; // Theme Colors const THEME = { bg1: "#1a1a1a", bg2: "#000000", idle: "#00F0FF", active: "#28A745", textMain: "#FFFFFF", textDim: "#888888", danger: "#FF453A" }; async function fetchParkingState() { try { const req = new Request(BOXJS_URL); req.timeoutInterval = 5; const json = await req.loadJSON(); const val = json?.val ?? "null"; // ⭐ 规则 1:明确判断未停车 if (val === "null") { return { isParked: false, rawTime: "", startDate: null, durationText: "", lastUpdated: new Date(), }; } // ⭐ 规则 2:检测格式是否为合法日期 "YYYY-MM-DD HH:mm:ss" // 简单粗暴验证长度必须为 19 if (typeof val !== "string" || val.length !== 19) { return { isParked: false, rawTime: "", startDate: null, durationText: "", lastUpdated: new Date(), }; } // ⭐ 规则 3:尝试解析 const parsed = parseTimeString(val); if (!parsed) { return { isParked: false, rawTime: "", startDate: null, durationText: "", lastUpdated: new Date(), }; } // 正常停车 const durationText = buildDurationText(parsed, new Date()); return { isParked: true, rawTime: val, startDate: parsed, durationText, lastUpdated: new Date(), }; } catch (e) { console.log("Fetch error:", e); return { isParked: false, rawTime: "", startDate: null, durationText: "", lastUpdated: new Date(), }; } } // 解析 "YYYY-MM-DD HH:mm:ss" 为 Date function parseTimeString(str) { try { const [datePart, timePart] = str.split(" "); if (!datePart || !timePart) return null; const [y, m, d] = datePart.split("-").map(Number); const [hh, mm, ss] = timePart.split(":").map(Number); if ([y, m, d, hh, mm, ss].some(n => Number.isNaN(n))) return null; return new Date(y, m - 1, d, hh, mm, ss); } catch (e) { return null; } } // 生成“已停车 X天X小时X分” function buildDurationText(start, now) { let diffMs = now - start; if (diffMs < 0) diffMs = 0; const minuteMs = 60 * 1000; const hourMs = 60 * minuteMs; const dayMs = 24 * hourMs; const days = Math.floor(diffMs / dayMs); diffMs %= dayMs; const hours = Math.floor(diffMs / hourMs); diffMs %= hourMs; const mins = Math.floor(diffMs / minuteMs); let parts = []; if (days > 0) parts.push(`${days}天`); if (hours > 0) parts.push(`${hours}小时`); parts.push(`${mins}分`); return `已停车 ${parts.join("")}`; } // 渐变背景 function makeBackground(widget, isParked) { const gradient = new LinearGradient(); gradient.colors = isParked ? [new Color("#0f2027"), new Color("#203a43"), new Color("#2c5364")] : [new Color("#232526"), new Color("#414345")]; gradient.locations = [0, 1]; widget.backgroundGradient = gradient; } // 小组件 function buildSmallWidget(state) { const w = new ListWidget(); w.setPadding(12, 12, 12, 12); makeBackground(w, state.isParked); const mainStack = w.addStack(); mainStack.layoutVertically(); mainStack.centerAlignContent(); const status = mainStack.addText(state.isParked ? "停车中" : "未停车"); status.font = Font.boldSystemFont(18); status.textColor = new Color(THEME.textMain); w.addSpacer(8); if (state.isParked) { const timeText = w.addText(state.durationText); timeText.font = Font.boldSystemFont(24); timeText.textColor = new Color(THEME.active); } else { const idleMsg = w.addText("当前没有停车记录"); idleMsg.font = Font.mediumSystemFont(12); idleMsg.textColor = new Color(THEME.textDim); } w.addSpacer(8); const lastUpdated = w.addText( `最近刷新:${state.lastUpdated.toLocaleTimeString()}` ); lastUpdated.font = Font.italicSystemFont(10); lastUpdated.textColor = new Color(THEME.textDim); w.refreshAfterDate = new Date(Date.now() + REFRESH_INTERVAL_MINUTES * 60 * 1000); return w; } // 中号组件 function buildMediumWidget(state) { const w = new ListWidget(); w.setPadding(16, 16, 16, 16); makeBackground(w, state.isParked); const mainStack = w.addStack(); mainStack.layoutHorizontally(); // 左侧 const left = mainStack.addStack(); left.layoutVertically(); left.centerAlignContent(); const header = left.addText(state.isParked ? "停车中" : "未停车"); header.font = Font.boldSystemFont(20); header.textColor = new Color(THEME.textMain); left.addSpacer(8); if (state.isParked) { const timeText = left.addText(state.durationText); timeText.font = Font.boldSystemFont(30); timeText.textColor = new Color(THEME.active); left.addSpacer(8); const startLabel = left.addText(`开始:${state.rawTime}`); startLabel.font = Font.regularSystemFont(10); startLabel.textColor = new Color(THEME.textDim); } else { const idleMsg = left.addText("当前没有停车记录"); idleMsg.font = Font.mediumSystemFont(12); idleMsg.textColor = new Color(THEME.textDim); } // 右侧图标 const right = mainStack.addStack(); right.layoutVertically(); right.size = new Size(80, 0); right.centerAlignContent(); const iconStack = right.addStack(); iconStack.setPadding(10, 10, 10, 10); iconStack.cornerRadius = 30; iconStack.backgroundColor = new Color( state.isParked ? THEME.active : THEME.idle, 0.1 ); const icon = iconStack.addText(state.isParked ? "⏱" : "P"); icon.font = Font.systemFont(30); right.addSpacer(12); const refTime = right.addText( `更新:${state.lastUpdated.toLocaleTimeString()}` ); refTime.font = Font.italicSystemFont(10); refTime.textColor = new Color(THEME.textDim); return w; } async function createWidget() { const state = await fetchParkingState(); if (config.widgetFamily === "small") return buildSmallWidget(state); return buildMediumWidget(state); } const widget = await createWidget(); if (!config.runsInWidget) { await widget.presentMedium(); } else { Script.setWidget(widget); Script.complete(); }