一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
191 行
7.2 KiB
TypeScript
191 行
7.2 KiB
TypeScript
import Note from '@joplin/lib/models/Note';
|
||
import Folder from '@joplin/lib/models/Folder';
|
||
import time from '@joplin/lib/time';
|
||
|
||
export interface MentionNoteItem {
|
||
id: string;
|
||
title: string;
|
||
parentTitle?: string;
|
||
section?: string;
|
||
}
|
||
|
||
/**
|
||
* 纯前端 GB2312 汉字首字母转换,支持拼音首字母模糊检索中文笔记(例如 gzzb -> 工作周报)
|
||
*/
|
||
export const getChinesePinyinInitials = (str: string): string => {
|
||
const boundaries = [
|
||
{ p: 'a', c: '啊' }, { p: 'b', c: '芭' }, { p: 'c', c: '擦' }, { p: 'd', c: '搭' },
|
||
{ p: 'e', c: '蛾' }, { p: 'f', c: '发' }, { p: 'g', c: '噶' }, { p: 'h', c: '哈' },
|
||
{ p: 'j', c: '击' }, { p: 'k', c: '喀' }, { p: 'l', c: '垃' }, { p: 'm', c: '妈' },
|
||
{ p: 'n', c: '拿' }, { p: 'o', c: '哦' }, { p: 'p', c: '啪' }, { p: 'q', c: '期' },
|
||
{ p: 'r', c: '然' }, { p: 's', c: '撒' }, { p: 't', c: '塌' }, { p: 'w', c: '挖' },
|
||
{ p: 'x', c: '昔' }, { p: 'y', c: '压' }, { p: 'z', c: '匝' },
|
||
];
|
||
return str.split('').map(ch => {
|
||
if (/[a-zA-Z0-9]/.test(ch)) return ch.toLowerCase();
|
||
if (!/[\u4e00-\u9fa5]/.test(ch)) return '';
|
||
for (let i = boundaries.length - 1; i >= 0; i--) {
|
||
if (ch.localeCompare(boundaries[i].c, 'zh-Hans-CN') >= 0) {
|
||
return boundaries[i].p;
|
||
}
|
||
}
|
||
return '';
|
||
}).join('');
|
||
};
|
||
|
||
/**
|
||
* 中国地区标准日期星期格式化(统一 星期一 ~ 星期日)
|
||
*/
|
||
export const formatChineseDateTitle = (ms: number): string => {
|
||
const weekdaysZh = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||
const datePrefix = time.formatMsToLocal(ms, 'YYYY-MM-DD');
|
||
const d = new Date(ms);
|
||
return `${datePrefix} ${weekdaysZh[d.getDay()]}`;
|
||
};
|
||
|
||
/**
|
||
* 双链与 @ 快速提及的笔记查询与推荐算法
|
||
* 同时服务于 Markdown 编辑器(CodeMirror 6)与富文本编辑器(TinyMCE)
|
||
*/
|
||
export const searchNotesForMention = async (query: string): Promise<MentionNoteItem[]> => {
|
||
const trimmed = query.trim();
|
||
const lowerQuery = trimmed.toLowerCase();
|
||
|
||
const isDateSearch = (keywords: string[]) => {
|
||
return keywords.some(k => k === lowerQuery || k.includes(lowerQuery) || lowerQuery.includes(k));
|
||
};
|
||
|
||
const matchesToday = !trimmed || isDateSearch(['今天', '今日', 'jt', 'jr', 'today', '日志', '日记', 'rz', 'rj']);
|
||
const matchesYesterday = !trimmed || isDateSearch(['昨天', '昨日', 'zt', 'zr', 'yesterday']);
|
||
const matchesTomorrow = !trimmed || isDateSearch(['明天', '明日', 'mt', 'mr', 'tomorrow']);
|
||
const matchesDayBefore = isDateSearch(['前天', '前日', 'qt']);
|
||
const matchesDayAfter = isDateSearch(['后天', '后日', 'ht']);
|
||
|
||
const now = Date.now();
|
||
const oneDayMs = 24 * 60 * 60 * 1000;
|
||
|
||
const targetDates: Array<{ offsetDays: number; label: string; createLabel: string }> = [];
|
||
if (matchesToday) targetDates.push({ offsetDays: 0, label: '📅 今日日志', createLabel: '✨ 新建今日日志' });
|
||
if (matchesYesterday) targetDates.push({ offsetDays: -1, label: '📅 昨日日志', createLabel: '✨ 新建昨日日志' });
|
||
if (matchesTomorrow) targetDates.push({ offsetDays: 1, label: '📅 明日日志', createLabel: '✨ 新建明日日志' });
|
||
if (matchesDayBefore) targetDates.push({ offsetDays: -2, label: '📅 前天日志', createLabel: '✨ 新建前天日志' });
|
||
if (matchesDayAfter) targetDates.push({ offsetDays: 2, label: '📅 后天日志', createLabel: '✨ 新建后天日志' });
|
||
|
||
const specialItems: Array<MentionNoteItem> = [];
|
||
for (const item of targetDates) {
|
||
const targetMs = now + item.offsetDays * oneDayMs;
|
||
const prefix = time.formatMsToLocal(targetMs, 'YYYY-MM-DD');
|
||
const dateTitle = formatChineseDateTitle(targetMs);
|
||
|
||
const row = await Note.db().selectOne(
|
||
'SELECT id, title FROM notes WHERE is_conflict = 0 AND (deleted_time = 0 OR deleted_time IS NULL) AND title LIKE ? ORDER BY updated_time DESC LIMIT 1',
|
||
[`${prefix}%`],
|
||
);
|
||
if (row && typeof row.id === 'string' && typeof row.title === 'string') {
|
||
specialItems.push({
|
||
id: row.id,
|
||
title: row.title,
|
||
parentTitle: item.label,
|
||
section: '📅 日志推荐',
|
||
});
|
||
} else {
|
||
specialItems.push({
|
||
id: `__CREATE_${prefix}__`,
|
||
title: dateTitle,
|
||
parentTitle: item.createLabel,
|
||
section: '📅 日志推荐',
|
||
});
|
||
}
|
||
}
|
||
|
||
// 识别直接输入的指定日期数字(如 2026-11-05、2026/11/5、20261105)
|
||
const dateRegex = /^(\d{4})[-/.]?(\d{1,2})[-/.]?(\d{1,2})$/;
|
||
const directDateMatch = trimmed.match(dateRegex);
|
||
if (directDateMatch) {
|
||
const year = parseInt(directDateMatch[1], 10);
|
||
const month = parseInt(directDateMatch[2], 10);
|
||
const day = parseInt(directDateMatch[3], 10);
|
||
if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
|
||
const d = new Date(year, month - 1, day);
|
||
if (!isNaN(d.getTime())) {
|
||
const prefix = time.formatMsToLocal(d.getTime(), 'YYYY-MM-DD');
|
||
const dateTitle = formatChineseDateTitle(d.getTime());
|
||
if (!specialItems.some(s => s.id.includes(prefix))) {
|
||
const row = await Note.db().selectOne(
|
||
'SELECT id, title FROM notes WHERE is_conflict = 0 AND (deleted_time = 0 OR deleted_time IS NULL) AND title LIKE ? ORDER BY updated_time DESC LIMIT 1',
|
||
[`${prefix}%`],
|
||
);
|
||
if (row && typeof row.id === 'string' && typeof row.title === 'string') {
|
||
specialItems.push({
|
||
id: row.id,
|
||
title: row.title,
|
||
parentTitle: `📅 ${prefix} 日志`,
|
||
section: '📅 日志推荐',
|
||
});
|
||
} else {
|
||
specialItems.push({
|
||
id: `__CREATE_${prefix}__`,
|
||
title: dateTitle,
|
||
parentTitle: `✨ 新建 ${prefix} 日志`,
|
||
section: '📅 日志推荐',
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 查询笔记标题匹配
|
||
const likeQuery = `%${trimmed}%`;
|
||
const rows = await Note.db().selectAll<{ id: string; title: string; parent_id: string }>(
|
||
`SELECT id, title, parent_id FROM notes
|
||
WHERE is_conflict = 0 AND (deleted_time = 0 OR deleted_time IS NULL)
|
||
AND title LIKE ?
|
||
ORDER BY updated_time DESC LIMIT 15`,
|
||
[likeQuery],
|
||
);
|
||
|
||
const folders = await Folder.all();
|
||
const folderMap = new Map(folders.map(f => [f.id, f.title]));
|
||
|
||
const existingSpecialIds = new Set(specialItems.map(s => s.id));
|
||
const noteResults: Array<MentionNoteItem> = [];
|
||
|
||
for (const r of rows) {
|
||
if (!existingSpecialIds.has(r.id)) {
|
||
noteResults.push({
|
||
id: r.id,
|
||
title: r.title,
|
||
parentTitle: folderMap.get(r.parent_id),
|
||
section: '📝 关联笔记',
|
||
});
|
||
}
|
||
}
|
||
|
||
// 若输入为英文字母且普通模糊搜索无果或结果较少,尝试拼音首字母反查
|
||
if (trimmed && /^[a-zA-Z]+$/.test(trimmed) && noteResults.length < 5) {
|
||
const recentRows = await Note.db().selectAll<{ id: string; title: string; parent_id: string }>(
|
||
`SELECT id, title, parent_id FROM notes
|
||
WHERE is_conflict = 0 AND (deleted_time = 0 OR deleted_time IS NULL)
|
||
ORDER BY updated_time DESC LIMIT 60`,
|
||
);
|
||
for (const r of recentRows) {
|
||
if (!existingSpecialIds.has(r.id) && !noteResults.some(n => n.id === r.id)) {
|
||
const initials = getChinesePinyinInitials(r.title);
|
||
if (initials.includes(lowerQuery)) {
|
||
noteResults.push({
|
||
id: r.id,
|
||
title: r.title,
|
||
parentTitle: folderMap.get(r.parent_id),
|
||
section: '📝 关联笔记',
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return [...specialItems, ...noteResults];
|
||
};
|
||
|
||
export default searchNotesForMention;
|