先安装番茄工具箱、Query & View 插件、query 挂件

把下面粘贴覆盖即可
//!js
const query = async () => {
const dv = Query.DataView(protyle, item, top);
// ── 1. UI 渲染 ──────────────────────────────────────────────────
const widget = document.createElement("div");
widget.innerHTML = `
<style>
.card-tool-widget { background-color: var(--b3-theme-surface); padding: 16px; border-radius: 6px; border: 1px solid var(--b3-theme-border); margin-top: 8px; display: flex; flex-direction: column; gap: 16px; }
.ctw-section { padding: 12px; border: 1px solid var(--b3-theme-border-light); border-radius: 4px; }
.ctw-title { font-weight: bold; font-size: 1.1em; color: var(--b3-theme-on-surface); margin-bottom: 4px; }
.ctw-desc { font-size: 0.9em; color: var(--b3-theme-on-surface-light); margin-bottom: 12px; }
.ctw-controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.ctw-controls .b3-button { margin-left: auto; }
.ctw-status { margin-top: 16px; padding: 6px 10px; border-radius: 4px; font-size: 0.9em; display: none; }
.ctw-status.is-shown { display: block; }
.ctw-status--info { background-color: var(--b3-theme-surface-light); color: var(--b3-theme-on-surface); }
.ctw-status--success { background-color: var(--b3-theme-success); color: var(--b3-theme-on-success); }
.ctw-status--error { background-color: var(--b3-theme-error); color: var(--b3-theme-on-error); }
</style>
<div class="card-tool-widget">
<div class="ctw-section">
<div class="ctw-title">全局设置</div>
<div class="ctw-desc">指定处理范围:已到期及未来 <b>m</b> 天内到期的闪卡。</div>
<div class="ctw-controls">
<input id="ctw-m-days" class="b3-text-field" placeholder="m 天内(例如: 7 或 0.5)" style="width: 240px;">
</div>
</div>
<div class="ctw-section">
<div class="ctw-title">功能一:分散推迟</div>
<div class="ctw-desc">将上述范围内的卡,分散到接下来 <b>n</b> 天内(步长 <b>k</b>)。</div>
<div class="ctw-controls">
<input id="ctw-n-days" class="b3-text-field" placeholder="分散到 n 天" style="width: 160px;">
<input id="ctw-step" class="b3-text-field" value="1" placeholder="步长 k (天)" style="width: 120px;">
<select id="ctw-mode" class="b3-select" style="width: 120px;">
<option value="round">均匀轮转</option>
<option value="rand">随机分配</option>
</select>
<button id="ctw-scatter-btn" class="b3-button b3-button--primary">执行分散</button>
</div>
</div>
<div class="ctw-section">
<div class="ctw-title">功能二:释放高优先级卡</div>
<div class="ctw-desc">将上述范围内的卡,若其优先级 ≥ <b>a</b>,则立即设为可复习。</div>
<div class="ctw-controls">
<input id="ctw-priority-a" class="b3-text-field" value="55" placeholder="优先级 a (≥51)" style="width: 180px;">
<button id="ctw-release-btn" class="b3-button b3-button--outline">执行释放</button>
</div>
</div>
<div class="ctw-section">
<div class="ctw-title">功能三:推迟低优先级卡</div>
<div class="ctw-desc">将上述范围内的卡,若其优先级 < <b>b</b>,统一推迟 <b>t</b> 天。</div>
<div class="ctw-controls">
<input id="ctw-low-priority-b" class="b3-text-field" value="40" placeholder="优先级 b (以下视为低优)" style="width: 180px;">
<input id="ctw-low-delay-days" class="b3-text-field" value="7" placeholder="推迟 t 天" style="width: 140px;">
<button id="ctw-low-delay-btn" class="b3-button b3-button--outline">推迟低优卡</button>
</div>
</div>
<div id="ctw-status" class="ctw-status"></div>
</div>
`;
dv.addele(widget);
const mInput = widget.querySelector("#ctw-m-days");
const nInput = widget.querySelector("#ctw-n-days");
const stepInput = widget.querySelector("#ctw-step");
const modeSelect = widget.querySelector("#ctw-mode");
const scatterBtn = widget.querySelector("#ctw-scatter-btn");
const aInput = widget.querySelector("#ctw-priority-a");
const releaseBtn = widget.querySelector("#ctw-release-btn");
const lowBInput = widget.querySelector("#ctw-low-priority-b");
const lowDelayInput = widget.querySelector("#ctw-low-delay-days");
const lowDelayBtn = widget.querySelector("#ctw-low-delay-btn");
const statusBar = widget.querySelector("#ctw-status");
const allControls = [
mInput, nInput, stepInput, modeSelect,
scatterBtn, aInput, releaseBtn,
lowBInput, lowDelayInput, lowDelayBtn,
];
// ── 2. UI 控制与工具函数 ────────────────────────────────────────
function showStatus(message, type = 'info') {
statusBar.textContent = message;
statusBar.className = `ctw-status is-shown ctw-status--${type}`;
console.log(`[Card Tool] ${message}`);
}
function setBusy(isBusy) {
allControls.forEach(el => el.disabled = isBusy);
scatterBtn.textContent = isBusy ? "处理中..." : "执行分散";
releaseBtn.textContent = isBusy ? "处理中..." : "执行释放";
lowDelayBtn.textContent = isBusy ? "处理中..." : "推迟低优卡";
}
function atStartOfDay(d){ const x = new Date(d); x.setHours(0,0,0,0); return x; }
function addDays(d,n){ const x = new Date(d); x.setTime(x.getTime() + n*24*60*60*1000); return x; }
const fmtDays = (v)=> (Math.round(v*1000)/1000).toString().replace(/\.?0+$/,"");
const chunk = (arr, size=200) => Array.from({length: Math.ceil(arr.length/size)}, (_,i)=>arr.slice(i*size,(i+1)*size));
function readDec(raw, fallback=NaN){
if (raw == null) return fallback;
let s = String(raw).trim().replace(/[0-9.,-天日]/g, ch => ({
'0':'0','1':'1','2':'2','3':'3','4':'4',
'5':'5','6':'6','7':'7','8':'8','9':'9',
'.':'.',',':',','-':'-'
}[ch] || ''));
const m = s.match(/[-+]?\d+(?:[.,]\d+)?/);
if (!m) return fallback;
let num = m[0];
if (num.includes(",") && !num.includes(".")) num = num.replace(",",".");
else if (num.includes(",") && num.includes(".")) num = num.replace(/,/g,"");
const x = parseFloat(num);
return Number.isFinite(x) ? x : fallback;
}
function readInt(raw, fallback=NaN){
const x = Math.round(readDec(raw, fallback));
return Number.isFinite(x) ? x : fallback;
}
function parseRiffDue(raw){
if (!raw) return null;
if (raw instanceof Date) return raw;
if (typeof raw === "number") return new Date(raw);
const s = String(raw).trim();
const m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(s);
if (m) {
const [_, Y, Mo, D, H, Mi, S] = m;
return new Date(+Y, +Mo-1, +D, +H, +Mi, +S);
}
const t = Date.parse(s);
return Number.isFinite(t) ? new Date(t) : null;
}
async function siyuanPost(route, data){
if (tomato_zZmqus5PtYRi?.siyuan?.fetchPost) {
return await tomato_zZmqus5PtYRi.siyuan.fetchPost(route, data);
}
const resp = await fetch(route, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data || {})
});
return await resp.json();
}
const resolveBlockId = (card) => card?.riffCard?.blockId || card?.blockId || card?.block?.id || card?.id;
const resolveCardId = (card) => card?.riffCard?.id || card?.riffCard?.cardId || card?.cardId || card?.id;
const attrCache = new Map(), parentCache = new Map();
async function batchGetAttrs(ids){
const ret = await siyuanPost("/api/attr/batchGetBlockAttrs", { ids });
if (ret?.code !== 0) throw new Error(ret?.msg || "batchGetBlockAttrs 失败");
for (const [id, attrs] of Object.entries(ret.data || {})) {
attrCache.set(id, attrs || {});
}
}
async function getBlockAttrs(id){
if (attrCache.has(id)) return attrCache.get(id);
const ret = await siyuanPost("/api/attr/getBlockAttrs", { id });
const attrs = ret?.code === 0 ? (ret.data || {}) : {};
attrCache.set(id, attrs);
return attrs;
}
async function getParentID(id){
if (parentCache.has(id)) return parentCache.get(id);
const pid = (await siyuanPost("/api/block/getBlockInfo", { id }))?.data?.parentID || null;
parentCache.set(id, pid);
return pid;
}
const PRIORITY_KEYS = ["custom-card-priority", "card-priority", "custom_card_priority", "card_priority"];
function parsePriorityFromAttrs(attrs){
if (!attrs) return { value: NaN };
for (const k of PRIORITY_KEYS){
if (Object.prototype.hasOwnProperty.call(attrs, k)) {
const n = Number(String(attrs[k]).trim());
if (Number.isFinite(n)) return { value: n, key: k, raw: attrs[k] };
}
}
return { value: NaN };
}
async function getPriorityFromSelfOrParents(startId, maxUp=24){
let cur = startId;
for (let level=0; cur && level<=maxUp; level++){
const { value } = parsePriorityFromAttrs(await getBlockAttrs(cur));
if (Number.isFinite(value)) return { value };
cur = await getParentID(cur);
}
return { value: NaN };
}
// ── 3. 事件绑定与执行逻辑 ──────────────────────────────────────
// 功能一:分散推迟(沿用你原来的逻辑)
scatterBtn.onclick = async () => {
setBusy(true);
try {
showStatus("步骤 1/3: 校验输入参数...", 'info');
const mDays = readDec(mInput.value);
const nDays = readDec(nInput.value);
const stepDays = readDec(stepInput.value, 1);
const mode = modeSelect.value;
if (!(mDays > 0)) throw new Error("请输入有效的 m 天数 (>0)");
if (!(nDays > 0)) throw new Error("请输入有效的 n 天数 (>0)");
if (!(stepDays > 0)) throw new Error("请输入有效的步长 (>0)");
showStatus("步骤 2/3: 拉取所有闪卡并筛选...", 'info');
const all = await tomato_zZmqus5PtYRi.siyuan.getRiffCardsAllFlat();
const today = atStartOfDay(new Date()), mEnd = addDays(today, mDays);
const pool = all
.map(c => ({ c, due: parseRiffDue(c.riffCard?.due) }))
.filter(x => x.due && x.due < mEnd) // 包含已到期 + 未来 m 天
.sort((a,b) => a.due - b.due)
.map(x => x.c);
if (pool.length === 0) {
showStatus(`未来 ${fmtDays(mDays)} 天内及所有已到期卡均为 0,无需分散。`, 'success');
return;
}
showStatus(`步骤 3/3: 找到 ${pool.length} 张卡,开始分散...`, 'info');
const bucketCount = Math.max(1, Math.ceil(nDays / stepDays));
const offsets = Array.from({length: bucketCount}, (_,i)=> Math.min((i+1)*stepDays, nDays));
const buckets = Array.from({length: bucketCount}, () => []);
if (mode === "rand") {
for (const card of pool) {
buckets[Math.floor(Math.random()*bucketCount)].push(card);
}
} else {
for (let i=0;i<pool.length;i++) {
buckets[i % bucketCount].push(pool[i]);
}
}
let total = 0;
for (let k=0;k<bucketCount;k++){
const group = buckets[k];
if (group.length === 0) continue;
const days = offsets[k];
showStatus(`处理第 ${k+1}/${bucketCount} 桶: ${group.length} 张卡推迟 ${fmtDays(days)} 天...`, 'info');
for (const batch of chunk(group, 200)) {
await tomato_zZmqus5PtYRi.cardPriorityBox.stopCards(batch, false, days);
}
total += group.length;
}
showStatus(`完成:共分散推迟 ${total} 张卡(m=${fmtDays(mDays)}, n=${fmtDays(nDays)}, 步长=${fmtDays(stepDays)})`, 'success');
} catch(e) {
console.error(e);
showStatus(`分散推迟出错: ${e.message}`, 'error');
} finally {
setBusy(false);
}
};
// 功能二:释放高优先级卡(使用 stopCards,days = 0)
releaseBtn.onclick = async () => {
setBusy(true);
try {
showStatus("步骤 1/4: 校验输入参数...", 'info');
const mDays = readDec(mInput.value);
const a = readInt(aInput.value);
if (!(mDays > 0)) throw new Error("请先在全局设置中填写 m 天数 (>0)");
if (!Number.isInteger(a)) throw new Error("请输入正确的优先级阈值 a (整数)");
if (a < 50) throw new Error("为避免误操作,优先级阈值 a 需 ≥ 51");
showStatus(`步骤 2/4: 拉取所有闪卡,筛选已到期及未来 ${fmtDays(mDays)} 天内到期的卡...`, 'info');
const all = await tomato_zZmqus5PtYRi.siyuan.getRiffCardsAllFlat();
const today = atStartOfDay(new Date()), mEnd = addDays(today, mDays);
const rows = all
.map(c => ({
card: c,
bid: resolveBlockId(c),
due: parseRiffDue(c.riffCard?.due)
}))
.filter(x => x.bid && x.due && x.due < mEnd);
if (!rows.length) {
showStatus(`已到期及未来 ${fmtDays(mDays)} 天内无到期卡可处理。`, 'success');
return;
}
showStatus(`步骤 3/4: 批量获取 ${rows.length} 张卡片属性并按优先级筛选...`, 'info');
await batchGetAttrs(rows.map(x => x.bid));
const targetCards = [];
for (const r of rows) {
const prInfo = await getPriorityFromSelfOrParents(r.bid, 24);
if (Number.isFinite(prInfo.value) && prInfo.value >= a) {
targetCards.push(r.card);
}
}
if (!targetCards.length) {
showStatus(`指定范围内没有找到优先级 ≥ ${a} 的卡。`, 'success');
return;
}
// 使用番茄工具箱的 stopCards,把高优卡全部“拉回今天”
for (const part of chunk(targetCards, 300)) {
await tomato_zZmqus5PtYRi.cardPriorityBox.stopCards(part, false, 0);
}
showStatus(`完成:已将 ${targetCards.length} 张高优先级卡释放为“可复习”`, 'success');
} catch(e) {
console.error(e);
showStatus(`释放失败: ${e.message}`, 'error');
} finally {
setBusy(false);
}
};
// 功能三:推迟低优先级卡(使用 stopCards,days = t)
lowDelayBtn.onclick = async () => {
setBusy(true);
try {
showStatus("步骤 1/4: 校验输入参数...", 'info');
const mDays = readDec(mInput.value);
const b = readInt(lowBInput.value);
const delayDays = readDec(lowDelayInput.value);
if (!(mDays > 0)) throw new Error("请先在全局设置中填写 m 天数 (>0)");
if (!Number.isInteger(b)) throw new Error("请输入正确的优先级阈值 b (整数)");
if (!(delayDays > 0)) throw new Error("请填写要推迟的天数 t (>0)");
showStatus(`步骤 2/4: 拉取所有闪卡,筛选已到期及未来 ${fmtDays(mDays)} 天内到期的卡...`, 'info');
const all = await tomato_zZmqus5PtYRi.siyuan.getRiffCardsAllFlat();
const today = atStartOfDay(new Date()), mEnd = addDays(today, mDays);
const rows = all
.map(c => ({
card: c,
bid: resolveBlockId(c),
due: parseRiffDue(c.riffCard?.due)
}))
.filter(x => x.bid && x.due && x.due < mEnd);
if (!rows.length) {
showStatus(`已到期及未来 ${fmtDays(mDays)} 天内无到期卡可处理。`, 'success');
return;
}
showStatus(`步骤 3/4: 批量获取 ${rows.length} 张卡片属性并按优先级筛选低优卡...`, 'info');
await batchGetAttrs(rows.map(x => x.bid));
const targetCards = [];
for (const r of rows) {
const prInfo = await getPriorityFromSelfOrParents(r.bid, 24);
// 低优:优先级 < b
if (Number.isFinite(prInfo.value) && prInfo.value < b) {
targetCards.push(r.card);
}
}
if (!targetCards.length) {
showStatus(`指定范围内没有找到优先级 < ${b} 的低优卡。`, 'success');
return;
}
showStatus(`步骤 4/4: 找到 ${targetCards.length} 张低优卡,统一推迟 ${fmtDays(delayDays)} 天...`, 'info');
for (const part of chunk(targetCards, 200)) {
await tomato_zZmqus5PtYRi.cardPriorityBox.stopCards(part, false, delayDays);
}
showStatus(`完成:已将 ${targetCards.length} 张低优先级卡统一推迟 ${fmtDays(delayDays)} 天`, 'success');
} catch(e) {
console.error(e);
showStatus(`推迟低优卡失败: ${e.message}`, 'error');
} finally {
setBusy(false);
}
};
dv.render();
};
return query();


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