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