参考文章《利用 数据库 和 Chart(图表) 做小朋友身高管理》,实现了数据库的进一步可视化展示
Chart 代码
weekStatistics.js
功能:根据日期列,统计本周周一到周日各天的用时情况
(async () => {
// 关联的数据库块id,这里的id根据需要改成自己的
const avBlockId = '20250113200532-xns3mip';
const stats_column = '开始时间'; // 用于统计的数据库列名称
const chartTitle = '' // 如果为空,则自动取为数据库标题
const labelFont = 10
// 关联的图表块id,这里的id根据需要改成自己的
const chartBlockId = '20250330181059-28x0usn';
// 自动刷新延迟,单位是毫秒,默认是1秒,0则不自动刷新
const autoFreshDelay = 1000;
// 实际测量数据(将在获取数据库后初始化)
let option = {
title: {
text: chartTitle,
left: 'center',
// top: 20,
textStyle: {
fontSize: 18
},
},
grid: {
top: 50, // 顶部间距
},
xAxis: {
type: 'category', // 改为分类轴
},
yAxis: {
name: '累计时长 (小时)',
},
series: {}
}
// 获取数据库信息并格式化数据,这里av是从数据库获取的数据
await getAVDataByBlockId(avBlockId, (av) => {
try {
// 1. 获取数据列
const xColumn = av.keyValues?.find(kv => kv.key?.name === stats_column);
if (!xColumn?.values) {
throw new Error(`数据列不完整: ${stats_column}`);
}
// 2. 持续时间分析
console.log('==== 持续时间分析 ====');
const durationEntries = [];
const durationHoursByIndex = {};
xColumn.values.forEach((dateValue, index) => {
try {
if (dateValue?.date?.content2 && dateValue.date.content2 !== '0') {
const startDate = new Date(dateValue.date.content);
const endDate = new Date(dateValue.date.content2);
// 有效性校验
if (isNaN(startDate) || isNaN(endDate)) {
console.warn(`[行${index}] 无效日期格式`);
return;
}
// 计算持续时间
const durationMs = endDate - startDate;
const durationHours = (durationMs / 3600000).toFixed(2);
durationHoursByIndex[index] = parseFloat(durationHours);
// 记录条目
durationEntries.push({
index: index + 1,
start: dateValue.date.content,
end: dateValue.date.content2,
hours: durationHours
});
}
} catch (e) {
console.error(`[行${index}] 数据处理失败:`, e);
}
});
// 3. 控制台输出分析结果
if (durationEntries.length > 0) {
console.groupCollapsed(`发现 ${durationEntries.length} 个时段`);
durationEntries.forEach(entry => {
console.log(`行${entry.index}: ${entry.hours}小时`);
});
console.groupEnd();
} else {
console.log('未发现有效时段');
}
// 4. 构建完整周数据结构
const weekdaysCN = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const weeklyData = weekdaysCN.map(weekday => ({
weekday,
totalHours: 0,
order: weekdaysCN.indexOf(weekday) // 排序依据
}));
// 5. 日期统计
const dataStatistic = xColumn.values
.map((dateValue, index) => {
// 5.1 仅处理有效时间段
if (!(dateValue?.date?.content2 && dateValue.date.content2 !== '0')) {
return null;
}
// 5.2 获取持续时间
const duration = durationHoursByIndex[index];
if (typeof duration === 'undefined') return null;
// 5.3 解析日期
try {
const date = new Date(dateValue.date.content);
if (isNaN(date)) return null;
return {
date,
weekday: getChineseWeekday(date),
duration
};
} catch (e) {
console.warn(`[行${index}] 日期解析失败`);
return null;
}
})
.filter(Boolean);
// 6. 填充周数据
dataStatistic.forEach(item => {
if (!isDateInCurrentWeek(item.date)) return;
const targetDay = weeklyData.find(d => d.weekday === item.weekday);
if (targetDay) {
targetDay.totalHours += item.duration;
}
});
// 7. 排序并生成图表数据
const sortedData = weeklyData
.sort((a, b) => a.order - b.order)
.map(item => ({
...item,
totalHours: Number(item.totalHours.toFixed(2))
}));
// 计算持续时间范围
let maxDuration = 0;
let minDuration = Infinity;
dataStatistic.forEach(item => {
if (item.duration > maxDuration) maxDuration = item.duration;
if (item.duration < minDuration) minDuration = item.duration;
});
// 根据持续时间范围,修改颜色生成函数
function getColorByDuration(weekday, duration) {
const colorConfig = {
// 工作日:白(100%) → 深蓝(80%)
'default': { h: 210, s: 100, range: [100, 80] },
// 周末:白(100%) → 深橙(80%)
'weekend': { h: 30, s: 100, range: [100, 80] }
};
const isWeekend = ['周六', '周日'].includes(weekday);
const config = isWeekend ? colorConfig.weekend : colorConfig.default;
const ratio = duration / (maxDuration || 1); // 持续时间比例
const lightness = config.range[0] - ratio * (config.range[0] - config.range[1]);
// 确保亮度不低于20%(保持可读性)
return `hsl(${config.h}, ${config.s}%, ${Math.max(20, lightness)}%)`;
}
// 8. 更新图表配置
option.xAxis.data = sortedData.map(d => d.weekday);
option.series = {
name: '持续时间',
type: 'bar',
data: sortedData.map(d => ({
value: d.totalHours,
// 携带元数据用于颜色生成
meta: {
weekday: d.weekday,
duration: d.totalHours
}
})),
label: {
show: true,
fontSize: labelFont,
formatter: function (params) {
// 当值大于0时显示标签
return params.value > 0 ? `${params.value} 小时` : '';
},
color: '#333'
},
itemStyle: {
color: function (params) {
const data = params.data;
return getColorByDuration(data.meta.weekday, data.meta.duration);
}
}
};
option.title.text = `${chartTitle || av.name} (本周统计)`;
// 9. 空数据处理
if (sortedData.every(d => d.totalHours === 0)) {
option.graphic = {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '本周无持续时间记录',
fill: '#999',
fontSize: 14
}
};
}
} catch (e) {
console.error('数据处理失败:', e);
option.title = {
text: '数据加载失败',
subtext: e.message,
subtextStyle: { color: '#ff4d4f' }
};
option.series.data = [];
}
});
////////////////////////////////// 以下代码不涉及数据配置,一般不需要改动 ////////////////////////////////////
// 监听av变化,当数据库块被修改时,重新获取数据
if (autoFreshDelay > 0 && !window['__chat_observe__' + avBlockId]) {
window['__chat_observe__' + avBlockId] = observeDOMChanges(document.querySelector('.layout__center div[data-node-id="' + avBlockId + '"]'), () => {
freshChart(chartBlockId);
}, autoFreshDelay, { attributes: false });
}
// 输出运行状态,方便调试
console.log('render chart start');
// 星期转换函数
function getChineseWeekday(date) {
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
const dayIndex = date.getDay(); // 0是周日,6是周六
return `周${weekdays[dayIndex]}`; // 返回示例:周一
}
// 新增判断函数
function isWeekend(weekday) {
const weekendKeywords = ['周六', '周日'];
return weekendKeywords.some(kw => weekday.includes(kw));
}
// 准确判断日期是否在当前周的函数
function isDateInCurrentWeek(date) {
if (!(date instanceof Date)) return false;
// 获取当前时间(确保与被比较日期相同时区)
const now = new Date();
// 计算本周一的00:00:00
const monday = new Date(now);
monday.setDate(now.getDate() - now.getDay() + (now.getDay() === 0 ? -6 : 1)); // 处理周日的情况
monday.setHours(0, 0, 0, 0);
// 计算下周一00:00:00
const nextMonday = new Date(monday);
nextMonday.setDate(monday.getDate() + 7);
// 判断日期是否在 [本周一, 下周一) 区间内
return date >= monday && date < nextMonday;
}
// 获取数据库信息并格式化数据
async function getAVDataByBlockId(blockId, callback) {
// 获取块信息
const block = await fetchSyncPost('/api/query/sql', { "stmt": `SELECT * FROM blocks WHERE id = '${blockId}'` })
const markdown = block.data[0]?.markdown;
// 获取数据库信息
if (markdown) {
// 获取数据库文件地址
const avId = getDataAvIdFromHtml(markdown);
// 通过sy文件获取表格数据,按列排列,这里更合适
const av = await fetchSyncPost('/api/file/getFile', { "path": `/data/storage/av/${avId}.json` });
// 通过renderAttributeView获取表格数据,按行排列
//const av = await fetchSyncPost('/api/av/renderAttributeView', {"id":avId,"viewID":"","query":""});
// 格式化数据选项
if (av) {
if (typeof callback === 'function') callback(av);
} else {
option = "未找到av-id=" + avId + "的数据库文件";
}
} else {
option = "未找到id=" + avBlockId + "的数据库块";
}
}
// 请求api
async function fetchSyncPost(url, data) {
const init = {
method: "POST",
};
if (data) {
if (data instanceof FormData) {
init.body = data;
} else {
init.body = JSON.stringify(data);
}
}
const res = await fetch(url, init);
const res2 = await res.json();
return res2;
}
// 获取avid
function getDataAvIdFromHtml(htmlString) {
// 使用正则表达式匹配data-av-id的值
const match = htmlString.match(/data-av-id="([^"]+)"/);
if (match && match[1]) {
return match[1]; // 返回匹配的值
}
return ""; // 如果没有找到匹配项,则返回空
}
// 刷新图表
async function freshChart(chartBlockId) {
const ZWSP = "\u200b";
const looseJsonParse = (text) => {
return Function(`"use strict";return (${text})`)();
};
const e = document.querySelector('.layout__center div[data-subtype="echarts"][data-node-id="' + chartBlockId + '"]')
let width = undefined;
if (e.firstElementChild.clientWidth === 0) {
const tabElement = hasClosestByClassName(e, "layout-tab-container", true);
if (tabElement) {
Array.from(tabElement.children).find(item => {
if (item.classList.contains("protyle") && !item.classList.contains("fn__none")) {
width = item.querySelector(".protyle-wysiwyg").firstElementChild.clientWidth;
return true;
}
});
}
}
const wysiswgElement = hasClosestByClassName(e, "protyle-wysiwyg", true);
if (!e.firstElementChild.classList.contains("protyle-icons")) {
e.insertAdjacentHTML("afterbegin", genIconHTML(wysiswgElement));
}
const renderElement = e.firstElementChild.nextElementSibling;
try {
renderElement.style.height = e.style.height;
const option = await looseJsonParse(Lute.UnEscapeHTMLStr(e.getAttribute("data-content")));
window.echarts.init(renderElement, window.siyuan.config.appearance.mode === 1 ? "dark" : undefined, { width }).setOption(option);
e.setAttribute("data-render", "true");
renderElement.classList.remove("ft__error");
if (!renderElement.textContent.endsWith(ZWSP)) {
renderElement.firstElementChild.insertAdjacentText("beforeend", ZWSP);
}
} catch (error) {
window.echarts.dispose(renderElement);
renderElement.classList.add("ft__error");
renderElement.innerHTML = `echarts render error: <br>${error}`;
}
}
function hasClosestByClassName(element, className, top = false) {
if (!element) {
return false;
}
if (element.nodeType === 3) {
element = element.parentElement;
}
let e = element;
let isClosest = false;
while (e && !isClosest && (top ? e.tagName !== "BODY" : !e.classList.contains("protyle-wysiwyg"))) {
if (e.classList?.contains(className)) {
isClosest = true;
} else {
e = e.parentElement;
}
}
return isClosest && e;
}
function genIconHTML(element) {
let enable = true;
if (element) {
const readonly = element.getAttribute("contenteditable");
if (typeof readonly === "string") {
enable = element.getAttribute("contenteditable") === "true";
} else {
return '<div class="protyle-icons"></div>';
}
}
return `<div class="protyle-icons">
<span aria-label="${window.siyuan.languages.edit}" class="b3-tooltips__nw b3-tooltips protyle-icon protyle-icon--first protyle-action__edit${enable ? "" : " fn__none"}"><svg><use xlink:href="#iconEdit"></use></svg></span>
<span aria-label="${window.siyuan.languages.more}" class="b3-tooltips__nw b3-tooltips protyle-icon protyle-action__menu protyle-icon--last${enable ? "" : " protyle-icon--first"}"><svg><use xlink:href="#iconMore"></use></svg></span>
</div>`;
}
// 监听dom变化
let observeTimer = null;
function observeDOMChanges(targetNode, callback, debounceTime = 1000, options = {}) {
// 默认配置
const defaultOptions = {
attributes: true,
childList: true,
subtree: true,
};
// 合并默认配置与传入的配置
const config = Object.assign({}, defaultOptions, options);
// 创建一个观察器实例
const observer = new MutationObserver((mutationsList) => {
// 使用防抖函数确保单位时间内最多只调用一次回调
if (observeTimer) {
clearTimeout(observeTimer);
}
observeTimer = setTimeout(() => {
// 处理变化
callback(mutationsList);
}, debounceTime);
});
// 开始观察目标节点
observer.observe(targetNode, config);
// 返回一个函数,以便在不需要时能够停止观察
return () => {
observer.disconnect();
};
}
return option;
})()
categoryStatistics.js
功能:根据所选分类列,统计一个月之内各类别的用时情况
(async () => {
// 关联的数据库块id,这里的id根据需要改成自己的
const avBlockId = '20250113200532-xns3mip';
// 改为你的分类列名称
const categoryColumn = '分类';
// 改为你的日期列
const dateColumn = '开始时间';
// 分类的颜色1,会循环显示柔和的粉红、浅蓝、薄荷绿、深灰
const legendColors = ['#ff6666', '#6699ff', '#66cc99', '#999999'];
// 分类的颜色2,会循环显示柔和的粉红、浅蓝、薄荷绿、深灰。可以任选一个,也可以自行定义
// const legendColors = ['#E3C4B5', '#B7C9D6', '#D9CAB3', '#BCB8B1'];
const labelFont = 10;
const chartTitle = '月度项目统计';
const is_customMonth = null; // 默认显示当前月
// 可修改为:
// const is_customMonth = null; // 默认显示当前月
// const is_customMonth = 202507; // 显示2025年7月
// const is_customMonth = "202412"; // 支持字符串格式
// 关联的图表块id,这里的id根据需要改成自己的
const chartBlockId = '20250330201447-w1fm4qr';
// 自动刷新延迟,单位是毫秒,默认是1秒,0则不自动刷新
const autoFreshDelay = 1000;
// chart配置
let option = {
title: {
text: chartTitle,
left: 'center',
// top: 20,
textStyle: {
fontSize: 18
},
},
grid: {
top: 50, // 顶部间距
},
xAxis: {
type: 'category', // 改为分类轴
},
yAxis: {
name: '累计时长 (小时)',
},
series: {}
}
// 获取数据库信息并格式化数据,这里av是从数据库获取的数据
// 修改后的核心处理函数
await getAVDataByBlockId(avBlockId, (av) => {
try {
// 1. 获取列对象
const dateCol = av.keyValues?.find(kv => kv.key?.name === dateColumn);
const categoryCol = av.keyValues?.find(kv => kv.key?.name === categoryColumn);
// 2. 创建blockID映射表
const dateMap = new Map(dateCol?.values?.map(v => [v.blockID, v.date]) || []);
const categoryMap = new Map(categoryCol?.values?.map(v => [v.blockID, v.mSelect]) || []);
// 3. 获取有效blockID(同时存在于两列)
const validBlockIDs = [...new Set([
...Array.from(dateMap.keys()),
...Array.from(categoryMap.keys())
])].filter(blockID => dateMap.has(blockID) && categoryMap.has(blockID));
// 4. 初始化统计Map
const durationMap = new Map();
// 新增代码:日期过滤逻辑 -------------------------------------------------
let targetYear, targetMonth;
if (is_customMonth) {
// 解析自定义年月
const dateStr = String(is_customMonth).padStart(6, '0');
const yearPart = dateStr.slice(0, 4);
const monthPart = dateStr.slice(4);
targetYear = parseInt(yearPart);
targetMonth = parseInt(monthPart) - 1; // 转换为0基月份
// 有效性校验
if (isNaN(targetYear) || isNaN(targetMonth) || targetMonth < 0 || targetMonth > 11) {
console.warn(`无效的月份参数 ${is_customMonth},已切换至当前月`);
const now = new Date();
targetYear = now.getFullYear();
targetMonth = now.getMonth();
}
} else {
// 使用当前年月
const now = new Date();
targetYear = now.getFullYear();
targetMonth = now.getMonth();
}
// ---------------------------------------------------------------------
// 5. 遍历有效行
validBlockIDs.forEach(blockID => {
try {
const dateValue = dateMap.get(blockID);
const categories = categoryMap.get(blockID)?.map(s => s.content) || [];
// 5.1 过滤无效时间段
if (!dateValue?.content2 || dateValue.content2 === '0') return;
// 5.2 解析时间
const startDate = new Date(dateValue.content);
const endDate = new Date(dateValue.content2);
if (isNaN(startDate) || isNaN(endDate)) return;
// 修改点:日期过滤逻辑 ---------------------------------------------
// 原代码:if (startDate.getMonth() !== currentMonth) return;
if (
startDate.getFullYear() !== targetYear ||
startDate.getMonth() !== targetMonth
) return;
// -----------------------------------------------------------------
// 5.4 计算持续时间
const durationHours = (endDate - startDate) / 3600000;
// 5.5 处理分类数据
const validCategories = categories.length > 0 ? categories : ['未分类'];
// 5.6 累计统计
validCategories.forEach(cat => {
durationMap.set(cat, (durationMap.get(cat) || 0) + durationHours);
});
} catch (e) {
console.error(`[行${blockID}] 处理失败:`, e);
}
});
// 生成图表数据
const chartData = Array.from(durationMap.entries())
.map(([category, total]) => ({
category,
total: Number(total.toFixed(2))
}))
.sort((a, b) => b.total - a.total);
// 计算总时长
let totalHours = chartData.reduce((sum, d) => sum + d.total, 0);
totalHours = Number(totalHours.toFixed(2));
let monthDisplay;
if (is_customMonth) {
const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
monthDisplay = `${targetYear}年${monthNames[targetMonth]}`;
} else {
monthDisplay = new Date().toLocaleDateString('zh-CN', {
year: 'numeric', month: 'long'
}).replace('年', '年');
}
// +++ 修改标题配置 +++
option.title = {
text: `${chartTitle} (${monthDisplay})`,
subtext: `总时长:${totalHours} 小时`,
left: 'center',
textStyle: {
fontSize: 18
},
subtextStyle: {
fontSize: 14,
color: '#666',
padding: [5, 0]
}
};
// ---------------------------------------------------------------------
// 更新图表系列数据
option.xAxis.data = chartData.map(d => d.category);
option.series = {
type: 'bar',
data: chartData.map(d => d.total),
itemStyle: {
color: function (params) {
const colors = legendColors;
return colors[params.dataIndex % colors.length];
}
},
label: {
show: true,
fontSize: labelFont,
formatter: '{@[1]}小时',
color: '#333'
}
};
// 空数据处理
if (chartData.length === 0) {
option.graphic = {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: is_customMonth ?
`${targetYear}年${targetMonth + 1}月无数据` :
'本月无有效数据',
fill: '#999',
fontSize: 14
}
};
}
} catch (e) {
console.error('数据处理失败:', e);
option.title = {
text: '数据加载失败',
subtext: e.message,
subtextStyle: { color: '#ff4d4f' }
};
}
});
////////////////////////////////// 以下代码不涉及数据配置,一般不需要改动 ////////////////////////////////////
// 监听av变化,当数据库块被修改时,重新获取数据
if (autoFreshDelay > 0 && !window['__chat_observe__' + avBlockId]) {
window['__chat_observe__' + avBlockId] = observeDOMChanges(document.querySelector('.layout__center div[data-node-id="' + avBlockId + '"]'), () => {
freshChart(chartBlockId);
}, autoFreshDelay, { attributes: false });
}
// 输出运行状态,方便调试
console.log('render chart start');
// 获取数据库信息并格式化数据
async function getAVDataByBlockId(blockId, callback) {
// 获取块信息
const block = await fetchSyncPost('/api/query/sql', { "stmt": `SELECT * FROM blocks WHERE id = '${blockId}'` })
const markdown = block.data[0]?.markdown;
// 获取数据库信息
if (markdown) {
// 获取数据库文件地址
const avId = getDataAvIdFromHtml(markdown);
// 通过sy文件获取表格数据,按列排列,这里更合适
const av = await fetchSyncPost('/api/file/getFile', { "path": `/data/storage/av/${avId}.json` });
// 通过renderAttributeView获取表格数据,按行排列
//const av = await fetchSyncPost('/api/av/renderAttributeView', {"id":avId,"viewID":"","query":""});
// 格式化数据选项
if (av) {
if (typeof callback === 'function') callback(av);
} else {
option = "未找到av-id=" + avId + "的数据库文件";
}
} else {
option = "未找到id=" + avBlockId + "的数据库块";
}
}
// 请求api
async function fetchSyncPost(url, data) {
const init = {
method: "POST",
};
if (data) {
if (data instanceof FormData) {
init.body = data;
} else {
init.body = JSON.stringify(data);
}
}
const res = await fetch(url, init);
const res2 = await res.json();
return res2;
}
// 获取avid
function getDataAvIdFromHtml(htmlString) {
// 使用正则表达式匹配data-av-id的值
const match = htmlString.match(/data-av-id="([^"]+)"/);
if (match && match[1]) {
return match[1]; // 返回匹配的值
}
return ""; // 如果没有找到匹配项,则返回空
}
// 刷新图表
async function freshChart(chartBlockId) {
const ZWSP = "\u200b";
const looseJsonParse = (text) => {
return Function(`"use strict";return (${text})`)();
};
const e = document.querySelector('.layout__center div[data-subtype="echarts"][data-node-id="' + chartBlockId + '"]')
let width = undefined;
if (e.firstElementChild.clientWidth === 0) {
const tabElement = hasClosestByClassName(e, "layout-tab-container", true);
if (tabElement) {
Array.from(tabElement.children).find(item => {
if (item.classList.contains("protyle") && !item.classList.contains("fn__none")) {
width = item.querySelector(".protyle-wysiwyg").firstElementChild.clientWidth;
return true;
}
});
}
}
const wysiswgElement = hasClosestByClassName(e, "protyle-wysiwyg", true);
if (!e.firstElementChild.classList.contains("protyle-icons")) {
e.insertAdjacentHTML("afterbegin", genIconHTML(wysiswgElement));
}
const renderElement = e.firstElementChild.nextElementSibling;
try {
renderElement.style.height = e.style.height;
const option = await looseJsonParse(Lute.UnEscapeHTMLStr(e.getAttribute("data-content")));
window.echarts.init(renderElement, window.siyuan.config.appearance.mode === 1 ? "dark" : undefined, { width }).setOption(option);
e.setAttribute("data-render", "true");
renderElement.classList.remove("ft__error");
if (!renderElement.textContent.endsWith(ZWSP)) {
renderElement.firstElementChild.insertAdjacentText("beforeend", ZWSP);
}
} catch (error) {
window.echarts.dispose(renderElement);
renderElement.classList.add("ft__error");
renderElement.innerHTML = `echarts render error: <br>${error}`;
}
}
function hasClosestByClassName(element, className, top = false) {
if (!element) {
return false;
}
if (element.nodeType === 3) {
element = element.parentElement;
}
let e = element;
let isClosest = false;
while (e && !isClosest && (top ? e.tagName !== "BODY" : !e.classList.contains("protyle-wysiwyg"))) {
if (e.classList?.contains(className)) {
isClosest = true;
} else {
e = e.parentElement;
}
}
return isClosest && e;
}
function genIconHTML(element) {
let enable = true;
if (element) {
const readonly = element.getAttribute("contenteditable");
if (typeof readonly === "string") {
enable = element.getAttribute("contenteditable") === "true";
} else {
return '<div class="protyle-icons"></div>';
}
}
return `<div class="protyle-icons">
<span aria-label="${window.siyuan.languages.edit}" class="b3-tooltips__nw b3-tooltips protyle-icon protyle-icon--first protyle-action__edit${enable ? "" : " fn__none"}"><svg><use xlink:href="#iconEdit"></use></svg></span>
<span aria-label="${window.siyuan.languages.more}" class="b3-tooltips__nw b3-tooltips protyle-icon protyle-action__menu protyle-icon--last${enable ? "" : " protyle-icon--first"}"><svg><use xlink:href="#iconMore"></use></svg></span>
</div>`;
}
// 监听dom变化
let observeTimer = null;
function observeDOMChanges(targetNode, callback, debounceTime = 1000, options = {}) {
// 默认配置
const defaultOptions = {
attributes: true,
childList: true,
subtree: true,
};
// 合并默认配置与传入的配置
const config = Object.assign({}, defaultOptions, options);
// 创建一个观察器实例
const observer = new MutationObserver((mutationsList) => {
// 使用防抖函数确保单位时间内最多只调用一次回调
if (observeTimer) {
clearTimeout(observeTimer);
}
observeTimer = setTimeout(() => {
// 处理变化
callback(mutationsList);
}, debounceTime);
});
// 开始观察目标节点
observer.observe(targetNode, config);
// 返回一个函数,以便在不需要时能够停止观察
return () => {
observer.disconnect();
};
}
return option;
})()
视频功能演示
补充说明
2025 年 3 月 31 日
当第一次打开思源笔记后,图表块可能会报错 echarts render error: TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
这是由于刚打开思源笔记软件,用于图表块渲染的目标数据库还未加载。解决方法也很简单,手动把目标数据库页面打开一下(后续无需常开该页面),之后就能正常显示了。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于