Obsidian美化:像Notion一样拥有动态小组件 (时间/电量/日历) | 插件+DataviewJS两种方法
下载文件
Dataview时间进度条
样例代码:
// --- 最终版代码 ---
const { DateTime } = dv.luxon;
const now = DateTime.now();
const startOfDay = now.startOf('day');
const passedMinutesInDay = now.diff(startOfDay, 'minutes').minutes;
const dayPercent = (passedMinutesInDay / (24 * 60)) * 100;
const startOfWeek = now.startOf('week');
const passedHoursInWeek = now.diff(startOfWeek, 'hours').hours;
const weekPercent = (passedHoursInWeek / (7 * 24)) * 100;
const monthPercent = (now.day / now.daysInMonth) * 100;
const passedDaysInYear = now.ordinal;
const totalDaysInYear = now.isInLeapYear ? 366 : 365;
const yearPercent = (passedDaysInYear / totalDaysInYear) * 100;
const progressBars = [
{ label: "今日进度", value: passedMinutesInDay, max: 24 * 60, percent: dayPercent },
{ label: "本周进度", value: passedHoursInWeek, max: 7 * 24, percent: weekPercent },
{ label: "本月进度", value: now.day, max: now.daysInMonth, percent: monthPercent },
{ label: "本年进度", value: passedDaysInYear, max: totalDaysInYear, percent: yearPercent }
];
function createProgressBar(data) {
const container = dv.el("div", "");
container.style.display = "flex";
container.style.alignItems = "center";
container.style.marginBottom = "8px";
const label = dv.el("span", `${data.label}:`);
label.style.minWidth = "75px";
label.style.flexShrink = "0";
const progress = dv.el("progress", "");
progress.setAttribute("value", data.value);
progress.setAttribute("max", data.max);
progress.style.flexGrow = "1";
progress.style.width = "100%";
progress.style.height = "14px";
const percentage = dv.el("span", ` ${data.percent.toFixed(2)}%`);
percentage.style.marginLeft = "10px";
container.append(label, progress, percentage);
dv.paragraph(container);
}
progressBars.forEach(bar => createProgressBar(bar));
Dataview年倒计时
样例代码:
const year = dv.current().year || new Date().getFullYear();
let rawCssClasses = dv.current().cssclasses;
let classesArray = [];
if (rawCssClasses) {
if (Array.isArray(rawCssClasses)) {
classesArray = rawCssClasses;
} else {
classesArray = [String(rawCssClasses)];
}
}
// --- End of Enhanced Code ---
const titleAlignClass = classesArray.find(c => c.startsWith("align-")) ?? "align-left";
// 使用 div 显示为块级元素,字体大小和颜色由配套CSS控制
dv.el("div", `${year} 年倒计时`, { cls: `year-title ${titleAlignClass}` });
const startDate = moment(`${year}-01-01`);
const endDate = moment(`${year}-12-31`);
const today = moment();
const container = dv.el("div", "", { cls: "year-calendar" });
for (let d = startDate.clone(); d.isSameOrBefore(endDate); d.add(1, "day")) {
const dayBox = document.createElement("div");
dayBox.classList.add("day-box");
dayBox.setAttribute("title", d.format("YYYY-MM-DD"));
if (d.isBefore(today, 'day')) {
dayBox.classList.add("past");
} else if (d.isSame(today, 'day')) {
dayBox.classList.add("today");
} else {
dayBox.classList.add("future");
}
container.appendChild(dayBox);
}
CSS代码(添加到.obsidian/snippets文件夹下):
.year-title {
margin-bottom: 0.5em;
font-size: 1.2em;
color: var(--text-muted);
}
.align-left .year-title {
text-align: left;
}
.align-center .year-title {
text-align: center;
}
.align-right .year-title {
text-align: right;
}
.year-calendar {
display: flex;
flex-wrap: wrap;
justify-content: start;
gap: 0.5px;
}
.day-box {
width: 10px;
height: 10px;
border-radius: 1px;
margin: 1px;
background-color: white;
transition: transform 0.2s;
}
.day-box.past {
background-color: #3A353F;
}
.day-box.today {
background-color: #A6D676;
}
.day-box.future {
background-color: #EEE7DF;
border: 0.5px solid #ddd;
}
.day-box:hover {
transform: scale(1.4);
cursor: pointer;
}
@media (max-width: 600px) {
.year-calendar {
justify-content: center;
gap: 1px;
}
.day-box {
width: 8px;
height: 8px;
margin: 0.5px;
}
}
Dataview彩色时钟
样例代码
// --- 1. 使用 Dataview API 构建一个与原始文件完全相同的扁平化 HTML 结构 ---
// 创建最外层的容器
const clock = dv.container.createEl('div', {
cls: 'progress-clock',
attr: { id: 'clock' }
});
// 定义一个对象来存放需要更新的元素引用
const elements = {};
// 将所有元素直接创建为 clock 的子元素
const dateButton = clock.createEl('button', { cls: 'progress-clock__time-date', attr: { 'data-group': 'd', type: 'button' } });
elements.week = dateButton.createEl('small', { attr: { 'data-unit': 'w' } });
dateButton.createEl('br');
elements.month = dateButton.createEl('span', { attr: { 'data-unit': 'mo' } });
elements.day = dateButton.createEl('span', { attr: { 'data-unit': 'd' } });
elements.hour = clock.createEl('button', { cls: 'progress-clock__time-digit', attr: { 'data-unit': 'h', 'data-group': 'h' } });
clock.createEl('span', { cls: 'progress-clock__time-colon', text: ':' });
elements.minute = clock.createEl('button', { cls: 'progress-clock__time-digit', attr: { 'data-unit': 'm', 'data-group': 'm' } });
clock.createEl('span', { cls: 'progress-clock__time-colon', text: ':' });
elements.second = clock.createEl('button', { cls: 'progress-clock__time-digit', attr: { 'data-unit': 's', 'data-group': 's' } });
elements.ampm = clock.createEl('span', { cls: 'progress-clock__time-ampm', attr: { 'data-unit': 'ap' } });
// SVG 仍然作为 clock 的直接子元素
const svgHTML = `
<svg class="progress-clock__rings" width="256" height="256" viewBox="0 0 256 256">
<g data-units="d"><circle class="progress-clock__ring" cx="128" cy="128" r="74" fill="none" opacity="0.1" stroke="#e13e78" stroke-width="12"></circle><circle class="progress-clock__ring-fill" data-ring="mo" cx="128" cy="128" r="74" fill="none" stroke="#e13e78" stroke-width="12" stroke-dasharray="465 465" stroke-linecap="round" transform="rotate(-90,128,128)"></circle></g>
<g data-units="h"><circle class="progress-clock__ring" cx="128" cy="128" r="90" fill="none" opacity="0.1" stroke="#e79742" stroke-width="12"></circle><circle class="progress-clock__ring-fill" data-ring="d" cx="128" cy="128" r="90" fill="none" stroke="#e79742" stroke-width="12" stroke-dasharray="565.5 565.5" stroke-linecap="round" transform="rotate(-90,128,128)"></circle></g>
<g data-units="m"><circle class="progress-clock__ring" cx="128" cy="128" r="106" fill="none" opacity="0.1" stroke="#4483ec" stroke-width="12"></circle><circle class="progress-clock__ring-fill" data-ring="h" cx="128" cy="128" r="106" fill="none" stroke="#4483ec" stroke-width="12" stroke-dasharray="666 666" stroke-linecap="round" transform="rotate(-90,128,128)"></circle></g>
<g data-units="s"><circle class="progress-clock__ring" cx="128" cy="128" r="122" fill="none" opacity="0.1" stroke="#8f30eb" stroke-width="12"></circle><circle class="progress-clock__ring-fill" data-ring="m" cx="128" cy="128" r="122" fill="none" stroke="#8f30eb" stroke-width="12" stroke-dasharray="766.5 766.5" stroke-linecap="round" transform="rotate(-90,128,128)"></circle></g>
</svg>
`;
clock.innerHTML += svgHTML;
const ringFills = {
day: clock.querySelector('[data-ring="mo"]'),
hour: clock.querySelector('[data-ring="d"]'),
minute: clock.querySelector('[data-ring="h"]'),
second: clock.querySelector('[data-ring="m"]'),
};
// --- 更新逻辑部分保持不变 ---
function updateClock() {
moment.locale('zh-cn');
const now = moment();
const formatDate = now.format("dddd-MMMM-D-H-mm-ss-a").split("-");
const [week, month, day, hour, minute, second, ampm] = formatDate;
elements.week.textContent = week;
elements.month.textContent = month;
elements.day.textContent = day;
elements.hour.textContent = hour;
elements.minute.textContent = minute;
elements.second.textContent = second;
elements.ampm.textContent = ampm;
const daysInMonth = now.daysInMonth();
const secProgress = second / 60;
const minProgress = (parseInt(minute) + secProgress) / 60;
const hourProgress = (parseInt(hour) + minProgress) / 24;
const dayProgress = (parseInt(day) - 1 + hourProgress) / daysInMonth;
const circumferences = { day: 465, hour: 565.5, minute: 666, second: 766.5 };
if (ringFills.second) ringFills.second.setAttribute('stroke-dashoffset', (1 - secProgress) * circumferences.second);
if (ringFills.minute) ringFills.minute.setAttribute('stroke-dashoffset', (1 - minProgress) * circumferences.minute);
if (ringFills.hour) ringFills.hour.setAttribute('stroke-dashoffset', (1 - hourProgress) * circumferences.hour);
if (ringFills.day) ringFills.day.setAttribute('stroke-dashoffset', (1 - dayProgress) * circumferences.day);
}
updateClock();
const intervalId = window.setInterval(updateClock, 1000);
dv.container.onunload = () => { window.clearInterval(intervalId); }
CSS代码(添加到.obsidian/snippets文件夹下):
/*
Colorful Clock CSS Snippet for Obsidian
Version 2 - Corrected Selector
*/
/*
我们直接选择 .progress-clock 类。
这个类应该足够独特,不会与主题冲突。
同时,我们使用 margin: auto 来将整个时钟在 Dataview 块中居中。
*/
.progress-clock {
display: grid;
justify-content: center;
align-content: center;
position: relative;
text-align: center;
height: 15em;
width: 15em;
margin: 2em auto;
}
/* --- 以下的规则保持不变,但现在会因为父级规则生效而正常工作 --- */
.progress-clock button {
padding: 0;
border: none;
box-shadow: none;
background-color: transparent;
display: block;
color: var(--text-normal);
}
.progress-clock button:hover {
background-color: transparent;
}
.progress-clock__time-date,
.progress-clock__time-digit,
.progress-clock__time-colon,
.progress-clock__time-ampm {
font: 1em/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
transition: color 0.2s linear;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.progress-clock__time-date,
.progress-clock__time-digit {
background: transparent;
}
.progress-clock__time-date,
.progress-clock__time-ampm {
grid-column: 1 / 6;
}
.progress-clock__time-date {
font-size: 0.75em;
line-height: 1.33;
}
.progress-clock__time-digit,
.progress-clock__time-colon {
font-size: 1.6em;
font-weight: 400;
grid-row: 2;
margin: 0;
}
.progress-clock__time-colon {
line-height: 1.5em;
}
.progress-clock__time-ampm {
cursor: default;
grid-row: 3;
}
.progress-clock__rings {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.6;
}
.progress-clock__ring {
opacity: 0.1;
}
.progress-clock__ring-fill {
transition:
opacity 0s 0.3s linear,
stroke-dashoffset 0.3s ease-in-out;
}
[data-group]:focus {
outline: transparent;
}
[data-units] {
transition: opacity 0.2s linear;
}
[data-group="d"]:focus,
[data-group="d"]:hover {
color: hsl(333,90%,55%);
}
[data-group="h"]:focus,
[data-group="h"]:hover {
color: hsl(33,90%,55%);
}
[data-group="m"]:focus,
[data-group="m"]:hover {
color: hsl(213,90%,55%);
}
[data-group="s"]:focus,
[data-group="s"]:hover {
color: hsl(273,90%,55%);
}
[data-group]:focus ~ .progress-clock__rings [data-units],
[data-group]:hover ~ .progress-clock__rings [data-units] {
opacity: 0.2;
}
[data-group="d"]:focus ~ .progress-clock__rings [data-units="d"],
[data-group="d"]:hover ~ .progress-clock__rings [data-units="d"],
[data-group="h"]:focus ~ .progress-clock__rings [data-units="h"],
[data-group="h"]:hover ~ .progress-clock__rings [data-units="h"],
[data-group="m"]:focus ~ .progress-clock__rings [data-units="m"],
[data-group="m"]:hover ~ .progress-clock__rings [data-units="m"],
[data-group="s"]:focus ~ .progress-clock__rings [data-units="s"],
[data-group="s"]:hover ~ .progress-clock__rings [data-units="s"] {
opacity: 1;
}
Obsidian电池电量
样例代码
// --- 电池小组件 (DataviewJS 版) ---
// 1. 获取当前代码块的容器,我们将在这里渲染所有内容
const container = dv.container;
container.classList.add("battery-real-widget-container"); // 添加自定义CSS类
// 2. 检查浏览器是否支持电池API
if (!('getBattery' in navigator)) {
container.setText("❌ 您的设备或浏览器不支持电池状态API。");
} else {
// 3. 创建UI元素 (先创建骨架,再填充内容)
const iconEl = dv.el("span", "", { cls: "battery-real-icon" });
const textEl = dv.el("span", "", { cls: "battery-real-text" });
// 4. 定义一个异步函数来获取并更新UI
async function updateBatteryDisplay() {
try {
// @ts-ignore
const battery = await navigator.getBattery();
const level = Math.floor(battery.level * 100);
// 更新电量文本
textEl.setText(`${level}%`);
// 更新图标和颜色
if (battery.charging) {
iconEl.setText("⚡️"); // 充电中用闪电Emoji
container.style.color = "var(--color-green)"; // 使用主题颜色
} else {
iconEl.setText("🔋"); // 未充电用电池Emoji
// 根据电量设置颜色
if (level > 50) {
container.style.color = "var(--text-normal)";
} else if (level > 20) {
container.style.color = "var(--color-yellow)";
} else {
container.style.color = "var(--color-red)";
}
}
} catch (error) {
console.error("获取电池状态失败:", error);
container.setText("❌ 获取电池状态失败");
}
}
// 5. 首次立即执行
updateBatteryDisplay();
// 6. 设置定时器,每分钟更新一次
// 警告:这个定时器在离开页面后不会被自动清理,这是DataviewJS的局限性。
// 它会持续运行直到你关闭或重载Obsidian。
const intervalID = setInterval(updateBatteryDisplay, 60000); // 60000毫秒 = 1分钟
// (一个不完美的清理尝试) 当代码块被重新渲染时,Obsidian会清空旧容器。
// 我们可以将intervalID附加到容器上,但这并不能保证在所有情况下都能被清理。
container.intervalID = intervalID;
}
CSS代码(添加到.obsidian/snippets文件夹下):
/* file: .obsidian/snippets/battery-real-widget.css (大尺寸版本) */
.battery-real-widget-container {
display: inline-flex;
align-items: center;
/* --- 以下是主要修改区域 --- */
/* 1. 增大图标和文字的间距 */
gap: 10px;
/* 原来是 6px */
/* 2. 增大字体大小 */
font-size: 20px;
/* 原来是 var(--font-ui-small),这里直接指定一个较大的像素值 */
font-weight: 500;
/* 3. 增大内部空间,让整个框更大 */
padding: 10px 16px;
/* 原来是 4px 8px */
/* 4. 加粗边框 */
border: 2px solid var(--background-modifier-border);
/* 原来是 1px */
/* 5. 增大圆角,以匹配更大的尺寸 */
border-radius: 12px;
/* 原来是 6px */
/* --- 其他属性保持不变 --- */
transition: color 0.5s ease;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.battery-real-icon {
/* 6. 再次微调图标大小,使其比文字稍大一点 */
font-size: 1.3em;
/* 原来是 1.2em */
}
代码逻辑
- 环境: 在Obsidian的浏览器环境中运行。
- 请求: 通过 navigator.getBattery() 向浏览器发起获取电池信息的异步请求。
- 等待: 使用 await 等待请求完成并返回 BatteryManager 对象。
- 解析: 从返回的对象中读取 level 和 charging 属性。
- 渲染: 使用Dataview的 dv.el() 函数将这些信息格式化(如level * 100 + ’%’),并创建HTML元素(图标和文本)显示在笔记中。
- 刷新: 使用 setInterval 定期重复2-5步,以实现数据的实时更新。