DEV-009: M20环节4-搜索历史+筛选预设+实时高亮+localStorage持久化

This commit is contained in:
米米米叮 2026-03-10 02:26:29 +08:00
parent 6360e43a9b
commit 0a9529c922
4 changed files with 928 additions and 0 deletions

406
search-filter/css/style.css Normal file
View File

@ -0,0 +1,406 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e9ecf2 100%);
min-height: 100vh;
padding: 30px 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background: white;
border-radius: 32px;
box-shadow: 0 20px 40px rgba(0,0,0,0.08);
padding: 40px;
}
h1 {
font-size: 2.2rem;
color: #1a2639;
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.subtitle {
color: #5d6d7e;
margin-bottom: 40px;
font-size: 1rem;
border-left: 4px solid #ffd166;
padding-left: 16px;
}
/* 搜索区域 */
.search-section {
margin-bottom: 40px;
position: relative;
}
.search-input-wrapper {
display: flex;
gap: 12px;
}
#searchInput {
flex: 1;
padding: 16px 20px;
border: 2px solid #e0e4e9;
border-radius: 60px;
font-size: 1.1rem;
transition: all 0.2s;
outline: none;
}
#searchInput:focus {
border-color: #ffb347;
box-shadow: 0 0 0 4px rgba(255, 180, 70, 0.15);
}
#searchBtn {
padding: 0 32px;
background: #1a2639;
color: white;
border: none;
border-radius: 60px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
#searchBtn:hover {
background: #2c3e50;
transform: scale(1.02);
}
/* 历史面板 */
.history-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: 20px;
z-index: 100;
border: 1px solid #eef2f6;
}
.history-panel.hidden {
display: none;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
color: #4a5b6e;
font-weight: 600;
}
.small-btn {
padding: 6px 12px;
background: #f1f3f5;
border: none;
border-radius: 40px;
font-size: 0.85rem;
color: #495057;
cursor: pointer;
transition: all 0.2s;
}
.small-btn:hover {
background: #e2e6ea;
}
.history-list {
list-style: none;
max-height: 250px;
overflow-y: auto;
}
.history-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid #f0f3f7;
cursor: pointer;
transition: background 0.2s;
}
.history-item:hover {
background: #f8fafd;
}
.history-item .keyword {
font-weight: 500;
color: #1a2639;
}
.delete-history {
background: none;
border: none;
color: #adb5bd;
font-size: 1.2rem;
cursor: pointer;
padding: 0 8px;
}
.delete-history:hover {
color: #dc3545;
}
/* 筛选区域 */
.filter-section {
background: #f8fafd;
border-radius: 24px;
padding: 24px;
margin-bottom: 40px;
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.filter-header h3 {
color: #1a2639;
}
.secondary-btn {
padding: 8px 18px;
background: white;
border: 1.5px solid #d0d9e2;
border-radius: 40px;
color: #2c3e50;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.secondary-btn:hover {
background: #eef2f6;
border-color: #adb5bd;
}
.filter-options {
display: flex;
gap: 30px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-options label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
color: #2c3e50;
}
.price-range {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 25px;
}
.price-range input {
width: 120px;
padding: 10px 15px;
border: 1.5px solid #d0d9e2;
border-radius: 40px;
outline: none;
}
/* 预设列表 */
.presets-panel h4 {
color: #5d6d7e;
margin-bottom: 15px;
font-size: 0.95rem;
}
.presets-list {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.preset-item {
background: white;
border: 1.5px solid #e0e4e9;
border-radius: 40px;
padding: 8px 16px;
display: flex;
align-items: center;
gap: 8px;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.2s;
}
.preset-item:hover {
border-color: #ffb347;
background: #fff9f0;
}
.preset-name {
color: #1a2639;
}
.delete-preset {
background: none;
border: none;
color: #adb5bd;
cursor: pointer;
font-size: 1.2rem;
padding: 0 4px;
}
.delete-preset:hover {
color: #dc3545;
}
/* 搜索结果区域 */
.results-section h3 {
margin-bottom: 20px;
color: #1a2639;
}
.results-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 20px;
}
.result-item {
background: white;
border: 1.5px solid #eef2f6;
border-radius: 20px;
padding: 20px;
transition: all 0.2s;
box-shadow: 0 4px 8px rgba(0,0,0,0.02);
}
.result-item:hover {
border-color: #ffd166;
transform: translateY(-2px);
box-shadow: 0 12px 24px rgba(0,0,0,0.06);
}
.result-title {
font-weight: 600;
margin-bottom: 8px;
color: #1a2639;
font-size: 1.1rem;
}
.result-price {
color: #e67e22;
font-weight: 700;
margin-bottom: 6px;
}
.result-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin: 10px 0;
}
.tag {
background: #f1f3f5;
padding: 4px 10px;
border-radius: 40px;
font-size: 0.75rem;
color: #4a5b6e;
}
.highlight {
background-color: #ffeb99;
padding: 2px 0;
border-radius: 4px;
font-weight: 600;
color: #b85e00;
}
/* 弹窗 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal.hidden {
display: none;
}
.modal-content {
background: white;
border-radius: 32px;
padding: 30px;
width: 90%;
max-width: 400px;
}
.modal-content h3 {
margin-bottom: 20px;
color: #1a2639;
}
#presetNameInput {
width: 100%;
padding: 16px 20px;
border: 2px solid #e0e4e9;
border-radius: 60px;
margin-bottom: 25px;
font-size: 1rem;
outline: none;
}
#presetNameInput:focus {
border-color: #ffb347;
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.modal-actions button {
padding: 12px 24px;
border: none;
border-radius: 40px;
font-weight: 600;
cursor: pointer;
}
#cancelPresetBtn {
background: #f1f3f5;
color: #495057;
}
#confirmPresetBtn {
background: #1a2639;
color: white;
}

79
search-filter/index.html Normal file
View File

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>搜索与筛选 · 糖星云陪妈妈</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container">
<h1>🔍 搜索与筛选系统</h1>
<p class="subtitle">糖星云陪妈妈一起做 · 环节4搜索历史+筛选预设+实时高亮</p>
<!-- 搜索历史区域 -->
<div class="search-section">
<div class="search-input-wrapper">
<input type="text" id="searchInput" placeholder="输入关键词搜索..." autocomplete="off">
<button id="searchBtn">搜索</button>
</div>
<!-- 搜索历史下拉面板 -->
<div id="historyPanel" class="history-panel hidden">
<div class="history-header">
<span>📜 搜索历史</span>
<button id="clearHistoryBtn" class="small-btn">清空全部</button>
</div>
<ul id="historyList" class="history-list"></ul>
</div>
</div>
<!-- 筛选预设区域 -->
<div class="filter-section">
<div class="filter-header">
<h3>🎛️ 筛选条件</h3>
<button id="savePresetBtn" class="secondary-btn"> 保存为预设</button>
</div>
<div class="filter-options">
<label><input type="checkbox" id="filterInStock" value="inStock"> 仅显示有货</label>
<label><input type="checkbox" id="filterDiscount" value="discount"> 仅显示折扣</label>
<label><input type="checkbox" id="filterFreeShipping" value="freeShipping"> 包邮</label>
</div>
<div class="price-range">
<label>价格区间:</label>
<input type="number" id="minPrice" placeholder="最低价" min="0">
<input type="number" id="maxPrice" placeholder="最高价" min="0">
</div>
<!-- 预设列表 -->
<div id="presetsPanel" class="presets-panel">
<h4>📋 我的筛选预设</h4>
<ul id="presetsList" class="presets-list"></ul>
</div>
</div>
<!-- 搜索结果区域 -->
<div class="results-section">
<h3>📦 搜索结果</h3>
<div id="resultsList" class="results-list"></div>
</div>
</div>
<!-- 保存预设的弹窗 -->
<div id="presetModal" class="modal hidden">
<div class="modal-content">
<h3>💾 保存筛选预设</h3>
<input type="text" id="presetNameInput" placeholder="给这个预设起个名字(如:我的常用)">
<div class="modal-actions">
<button id="cancelPresetBtn">取消</button>
<button id="confirmPresetBtn">保存</button>
</div>
</div>
</div>
<script src="js/storage.js"></script>
<script src="js/app.js"></script>
</body>
</html>

336
search-filter/js/app.js Normal file
View File

@ -0,0 +1,336 @@
// ============================================
// app.js - 糖星云的主逻辑
// 搜索历史 + 筛选预设 + 实时高亮
// ============================================
document.addEventListener('DOMContentLoaded', function() {
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 filterInStock = document.getElementById('filterInStock');
const filterDiscount = document.getElementById('filterDiscount');
const filterFreeShipping = document.getElementById('filterFreeShipping');
const minPrice = document.getElementById('minPrice');
const maxPrice = document.getElementById('maxPrice');
const savePresetBtn = document.getElementById('savePresetBtn');
const presetsList = document.getElementById('presetsList');
const presetModal = document.getElementById('presetModal');
const presetNameInput = document.getElementById('presetNameInput');
const cancelPresetBtn = document.getElementById('cancelPresetBtn');
const confirmPresetBtn = document.getElementById('confirmPresetBtn');
const resultsList = document.getElementById('resultsList');
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 }
];
let currentFilterState = {
inStock: false,
discount: false,
freeShipping: false,
minPrice: '',
maxPrice: ''
};
function initFromStorage() {
const history = Storage.getHistory();
renderHistoryList(history);
const presets = Storage.getPresets();
renderPresetsList(presets);
renderResults(mockProducts);
}
function renderHistoryList(history) {
if (!historyList) return;
if (history.length === 0) {
historyList.innerHTML = '<li class="history-item" style="justify-content: center; color: #adb5bd;">暂无搜索历史</li>';
return;
}
let html = '';
history.forEach(keyword => {
html += `
<li class="history-item" data-keyword="${keyword}">
<span class="keyword">🔍 ${keyword}</span>
<button class="delete-history" data-keyword="${keyword}"></button>
</li>
`;
});
historyList.innerHTML = html;
document.querySelectorAll('.history-item').forEach(item => {
item.addEventListener('click', function(e) {
if (e.target.classList.contains('delete-history')) return;
const keyword = this.dataset.keyword;
if (keyword) {
searchInput.value = keyword;
performSearch(keyword);
historyPanel.classList.add('hidden');
}
});
});
document.querySelectorAll('.delete-history').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const keyword = this.dataset.keyword;
if (keyword) {
const newHistory = Storage.deleteHistoryItem(keyword);
renderHistoryList(newHistory);
}
});
});
}
if (clearHistoryBtn) {
clearHistoryBtn.addEventListener('click', function() {
const newHistory = Storage.clearAllHistory();
renderHistoryList(newHistory);
});
}
if (searchInput) {
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');
}
});
}
function performSearch(keyword) {
if (!keyword || keyword.trim() === '') {
renderResults(mockProducts);
return;
}
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);
return;
}
const lowerKeyword = keyword.toLowerCase();
const filtered = mockProducts.filter(product =>
product.title.toLowerCase().includes(lowerKeyword)
);
renderResults(filtered, lowerKeyword);
});
}
function getCurrentFilterState() {
return {
inStock: filterInStock ? filterInStock.checked : false,
discount: filterDiscount ? filterDiscount.checked : false,
freeShipping: filterFreeShipping ? filterFreeShipping.checked : false,
minPrice: minPrice ? minPrice.value : '',
maxPrice: maxPrice ? maxPrice.value : ''
};
}
function applyFilterState(state) {
if (filterInStock) filterInStock.checked = state.inStock || false;
if (filterDiscount) filterDiscount.checked = state.discount || false;
if (filterFreeShipping) filterFreeShipping.checked = state.freeShipping || false;
if (minPrice) minPrice.value = state.minPrice || '';
if (maxPrice) maxPrice.value = state.maxPrice || '';
applyFilters();
}
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;
if (presets.length === 0) {
presetsList.innerHTML = '<li style="color: #adb5bd; list-style: none;">暂无保存的预设</li>';
return;
}
let html = '';
presets.forEach(preset => {
html += `
<li class="preset-item" data-preset-id="${preset.id}">
<span class="preset-name">📋 ${preset.name}</span>
<button class="delete-preset" data-preset-id="${preset.id}"></button>
</li>
`;
});
presetsList.innerHTML = html;
document.querySelectorAll('.preset-item').forEach(item => {
item.addEventListener('click', function(e) {
if (e.target.classList.contains('delete-preset')) return;
const presetId = this.dataset.presetId;
const preset = Storage.getPresetById(presetId);
if (preset) {
applyFilterState(preset.filters);
}
});
});
document.querySelectorAll('.delete-preset').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const presetId = this.dataset.presetId;
if (presetId) {
const newPresets = Storage.deletePreset(presetId);
renderPresetsList(newPresets);
}
});
});
}
if (savePresetBtn) {
savePresetBtn.addEventListener('click', function() {
currentFilterState = getCurrentFilterState();
if (presetModal) {
presetModal.classList.remove('hidden');
presetNameInput.value = '';
presetNameInput.focus();
}
});
}
if (cancelPresetBtn) {
cancelPresetBtn.addEventListener('click', function() {
presetModal.classList.add('hidden');
});
}
if (confirmPresetBtn) {
confirmPresetBtn.addEventListener('click', function() {
const presetName = presetNameInput.value.trim();
if (!presetName) {
alert('给预设起个名字吧~');
return;
}
Storage.addPreset(presetName, currentFilterState);
const presets = Storage.getPresets();
renderPresetsList(presets);
presetModal.classList.add('hidden');
});
}
if (presetModal) {
presetModal.addEventListener('click', function(e) {
if (e.target === presetModal) {
presetModal.classList.add('hidden');
}
});
}
initFromStorage();
});

107
search-filter/js/storage.js Normal file
View File

@ -0,0 +1,107 @@
// ============================================
// storage.js - 糖星云的数据存储模块
// 管理搜索历史 + 筛选预设的 localStorage 读写
// ============================================
const Storage = {
// 存储键名常量
KEYS: {
SEARCH_HISTORY: 'searchHistory',
FILTER_PRESETS: 'filterPresets'
},
// ---------- 搜索历史 ----------
// 获取所有历史记录
getHistory: function() {
const history = localStorage.getItem(this.KEYS.SEARCH_HISTORY);
return history ? JSON.parse(history) : [];
},
// 保存历史记录最多20条按时间倒序
saveHistory: function(historyArray) {
// 确保不超过20条
const limited = historyArray.slice(0, 20);
localStorage.setItem(this.KEYS.SEARCH_HISTORY, JSON.stringify(limited));
},
// 添加一条新搜索记录
addHistoryItem: function(keyword) {
if (!keyword || keyword.trim() === '') return;
let history = this.getHistory();
const trimmedKeyword = keyword.trim();
// 移除已存在的相同关键词(避免重复)
history = history.filter(item => item !== trimmedKeyword);
// 插入到最前面
history.unshift(trimmedKeyword);
// 保存
this.saveHistory(history);
return history;
},
// 删除单条历史
deleteHistoryItem: function(keyword) {
let history = this.getHistory();
history = history.filter(item => item !== keyword);
this.saveHistory(history);
return history;
},
// 清空全部历史
clearAllHistory: function() {
localStorage.removeItem(this.KEYS.SEARCH_HISTORY);
return [];
},
// ---------- 筛选预设 ----------
// 获取所有预设
getPresets: function() {
const presets = localStorage.getItem(this.KEYS.FILTER_PRESETS);
return presets ? JSON.parse(presets) : [];
},
// 保存预设数组
savePresets: function(presetsArray) {
localStorage.setItem(this.KEYS.FILTER_PRESETS, JSON.stringify(presetsArray));
},
// 添加新预设
addPreset: function(name, filterState) {
if (!name || name.trim() === '') return;
const presets = this.getPresets();
const newPreset = {
id: Date.now().toString(),
name: name.trim(),
filters: { ...filterState }
};
presets.push(newPreset);
this.savePresets(presets);
return presets;
},
// 删除预设
deletePreset: function(presetId) {
let presets = this.getPresets();
presets = presets.filter(p => p.id !== presetId);
this.savePresets(presets);
return presets;
},
// 根据id获取预设
getPresetById: function(presetId) {
const presets = this.getPresets();
return presets.find(p => p.id === presetId) || null;
}
};
// 确保 Storage 对象全局可用
window.HoloLake = window.HoloLake || {};
window.HoloLake.Storage = Storage;
// 加一行测试输出,看看有没有加载成功
console.log('✅ storage.js 已加载', Storage);