文件
mynode/packages/app-desktop/gui/NoteEditor/utils/searchNotesForMention.ts
chen 301194963b
一些检查失败了
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
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

191 行
7.2 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

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;