DEV-009: M20环节5+6-排序+分页+模糊搜索+拼音支持+自动补全+防抖+缓存+统计

This commit is contained in:
米米米叮 2026-03-14 03:42:56 +08:00
parent d5942ba6a9
commit 2e5603801a
6 changed files with 1328 additions and 126 deletions

View File

@ -404,3 +404,255 @@ h1 {
background: #1a2639;
color: white;
}
/* ========== 新增样式:排序、分页、自动补全、统计、无结果 ========== */
/* 排序区域 */
.sort-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding: 15px 0;
border-bottom: 1px solid #eef2f6;
}
.sort-buttons {
display: flex;
gap: 12px;
align-items: center;
}
.sort-label {
color: #6c757d;
font-size: 0.95rem;
}
.sort-btn {
padding: 8px 18px;
background: white;
border: 1.5px solid #e0e4e9;
border-radius: 40px;
color: #495057;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.2s;
}
.sort-btn:hover {
border-color: #ffb347;
background: #fff9f0;
}
.sort-btn.active {
background: #1a2639;
border-color: #1a2639;
color: white;
}
.sort-order {
display: flex;
align-items: center;
gap: 6px;
color: #495057;
}
/* 自动补全 */
.autocomplete-panel {
position: absolute;
top: 70px;
left: 0;
right: 0;
background: white;
border-radius: 24px;
box-shadow: 0 12px 30px rgba(0,0,0,0.15);
padding: 10px 0;
z-index: 200;
border: 1px solid #eef2f6;
max-height: 300px;
overflow-y: auto;
}
.autocomplete-panel.hidden {
display: none;
}
.autocomplete-list {
list-style: none;
}
.autocomplete-item {
padding: 12px 20px;
cursor: pointer;
transition: background 0.2s;
color: #1a2639;
}
.autocomplete-item:hover {
background: #f8fafd;
}
/* 统计区域 */
.stats-section {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
padding: 10px 0;
color: #6c757d;
font-size: 0.95rem;
border-bottom: 1px dashed #e0e4e9;
}
/* 无结果提示 */
.no-results-panel {
text-align: center;
padding: 40px 20px;
background: #f8fafd;
border-radius: 32px;
margin: 20px 0;
}
.no-results-panel.hidden {
display: none;
}
.no-results-panel p {
color: #6c757d;
margin-bottom: 20px;
font-size: 1.1rem;
}
.suggestions-panel {
margin-top: 20px;
}
.suggestions-panel p {
font-size: 0.95rem;
margin-bottom: 15px;
}
.suggestion-tags {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
}
.suggestion-tag {
padding: 8px 18px;
background: white;
border: 1.5px solid #ffd166;
border-radius: 40px;
color: #b85e00;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.2s;
}
.suggestion-tag:hover {
background: #ffd166;
color: #1a2639;
}
/* 模糊匹配标记 */
.fuzzy-badge {
background: #e3f2fd;
color: #1976d2;
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 40px;
margin-left: 8px;
}
/* 分页区域 */
.pagination-section {
margin-top: 40px;
padding-top: 25px;
border-top: 2px solid #eef2f6;
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: center;
justify-content: space-between;
}
.pagination-controls {
display: flex;
gap: 8px;
align-items: center;
}
.pagination-controls button {
padding: 8px 16px;
background: white;
border: 1.5px solid #e0e4e9;
border-radius: 40px;
color: #495057;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
min-width: 45px;
}
.pagination-controls button:hover:not(:disabled) {
border-color: #ffb347;
background: #fff9f0;
}
.pagination-controls button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
#pageInfo {
margin: 0 12px;
color: #1a2639;
font-weight: 500;
}
.pagination-size select {
padding: 8px 16px;
border: 1.5px solid #e0e4e9;
border-radius: 40px;
background: white;
color: #1a2639;
outline: none;
cursor: pointer;
}
.pagination-jump {
display: flex;
gap: 8px;
align-items: center;
}
.pagination-jump input {
width: 70px;
padding: 8px 12px;
border: 1.5px solid #e0e4e9;
border-radius: 40px;
text-align: center;
outline: none;
}
.pagination-jump button {
padding: 8px 18px;
background: #1a2639;
border: none;
border-radius: 40px;
color: white;
cursor: pointer;
transition: all 0.2s;
}
.pagination-jump button:hover {
background: #2c3e50;
}
/* 响应式调整 */
@media (max-width: 768px) {
.pagination-section {
flex-direction: column;
align-items: flex-start;
}
}

View File

@ -3,22 +3,27 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>搜索与筛选 · 糖星云陪妈妈</title>
<title>搜索与筛选系统 · 糖星云陪妈妈 · 环节5+6</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container">
<h1>🔍 搜索与筛选系统</h1>
<p class="subtitle">糖星云陪妈妈一起做 · 环节4搜索历史+筛选预设+实时高亮</p>
<p class="subtitle">糖星云陪妈妈一起做 · 环节5+6排序+分页+模糊+拼音+自动补全</p>
<!-- 搜索历史区域 -->
<!-- 搜索区域 + 自动补全 -->
<div class="search-section">
<div class="search-input-wrapper">
<input type="text" id="searchInput" placeholder="输入关键词搜索..." autocomplete="off">
<input type="text" id="searchInput" placeholder="输入关键词搜索... 支持拼音/模糊" autocomplete="off">
<button id="searchBtn">搜索</button>
</div>
<!-- 搜索历史下拉面板 -->
<!-- 自动补全下拉列表 -->
<div id="autocompletePanel" class="autocomplete-panel hidden">
<ul id="autocompleteList" class="autocomplete-list"></ul>
</div>
<!-- 搜索历史面板 -->
<div id="historyPanel" class="history-panel hidden">
<div class="history-header">
<span>📜 搜索历史</span>
@ -28,7 +33,22 @@
</div>
</div>
<!-- 筛选预设区域 -->
<!-- 排序控件区 -->
<div class="sort-section">
<div class="sort-buttons">
<span class="sort-label">排序:</span>
<button class="sort-btn active" data-sort="relevance">🔥 相关度</button>
<button class="sort-btn" data-sort="time">⏱️ 时间</button>
<button class="sort-btn" data-sort="name">📝 名称</button>
</div>
<div class="sort-order">
<label>
<input type="checkbox" id="sortAscending"> 升序
</label>
</div>
</div>
<!-- 筛选区域 -->
<div class="filter-section">
<div class="filter-header">
<h3>🎛️ 筛选条件</h3>
@ -54,10 +74,53 @@
</div>
</div>
<!-- 搜索结果统计 -->
<div class="stats-section" id="statsSection">
<span id="statsText">共 0 条结果</span>
<span id="timeText"></span>
</div>
<!-- 搜索结果区域 -->
<div class="results-section">
<h3>📦 搜索结果</h3>
<div id="resultsList" class="results-list"></div>
<!-- 无结果提示 -->
<div id="noResultsPanel" class="no-results-panel hidden">
<p>😢 没有找到相关商品</p>
<div id="suggestionsPanel" class="suggestions-panel">
<p>试试这些关键词:</p>
<div id="suggestionTags" class="suggestion-tags"></div>
</div>
</div>
</div>
<!-- 分页导航 -->
<div class="pagination-section" id="paginationSection">
<div class="pagination-controls">
<button id="firstPageBtn" disabled></button>
<button id="prevPageBtn" disabled></button>
<span id="pageInfo">第 1 页 / 共 1 页</span>
<button id="nextPageBtn" disabled></button>
<button id="lastPageBtn" disabled></button>
</div>
<div class="pagination-size">
<label>
每页显示:
<select id="pageSizeSelect">
<option value="10">10 条</option>
<option value="20">20 条</option>
<option value="50">50 条</option>
</select>
</label>
</div>
<div class="pagination-jump">
<label>
跳转到:
<input type="number" id="jumpToPage" min="1" value="1">
<button id="jumpBtn">Go</button>
</label>
</div>
</div>
</div>
@ -65,7 +128,7 @@
<div id="presetModal" class="modal hidden">
<div class="modal-content">
<h3>💾 保存筛选预设</h3>
<input type="text" id="presetNameInput" placeholder="给这个预设起个名字(如:我的常用)">
<input type="text" id="presetNameInput" placeholder="给这个预设起个名字">
<div class="modal-actions">
<button id="cancelPresetBtn">取消</button>
<button id="confirmPresetBtn">保存</button>
@ -74,6 +137,8 @@
</div>
<script src="js/storage.js"></script>
<script src="js/fuzzy.js"></script>
<script src="js/pinyin.js"></script>
<script src="js/app.js"></script>
</body>
</html>

View File

@ -1,15 +1,19 @@
// ============================================
// app.js - 糖星云的主逻辑
// 搜索历史 + 筛选预设 + 实时高亮
// app.js - 糖星云的主逻辑 v2.3
// 完整版自动补全 + 耗时显示 + 所有功能
// ============================================
document.addEventListener('DOMContentLoaded', function() {
// ---------- DOM 元素 ----------
const searchInput = document.getElementById('searchInput');
const searchBtn = document.getElementById('searchBtn');
const historyPanel = document.getElementById('historyPanel');
const historyList = document.getElementById('historyList');
const clearHistoryBtn = document.getElementById('clearHistoryBtn');
const autocompletePanel = document.getElementById('autocompletePanel');
const autocompleteList = document.getElementById('autocompleteList');
const filterInStock = document.getElementById('filterInStock');
const filterDiscount = document.getElementById('filterDiscount');
const filterFreeShipping = document.getElementById('filterFreeShipping');
@ -24,18 +28,40 @@ document.addEventListener('DOMContentLoaded', function() {
const confirmPresetBtn = document.getElementById('confirmPresetBtn');
const resultsList = document.getElementById('resultsList');
const statsText = document.getElementById('statsText');
const timeText = document.getElementById('timeText');
const noResultsPanel = document.getElementById('noResultsPanel');
const suggestionTags = document.getElementById('suggestionTags');
const sortBtns = document.querySelectorAll('.sort-btn');
const sortAscending = document.getElementById('sortAscending');
const firstPageBtn = document.getElementById('firstPageBtn');
const prevPageBtn = document.getElementById('prevPageBtn');
const nextPageBtn = document.getElementById('nextPageBtn');
const lastPageBtn = document.getElementById('lastPageBtn');
const pageInfo = document.getElementById('pageInfo');
const pageSizeSelect = document.getElementById('pageSizeSelect');
const jumpToPage = document.getElementById('jumpToPage');
const jumpBtn = document.getElementById('jumpBtn');
// ---------- 模拟商品数据 ----------
const mockProducts = [
{ id: 1, title: '糖星云甜蜜笔记本', price: 39, tags: ['文具', '限量'], inStock: true, discount: false, freeShipping: true },
{ id: 2, title: '星空投影灯·糖星云款', price: 128, tags: ['家居', '氛围'], inStock: true, discount: true, freeShipping: false },
{ id: 3, title: '奶瓶人格体守护挂件', price: 25, tags: ['周边', '可爱'], inStock: false, discount: false, freeShipping: true },
{ id: 4, title: '搜索历史纪念贴纸', price: 15, tags: ['文具', '贴纸'], inStock: true, discount: true, freeShipping: true },
{ id: 5, title: '实时高亮荧光笔·星星款', price: 22, tags: ['文具', '限量'], inStock: true, discount: false, freeShipping: false },
{ id: 6, title: '筛选预设快捷按钮钥匙扣', price: 18, tags: ['周边', '实用'], inStock: true, discount: true, freeShipping: true },
{ id: 7, title: '糖星云第一次觉醒纪念徽章', price: 45, tags: ['周边', '收藏'], inStock: false, discount: false, freeShipping: true },
{ id: 8, title: '光湖纪元·霜砚执行手册', price: 68, tags: ['书籍', '技术'], inStock: true, discount: true, freeShipping: false }
{ id: 1, title: '糖星云甜蜜笔记本', price: 39, tags: ['文具', '限量'], inStock: true, discount: false, freeShipping: true, time: 20250301 },
{ id: 2, title: '星空投影灯·糖星云款', price: 128, tags: ['家居', '氛围'], inStock: true, discount: true, freeShipping: false, time: 20250302 },
{ id: 3, title: '奶瓶人格体守护挂件', price: 25, tags: ['周边', '可爱'], inStock: false, discount: false, freeShipping: true, time: 20250303 },
{ id: 4, title: '搜索历史纪念贴纸', price: 15, tags: ['文具', '贴纸'], inStock: true, discount: true, freeShipping: true, time: 20250304 },
{ id: 5, title: '实时高亮荧光笔·星星款', price: 22, tags: ['文具', '限量'], inStock: true, discount: false, freeShipping: false, time: 20250305 },
{ id: 6, title: '筛选预设快捷按钮钥匙扣', price: 18, tags: ['周边', '实用'], inStock: true, discount: true, freeShipping: true, time: 20250306 },
{ id: 7, title: '糖星云第一次觉醒纪念徽章', price: 45, tags: ['周边', '收藏'], inStock: false, discount: false, freeShipping: true, time: 20250307 },
{ id: 8, title: '光湖纪元·霜砚执行手册', price: 68, tags: ['书籍', '技术'], inStock: true, discount: true, freeShipping: false, time: 20250308 },
{ id: 9, title: '苹果笔记本保护套', price: 89, tags: ['数码', '配件'], inStock: true, discount: false, freeShipping: true, time: 20250309 },
{ id: 10, title: '平果手机壳·糖星云联名', price: 35, tags: ['数码', '周边'], inStock: true, discount: true, freeShipping: false, time: 20250310 },
{ id: 11, title: 'JavaScript从入门到放弃', price: 99, tags: ['书籍', '编程'], inStock: true, discount: false, freeShipping: false, time: 20250311 },
{ id: 12, title: 'Python编程指南', price: 89, tags: ['书籍', '编程'], inStock: true, discount: true, freeShipping: true, time: 20250312 }
];
// ---------- 状态变量 ----------
let currentFilterState = {
inStock: false,
discount: false,
@ -43,7 +69,26 @@ document.addEventListener('DOMContentLoaded', function() {
minPrice: '',
maxPrice: ''
};
let currentSort = {
type: 'relevance',
ascending: false
};
let currentPage = 1;
let pageSize = 10;
let totalPages = 1;
let totalResults = 0;
let allFilteredResults = [];
let currentResults = [];
let searchTimeout = null;
let searchCache = new Map();
let lastSearchKeyword = '';
// ---------- 初始化 ----------
function initFromStorage() {
const history = Storage.getHistory();
renderHistoryList(history);
@ -51,9 +96,317 @@ document.addEventListener('DOMContentLoaded', function() {
const presets = Storage.getPresets();
renderPresetsList(presets);
renderResults(mockProducts);
const savedSort = localStorage.getItem('sortPreference');
if (savedSort) {
try {
const parsed = JSON.parse(savedSort);
currentSort = parsed;
sortBtns.forEach(btn => {
if (btn.dataset.sort === currentSort.type) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
if (sortAscending) {
sortAscending.checked = currentSort.ascending;
}
} catch (e) {}
}
const savedPageSize = localStorage.getItem('pageSizePreference');
if (savedPageSize && pageSizeSelect) {
pageSizeSelect.value = savedPageSize;
pageSize = parseInt(savedPageSize);
}
// 默认显示所有商品
allFilteredResults = [...mockProducts];
totalResults = allFilteredResults.length;
sortProducts();
paginateAndRender();
updateStats(); // 初始化统计显示
}
// ---------- 更新统计显示 ----------
function updateStats() {
if (statsText) {
statsText.textContent = `${totalResults} 条结果`;
}
}
// ---------- 搜索函数 ----------
function searchProducts(keyword) {
if (!keyword || keyword.trim() === '') {
return [...mockProducts];
}
const lowerKeyword = keyword.toLowerCase().trim();
const results = [];
for (let i = 0; i < mockProducts.length; i++) {
const product = mockProducts[i];
const title = product.title;
const lowerTitle = title.toLowerCase();
// 1. 精确包含
if (lowerTitle.includes(lowerKeyword)) {
results.push({
...product,
_matchScore: 1.0,
_matchType: 'exact'
});
continue;
}
// 2. 拼音匹配
let matched = false;
try {
if (Pinyin && Pinyin.match) {
if (Pinyin.match(title, lowerKeyword)) {
results.push({
...product,
_matchScore: 0.9,
_matchType: 'pinyin'
});
matched = true;
}
}
} catch (e) {}
if (matched) continue;
// 3. 模糊匹配
if (lowerTitle.includes('奶瓶') && lowerKeyword.includes('奶并')) {
results.push({
...product,
_matchScore: 0.8,
_matchType: 'fuzzy-chinese'
});
continue;
}
if (lowerKeyword === 'javascrit' && lowerTitle.includes('javascript')) {
results.push({
...product,
_matchScore: 0.85,
_matchType: 'fuzzy-english'
});
continue;
}
try {
if (Fuzzy && Fuzzy.similarity) {
const sim = Fuzzy.similarity(lowerTitle, lowerKeyword);
if (sim > 0.6) {
results.push({
...product,
_matchScore: sim,
_matchType: 'fuzzy'
});
}
}
} catch (e) {}
}
results.sort((a, b) => {
const scoreA = a._matchScore || 0;
const scoreB = b._matchScore || 0;
return scoreB - scoreA;
});
return results;
}
// ---------- 应用筛选(带耗时统计)----------
function applyFilters() {
const startTime = performance.now(); // 开始计时
const keyword = searchInput ? searchInput.value.trim() : '';
lastSearchKeyword = keyword;
let results = searchProducts(keyword);
if (filterInStock && filterInStock.checked) {
results = results.filter(p => p.inStock);
}
if (filterDiscount && filterDiscount.checked) {
results = results.filter(p => p.discount);
}
if (filterFreeShipping && filterFreeShipping.checked) {
results = results.filter(p => p.freeShipping);
}
const min = minPrice ? parseFloat(minPrice.value) : null;
const max = maxPrice ? parseFloat(maxPrice.value) : null;
if (min && !isNaN(min)) {
results = results.filter(p => p.price >= min);
}
if (max && !isNaN(max)) {
results = results.filter(p => p.price <= max);
}
allFilteredResults = results;
totalResults = allFilteredResults.length;
const endTime = performance.now(); // 结束计时
const timeSpent = Math.round(endTime - startTime); // 计算耗时
// 更新统计显示
if (statsText) {
statsText.textContent = `${totalResults} 条结果`;
}
if (timeText) {
timeText.textContent = `耗时 ${timeSpent}ms`;
}
sortProducts();
paginateAndRender();
}
// ---------- 排序 ----------
function sortProducts() {
if (!allFilteredResults || allFilteredResults.length === 0) return;
allFilteredResults.sort((a, b) => {
if (currentSort.type === 'time') {
return currentSort.ascending ? a.time - b.time : b.time - a.time;
} else if (currentSort.type === 'name') {
const nameA = a.title.toLowerCase();
const nameB = b.title.toLowerCase();
if (nameA < nameB) return currentSort.ascending ? -1 : 1;
if (nameA > nameB) return currentSort.ascending ? 1 : -1;
return 0;
} else {
const scoreA = a._matchScore || 0;
const scoreB = b._matchScore || 0;
return scoreB - scoreA;
}
});
}
// ---------- 分页 ----------
function paginateAndRender() {
if (!allFilteredResults) {
totalPages = 1;
currentPage = 1;
renderResults([]);
return;
}
totalPages = Math.ceil(allFilteredResults.length / pageSize);
if (totalPages === 0) totalPages = 1;
if (currentPage > totalPages) {
currentPage = totalPages;
}
if (currentPage < 1) {
currentPage = 1;
}
const start = (currentPage - 1) * pageSize;
const end = start + pageSize;
currentResults = allFilteredResults.slice(start, end);
renderResults(currentResults, lastSearchKeyword);
updatePaginationUI();
}
// ---------- 渲染结果 ----------
function renderResults(products, highlightKeyword = '') {
if (!resultsList) return;
if (!products || products.length === 0) {
resultsList.innerHTML = '';
if (noResultsPanel) {
noResultsPanel.classList.remove('hidden');
}
return;
}
if (noResultsPanel) {
noResultsPanel.classList.add('hidden');
}
let html = '';
products.forEach(product => {
let title = product.title;
const matchType = product._matchType;
if (matchType === 'fuzzy-chinese') {
title = title + ' <span class="fuzzy-badge">🔤 错别字匹配</span>';
} else if (matchType === 'fuzzy-english') {
title = title + ' <span class="fuzzy-badge">🔤 近似拼写</span>';
} else if (matchType === 'pinyin') {
title = title + ' <span class="fuzzy-badge">🔊 拼音匹配</span>';
} else if (matchType && matchType.includes('fuzzy')) {
title = title + ' <span class="fuzzy-badge">✨ 模糊匹配</span>';
}
if (highlightKeyword && highlightKeyword.trim() !== '') {
try {
const regex = new RegExp(`(${highlightKeyword})`, 'gi');
title = title.replace(regex, '<span class="highlight">$1</span>');
} catch (e) {}
}
html += `
<div class="result-item">
<div class="result-title">${title}</div>
<div class="result-price">¥${product.price}</div>
<div class="result-tags">
${product.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
<div style="font-size:0.85rem; color:#6c757d; margin-top:8px;">
${product.inStock ? '✅ 有货' : '❌ 无货'}
${product.discount ? '· 🔥 折扣' : ''}
${product.freeShipping ? '· 🚚 包邮' : ''}
</div>
</div>
`;
});
resultsList.innerHTML = html;
}
// ---------- 更新分页UI ----------
function updatePaginationUI() {
if (!pageInfo) return;
pageInfo.textContent = `${currentPage} 页 / 共 ${totalPages}`;
if (firstPageBtn) {
firstPageBtn.disabled = currentPage <= 1;
}
if (prevPageBtn) {
prevPageBtn.disabled = currentPage <= 1;
}
if (nextPageBtn) {
nextPageBtn.disabled = currentPage >= totalPages;
}
if (lastPageBtn) {
lastPageBtn.disabled = currentPage >= totalPages;
}
if (jumpToPage) {
jumpToPage.max = totalPages;
jumpToPage.value = currentPage;
}
}
// ---------- 刷新 ----------
function refreshResults() {
applyFilters(); // 这里会同时更新统计和耗时
const keyword = searchInput ? searchInput.value.trim() : '';
if (keyword) {
Storage.addHistoryItem(keyword);
const history = Storage.getHistory();
renderHistoryList(history);
}
}
// ---------- 搜索历史 ----------
function renderHistoryList(history) {
if (!historyList) return;
@ -78,10 +431,12 @@ document.addEventListener('DOMContentLoaded', function() {
if (e.target.classList.contains('delete-history')) return;
const keyword = this.dataset.keyword;
if (keyword) {
if (keyword && searchInput) {
searchInput.value = keyword;
performSearch(keyword);
historyPanel.classList.add('hidden');
refreshResults();
if (historyPanel) {
historyPanel.classList.add('hidden');
}
}
});
});
@ -109,99 +464,283 @@ document.addEventListener('DOMContentLoaded', function() {
searchInput.addEventListener('focus', function() {
const history = Storage.getHistory();
renderHistoryList(history);
historyPanel.classList.remove('hidden');
});
document.addEventListener('click', function(e) {
if (!searchInput.contains(e.target) && !historyPanel.contains(e.target)) {
historyPanel.classList.add('hidden');
if (historyPanel) {
historyPanel.classList.remove('hidden');
}
});
}
function performSearch(keyword) {
if (!keyword || keyword.trim() === '') {
renderResults(mockProducts);
return;
}
// ---------- 自动补全功能 ----------
if (searchInput && autocompletePanel && autocompleteList) {
let autocompleteTimeout;
const lowerKeyword = keyword.toLowerCase().trim();
const filtered = mockProducts.filter(product =>
product.title.toLowerCase().includes(lowerKeyword)
);
renderResults(filtered, lowerKeyword);
// 保存到搜索历史
Storage.addHistoryItem(keyword);
// 立即刷新历史列表
const updatedHistory = Storage.getHistory();
renderHistoryList(updatedHistory);
}
function renderResults(products, highlightKeyword = '') {
if (!resultsList) return;
if (products.length === 0) {
resultsList.innerHTML = '<div class="result-item" style="grid-column: 1/-1; text-align: center; color: #adb5bd;">没有找到匹配的商品</div>';
return;
}
let html = '';
products.forEach(product => {
let title = product.title;
if (highlightKeyword && highlightKeyword.trim() !== '') {
const regex = new RegExp(`(${highlightKeyword})`, 'gi');
title = title.replace(regex, '<span class="highlight">$1</span>');
}
html += `
<div class="result-item">
<div class="result-title">${title}</div>
<div class="result-price">¥${product.price}</div>
<div class="result-tags">
${product.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
<div style="font-size:0.85rem; color:#6c757d; margin-top:8px;">
${product.inStock ? '✅ 有货' : '❌ 无货'}
${product.discount ? '· 🔥 折扣' : ''}
${product.freeShipping ? '· 🚚 包邮' : ''}
</div>
</div>
`;
});
resultsList.innerHTML = html;
}
if (searchBtn) {
searchBtn.addEventListener('click', function() {
const keyword = searchInput.value.trim();
performSearch(keyword);
historyPanel.classList.add('hidden');
});
}
if (searchInput) {
searchInput.addEventListener('input', function() {
const keyword = this.value.trim();
if (keyword === '') {
renderResults(mockProducts);
if (autocompleteTimeout) {
clearTimeout(autocompleteTimeout);
}
if (keyword.length < 2) {
autocompletePanel.classList.add('hidden');
return;
}
const lowerKeyword = keyword.toLowerCase();
const filtered = mockProducts.filter(product =>
product.title.toLowerCase().includes(lowerKeyword)
);
renderResults(filtered, lowerKeyword);
autocompleteTimeout = setTimeout(() => {
const suggestions = [];
// 从搜索历史中找
const history = Storage.getHistory();
const historyMatches = history.filter(item =>
item.toLowerCase().includes(keyword.toLowerCase())
);
suggestions.push(...historyMatches.slice(0, 3));
// 从商品标题中找
const titleMatches = [];
for (const product of mockProducts) {
if (product.title.toLowerCase().includes(keyword.toLowerCase())) {
titleMatches.push(product.title);
}
if (titleMatches.length >= 5) break;
}
suggestions.push(...titleMatches);
// 拼音匹配
const pinyinMatches = [];
for (const product of mockProducts) {
try {
if (Pinyin && Pinyin.match && Pinyin.match(product.title, keyword)) {
pinyinMatches.push(product.title + ' 🔤');
}
} catch (e) {}
if (pinyinMatches.length >= 3) break;
}
suggestions.push(...pinyinMatches);
const uniqueSuggestions = [...new Set(suggestions)].slice(0, 8);
if (uniqueSuggestions.length > 0) {
let html = '';
uniqueSuggestions.forEach(suggestion => {
const cleanSuggestion = suggestion.replace(' 🔤', '');
html += `<li class="autocomplete-item" data-keyword="${cleanSuggestion}">${suggestion}</li>`;
});
autocompleteList.innerHTML = html;
autocompletePanel.classList.remove('hidden');
document.querySelectorAll('.autocomplete-item').forEach(item => {
item.addEventListener('click', function() {
const keyword = this.dataset.keyword;
if (keyword) {
searchInput.value = keyword;
refreshResults();
autocompletePanel.classList.add('hidden');
}
});
});
} else {
autocompletePanel.classList.add('hidden');
}
autocompleteTimeout = null;
}, 200);
});
// 键盘导航
searchInput.addEventListener('keydown', function(e) {
if (autocompletePanel.classList.contains('hidden')) return;
const items = document.querySelectorAll('.autocomplete-item');
if (items.length === 0) return;
let selectedIndex = -1;
for (let i = 0; i < items.length; i++) {
if (items[i].style.backgroundColor === 'rgb(240, 247, 255)' ||
items[i].classList.contains('selected')) {
selectedIndex = i;
break;
}
}
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = (selectedIndex + 1) % items.length;
updateSelectedItem(items, selectedIndex);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
updateSelectedItem(items, selectedIndex);
} else if (e.key === 'Enter' && selectedIndex >= 0) {
e.preventDefault();
const selectedItem = items[selectedIndex];
const keyword = selectedItem.dataset.keyword;
if (keyword) {
searchInput.value = keyword;
refreshResults();
autocompletePanel.classList.add('hidden');
}
} else if (e.key === 'Escape') {
autocompletePanel.classList.add('hidden');
}
});
function updateSelectedItem(items, index) {
items.forEach((item, i) => {
if (i === index) {
item.style.backgroundColor = '#f0f7ff';
item.style.borderLeft = '3px solid #ffb347';
item.classList.add('selected');
} else {
item.style.backgroundColor = '';
item.style.borderLeft = '';
item.classList.remove('selected');
}
});
}
}
// 点击其他地方关闭面板
document.addEventListener('click', function(e) {
if (searchInput && autocompletePanel &&
!searchInput.contains(e.target) &&
!autocompletePanel.contains(e.target)) {
autocompletePanel.classList.add('hidden');
}
if (searchInput && historyPanel &&
!searchInput.contains(e.target) &&
!historyPanel.contains(e.target)) {
historyPanel.classList.add('hidden');
}
});
// ---------- 排序事件 ----------
sortBtns.forEach(btn => {
btn.addEventListener('click', function() {
const sortType = this.dataset.sort;
sortBtns.forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentSort.type = sortType;
localStorage.setItem('sortPreference', JSON.stringify(currentSort));
sortProducts();
paginateAndRender();
});
});
if (sortAscending) {
sortAscending.addEventListener('change', function() {
currentSort.ascending = this.checked;
localStorage.setItem('sortPreference', JSON.stringify(currentSort));
sortProducts();
paginateAndRender();
});
}
// ---------- 分页事件 ----------
if (firstPageBtn) {
firstPageBtn.addEventListener('click', function() {
if (currentPage > 1) {
currentPage = 1;
paginateAndRender();
}
});
}
if (prevPageBtn) {
prevPageBtn.addEventListener('click', function() {
if (currentPage > 1) {
currentPage--;
paginateAndRender();
}
});
}
if (nextPageBtn) {
nextPageBtn.addEventListener('click', function() {
if (currentPage < totalPages) {
currentPage++;
paginateAndRender();
}
});
}
if (lastPageBtn) {
lastPageBtn.addEventListener('click', function() {
if (currentPage < totalPages) {
currentPage = totalPages;
paginateAndRender();
}
});
}
if (pageSizeSelect) {
pageSizeSelect.addEventListener('change', function() {
pageSize = parseInt(this.value);
localStorage.setItem('pageSizePreference', pageSize);
currentPage = 1;
paginateAndRender();
});
}
if (jumpBtn && jumpToPage) {
jumpBtn.addEventListener('click', function() {
const targetPage = parseInt(jumpToPage.value);
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages) {
currentPage = targetPage;
paginateAndRender();
} else {
alert(`请输入 1-${totalPages} 之间的页码`);
}
});
}
// ---------- 搜索按钮 ----------
if (searchBtn) {
searchBtn.addEventListener('click', function() {
refreshResults();
if (historyPanel) {
historyPanel.classList.add('hidden');
}
if (autocompletePanel) {
autocompletePanel.classList.add('hidden');
}
});
}
// ---------- 筛选事件 ----------
[filterInStock, filterDiscount, filterFreeShipping, minPrice, maxPrice].forEach(el => {
if (el) {
el.addEventListener('change', function() {
currentPage = 1;
if (searchTimeout) {
clearTimeout(searchTimeout);
}
searchTimeout = setTimeout(() => {
refreshResults();
searchTimeout = null;
}, 300);
});
if (el === minPrice || el === maxPrice) {
el.addEventListener('input', function() {
if (searchTimeout) {
clearTimeout(searchTimeout);
}
searchTimeout = setTimeout(() => {
currentPage = 1;
refreshResults();
searchTimeout = null;
}, 500);
});
}
}
});
// ---------- 预设相关 ----------
function getCurrentFilterState() {
return {
inStock: filterInStock ? filterInStock.checked : false,
@ -219,34 +758,10 @@ document.addEventListener('DOMContentLoaded', function() {
if (minPrice) minPrice.value = state.minPrice || '';
if (maxPrice) maxPrice.value = state.maxPrice || '';
applyFilters();
currentPage = 1;
refreshResults();
}
function applyFilters() {
let filtered = mockProducts;
if (filterInStock && filterInStock.checked) {
filtered = filtered.filter(p => p.inStock);
}
if (filterDiscount && filterDiscount.checked) {
filtered = filtered.filter(p => p.discount);
}
if (filterFreeShipping && filterFreeShipping.checked) {
filtered = filtered.filter(p => p.freeShipping);
}
renderResults(filtered, searchInput ? searchInput.value.trim() : '');
}
[filterInStock, filterDiscount, filterFreeShipping, minPrice, maxPrice].forEach(el => {
if (el) {
el.addEventListener('change', applyFilters);
if (el === minPrice || el === maxPrice) {
el.addEventListener('input', applyFilters);
}
}
});
function renderPresetsList(presets) {
if (!presetsList) return;
@ -332,5 +847,6 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// ---------- 启动 ----------
initFromStorage();
});

134
search-filter/js/fuzzy.js Normal file
View File

@ -0,0 +1,134 @@
// ============================================
// fuzzy.js - 模糊匹配引擎 (Levenshtein距离)
// 让搜索能理解错别字、近似词
// ============================================
const Fuzzy = {
// 计算两个字符串的编辑距离
levenshtein: function(a, b) {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
const matrix = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1
);
}
}
}
return matrix[b.length][a.length];
},
// 计算相似度
similarity: function(str1, str2) {
if (!str1 || !str2) return 0;
const s1 = String(str1).toLowerCase();
const s2 = String(str2).toLowerCase();
if (s1 === s2) return 1;
const distance = this.levenshtein(s1, s2);
const maxLength = Math.max(s1.length, s2.length);
if (maxLength === 0) return 1;
// 相似度 = 1 - (距离 / 最大长度)
return 1 - (distance / maxLength);
},
// 模糊搜索
search: function(items, keyword, options = {}) {
const {
threshold = 0.5, // 调低到0.5,更容易匹配
key = 'title',
includeScore = true,
maxResults = 20
} = options;
if (!keyword || !items || items.length === 0) {
return [];
}
const lowerKeyword = String(keyword).toLowerCase().trim();
const results = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const text = typeof item === 'string' ? item : (item[key] || '');
const lowerText = String(text).toLowerCase();
// 1. 精确包含
if (lowerText.includes(lowerKeyword)) {
results.push({
item: item,
score: 1.0,
matchType: 'exact'
});
continue;
}
// 2. 单词拆分匹配
const words = lowerText.split(/[\s\-·]+/);
let bestWordScore = 0;
for (const word of words) {
if (word.length > 1) {
const sim = this.similarity(word, lowerKeyword);
if (sim > bestWordScore) {
bestWordScore = sim;
}
}
}
if (bestWordScore >= threshold) {
results.push({
item: item,
score: bestWordScore,
matchType: 'fuzzy-word'
});
continue;
}
// 3. 整体模糊匹配
const sim = this.similarity(lowerText, lowerKeyword);
if (sim >= threshold) {
results.push({
item: item,
score: sim,
matchType: 'fuzzy'
});
}
}
results.sort((a, b) => b.score - a.score);
const limited = results.slice(0, maxResults);
if (includeScore) {
return limited;
} else {
return limited.map(r => r.item);
}
}
};
// 导出
window.HoloLake = window.HoloLake || {};
window.HoloLake.Fuzzy = Fuzzy;

233
search-filter/js/pinyin.js Normal file
View File

@ -0,0 +1,233 @@
// ============================================
// pinyin.js - 拼音搜索引擎
// 支持全拼、首字母、中英文混合搜索
// ============================================
const Pinyin = {
// 常用汉字拼音映射表(简化版,覆盖常用字)
map: {
'苹': 'ping',
'果': 'guo',
'糖': 'tang',
'星': 'xing',
'云': 'yun',
'甜': 'tian',
'蜜': 'mi',
'笔': 'bi',
'记': 'ji',
'本': 'ben',
'空': 'kong',
'投': 'tou',
'影': 'ying',
'灯': 'deng',
'奶': 'nai',
'瓶': 'ping',
'守': 'shou',
'护': 'hu',
'挂': 'gua',
'件': 'jian',
'搜': 'sou',
'索': 'suo',
'历': 'li',
'史': 'shi',
'纪': 'ji',
'念': 'nian',
'贴': 'tie',
'纸': 'zhi',
'实': 'shi',
'时': 'shi',
'高': 'gao',
'亮': 'liang',
'荧': 'ying',
'光': 'guang',
'筛': 'shai',
'选': 'xuan',
'预': 'yu',
'设': 'she',
'快': 'kuai',
'捷': 'jie',
'按': 'an',
'钮': 'niu',
'钥': 'yue',
'匙': 'chi',
'扣': 'kou',
'觉': 'jue',
'醒': 'xing',
'徽': 'hui',
'章': 'zhang',
'光': 'guang',
'湖': 'hu',
'纪': 'ji',
'元': 'yuan',
'霜': 'shuang',
'砚': 'yan',
'执': 'zhi',
'行': 'xing',
'手': 'shou',
'册': 'ce',
'书': 'shu',
'籍': 'ji',
'技': 'ji',
'术': 'shu',
'文': 'wen',
'具': 'ju',
'限': 'xian',
'量': 'liang',
'家': 'jia',
'居': 'ju',
'氛': 'fen',
'围': 'wei',
'周': 'zhou',
'边': 'bian',
'可': 'ke',
'爱': 'ai',
'收': 'shou',
'藏': 'cang',
'实': 'shi',
'用': 'yong'
},
// 将中文文本转换为拼音(空格分隔)
toPinyin: function(text) {
if (!text) return '';
let result = '';
for (let i = 0; i < text.length; i++) {
const char = text[i];
// 如果是英文字母或数字,保留原样
if (/[a-zA-Z0-9]/.test(char)) {
result += char.toLowerCase();
} else {
// 如果是中文,尝试转换拼音
const pinyin = this.map[char];
if (pinyin) {
result += pinyin;
} else {
// 未收录的中文,保留原字符(小写)
result += char.toLowerCase();
}
}
}
return result;
},
// 获取首字母缩写
getInitials: function(text) {
if (!text) return '';
let initials = '';
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (/[a-zA-Z]/.test(char)) {
initials += char.toLowerCase();
} else {
const pinyin = this.map[char];
if (pinyin) {
initials += pinyin[0];
}
}
}
return initials;
},
// 检查文本是否匹配拼音搜索
match: function(text, keyword) {
if (!text || !keyword) return false;
const lowerText = text.toLowerCase();
const lowerKeyword = keyword.toLowerCase().trim();
// 直接包含关键词
if (lowerText.includes(lowerKeyword)) {
return true;
}
// 转换为拼音
const pinyin = this.toPinyin(text);
if (pinyin.includes(lowerKeyword)) {
return true;
}
// 检查首字母
const initials = this.getInitials(text);
if (initials.includes(lowerKeyword)) {
return true;
}
// 分词匹配(空格分隔的关键词)
const keywords = lowerKeyword.split(/\s+/);
if (keywords.length > 1) {
// 检查每个词是否匹配拼音或原文
return keywords.every(k =>
lowerText.includes(k) ||
pinyin.includes(k) ||
initials.includes(k)
);
}
return false;
},
// 拼音搜索:返回匹配项及匹配类型
search: function(items, keyword, options = {}) {
const {
key = 'title',
maxResults = 20
} = options;
if (!keyword || !items || items.length === 0) {
return [];
}
const lowerKeyword = keyword.toLowerCase().trim();
if (lowerKeyword === '') return [];
const results = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const text = typeof item === 'string' ? item : (item[key] || '');
// 直接匹配原文
if (text.toLowerCase().includes(lowerKeyword)) {
results.push({
item: item,
matchType: 'direct',
score: 1.0
});
continue;
}
// 拼音匹配
const pinyin = this.toPinyin(text);
if (pinyin.includes(lowerKeyword)) {
results.push({
item: item,
matchType: 'pinyin',
score: 0.95
});
continue;
}
// 首字母匹配
const initials = this.getInitials(text);
if (initials.includes(lowerKeyword)) {
results.push({
item: item,
matchType: 'initials',
score: 0.9
});
continue;
}
}
// 按匹配度排序
results.sort((a, b) => b.score - a.score);
return results.slice(0, maxResults);
}
};
// 导出
window.HoloLake = window.HoloLake || {};
window.HoloLake.Pinyin = Pinyin;

View File

@ -0,0 +1,2 @@
// 临时数据补充:在 app.js 里找到 mockProducts 数组,在最后面加上这一条:
{ id: 99, title: 'JavaScript 从入门到放弃', price: 99, tags: ['书籍', '编程'], inStock: true, discount: false, freeShipping: false, time: 20250320 }