一点 js 也不懂的纯萌新
参考了两位前辈的代码喂给了 AI,我用的 deepseek 帮写代码,稍微微调一下即可食用。
条目统计
(async function() {
// =============== 用户配置区域 ===============
const avBlockId = '20240822122302-wi2jx0n'; // 数据库块ID
const chartBlockId = '20250530150522-1y6vetn'; // 图表块ID
const chartTitle = '数据库条目统计'; // 图表标题
const autoFreshDelay = 1000; // 自动刷新延迟(毫秒)
// 显示样式配置
const valueFontSize = 64; // 数字字体大小
const titleFontSize = 18; // 标题字体大小
const subtitleFontSize = 14; // 副标题字体大小
const chartColor = '#5470c6'; // 主色调
// =============== 图表配置 ===============
let option = {
title: [{
text: chartTitle,
left: 'center',
top: '10%',
textStyle: {
fontSize: titleFontSize,
fontWeight: 'normal'
}
}, {
text: '加载中...',
left: 'center',
top: '85%',
textStyle: {
fontSize: subtitleFontSize,
color: '#999'
}
}],
graphic: [{
type: 'circle',
shape: {
cx: 0.5,
cy: 0.5,
r: 100
},
style: {
fill: 'transparent',
stroke: chartColor,
lineWidth: 2,
opacity: 0.3
},
left: 'center',
top: 'center'
}, {
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
fontSize: valueFontSize,
fontWeight: 'bold',
fill: chartColor,
textAlign: 'center',
textVerticalAlign: 'middle'
}
}]
};
// =============== 数据处理逻辑 ===============
await getAVDataByBlockId(avBlockId, (av) => {
try {
const totalCount = av.keyValues?.[0]?.values?.length || 0;
// 更新数字显示
option.graphic[1].style.text = totalCount.toString();
// 更新副标题
option.title[1].text = `共 ${totalCount} 条记录`;
console.log(`数据库条目统计完成,共 ${totalCount} 条记录`);
} catch (e) {
console.error('数据处理失败:', e);
option.title[1].text = '数据加载失败: ' + e.message;
option.title[1].textStyle.color = '#ff4d4f';
}
});
// =============== 以下为通用函数 ===============
// 获取数据库信息
async function getAVDataByBlockId(blockId, callback) {
try {
const block = await fetchSyncPost('/api/query/sql', {
"stmt": `SELECT * FROM blocks WHERE id = '${blockId}'`
});
const markdown = block.data[0]?.markdown;
if (!markdown) throw new Error(`未找到ID为 ${blockId} 的数据库块`);
const avId = getDataAvIdFromHtml(markdown);
if (!avId) throw new Error(`在数据库块中未找到有效的av-id`);
const av = await fetchSyncPost('/api/file/getFile', {
"path": `/data/storage/av/${avId}.json`
});
if (av && typeof callback === 'function') callback(av);
else throw new Error(`未找到av-id=${avId}的数据库文件`);
} catch (e) {
console.error('获取数据库失败:', e);
option.title[1].text = '数据库加载失败: ' + e.message;
option.title[1].textStyle.color = '#ff4d4f';
}
}
// 获取avid
function getDataAvIdFromHtml(htmlString) {
const match = htmlString.match(/data-av-id="([^"]+)"/);
return match && match[1] ? match[1] : "";
}
// API请求
async function fetchSyncPost(url, data) {
const init = { method: "POST" };
init.body = data instanceof FormData ? data : JSON.stringify(data);
const res = await fetch(url, init);
return await res.json();
}
// 刷新图表
async function freshChart(chartBlockId) {
const ZWSP = "\u200b";
const looseJsonParse = (text) => Function(`"use strict";return (${text})`)();
const chartElement = document.querySelector(`.layout__center div[data-subtype="echarts"][data-node-id="${chartBlockId}"]`);
if (!chartElement) return;
let width;
if (chartElement.firstElementChild.clientWidth === 0) {
const tabElement = hasClosestByClassName(chartElement, "layout-tab-container", true);
if (tabElement) {
const visibleTab = Array.from(tabElement.children).find(
item => item.classList.contains("protyle") && !item.classList.contains("fn__none")
);
if (visibleTab) {
width = visibleTab.querySelector(".protyle-wysiwyg")?.firstElementChild?.clientWidth;
}
}
}
const wysiswgElement = hasClosestByClassName(chartElement, "protyle-wysiwyg", true);
if (!chartElement.firstElementChild.classList.contains("protyle-icons")) {
chartElement.insertAdjacentHTML("afterbegin", genIconHTML(wysiswgElement));
}
const renderElement = chartElement.firstElementChild.nextElementSibling;
try {
renderElement.style.height = chartElement.style.height;
const chartOption = await looseJsonParse(Lute.UnEscapeHTMLStr(chartElement.getAttribute("data-content")));
window.echarts.init(
renderElement,
window.siyuan.config.appearance.mode === 1 ? "dark" : undefined,
{ width }
).setOption(chartOption);
chartElement.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}`;
}
}
// DOM辅助函数
function hasClosestByClassName(element, className, top = false) {
if (!element) return false;
if (element.nodeType === 3) element = element.parentElement;
let e = element;
while (e && (top ? e.tagName !== "BODY" : !e.classList.contains("protyle-wysiwyg"))) {
if (e.classList?.contains(className)) return e;
e = e.parentElement;
}
return null;
}
function genIconHTML(element) {
const enable = element?.getAttribute("contenteditable") === "true";
return `<div class="protyle-icons">
<span class="protyle-icon protyle-icon--first protyle-action__edit${enable ? '' : ' fn__none'}">
<svg><use xlink:href="#iconEdit"></use></svg>
</span>
<span class="protyle-icon protyle-action__menu protyle-icon--last${enable ? '' : ' fn__none'}">
<svg><use xlink:href="#iconMore"></use></svg>
</span>
</div>`;
}
// 监听数据库变化
if (autoFreshDelay > 0 && !window[`__chart_observe__${avBlockId}`]) {
const targetNode = document.querySelector(`.layout__center div[data-node-id="${avBlockId}"]`);
if (targetNode) {
window[`__chart_observe__${avBlockId}`] = observeDOMChanges(
targetNode,
() => freshChart(chartBlockId),
autoFreshDelay,
{ attributes: false, childList: true, subtree: true }
);
}
}
// DOM变化监听器
function observeDOMChanges(targetNode, callback, debounceTime = 1000, options = {}) {
const config = { attributes: false, childList: true, subtree: true, ...options };
const observer = new MutationObserver(() => {
clearTimeout(observer.timer);
observer.timer = setTimeout(callback, debounceTime);
});
observer.observe(targetNode, config);
return () => observer.disconnect();
}
// 返回配置对象
return option;
})()
统计多选项数量并生成饼图
(async function() {
// =============== 用户配置区域 ===============
const avBlockId = '20240822122302-wi2jx0n'; // 数据库块ID
const tagColumn = '标签'; // 多选标签列名称
const chartBlockId = '20250530155754-eq67341'; // 图表块ID
const chartTitle = '标签分布统计'; // 图表标题
const autoFreshDelay = 1000; // 自动刷新延迟(毫秒)
// 饼图样式配置
const colors = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4'];
const labelFontSize = 14; // 标签字体大小
const valueFontSize = 16; // 数值字体大小
const legendFontSize = 12; // 图例字体大小
const minPercentage = 2; // 最小显示百分比(低于此值合并为"其他")
// =============== 图表配置 ===============
let option = {
title: {
text: chartTitle,
left: 'center',
textStyle: {
fontSize: 18
}
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
textStyle: {
fontSize: legendFontSize
}
},
series: [{
name: '标签分布',
type: 'pie',
radius: ['40%', '70%'],
center: ['40%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: function(params) {
return `${params.name}\n${params.value}次 (${params.percent}%)`;
},
fontSize: labelFontSize,
fontWeight: 'bold'
},
emphasis: {
label: {
show: true,
fontSize: valueFontSize,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [] // 将在数据处理后填充
}]
};
// =============== 数据处理逻辑 ===============
await getAVDataByBlockId(avBlockId, (av) => {
try {
// 1. 获取标签列数据
const tagColumnData = av.keyValues?.find(kv => kv.key?.name === tagColumn);
if (!tagColumnData?.values) {
throw new Error(`未找到标签列: ${tagColumn}`);
}
// 2. 统计标签频率
const tagFrequency = {};
let totalTags = 0;
tagColumnData.values.forEach(tagSet => {
if (tagSet?.mSelect?.length > 0) {
tagSet.mSelect.forEach(tag => {
const tagName = tag.content;
tagFrequency[tagName] = (tagFrequency[tagName] || 0) + 1;
totalTags++;
});
}
});
// 3. 转换格式并排序
let tagData = Object.entries(tagFrequency)
.map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value);
// 4. 处理小百分比标签(合并为"其他")
if (minPercentage > 0) {
const otherTags = [];
let otherCount = 0;
tagData = tagData.filter(item => {
const percentage = (item.value / totalTags) * 100;
if (percentage >= minPercentage) {
return true;
} else {
otherCount += item.value;
otherTags.push(item.name);
return false;
}
});
if (otherCount > 0) {
tagData.push({
name: `其他 (${otherTags.length}个标签)`,
value: otherCount
});
}
}
// 5. 应用颜色
tagData = tagData.map((item, index) => ({
...item,
itemStyle: {
color: colors[index % colors.length]
}
}));
// 6. 更新图表数据
option.series[0].data = tagData;
option.title.text = `${chartTitle} (共${totalTags}个标签)`;
console.log(`标签统计完成,共${Object.keys(tagFrequency).length}种标签`);
} catch (e) {
console.error('数据处理失败:', e);
option.title.text = '数据加载失败';
option.series[0].data = [];
option.graphic = {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: `错误: ${e.message}`,
fill: '#ff4d4f',
fontSize: 16
}
};
}
});
// =============== 通用函数 (与之前相同) ===============
// 获取数据库信息
async function getAVDataByBlockId(blockId, callback) {
try {
const block = await fetchSyncPost('/api/query/sql', {
"stmt": `SELECT * FROM blocks WHERE id = '${blockId}'`
});
const markdown = block.data[0]?.markdown;
if (!markdown) throw new Error(`未找到ID为 ${blockId} 的数据库块`);
const avId = getDataAvIdFromHtml(markdown);
if (!avId) throw new Error(`在数据库块中未找到有效的av-id`);
const av = await fetchSyncPost('/api/file/getFile', {
"path": `/data/storage/av/${avId}.json`
});
if (av && typeof callback === 'function') callback(av);
else throw new Error(`未找到av-id=${avId}的数据库文件`);
} catch (e) {
console.error('获取数据库失败:', e);
option.title.text = '数据库加载失败';
option.series[0].data = [];
option.graphic = {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: `错误: ${e.message}`,
fill: '#ff4d4f',
fontSize: 16
}
};
}
}
// 获取avid
function getDataAvIdFromHtml(htmlString) {
const match = htmlString.match(/data-av-id="([^"]+)"/);
return match && match[1] ? match[1] : "";
}
// API请求
async function fetchSyncPost(url, data) {
const init = { method: "POST" };
init.body = data instanceof FormData ? data : JSON.stringify(data);
const res = await fetch(url, init);
return await res.json();
}
// 刷新图表
async function freshChart(chartBlockId) {
const ZWSP = "\u200b";
const looseJsonParse = (text) => Function(`"use strict";return (${text})`)();
const chartElement = document.querySelector(`.layout__center div[data-subtype="echarts"][data-node-id="${chartBlockId}"]`);
if (!chartElement) return;
let width;
if (chartElement.firstElementChild.clientWidth === 0) {
const tabElement = hasClosestByClassName(chartElement, "layout-tab-container", true);
if (tabElement) {
const visibleTab = Array.from(tabElement.children).find(
item => item.classList.contains("protyle") && !item.classList.contains("fn__none")
);
if (visibleTab) {
width = visibleTab.querySelector(".protyle-wysiwyg")?.firstElementChild?.clientWidth;
}
}
}
const wysiswgElement = hasClosestByClassName(chartElement, "protyle-wysiwyg", true);
if (!chartElement.firstElementChild.classList.contains("protyle-icons")) {
chartElement.insertAdjacentHTML("afterbegin", genIconHTML(wysiswgElement));
}
const renderElement = chartElement.firstElementChild.nextElementSibling;
try {
renderElement.style.height = chartElement.style.height;
const chartOption = await looseJsonParse(Lute.UnEscapeHTMLStr(chartElement.getAttribute("data-content")));
window.echarts.init(
renderElement,
window.siyuan.config.appearance.mode === 1 ? "dark" : undefined,
{ width }
).setOption(chartOption);
chartElement.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}`;
}
}
// DOM辅助函数
function hasClosestByClassName(element, className, top = false) {
if (!element) return false;
if (element.nodeType === 3) element = element.parentElement;
let e = element;
while (e && (top ? e.tagName !== "BODY" : !e.classList.contains("protyle-wysiwyg"))) {
if (e.classList?.contains(className)) return e;
e = e.parentElement;
}
return null;
}
function genIconHTML(element) {
const enable = element?.getAttribute("contenteditable") === "true";
return `<div class="protyle-icons">
<span class="protyle-icon protyle-icon--first protyle-action__edit${enable ? '' : ' fn__none'}">
<svg><use xlink:href="#iconEdit"></use></svg>
</span>
<span class="protyle-icon protyle-action__menu protyle-icon--last${enable ? '' : ' fn__none'}">
<svg><use xlink:href="#iconMore"></use></svg>
</span>
</div>`;
}
// 监听数据库变化
if (autoFreshDelay > 0 && !window[`__chart_observe__${avBlockId}`]) {
const targetNode = document.querySelector(`.layout__center div[data-node-id="${avBlockId}"]`);
if (targetNode) {
window[`__chart_observe__${avBlockId}`] = observeDOMChanges(
targetNode,
() => freshChart(chartBlockId),
autoFreshDelay,
{ attributes: false, childList: true, subtree: true }
);
}
}
// DOM变化监听器
function observeDOMChanges(targetNode, callback, debounceTime = 1000, options = {}) {
const config = { attributes: false, childList: true, subtree: true, ...options };
const observer = new MutationObserver(() => {
clearTimeout(observer.timer);
observer.timer = setTimeout(callback, debounceTime);
});
observer.observe(targetNode, config);
return () => observer.disconnect();
}
// 返回配置对象
return option;
})()
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于