DEV-009: M20环节4-搜索历史+筛选预设+实时高亮+localStorage持久化
This commit is contained in:
parent
6360e43a9b
commit
0a9529c922
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
Loading…
Reference in New Issue