Обновлена логика фильтрации заметок по тегам для регистронезависимого поиска. Фильтрация по тегам теперь выполняется на клиенте, чтобы избежать проблем с кириллицей в SQLite. Также добавлено сохранение тегов в нижнем регистре для единообразия и улучшена обработка уникальных тегов в компоненте TagsFilter.

This commit is contained in:
Fovway 2025-11-02 23:35:22 +07:00
parent feca528d1b
commit 143338bc2b
6 changed files with 45 additions and 14 deletions

View File

@ -803,11 +803,13 @@ app.get("/api/notes/search", requireApiAuth, (req, res) => {
params.push(`%${q.trim()}%`); params.push(`%${q.trim()}%`);
} }
// Поиск по тегу // Поиск по тегу (регистронезависимый)
if (tag && tag.trim()) { // SQLite LOWER() плохо работает с кириллицей, поэтому фильтрация по тегу выполняется на клиенте
whereClause += " AND n.content LIKE ?"; // Здесь не фильтруем по тегу, чтобы получить все заметки для последующей фильтрации на клиенте
params.push(`%#${tag.trim()}%`); // Если есть другие фильтры (дата, поиск), они применяются здесь
} // if (tag && tag.trim()) {
// // Фильтрация по тегу выполняется на клиенте
// }
// Поиск по дате (используем created_at вместо date) // Поиск по дате (используем created_at вместо date)
if (date && date.trim()) { if (date && date.trim()) {

View File

@ -85,7 +85,7 @@ define(['./workbox-8cfb3eb5'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "index.html", "url": "index.html",
"revision": "0.h9luj2l3mv8" "revision": "0.tihabijh3s"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View File

@ -978,7 +978,8 @@ export const NoteItem: React.FC<NoteItemProps> = ({
const handleTagClick = (e: React.MouseEvent, tag: string) => { const handleTagClick = (e: React.MouseEvent, tag: string) => {
e.stopPropagation(); e.stopPropagation();
dispatch(setSelectedTag(tag)); // Всегда сохраняем тег в нижнем регистре для единообразия
dispatch(setSelectedTag(tag.toLowerCase()));
}; };
const toggleExpand = () => { const toggleExpand = () => {

View File

@ -4,6 +4,7 @@ import { useAppSelector, useAppDispatch } from "../../store/hooks";
import { notesApi } from "../../api/notesApi"; import { notesApi } from "../../api/notesApi";
import { setNotes, setAllNotes } from "../../store/slices/notesSlice"; import { setNotes, setAllNotes } from "../../store/slices/notesSlice";
import { useNotification } from "../../hooks/useNotification"; import { useNotification } from "../../hooks/useNotification";
import { extractTags } from "../../utils/markdown";
export interface NotesListRef { export interface NotesListRef {
reloadNotes: () => void; reloadNotes: () => void;
@ -41,6 +42,16 @@ export const NotesList = forwardRef<NotesListRef>((props, ref) => {
if (userId) { if (userId) {
notesData = notesData.filter((note) => note.user_id === userId); notesData = notesData.filter((note) => note.user_id === userId);
} }
// Точная фильтрация по тегу на клиенте (регистронезависимо)
// SQLite LOWER() плохо работает с кириллицей, поэтому фильтруем здесь
if (selectedTag) {
const selectedTagLower = selectedTag.toLowerCase();
notesData = notesData.filter((note) => {
const noteTags = extractTags(note.content);
return noteTags.some(tag => tag.toLowerCase() === selectedTagLower);
});
}
} else { } else {
// Если нет фильтров, используем все заметки // Если нет фильтров, используем все заметки
notesData = filteredAllData; notesData = filteredAllData;

View File

@ -14,17 +14,30 @@ export const TagsFilter: React.FC<TagsFilterProps> = ({ notes = [] }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
// Получаем все уникальные теги из заметок // Получаем все уникальные теги из заметок
// Группируем регистронезависимо, но сохраняем оригинальный регистр первого вхождения
const getAllTags = () => { const getAllTags = () => {
const tagCounts: Record<string, number> = {}; const tagCounts: Record<string, number> = {};
const tagVariants: Record<string, string> = {}; // Хранит оригинальный регистр для каждого тега
notes.forEach((note) => { notes.forEach((note) => {
const tags = extractTags(note.content); const tags = extractTags(note.content);
tags.forEach((tag) => { tags.forEach((tag) => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1; const tagKey = tag.toLowerCase();
tagCounts[tagKey] = (tagCounts[tagKey] || 0) + 1;
// Сохраняем первый вариант с оригинальным регистром
if (!tagVariants[tagKey]) {
tagVariants[tagKey] = tag;
}
}); });
}); });
return tagCounts; // Преобразуем обратно в Record с оригинальными тегами
const result: Record<string, number> = {};
Object.keys(tagCounts).forEach((tagKey) => {
result[tagVariants[tagKey]] = tagCounts[tagKey];
});
return result;
}; };
const tagCounts = getAllTags(); const tagCounts = getAllTags();
@ -32,10 +45,12 @@ export const TagsFilter: React.FC<TagsFilterProps> = ({ notes = [] }) => {
const handleTagClick = (tag: string, event: React.MouseEvent<HTMLSpanElement>) => { const handleTagClick = (tag: string, event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault(); event.preventDefault();
if (selectedTag === tag) { // Всегда сохраняем тег в нижнем регистре для единообразия
const tagLower = tag.toLowerCase();
if (selectedTag?.toLowerCase() === tagLower) {
dispatch(setSelectedTag(null)); dispatch(setSelectedTag(null));
} else { } else {
dispatch(setSelectedTag(tag)); dispatch(setSelectedTag(tagLower));
} }
// Сбрасываем фокус после клика для предотвращения сохранения состояния :active // Сбрасываем фокус после клика для предотвращения сохранения состояния :active
(event.currentTarget as HTMLElement).blur(); (event.currentTarget as HTMLElement).blur();
@ -68,7 +83,7 @@ export const TagsFilter: React.FC<TagsFilterProps> = ({ notes = [] }) => {
<div className="tags-container"> <div className="tags-container">
{sortedTags.map((tag) => { {sortedTags.map((tag) => {
const count = tagCounts[tag]; const count = tagCounts[tag];
const isActive = selectedTag === tag; const isActive = selectedTag?.toLowerCase() === tag.toLowerCase();
return ( return (
<span <span
@ -90,3 +105,4 @@ export const TagsFilter: React.FC<TagsFilterProps> = ({ notes = [] }) => {
</div> </div>
); );
}; };

View File

@ -109,8 +109,9 @@ export const extractTags = (content: string): string[] => {
} }
} }
const tag = match[1].toLowerCase(); const tag = match[1];
if (!tags.includes(tag)) { // Проверяем, есть ли уже тег с таким же именем (регистронезависимо)
if (!tags.some(t => t.toLowerCase() === tag.toLowerCase())) {
tags.push(tag); tags.push(tag);
} }
} }