DEV-009: M20环节5+6-排序+分页+模糊搜索+拼音支持+自动补全+防抖+缓存+统计
This commit is contained in:
parent
d5942ba6a9
commit
2e5603801a
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
// 临时数据补充:在 app.js 里找到 mockProducts 数组,在最后面加上这一条:
|
||||
{ id: 99, title: 'JavaScript 从入门到放弃', price: 99, tags: ['书籍', '编程'], inStock: true, discount: false, freeShipping: false, time: 20250320 }
|
||||
Loading…
Reference in New Issue