每天完成最重要的几件事,这一天就算没有虚度。
每天打开手机,看一眼,就知道还有什么事没做,简单而清晰。
点击一下按钮,输入内容,即可完成打卡。
实现方式还是利用自己的“三板斧”:Notion、Scriptable和Apple快捷指令。
“今日打卡”部分
Notion api+Scriptable app,这个之前也提到过,需要做的就是明确一下需求和实现思路,脚本部分让AI帮忙即可。
我的实现思路是调用Notion api,找到当天的所有记录,如果记录里符合特定的tag就表明这一项已经完成了。
const TASK_TAGS = {
"晨间日记": "Diary",
"背单词+多邻国": "Vocabulary",
"运动": "Workout",
"平板支撑": "Plank",
"阅读": "Reading",
"学习数据库+编程": "DBLearning",
"总结工作": "WorkingSummary",
"等等的每日一照": "DengDengPhoto"
};

Scriptable的脚本:
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-gray; icon-glyph: magic;
// icon-color: blue; icon-glyph: check-square;
// ========= 今日要打卡的项目 =========
const TASKS = [
"晨间日记",
"背单词+多邻国",
"运动",
"平板支撑",
"阅读",
"学习数据库+编程",
"总结工作",
"等等的每日一照"
];
// 每个项目对应的 Notion Tag 名
const TASK_TAGS = {
"晨间日记": "Diary",
"背单词+多邻国": "Vocabulary",
"运动": "Workout",
"平板支撑": "Plank",
"阅读": "Reading",
"学习数据库+编程": "DBLearning",
"总结工作": "WorkingSummary",
"等等的每日一照": "DengDengPhoto"
};
// 反向映射:tag -> taskName
const TAG_TO_TASK = {};
for (const [task, tag] of Object.entries(TASK_TAGS)) {
TAG_TO_TASK[tag] = task;
}
// ========= 颜色(和你月视图一致) =========
const COLOR_BLUE = new Color("#1a7ee8d4"); // 已完成
const COLOR_GRAY = new Color("#DFE3EB"); // 未完成
const COLOR_TEXT = Color.black();
const BOX_SIZE = 18;
const BOX_RADIUS = 4;
// ========= Notion 配置(改这里) =========
const NOTION_API_URL = "https://api.notion.com/v1/databases/xxx/query"; // ← 换成你的 Daily 数据库 query URL
const NOTION_TOKEN = "ntn_xxx";
const NOTION_VERSION = "2022-06-28";
// 分页
const PAGE_SIZE = 100;
const SAFETY_MULTIPLIER = 3;
const MAX_PAGES = SAFETY_MULTIPLIER;
// ========= 通用工具 =========
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function pad(n) { return n < 10 ? "0" + n : "" + n; }
function formatDateKey(d) {
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
// 今天 00:00 ~ 明天 00:00
function getTodayRange() {
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
const start = new Date(y, m, d);
const end = new Date(y, m, d + 1);
return { start, end };
}
// ========= Notion 请求封装 =========
async function notionQuery(body) {
let req = new Request(NOTION_API_URL);
req.method = "POST";
req.headers = {
"Authorization": `Bearer ${NOTION_TOKEN}`,
"Content-Type": "application/json",
"Notion-Version": NOTION_VERSION
};
req.body = JSON.stringify(body);
const res = await req.loadJSON();
return {
results: res.results || [],
has_more: res.has_more || false,
next_cursor: res.next_cursor || null
};
}
// 从 page 中抽日期:优先 Date / Time,其次 created_time
function extractDailyDate(page) {
try {
const d1 = page.properties?.Date?.date?.start;
if (d1) return new Date(d1);
} catch (_) {}
try {
const d2 = page.properties?.Time?.date?.start;
if (d2) return new Date(d2);
} catch (_) {}
try {
const d3 = page.properties?.Time?.created_time;
if (d3) return new Date(d3);
} catch (_) {}
if (page.created_time) return new Date(page.created_time);
if (page.last_edited_time) return new Date(page.last_edited_time);
return null;
}
// ========= 核心:从 Notion 拉取“今天的所有条目”,看 tag 是否命中 =========
async function loadTodayCompletedTasks() {
const { start, end } = getTodayRange();
const completedSet = new Set();
// 构造 tag 过滤:Tags 包含任意一个对应 tag 即可
const tagFilters = Object.values(TASK_TAGS).map(tagName => ({
property: "Tags",
multi_select: { contains: tagName }
}));
let hasMore = true;
let nextCursor = null;
let pageCount = 0;
while (hasMore && pageCount < MAX_PAGES) {
const payload = {
filter: {
or: tagFilters // Tags 包含任何一个目标 tag
},
sorts: [
{ timestamp: "created_time", direction: "descending" }
],
page_size: PAGE_SIZE
};
if (nextCursor) payload.start_cursor = nextCursor;
const { results, has_more, next_cursor } = await notionQuery(payload);
pageCount += 1;
hasMore = !!has_more;
nextCursor = next_cursor || null;
let seenOlderThanToday = false;
for (const page of results || []) {
const d = extractDailyDate(page);
if (!d) continue;
if (d >= start && d < end) {
// 今天的记录:检查它有哪些 tag
const tagList = page.properties?.Tags?.multi_select || [];
for (const t of tagList) {
const tagName = t.name;
const taskName = TAG_TO_TASK[tagName];
if (taskName) {
completedSet.add(taskName); // 该项目视为已完成
}
}
} else if (d < start) {
// 已经翻到昨天之前,可以停了
seenOlderThanToday = true;
}
}
if (seenOlderThanToday) break;
if (hasMore) await sleep(150);
}
return completedSet;
}
// ========= 拉取今天已完成的项目 =========
let COMPLETED_TODAY = new Set();
try {
COMPLETED_TODAY = await loadTodayCompletedTasks();
} catch (e) {
console.error("Load Notion failed:", e);
COMPLETED_TODAY = new Set();
}
// 某个项目是否完成
function isTaskDone(taskName) {
return COMPLETED_TODAY.has(taskName);
}
// ========= 构建 Widget(左右两列,齐头对齐) =========
let widget = new ListWidget();
// 左边距调大一点,美观
widget.setPadding(10, 26, 10, 12);
widget.backgroundColor = Color.clear();
// 标题
let titleText = widget.addText("今日打卡");
titleText.font = Font.boldSystemFont(16);
titleText.textColor = COLOR_TEXT;
widget.addSpacer(6);
// 左右列容器
let grid = widget.addStack();
grid.layoutHorizontally();
// 左列
let leftCol = grid.addStack();
leftCol.layoutVertically();
leftCol.spacing = 4;
// 中间间隔
grid.addSpacer(24);
// 右列
let rightCol = grid.addStack();
rightCol.layoutVertically();
rightCol.spacing = 4;
// 每列的行数
const rowsPerCol = Math.ceil(TASKS.length / 2);
// 左列放 0,2,4,6 ;右列放 1,3,5,7
for (let i = 0; i < rowsPerCol; i++) {
const leftIndex = 2 * i;
if (leftIndex < TASKS.length) {
const name = TASKS[leftIndex];
addTaskRow(leftCol, name, isTaskDone(name));
}
const rightIndex = 2 * i + 1;
if (rightIndex < TASKS.length) {
const name = TASKS[rightIndex];
addTaskRow(rightCol, name, isTaskDone(name));
}
}
// 单行:方块 + 文本
function addTaskRow(parentStack, taskName, checked) {
let row = parentStack.addStack();
row.layoutHorizontally();
row.spacing = 6;
row.centerAlignContent();
let box = row.addStack();
box.size = new Size(BOX_SIZE, BOX_SIZE);
box.cornerRadius = BOX_RADIUS;
box.centerAlignContent();
if (checked) {
box.backgroundColor = COLOR_BLUE;
let check = box.addText("✓");
check.font = Font.systemFont(12);
check.textColor = Color.white();
check.centerAlignText();
} else {
box.backgroundColor = COLOR_GRAY;
}
let label = row.addText(taskName);
label.font = Font.systemFont(13);
label.textColor = COLOR_TEXT;
label.lineLimit = 1;
}
// ========= 完成 =========
if (config.runsInWidget) {
Script.setWidget(widget);
} else {
await widget.presentMedium();
}
Script.complete();
点击一下看看效果。
然后设置桌面小组件,效果如文中的第一张图。
按钮部分
使用Notion api+快捷指令app。

这里有两种按钮,一种是不需要输入内容的,例如“晨间日记”。另一种是需要输入具体内容的,比如“运动”。
第一种,内容和标记这两个变量的值都是固定的:

第二种,内容从固定值Text改为“Ask for Input”即可,点击以后会提示输入,效果如文中的第二张图。

搞定!