基于 chart 图表进行数据库条目信息可视化统计的分享

参考文章《利用 数据库 和 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'.

这是由于刚打开思源笔记软件,用于图表块渲染的目标数据库还未加载。解决方法也很简单,手动把目标数据库页面打开一下(后续无需常开该页面),之后就能正常显示了。

  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    25203 引用 • 103941 回帖 • 1 关注
1 操作
ABin666 在 2025-03-31 10:42:18 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • stevehfut

    牛的,我的插件又焕发出第二春了trollface

    1 回复
  • 真牛,这个应该可以被官方封装一下当做是默认的

    嘿嘿,希望有机会
    ABin666
  • ABin666 1 评论

    哇,大大你好啊。很高兴这个程序对你有帮助。
    也是非常感谢你开发的 STtools 插件啊。我正是在使用了你开发的插件后,才有了这个图表可视化的灵感!

    也十分感谢你的分享 😊
    stevehfut