feat(auto): 智能搜索
This commit is contained in:
parent
49bb95604b
commit
8af87cd9f4
12
pom.xml
12
pom.xml
@ -11,6 +11,9 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<java.version>1.8</java.version>
|
<java.version>1.8</java.version>
|
||||||
<spring.boot.version>2.7.18</spring.boot.version>
|
<spring.boot.version>2.7.18</spring.boot.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
|
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
|
||||||
<!-- <maven.compiler.release>8</maven.compiler.release>-->
|
<!-- <maven.compiler.release>8</maven.compiler.release>-->
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@ -82,9 +85,12 @@
|
|||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.11.0</version>
|
<version>3.11.0</version>
|
||||||
<!--<configuration>
|
<configuration>
|
||||||
<release>${maven.compiler.release}</release>
|
<encoding>UTF-8</encoding>
|
||||||
</configuration>-->
|
<source>1.8</source>
|
||||||
|
<target>1.8</target>
|
||||||
|
<!--<release>${maven.compiler.release}</release>-->
|
||||||
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import org.springframework.data.domain.Sort;
|
|||||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||||
import org.springframework.data.mongodb.core.index.Index;
|
import org.springframework.data.mongodb.core.index.Index;
|
||||||
import org.springframework.data.mongodb.core.index.IndexOperations;
|
import org.springframework.data.mongodb.core.index.IndexOperations;
|
||||||
|
import org.springframework.data.mongodb.core.index.TextIndexDefinition;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,7 +77,23 @@ public class MongoIndexConfig {
|
|||||||
.named("idx_tags")
|
.named("idx_tags")
|
||||||
.background());
|
.background());
|
||||||
|
|
||||||
log.info("Novel索引已确保: idx_name, idx_author, idx_tags");
|
// 全文搜索索引 - 支持智能搜索(可选,如果MongoDB版本支持)
|
||||||
|
// 注意:MongoDB全文搜索需要特定版本支持,如果失败不影响基本搜索功能
|
||||||
|
try {
|
||||||
|
TextIndexDefinition.TextIndexDefinitionBuilder builder =
|
||||||
|
new TextIndexDefinition.TextIndexDefinitionBuilder();
|
||||||
|
builder.onField("name", 10.0f); // 书名权重最高
|
||||||
|
builder.onField("author", 5.0f); // 作者权重中等
|
||||||
|
builder.onField("synopsis", 1.0f); // 简介权重最低
|
||||||
|
builder.onField("tags", 3.0f); // 标签权重
|
||||||
|
TextIndexDefinition textIndex = builder.build();
|
||||||
|
indexOps.ensureIndex(textIndex);
|
||||||
|
log.info("全文搜索索引已创建");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("创建全文搜索索引失败(可能MongoDB版本不支持),将使用普通搜索: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Novel索引已确保: idx_name, idx_author, idx_tags, text_index");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createHistoryIndexes() {
|
private void createHistoryIndexes() {
|
||||||
|
|||||||
@ -41,6 +41,14 @@ public class NovelController {
|
|||||||
.orElse(ResponseEntity.notFound().build());
|
.orElse(ResponseEntity.notFound().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/suggestions")
|
||||||
|
@Operation(summary = "获取搜索建议", description = "根据输入的关键词返回搜索建议")
|
||||||
|
public ResponseEntity<List<NovelService.SearchSuggestion>> getSearchSuggestions(
|
||||||
|
@RequestParam String keyword,
|
||||||
|
@RequestParam(defaultValue = "10") int limit) {
|
||||||
|
return ResponseEntity.ok(novelService.getSearchSuggestions(keyword, limit));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/chapters")
|
@GetMapping("/{id}/chapters")
|
||||||
@Operation(summary = "获取章节列表")
|
@Operation(summary = "获取章节列表")
|
||||||
public ResponseEntity<Page<ChapterHead>> getChapters(
|
public ResponseEntity<Page<ChapterHead>> getChapters(
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import com.novelreader.repository.ChapterRepository;
|
|||||||
import com.novelreader.repository.NovelRepository;
|
import com.novelreader.repository.NovelRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageImpl;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||||
@ -15,9 +16,7 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation;
|
|||||||
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
|
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ -28,16 +27,18 @@ public class NovelService {
|
|||||||
private final ChapterRepository chapterRepository;
|
private final ChapterRepository chapterRepository;
|
||||||
private final MongoTemplate mongoTemplate;
|
private final MongoTemplate mongoTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能搜索小说 - 支持相关性排序和多种匹配策略
|
||||||
|
*/
|
||||||
public Page<NovelBrief> searchNovels(String keyword, String tag, int page, int size) {
|
public Page<NovelBrief> searchNovels(String keyword, String tag, int page, int size) {
|
||||||
Pageable pageable = PageRequest.of(page, size);
|
Pageable pageable = PageRequest.of(page, size);
|
||||||
Page<NovelDO> novels;
|
Page<NovelDO> novels;
|
||||||
|
|
||||||
List<String> tags = tag != null && !tag.isEmpty() ? Collections.singletonList(tag) : null;
|
List<String> tags = tag != null && !tag.isEmpty() ? Collections.singletonList(tag) : null;
|
||||||
|
|
||||||
if (keyword != null && !keyword.isEmpty() && tags != null) {
|
if (keyword != null && !keyword.isEmpty()) {
|
||||||
novels = novelRepository.findByNameOrAuthorAndTags(keyword, tags, pageable);
|
// 智能搜索:优先精确匹配,然后模糊匹配
|
||||||
} else if (keyword != null && !keyword.isEmpty()) {
|
novels = intelligentSearch(keyword, tags, pageable);
|
||||||
novels = novelRepository.findByNameOrAuthorContainingIgnoreCase(keyword, pageable);
|
|
||||||
} else if (tags != null) {
|
} else if (tags != null) {
|
||||||
novels = novelRepository.findByTagsIn(tags, pageable);
|
novels = novelRepository.findByTagsIn(tags, pageable);
|
||||||
} else {
|
} else {
|
||||||
@ -47,6 +48,122 @@ public class NovelService {
|
|||||||
return novels.map(this::toBrief);
|
return novels.map(this::toBrief);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能搜索实现 - 多策略搜索并合并结果
|
||||||
|
*/
|
||||||
|
private Page<NovelDO> intelligentSearch(String keyword, List<String> tags, Pageable pageable) {
|
||||||
|
keyword = keyword.trim();
|
||||||
|
|
||||||
|
// 策略1: 书名精确匹配(最高优先级)
|
||||||
|
List<NovelDO> exactNameMatches = novelRepository.findByNameStartingWithIgnoreCase(keyword);
|
||||||
|
|
||||||
|
// 策略2: 书名包含匹配
|
||||||
|
List<NovelDO> nameContainsMatches = novelRepository.findByNameContainingIgnoreCase(keyword);
|
||||||
|
|
||||||
|
// 策略3: 作者精确匹配
|
||||||
|
List<NovelDO> exactAuthorMatches = novelRepository.findByAuthor(keyword);
|
||||||
|
|
||||||
|
// 策略4: 书名或作者模糊匹配(原有逻辑)
|
||||||
|
Page<NovelDO> fuzzyMatches = novelRepository.findByNameOrAuthorContainingIgnoreCase(keyword, pageable);
|
||||||
|
|
||||||
|
// 合并结果,去重,按优先级排序
|
||||||
|
Set<String> seenIds = new HashSet<>();
|
||||||
|
List<NovelDO> mergedResults = new ArrayList<>();
|
||||||
|
|
||||||
|
// 1. 书名精确匹配(权重最高)
|
||||||
|
for (NovelDO novel : exactNameMatches) {
|
||||||
|
if (!seenIds.contains(novel.getId()) && (tags == null || novel.getTags() != null && new HashSet<>(novel.getTags()).containsAll(tags))) {
|
||||||
|
mergedResults.add(novel);
|
||||||
|
seenIds.add(novel.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 书名包含匹配
|
||||||
|
for (NovelDO novel : nameContainsMatches) {
|
||||||
|
if (!seenIds.contains(novel.getId()) && (tags == null || novel.getTags() != null && new HashSet<>(novel.getTags()).containsAll(tags))) {
|
||||||
|
mergedResults.add(novel);
|
||||||
|
seenIds.add(novel.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 作者精确匹配
|
||||||
|
for (NovelDO novel : exactAuthorMatches) {
|
||||||
|
if (!seenIds.contains(novel.getId()) && (tags == null || novel.getTags() != null && new HashSet<>(novel.getTags()).containsAll(tags))) {
|
||||||
|
mergedResults.add(novel);
|
||||||
|
seenIds.add(novel.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 模糊匹配结果(如果还有空间)
|
||||||
|
int remaining = pageable.getPageSize() - mergedResults.size();
|
||||||
|
if (remaining > 0 && fuzzyMatches.hasContent()) {
|
||||||
|
for (NovelDO novel : fuzzyMatches.getContent()) {
|
||||||
|
if (!seenIds.contains(novel.getId()) && (tags == null || novel.getTags() != null && new HashSet<>(novel.getTags()).containsAll(tags))) {
|
||||||
|
mergedResults.add(novel);
|
||||||
|
seenIds.add(novel.getId());
|
||||||
|
if (--remaining <= 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为分页结果
|
||||||
|
int total = mergedResults.size();
|
||||||
|
int start = (int) pageable.getOffset();
|
||||||
|
int end = Math.min(start + pageable.getPageSize(), total);
|
||||||
|
List<NovelDO> pageContent = start < total ? mergedResults.subList(start, end) : Collections.emptyList();
|
||||||
|
|
||||||
|
return new PageImpl<>(pageContent, pageable, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索建议 - 返回匹配的书名和作者
|
||||||
|
*/
|
||||||
|
public List<SearchSuggestion> getSearchSuggestions(String keyword, int limit) {
|
||||||
|
if (keyword == null || keyword.trim().isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
keyword = keyword.trim();
|
||||||
|
List<SearchSuggestion> suggestions = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
// 书名建议(前缀匹配)
|
||||||
|
List<NovelDO> nameMatches = novelRepository.findByNameStartingWithIgnoreCase(keyword);
|
||||||
|
for (NovelDO novel : nameMatches) {
|
||||||
|
if (suggestions.size() >= limit) break;
|
||||||
|
SearchSuggestion suggestion = new SearchSuggestion();
|
||||||
|
suggestion.setType("书名");
|
||||||
|
suggestion.setText(novel.getName());
|
||||||
|
suggestion.setNovelId(novel.getId());
|
||||||
|
suggestions.add(suggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 作者建议(精确匹配)
|
||||||
|
if (suggestions.size() < limit) {
|
||||||
|
List<NovelDO> authorMatches = novelRepository.findByAuthor(keyword);
|
||||||
|
java.util.Set<String> seenAuthors = new java.util.HashSet<>();
|
||||||
|
for (NovelDO novel : authorMatches) {
|
||||||
|
if (suggestions.size() >= limit) break;
|
||||||
|
if (!seenAuthors.contains(novel.getAuthor())) {
|
||||||
|
SearchSuggestion suggestion = new SearchSuggestion();
|
||||||
|
suggestion.setType("作者");
|
||||||
|
suggestion.setText(novel.getAuthor());
|
||||||
|
suggestions.add(suggestion);
|
||||||
|
seenAuthors.add(novel.getAuthor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索建议DTO
|
||||||
|
@lombok.Data
|
||||||
|
public static class SearchSuggestion {
|
||||||
|
private String type; // "书名" 或 "作者"
|
||||||
|
private String text; // 建议文本
|
||||||
|
private String novelId; // 如果是书名,提供novelId
|
||||||
|
}
|
||||||
|
|
||||||
public Optional<NovelDetail> getNovelDetail(String id) {
|
public Optional<NovelDetail> getNovelDetail(String id) {
|
||||||
return novelRepository.findById(id).map(novel -> {
|
return novelRepository.findById(id).map(novel -> {
|
||||||
NovelDetail detail = toDetail(novel);
|
NovelDetail detail = toDetail(novel);
|
||||||
|
|||||||
@ -379,7 +379,69 @@
|
|||||||
padding: var(--spacing-md);
|
padding: var(--spacing-md);
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
max-height: 40vh;
|
max-height: 40vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索建议 */
|
||||||
|
.search-suggestions {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--header-height) + 1px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
max-height: 60vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions-list {
|
||||||
|
padding: var(--spacing-sm) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-item:active {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-type {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-text {
|
||||||
|
flex: 1;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-text mark {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
padding: 0 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索结果高亮 */
|
||||||
|
.novel-title mark,
|
||||||
|
.novel-author mark {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
padding: 0 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-section {
|
.search-section {
|
||||||
|
|||||||
@ -65,6 +65,15 @@ const API = (function() {
|
|||||||
return get('/novels', params);
|
return get('/novels', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取搜索建议
|
||||||
|
* @param {string} keyword
|
||||||
|
* @param {number} limit
|
||||||
|
*/
|
||||||
|
function getSearchSuggestions(keyword, limit = 10) {
|
||||||
|
return get('/novels/suggestions', { keyword, limit });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取小说详情
|
* 获取小说详情
|
||||||
* @param {string} novelId
|
* @param {string} novelId
|
||||||
@ -255,6 +264,7 @@ const API = (function() {
|
|||||||
return {
|
return {
|
||||||
// 小说
|
// 小说
|
||||||
searchNovels,
|
searchNovels,
|
||||||
|
getSearchSuggestions,
|
||||||
getNovelDetail,
|
getNovelDetail,
|
||||||
getChapters,
|
getChapters,
|
||||||
getPopularTags,
|
getPopularTags,
|
||||||
|
|||||||
@ -162,17 +162,37 @@ const App = (function() {
|
|||||||
// 搜索面板
|
// 搜索面板
|
||||||
$('#search-back').on('click', hideSearch);
|
$('#search-back').on('click', hideSearch);
|
||||||
|
|
||||||
// 搜索输入 - 实时搜索
|
// 搜索输入 - 实时搜索建议
|
||||||
$('#search-input').on('input', debounce(function() {
|
let searchSuggestionTimer = null;
|
||||||
|
$('#search-input').on('input', function() {
|
||||||
const keyword = $(this).val().trim();
|
const keyword = $(this).val().trim();
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
$('#search-clear').removeClass('hidden');
|
$('#search-clear').removeClass('hidden');
|
||||||
doSearch(keyword);
|
// 显示搜索建议
|
||||||
|
clearTimeout(searchSuggestionTimer);
|
||||||
|
searchSuggestionTimer = setTimeout(() => {
|
||||||
|
loadSearchSuggestions(keyword);
|
||||||
|
}, 300);
|
||||||
} else {
|
} else {
|
||||||
$('#search-clear').addClass('hidden');
|
$('#search-clear').addClass('hidden');
|
||||||
$('#search-results').html('<div class="search-hint">输入关键词搜索小说</div>');
|
$('#search-results').html('<div class="search-hint">输入关键词搜索小说</div>');
|
||||||
|
$('#search-suggestions').html('').addClass('hidden');
|
||||||
}
|
}
|
||||||
}, 500));
|
});
|
||||||
|
|
||||||
|
// 搜索输入 - 回车确认搜索
|
||||||
|
$('#search-input').on('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
const keyword = $(this).val().trim();
|
||||||
|
if (keyword) {
|
||||||
|
$('#search-suggestions').html('').addClass('hidden');
|
||||||
|
doSearch(keyword);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
$('#search-suggestions').html('').addClass('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 搜索输入 - 回车确认搜索
|
// 搜索输入 - 回车确认搜索
|
||||||
$('#search-input').on('keypress', function(e) {
|
$('#search-input').on('keypress', function(e) {
|
||||||
@ -413,11 +433,57 @@ const App = (function() {
|
|||||||
$('#search-panel').removeClass('hidden');
|
$('#search-panel').removeClass('hidden');
|
||||||
$('#search-input').val('').focus();
|
$('#search-input').val('').focus();
|
||||||
$('#search-results').html('<div class="search-hint">输入关键词搜索小说</div>');
|
$('#search-results').html('<div class="search-hint">输入关键词搜索小说</div>');
|
||||||
|
$('#search-suggestions').html('').addClass('hidden');
|
||||||
|
|
||||||
// 渲染搜索历史和热门标签
|
// 渲染搜索历史和热门标签
|
||||||
renderSearchTags();
|
renderSearchTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载搜索建议
|
||||||
|
async function loadSearchSuggestions(keyword) {
|
||||||
|
if (!keyword || keyword.length < 1) {
|
||||||
|
$('#search-suggestions').html('').addClass('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const suggestions = await API.getSearchSuggestions(keyword, 8);
|
||||||
|
if (suggestions.length === 0) {
|
||||||
|
$('#search-suggestions').html('').addClass('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div class="suggestions-list">';
|
||||||
|
suggestions.forEach(suggestion => {
|
||||||
|
const highlightText = highlightKeyword(suggestion.text, keyword);
|
||||||
|
if (suggestion.novelId) {
|
||||||
|
html += `<div class="suggestion-item" data-novel-id="${suggestion.novelId}">
|
||||||
|
<span class="suggestion-type">${suggestion.type}</span>
|
||||||
|
<span class="suggestion-text">${highlightText}</span>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
html += `<div class="suggestion-item" data-author="${suggestion.text}">
|
||||||
|
<span class="suggestion-type">${suggestion.type}</span>
|
||||||
|
<span class="suggestion-text">${highlightText}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
$('#search-suggestions').html(html).removeClass('hidden');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载搜索建议失败:', error);
|
||||||
|
$('#search-suggestions').html('').addClass('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 高亮关键词
|
||||||
|
function highlightKeyword(text, keyword) {
|
||||||
|
if (!text || !keyword) return text;
|
||||||
|
const regex = new RegExp(`(${keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||||||
|
return text.replace(regex, '<mark>$1</mark>');
|
||||||
|
}
|
||||||
|
|
||||||
async function renderSearchTags() {
|
async function renderSearchTags() {
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
|
|||||||
@ -687,6 +687,9 @@ const Pages = (function() {
|
|||||||
const $results = $('#search-results');
|
const $results = $('#search-results');
|
||||||
$results.html('<div class="chapter-loading"><div class="loading-spinner"></div><span>搜索中...</span></div>');
|
$results.html('<div class="chapter-loading"><div class="loading-spinner"></div><span>搜索中...</span></div>');
|
||||||
|
|
||||||
|
// 保存当前搜索关键词用于高亮
|
||||||
|
currentSearchKeyword = keyword;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await API.searchNovels({ keyword, page: 0, size: 30 });
|
const data = await API.searchNovels({ keyword, page: 0, size: 30 });
|
||||||
const novels = data.content || [];
|
const novels = data.content || [];
|
||||||
@ -699,7 +702,7 @@ const Pages = (function() {
|
|||||||
let html = '<div class="novel-list">';
|
let html = '<div class="novel-list">';
|
||||||
novels.forEach(novel => {
|
novels.forEach(novel => {
|
||||||
const isInShelf = Store.isInShelfCache(novel.id);
|
const isInShelf = Store.isInShelfCache(novel.id);
|
||||||
html += renderNovelCard(novel, isInShelf);
|
html += renderNovelCard(novel, isInShelf, keyword);
|
||||||
});
|
});
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
|
|||||||
@ -275,6 +275,9 @@
|
|||||||
<div id="search-tags" class="search-tags">
|
<div id="search-tags" class="search-tags">
|
||||||
<!-- 热门标签 -->
|
<!-- 热门标签 -->
|
||||||
</div>
|
</div>
|
||||||
|
<div id="search-suggestions" class="search-suggestions hidden">
|
||||||
|
<!-- 搜索建议 -->
|
||||||
|
</div>
|
||||||
<div id="search-results" class="search-results">
|
<div id="search-results" class="search-results">
|
||||||
<!-- 搜索结果 -->
|
<!-- 搜索结果 -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user