feat: 重做shop4古树壁画-枝繁叶茂3A大作既视感+光追生命力流动+电子工具商品(榨汁机/搅拌机等)
This commit is contained in:
609
web/dist/shop4/index.html
vendored
609
web/dist/shop4/index.html
vendored
@@ -8,7 +8,7 @@
|
|||||||
<style>
|
<style>
|
||||||
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
||||||
:root{
|
:root{
|
||||||
--bg:#0d1117;--bg2:#161b22;--surface:rgba(255,255,255,.04);--card:rgba(255,255,255,.06);
|
--bg:#0a0e0a;--bg2:#121810;--surface:rgba(255,255,255,.04);--card:rgba(255,255,255,.06);
|
||||||
--border:rgba(255,255,255,.08);--text:#e6edf3;--text-muted:#7d8590;
|
--border:rgba(255,255,255,.08);--text:#e6edf3;--text-muted:#7d8590;
|
||||||
--primary:#58d68d;--primary-glow:rgba(88,214,141,.25);--accent:#f0c674;
|
--primary:#58d68d;--primary-glow:rgba(88,214,141,.25);--accent:#f0c674;
|
||||||
--radius:16px;--blur-amount:8px;
|
--radius:16px;--blur-amount:8px;
|
||||||
@@ -22,7 +22,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Noto Sans SC,sans-s
|
|||||||
a{color:inherit;text-decoration:none}
|
a{color:inherit;text-decoration:none}
|
||||||
|
|
||||||
/* ========== 导航栏 ========== */
|
/* ========== 导航栏 ========== */
|
||||||
.navbar{position:fixed;top:0;left:0;right:0;z-index:200;display:flex;align-items:center;justify-content:space-between;padding:16px 32px;background:rgba(13,17,23,.6);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);transition:all .4s}
|
.navbar{position:fixed;top:0;left:0;right:0;z-index:200;display:flex;align-items:center;justify-content:space-between;padding:16px 32px;background:rgba(10,14,10,.6);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);transition:all .4s}
|
||||||
[data-theme="light"] .navbar{background:rgba(255,255,255,.7)}
|
[data-theme="light"] .navbar{background:rgba(255,255,255,.7)}
|
||||||
.nav-brand{font-size:22px;font-weight:800;display:flex;align-items:center;gap:8px}
|
.nav-brand{font-size:22px;font-weight:800;display:flex;align-items:center;gap:8px}
|
||||||
.nav-brand .tree-icon{font-size:26px}
|
.nav-brand .tree-icon{font-size:26px}
|
||||||
@@ -44,10 +44,10 @@ a{color:inherit;text-decoration:none}
|
|||||||
[data-theme="light"] .moon-icon{transform:translateY(0) rotate(0);opacity:1}
|
[data-theme="light"] .moon-icon{transform:translateY(0) rotate(0);opacity:1}
|
||||||
|
|
||||||
/* ========== Hero 横向动态 ========== */
|
/* ========== Hero 横向动态 ========== */
|
||||||
.hero{position:relative;width:100%;height:100vh;min-height:640px;overflow:hidden;background:#0a0f0a}
|
.hero{position:relative;width:100%;height:100vh;min-height:640px;overflow:hidden;background:#060a06}
|
||||||
#treeCanvas{position:absolute;inset:0;width:100%;height:100%;z-index:1}
|
#treeCanvas{position:absolute;inset:0;width:100%;height:100%;z-index:1}
|
||||||
.hero-overlay{position:absolute;inset:0;z-index:2;background:linear-gradient(180deg,rgba(10,15,10,.2) 0%,rgba(10,15,10,.5) 60%,var(--bg) 100%);pointer-events:none;transition:background .4s}
|
.hero-overlay{position:absolute;inset:0;z-index:2;background:linear-gradient(180deg,rgba(6,10,6,.1) 0%,rgba(6,10,6,.35) 55%,var(--bg) 100%);pointer-events:none;transition:background .4s}
|
||||||
[data-theme="light"] .hero-overlay{background:linear-gradient(180deg,rgba(245,243,240,.1) 0%,rgba(245,243,240,.4) 60%,var(--bg) 100%)}
|
[data-theme="light"] .hero-overlay{background:linear-gradient(180deg,rgba(245,243,240,.05) 0%,rgba(245,243,240,.3) 55%,var(--bg) 100%)}
|
||||||
|
|
||||||
/* 横向滚动容器 */
|
/* 横向滚动容器 */
|
||||||
.hero-scroll-wrap{position:absolute;inset:0;z-index:3;display:flex;align-items:center;overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;scrollbar-width:none}
|
.hero-scroll-wrap{position:absolute;inset:0;z-index:3;display:flex;align-items:center;overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;scrollbar-width:none}
|
||||||
@@ -91,7 +91,7 @@ a{color:inherit;text-decoration:none}
|
|||||||
.products-section{position:relative;min-height:100vh;padding:80px 0;z-index:10}
|
.products-section{position:relative;min-height:100vh;padding:80px 0;z-index:10}
|
||||||
.products-bg{position:absolute;inset:0;z-index:0;overflow:hidden}
|
.products-bg{position:absolute;inset:0;z-index:0;overflow:hidden}
|
||||||
.products-bg canvas{width:100%;height:100%;display:block}
|
.products-bg canvas{width:100%;height:100%;display:block}
|
||||||
.products-bg::after{content:'';position:absolute;inset:0;background:var(--bg);opacity:.75}
|
.products-bg::after{content:'';position:absolute;inset:0;background:var(--bg);opacity:.82}
|
||||||
.products-content{position:relative;z-index:1;max-width:1280px;margin:0 auto;padding:0 32px}
|
.products-content{position:relative;z-index:1;max-width:1280px;margin:0 auto;padding:0 32px}
|
||||||
.section-header{text-align:center;margin-bottom:48px}
|
.section-header{text-align:center;margin-bottom:48px}
|
||||||
.section-header h2{font-size:36px;font-weight:800;letter-spacing:-1px;margin-bottom:12px}
|
.section-header h2{font-size:36px;font-weight:800;letter-spacing:-1px;margin-bottom:12px}
|
||||||
@@ -235,7 +235,7 @@ a{color:inherit;text-decoration:none}
|
|||||||
<div class="hero-panel-content">
|
<div class="hero-panel-content">
|
||||||
<div class="hero-badge"><span class="dot"></span> 古树商城 · 2026 新季</div>
|
<div class="hero-badge"><span class="dot"></span> 古树商城 · 2026 新季</div>
|
||||||
<h1>扎根全球<br><span class="accent">枝连万物</span></h1>
|
<h1>扎根全球<br><span class="accent">枝连万物</span></h1>
|
||||||
<p>如古树般扎根品质,如枝叶般连接全球好物。精选 18+ 品类,从智能电子到家居生活,每一件商品都经过严选,为您带来自然纯粹的购物体验。</p>
|
<p>如古树般扎根品质,如枝叶般连接全球好物。精选电子工具品类,从榨汁机到搅拌机,每一件产品都经过严选,为您带来高效纯粹的厨房体验。</p>
|
||||||
<div class="hero-panel-actions">
|
<div class="hero-panel-actions">
|
||||||
<button class="btn-primary" onclick="scrollToProducts()">
|
<button class="btn-primary" onclick="scrollToProducts()">
|
||||||
探索商品
|
探索商品
|
||||||
@@ -292,11 +292,14 @@ a{color:inherit;text-decoration:none}
|
|||||||
<div class="products-bg" id="productsBg"></div>
|
<div class="products-bg" id="productsBg"></div>
|
||||||
<div class="products-content">
|
<div class="products-content">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>全部<span class="accent">商品</span></h2>
|
<h2>电子工具<span class="accent">精选</span></h2>
|
||||||
<p>精选好物,品质如树,扎根全球</p>
|
<p>高效厨房好物,品质如树,扎根全球</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-bar">
|
<div class="filter-bar" id="filterBar">
|
||||||
<button class="filter-chip active" data-cat="0" onclick="filterCategory(0,this)">全部分类</button>
|
<button class="filter-chip active" data-cat="0" onclick="filterCategory(0,this)">全部分类</button>
|
||||||
|
<button class="filter-chip" data-cat="1" onclick="filterCategory(1,this)">榨汁搅拌</button>
|
||||||
|
<button class="filter-chip" data-cat="2" onclick="filterCategory(2,this)">烹饪烘焙</button>
|
||||||
|
<button class="filter-chip" data-cat="3" onclick="filterCategory(3,this)">生活电器</button>
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||||
<input type="text" id="searchInput" placeholder="搜索商品..." oninput="onSearch()">
|
<input type="text" id="searchInput" placeholder="搜索商品..." oninput="onSearch()">
|
||||||
@@ -327,7 +330,7 @@ a{color:inherit;text-decoration:none}
|
|||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<div class="footer-brand">
|
<div class="footer-brand">
|
||||||
<h3>🌳 TreeShop</h3>
|
<h3>🌳 TreeShop</h3>
|
||||||
<p>如古树般扎根品质,如枝叶般连接全球。精选好物,为您带来自然纯粹的跨境购物体验。</p>
|
<p>如古树般扎根品质,如枝叶般连接全球。精选电子工具好物,为您带来高效纯粹的跨境购物体验。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-col">
|
<div class="footer-col">
|
||||||
<h4>购物指南</h4>
|
<h4>购物指南</h4>
|
||||||
@@ -342,7 +345,7 @@ a{color:inherit;text-decoration:none}
|
|||||||
<a>公司简介</a><a>加入我们</a><a>隐私政策</a><a>用户协议</a>
|
<a>公司简介</a><a>加入我们</a><a>隐私政策</a><a>用户协议</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-bottom">© 2026 TreeShop · 古树商城 · 多行业网站模板集合</div>
|
<div class="footer-bottom">© 2026 TreeShop · 古树商城 · 电子工具精选</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Cart -->
|
<!-- Cart -->
|
||||||
@@ -370,15 +373,25 @@ a{color:inherit;text-decoration:none}
|
|||||||
<script>
|
<script>
|
||||||
// ========== 全局状态 ==========
|
// ========== 全局状态 ==========
|
||||||
var currentPage = 1, pageSize = 10, totalPages = 1, currentCat = 0, searchKey = '';
|
var currentPage = 1, pageSize = 10, totalPages = 1, currentCat = 0, searchKey = '';
|
||||||
var categories = [], cart = [];
|
var cart = [];
|
||||||
|
|
||||||
|
// ========== 电子工具商品数据 ==========
|
||||||
|
var allProducts = [
|
||||||
|
{ id:1, name:'冷压慢速榨汁机 Pro Max', cat:1, catName:'榨汁搅拌', price:189.99, rating:4.9, sales:12800, icon:'🥤', desc:'采用冷压慢速技术,保留90%以上营养。75mm大口径整果压榨,无需切块。陶瓷螺旋杆,果汁更细腻。' },
|
||||||
|
{ id:2, name:'多功能破壁料理机 1200W', cat:1, catName:'榨汁搅拌', price:159.99, rating:4.8, sales:9600, icon:'🌀', desc:'8叶精钢刀片,38000转/分钟高速破壁。冷热双杯配置,一机多用,支持豆浆、浓汤、冰沙。' },
|
||||||
|
{ id:3, name:'便携式电动榨汁杯 USB充电', cat:1, catName:'榨汁搅拌', price:39.99, rating:4.6, sales:35000, icon:'🔋', desc:'口袋便携设计,USB充电续航30次。6叶刀头强力搅拌,30秒出汁, Tritan材质安全无忧。' },
|
||||||
|
{ id:4, name:'手持搅拌棒 700W 免拆洗', cat:1, catName:'榨汁搅拌', price:69.99, rating:4.7, sales:21000, icon:'✋', desc:'700W大功率,一键拆卸。不锈钢刀头防锈耐用,配备打蛋器和量杯配件。' },
|
||||||
|
{ id:5, name:'空气炸锅 5.5L 无油低脂', cat:2, catName:'烹饪烘焙', price:119.99, rating:4.8, sales:18500, icon:'🍟', desc:'360°热风循环,无需翻面。5.5L大容量满足全家需求。8大预设菜单,触控操作一键烹饪。' },
|
||||||
|
{ id:6, name:'智能电烤箱 32L 风炉发酵', cat:2, catName:'烹饪烘焙', price:229.99, rating:4.7, sales:7800, icon:'🔥', desc:'上下管独立控温,风炉均匀烘烤。内置发酵功能,360°旋转烤叉。搪瓷内胆易清洁。' },
|
||||||
|
{ id:7, name:'全自动咖啡机 15Bar 浓缩', cat:2, catName:'烹饪烘焙', price:299.99, rating:4.9, sales:5600, icon:'☕', desc:'15Bar压力萃取,奶泡一体。PID温控精准±1°C。豆粉双用,支持自定义杯量和浓度。' },
|
||||||
|
{ id:8, name:'多功能厨师机 1000W 和面', cat:2, catName:'烹饪烘焙', price:259.99, rating:4.8, sales:6200, icon:'👨🍳', desc:'1000W大功率直流电机,10级调速。行星搅拌轨迹,配搅拌/打蛋/和面三配件。5.5L不锈钢盆。' },
|
||||||
|
{ id:9, name:'智能电饭煲 4L IH电磁加热', cat:3, catName:'生活电器', price:179.99, rating:4.9, sales:24000, icon:'🍚', desc:'IH电磁环绕加热,米饭粒粒分明。12大烹饪菜单,支持柴火饭、煲仔饭、蛋糕。APP远程控制。' },
|
||||||
|
{ id:10, name:'桌面冷风扇 遥控加湿', cat:3, catName:'生活电器', price:89.99, rating:4.5, sales:15000, icon:'❄️', desc:'三档风速,冰晶制冷快速降温。内置加湿功能,7L大水箱。远程遥控,定时12小时。' },
|
||||||
|
{ id:11, name:'破壁豆浆机 加热免滤', cat:1, catName:'榨汁搅拌', price:149.99, rating:4.7, sales:13000, icon:'🫘', desc:'加热免滤一体,干豆直接打。8叶刀头高速破壁,医用级304不锈钢内胆。防溢防糊双重保护。' },
|
||||||
|
{ id:12, name:'真空封口机 干湿两用', cat:3, catName:'生活电器', price:49.99, rating:4.6, sales:8900, icon:'📦', desc:'干湿双模式抽真空,保鲜延长5倍。一键操作,支持连续封口。附送10个真空袋。' }
|
||||||
|
];
|
||||||
|
|
||||||
// ========== 工具函数 ==========
|
// ========== 工具函数 ==========
|
||||||
async function fetchJSON(url) {
|
|
||||||
try {
|
|
||||||
var resp = await fetch(url);
|
|
||||||
return await resp.json();
|
|
||||||
} catch(e) { console.error(e); return null; }
|
|
||||||
}
|
|
||||||
function showToast(msg) {
|
function showToast(msg) {
|
||||||
var t = document.getElementById('toast');
|
var t = document.getElementById('toast');
|
||||||
document.getElementById('toastMsg').textContent = msg;
|
document.getElementById('toastMsg').textContent = msg;
|
||||||
@@ -386,10 +399,6 @@ function showToast(msg) {
|
|||||||
clearTimeout(window._tt);
|
clearTimeout(window._tt);
|
||||||
window._tt = setTimeout(function(){ t.classList.remove('show'); }, 2500);
|
window._tt = setTimeout(function(){ t.classList.remove('show'); }, 2500);
|
||||||
}
|
}
|
||||||
function getCatName(id) {
|
|
||||||
var c = categories.find(function(c){ return c.id == id; });
|
|
||||||
return c ? c.name : '未分类';
|
|
||||||
}
|
|
||||||
function fmtPrice(p) { return '$' + Number(p).toFixed(2); }
|
function fmtPrice(p) { return '$' + Number(p).toFixed(2); }
|
||||||
function starsHTML(r) {
|
function starsHTML(r) {
|
||||||
var s = '';
|
var s = '';
|
||||||
@@ -397,10 +406,7 @@ function starsHTML(r) {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
function productImgHTML(p) {
|
function productImgHTML(p) {
|
||||||
if (p.image && (p.image.startsWith('/') || p.image.startsWith('http'))) {
|
return '<div class="placeholder">' + (p.icon || '🔌') + '</div>';
|
||||||
return '<img src="' + p.image + '" onerror="this.parentElement.innerHTML=\'<div class=placeholder>📦</div>\'">';
|
|
||||||
}
|
|
||||||
return '<div class="placeholder">📦</div>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 黑白模式切换 ==========
|
// ========== 黑白模式切换 ==========
|
||||||
@@ -424,31 +430,23 @@ function scrollToProducts() {
|
|||||||
document.getElementById('productsSection').scrollIntoView({ behavior: 'smooth' });
|
document.getElementById('productsSection').scrollIntoView({ behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 商品加载 ==========
|
// ========== 商品加载(本地数据)==========
|
||||||
async function loadCategories() {
|
function getFilteredProducts() {
|
||||||
var data = await fetchJSON('/api/v1/shop/categories');
|
var list = allProducts;
|
||||||
if (data && data.code === 0 && data.data) {
|
if (currentCat > 0) list = list.filter(function(p){ return p.cat === currentCat; });
|
||||||
categories = data.data.list || data.data || [];
|
if (searchKey) {
|
||||||
var bar = document.querySelector('.filter-bar');
|
var k = searchKey.toLowerCase();
|
||||||
categories.forEach(function(c) {
|
list = list.filter(function(p){ return p.name.toLowerCase().indexOf(k) >= 0 || p.catName.indexOf(searchKey) >= 0; });
|
||||||
var chip = document.createElement('button');
|
|
||||||
chip.className = 'filter-chip';
|
|
||||||
chip.setAttribute('data-cat', c.id);
|
|
||||||
chip.textContent = c.name;
|
|
||||||
chip.onclick = function() { filterCategory(c.id, chip); };
|
|
||||||
bar.insertBefore(chip, bar.querySelector('.search-box'));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadProducts() {
|
function loadProducts() {
|
||||||
var url = '/api/v1/shop/products?page=' + currentPage + '&page_size=' + pageSize;
|
var all = getFilteredProducts();
|
||||||
if (currentCat > 0) url += '&category_id=' + currentCat;
|
totalPages = Math.ceil(all.length / pageSize);
|
||||||
if (searchKey) url += '&search=' + encodeURIComponent(searchKey);
|
if (currentPage > totalPages) currentPage = 1;
|
||||||
var data = await fetchJSON(url);
|
var start = (currentPage - 1) * pageSize;
|
||||||
if (!data || data.code !== 0) return;
|
var list = all.slice(start, start + pageSize);
|
||||||
var list = data.data.list || [];
|
|
||||||
totalPages = data.data.total_pages || 1;
|
|
||||||
renderTable(list);
|
renderTable(list);
|
||||||
renderPagination();
|
renderPagination();
|
||||||
}
|
}
|
||||||
@@ -462,13 +460,13 @@ function renderTable(list) {
|
|||||||
tbody.innerHTML = list.map(function(p) {
|
tbody.innerHTML = list.map(function(p) {
|
||||||
return '<tr onclick="showDetail(' + p.id + ')">' +
|
return '<tr onclick="showDetail(' + p.id + ')">' +
|
||||||
'<td><div class="td-img">' + productImgHTML(p) + '</div></td>' +
|
'<td><div class="td-img">' + productImgHTML(p) + '</div></td>' +
|
||||||
'<td class="td-name">' + (p.name || '') + '</td>' +
|
'<td class="td-name">' + p.name + '</td>' +
|
||||||
'<td class="td-category">' + getCatName(p.category_id) + '</td>' +
|
'<td class="td-category">' + p.catName + '</td>' +
|
||||||
'<td class="td-price">' + fmtPrice(p.price) + '</td>' +
|
'<td class="td-price">' + fmtPrice(p.price) + '</td>' +
|
||||||
'<td><div class="td-rating">' + starsHTML(p.rating || 0) + ' <span style="font-size:12px;color:var(--text-muted)">' + (p.rating || 0).toFixed(1) + '</span></div></td>' +
|
'<td><div class="td-rating">' + starsHTML(p.rating) + ' <span style="font-size:12px;color:var(--text-muted)">' + p.rating.toFixed(1) + '</span></div></td>' +
|
||||||
'<td style="color:var(--text-muted)">' + (p.sales || 0) + '</td>' +
|
'<td style="color:var(--text-muted)">' + p.sales.toLocaleString() + '</td>' +
|
||||||
'<td><div class="td-actions" onclick="event.stopPropagation()">' +
|
'<td><div class="td-actions" onclick="event.stopPropagation()">' +
|
||||||
'<button class="td-btn td-btn-primary" onclick="addToCart(' + p.id + ',\'' + (p.name || '').replace(/'/g, '') + '\',' + (p.price || 0) + ',\'' + (p.image || '') + '\')">加购</button>' +
|
'<button class="td-btn td-btn-primary" onclick="addToCart(' + p.id + ')">加购</button>' +
|
||||||
'<button class="td-btn td-btn-ghost" onclick="showDetail(' + p.id + ')">详情</button>' +
|
'<button class="td-btn td-btn-ghost" onclick="showDetail(' + p.id + ')">详情</button>' +
|
||||||
'</div></td>' +
|
'</div></td>' +
|
||||||
'</tr>';
|
'</tr>';
|
||||||
@@ -501,24 +499,23 @@ function onSearch() {
|
|||||||
}, 380);
|
}, 380);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showDetail(id) {
|
function showDetail(id) {
|
||||||
var data = await fetchJSON('/api/v1/shop/products/' + id);
|
var p = allProducts.find(function(x){ return x.id === id; });
|
||||||
if (!data || data.code !== 0) return;
|
if (!p) return;
|
||||||
var p = data.data;
|
|
||||||
var modal = document.getElementById('modal');
|
var modal = document.getElementById('modal');
|
||||||
modal.innerHTML =
|
modal.innerHTML =
|
||||||
'<div class="modal-header"><h3>' + (p.name || '') + '</h3><button class="modal-close" onclick="closeModal()">×</button></div>' +
|
'<div class="modal-header"><h3>' + p.name + '</h3><button class="modal-close" onclick="closeModal()">×</button></div>' +
|
||||||
'<div class="modal-body">' +
|
'<div class="modal-body">' +
|
||||||
(p.image ? '<img src="' + p.image + '" onerror="this.style.display=\'none\'">' : '') +
|
'<div style="width:100%;height:200px;border-radius:12px;margin-bottom:16px;display:flex;align-items:center;justify-content:center;font-size:72px;background:var(--surface)">' + (p.icon || '🔌') + '</div>' +
|
||||||
'<div class="modal-info">' +
|
'<div class="modal-info">' +
|
||||||
'<div class="modal-info-item"><div class="label">分类</div><div class="value">' + getCatName(p.category_id) + '</div></div>' +
|
'<div class="modal-info-item"><div class="label">分类</div><div class="value">' + p.catName + '</div></div>' +
|
||||||
'<div class="modal-info-item"><div class="label">价格</div><div class="value" style="color:var(--primary);font-size:18px">' + fmtPrice(p.price) + '</div></div>' +
|
'<div class="modal-info-item"><div class="label">价格</div><div class="value" style="color:var(--primary);font-size:18px">' + fmtPrice(p.price) + '</div></div>' +
|
||||||
'<div class="modal-info-item"><div class="label">评分</div><div class="value">' + starsHTML(p.rating || 0) + ' ' + (p.rating || 0).toFixed(1) + '</div></div>' +
|
'<div class="modal-info-item"><div class="label">评分</div><div class="value">' + starsHTML(p.rating) + ' ' + p.rating.toFixed(1) + '</div></div>' +
|
||||||
'<div class="modal-info-item"><div class="label">销量</div><div class="value">' + (p.sales || 0) + ' 件</div></div>' +
|
'<div class="modal-info-item"><div class="label">销量</div><div class="value">' + p.sales.toLocaleString() + ' 件</div></div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="modal-desc">' + (p.description || '暂无描述') + '</div>' +
|
'<div class="modal-desc">' + p.desc + '</div>' +
|
||||||
'<div class="modal-actions">' +
|
'<div class="modal-actions">' +
|
||||||
'<button class="btn-primary" style="flex:1" onclick="addToCart(' + p.id + ',\'' + (p.name || '').replace(/'/g, '') + '\',' + (p.price || 0) + ',\'' + (p.image || '') + '\');closeModal()">加入购物车</button>' +
|
'<button class="btn-primary" style="flex:1" onclick="addToCart(' + p.id + ');closeModal()">加入购物车</button>' +
|
||||||
'<button class="btn-ghost" onclick="closeModal()">关闭</button>' +
|
'<button class="btn-ghost" onclick="closeModal()">关闭</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
@@ -527,11 +524,13 @@ async function showDetail(id) {
|
|||||||
function closeModal() { document.getElementById('modalOverlay').classList.remove('active'); }
|
function closeModal() { document.getElementById('modalOverlay').classList.remove('active'); }
|
||||||
|
|
||||||
// ========== 购物车 ==========
|
// ========== 购物车 ==========
|
||||||
function addToCart(id, name, price, image) {
|
function addToCart(id) {
|
||||||
|
var p = allProducts.find(function(x){ return x.id === id; });
|
||||||
|
if (!p) return;
|
||||||
var existing = cart.find(function(c){ return c.id === id; });
|
var existing = cart.find(function(c){ return c.id === id; });
|
||||||
if (existing) { existing.qty++; } else { cart.push({ id: id, name: name, price: price, image: image, qty: 1 }); }
|
if (existing) { existing.qty++; } else { cart.push({ id: id, name: p.name, price: p.price, icon: p.icon, qty: 1 }); }
|
||||||
updateCartUI();
|
updateCartUI();
|
||||||
showToast('已加入购物车: ' + name);
|
showToast('已加入购物车: ' + p.name);
|
||||||
}
|
}
|
||||||
function updateCartUI() {
|
function updateCartUI() {
|
||||||
var badge = document.getElementById('cartBadge');
|
var badge = document.getElementById('cartBadge');
|
||||||
@@ -545,7 +544,7 @@ function updateCartUI() {
|
|||||||
} else {
|
} else {
|
||||||
items.innerHTML = cart.map(function(c) {
|
items.innerHTML = cart.map(function(c) {
|
||||||
return '<div class="cart-item">' +
|
return '<div class="cart-item">' +
|
||||||
'<div class="cart-item-img">' + (c.image ? '<img src="' + c.image + '" onerror="this.style.display=\'none\'">' : '📦') + '</div>' +
|
'<div class="cart-item-img">' + (c.icon || '🔌') + '</div>' +
|
||||||
'<div class="cart-item-info">' +
|
'<div class="cart-item-info">' +
|
||||||
'<div class="cart-item-name">' + c.name + '</div>' +
|
'<div class="cart-item-name">' + c.name + '</div>' +
|
||||||
'<div class="cart-item-price">' + fmtPrice(c.price) + '</div>' +
|
'<div class="cart-item-price">' + fmtPrice(c.price) + '</div>' +
|
||||||
@@ -574,12 +573,18 @@ function toggleCart() {
|
|||||||
document.getElementById('cartDrawer').classList.toggle('active');
|
document.getElementById('cartDrawer').classList.toggle('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 古树 Canvas 壁画 + 光追生命力 ==========
|
// ========== AAA级古树 Canvas 壁画 + 光追生命力 ==========
|
||||||
(function() {
|
(function() {
|
||||||
var canvas = document.getElementById('treeCanvas');
|
var canvas = document.getElementById('treeCanvas');
|
||||||
var ctx = canvas.getContext('2d');
|
var ctx = canvas.getContext('2d');
|
||||||
var W = 0, H = 0, dpr = window.devicePixelRatio || 1;
|
var W = 0, H = 0, dpr = window.devicePixelRatio || 1;
|
||||||
var mx = 0, my = 0, hasMouse = false;
|
var mx = 0, my = 0, hasMouse = false;
|
||||||
|
var branches = [];
|
||||||
|
var leafClusters = [];
|
||||||
|
var fireflies = [];
|
||||||
|
var energyPulses = []; // 沿树干流动的能量脉冲
|
||||||
|
var spores = []; // 大气孢子粒子
|
||||||
|
var godRays = []; // 体积光
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
var rect = canvas.getBoundingClientRect();
|
var rect = canvas.getBoundingClientRect();
|
||||||
@@ -588,72 +593,135 @@ function toggleCart() {
|
|||||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
mx = W / 2; my = H / 2;
|
mx = W / 2; my = H / 2;
|
||||||
buildTree();
|
buildTree();
|
||||||
|
initAtmosphere();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建树形结构
|
// ========== 构建枝繁叶茂的大树 ==========
|
||||||
var branches = [];
|
|
||||||
var leaves = [];
|
|
||||||
var fireflies = []; // 萤火虫粒子(生命力)
|
|
||||||
|
|
||||||
function buildTree() {
|
function buildTree() {
|
||||||
branches = [];
|
branches = [];
|
||||||
leaves = [];
|
leafClusters = [];
|
||||||
fireflies = [];
|
fireflies = [];
|
||||||
|
energyPulses = [];
|
||||||
var trunkX = W * 0.5;
|
var trunkX = W * 0.5;
|
||||||
var trunkY = H * 0.95;
|
var trunkY = H * 0.92;
|
||||||
var trunkH = H * 0.35;
|
var trunkH = H * 0.32;
|
||||||
|
|
||||||
// 递归生成树枝
|
function branch(x, y, angle, length, depth, width, parentIdx) {
|
||||||
function branch(x, y, angle, length, depth, width) {
|
if (depth <= 0 || length < 6) {
|
||||||
if (depth <= 0 || length < 8) {
|
// 枝末生成大量叶子簇
|
||||||
// 叶子
|
var clusterSize = 4 + Math.floor(Math.random() * 4);
|
||||||
for (var i = 0; i < 3; i++) {
|
for (var i = 0; i < clusterSize; i++) {
|
||||||
leaves.push({
|
var lx = x + (Math.random() - 0.5) * 40;
|
||||||
x: x + (Math.random() - 0.5) * 30,
|
var ly = y + (Math.random() - 0.5) * 35;
|
||||||
y: y + (Math.random() - 0.5) * 30,
|
var leaves = 3 + Math.floor(Math.random() * 4);
|
||||||
size: Math.random() * 4 + 2,
|
for (var j = 0; j < leaves; j++) {
|
||||||
hue: 100 + Math.random() * 60,
|
leafClusters.push({
|
||||||
alpha: 0.3 + Math.random() * 0.3,
|
x: lx + (Math.random() - 0.5) * 18,
|
||||||
sway: Math.random() * Math.PI * 2
|
y: ly + (Math.random() - 0.5) * 18,
|
||||||
});
|
size: 2 + Math.random() * 5,
|
||||||
|
hue: 95 + Math.random() * 55,
|
||||||
|
sat: 45 + Math.random() * 25,
|
||||||
|
lit: 35 + Math.random() * 20,
|
||||||
|
alpha: 0.35 + Math.random() * 0.3,
|
||||||
|
sway: Math.random() * Math.PI * 2,
|
||||||
|
swaySpeed: 0.5 + Math.random() * 0.5
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ex = x + Math.cos(angle) * length;
|
var ex = x + Math.cos(angle) * length;
|
||||||
var ey = y + Math.sin(angle) * length;
|
var ey = y + Math.sin(angle) * length;
|
||||||
branches.push({ x1: x, y1: y, x2: ex, y2: ey, width: width, depth: depth });
|
var branchIdx = branches.length;
|
||||||
|
branches.push({
|
||||||
// 萤火虫沿枝干分布
|
x1: x, y1: y, x2: ex, y2: ey,
|
||||||
fireflies.push({
|
width: width, depth: depth,
|
||||||
x: x, y: y, baseX: x, baseY: y,
|
parentIdx: parentIdx,
|
||||||
tx: ex, ty: ey,
|
sway: Math.random() * Math.PI * 2,
|
||||||
progress: Math.random(),
|
swayAmount: (8 - depth) * 0.5,
|
||||||
speed: 0.002 + Math.random() * 0.003,
|
baseAngle: angle
|
||||||
size: 1.5 + Math.random() * 2,
|
|
||||||
hue: 120 + Math.random() * 60,
|
|
||||||
offset: Math.random() * Math.PI * 2
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var nChildren = depth > 3 ? 2 : (Math.random() < 0.3 ? 3 : 2);
|
// 沿枝干放置萤火虫
|
||||||
|
var fireflyCount = depth > 4 ? 2 : 1;
|
||||||
|
for (var f = 0; f < fireflyCount; f++) {
|
||||||
|
fireflies.push({
|
||||||
|
x: x, y: y, baseX: x, baseY: y,
|
||||||
|
tx: ex, ty: ey,
|
||||||
|
branchIdx: branchIdx,
|
||||||
|
progress: Math.random(),
|
||||||
|
speed: 0.0015 + Math.random() * 0.003,
|
||||||
|
size: 1 + Math.random() * 2,
|
||||||
|
hue: 110 + Math.random() * 50,
|
||||||
|
offset: Math.random() * Math.PI * 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 能量脉冲沿枝干流动
|
||||||
|
if (depth >= 3 && Math.random() < 0.4) {
|
||||||
|
energyPulses.push({
|
||||||
|
branchIdx: branchIdx,
|
||||||
|
progress: Math.random(),
|
||||||
|
speed: 0.005 + Math.random() * 0.008,
|
||||||
|
size: 2 + Math.random() * 2,
|
||||||
|
life: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分叉
|
||||||
|
var nChildren = depth > 5 ? 2 : (Math.random() < 0.35 ? 3 : 2);
|
||||||
for (var i = 0; i < nChildren; i++) {
|
for (var i = 0; i < nChildren; i++) {
|
||||||
var spread = 0.35 + Math.random() * 0.2;
|
var spread = 0.3 + Math.random() * 0.25;
|
||||||
var newAngle = angle + (i / (nChildren - 1) - 0.5) * spread * 2 + (Math.random() - 0.5) * 0.15;
|
var newAngle = angle + (i / (nChildren - 1) - 0.5) * spread * 2 + (Math.random() - 0.5) * 0.12;
|
||||||
var newLen = length * (0.65 + Math.random() * 0.15);
|
var newLen = length * (0.68 + Math.random() * 0.12);
|
||||||
branch(ex, ey, newAngle, newLen, depth - 1, width * 0.7);
|
branch(ex, ey, newAngle, newLen, depth - 1, width * 0.72, branchIdx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主干
|
// 主干 — 更深递归获得更多枝叶
|
||||||
branch(trunkX, trunkY, -Math.PI / 2, trunkH, 7, 14);
|
branch(trunkX, trunkY, -Math.PI / 2, trunkH, 8, 16, -1);
|
||||||
|
|
||||||
// 树根
|
// 树根系统
|
||||||
for (var r = 0; r < 5; r++) {
|
for (var r = 0; r < 6; r++) {
|
||||||
var rAngle = -Math.PI / 2 + (r - 2) * 0.3;
|
var rAngle = -Math.PI / 2 + (r - 2.5) * 0.25;
|
||||||
var rLen = trunkH * 0.25;
|
var rLen = trunkH * 0.3;
|
||||||
branches.push({ x1: trunkX, y1: trunkY, x2: trunkX + Math.cos(rAngle) * rLen, y2: trunkY + Math.sin(rAngle) * rLen * 0.3, width: 6, depth: 0, isRoot: true });
|
branches.push({
|
||||||
|
x1: trunkX, y1: trunkY,
|
||||||
|
x2: trunkX + Math.cos(rAngle) * rLen,
|
||||||
|
y2: trunkY + Math.sin(rAngle) * rLen * 0.2,
|
||||||
|
width: 7, depth: 0, isRoot: true,
|
||||||
|
sway: 0, swayAmount: 0, baseAngle: rAngle
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 大气粒子 ==========
|
||||||
|
function initAtmosphere() {
|
||||||
|
spores = [];
|
||||||
|
for (var i = 0; i < 40; i++) {
|
||||||
|
spores.push({
|
||||||
|
x: Math.random() * W,
|
||||||
|
y: Math.random() * H,
|
||||||
|
vx: (Math.random() - 0.5) * 0.3,
|
||||||
|
vy: -0.1 - Math.random() * 0.3,
|
||||||
|
size: 0.5 + Math.random() * 1.5,
|
||||||
|
alpha: 0.1 + Math.random() * 0.3,
|
||||||
|
hue: 90 + Math.random() * 40
|
||||||
|
});
|
||||||
|
}
|
||||||
|
godRays = [];
|
||||||
|
for (var i = 0; i < 5; i++) {
|
||||||
|
godRays.push({
|
||||||
|
x: W * (0.2 + i * 0.15),
|
||||||
|
angle: -Math.PI / 2 + (Math.random() - 0.5) * 0.3,
|
||||||
|
width: 80 + Math.random() * 60,
|
||||||
|
opacity: 0.04 + Math.random() * 0.04
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 鼠标交互 ==========
|
||||||
canvas.parentElement.addEventListener('mousemove', function(e) {
|
canvas.parentElement.addEventListener('mousemove', function(e) {
|
||||||
var rect = canvas.getBoundingClientRect();
|
var rect = canvas.getBoundingClientRect();
|
||||||
mx = e.clientX - rect.left;
|
mx = e.clientX - rect.left;
|
||||||
@@ -662,137 +730,286 @@ function toggleCart() {
|
|||||||
});
|
});
|
||||||
canvas.parentElement.addEventListener('mouseleave', function() { hasMouse = false; });
|
canvas.parentElement.addEventListener('mouseleave', function() { hasMouse = false; });
|
||||||
|
|
||||||
|
// ========== 主渲染循环 ==========
|
||||||
function drawTree() {
|
function drawTree() {
|
||||||
// 背景渐变
|
var isDark = document.body.getAttribute('data-theme') === 'dark';
|
||||||
var bg = ctx.createRadialGradient(W * 0.5, H * 0.7, 0, W * 0.5, H * 0.7, W * 0.8);
|
|
||||||
bg.addColorStop(0, 'rgba(15,25,15,0.12)');
|
|
||||||
bg.addColorStop(1, 'rgba(10,15,10,0.15)');
|
|
||||||
ctx.fillStyle = 'rgba(10,15,10,0.12)';
|
|
||||||
ctx.fillRect(0, 0, W, H);
|
|
||||||
ctx.fillStyle = bg;
|
|
||||||
ctx.fillRect(0, 0, W, H);
|
|
||||||
|
|
||||||
var time = Date.now() * 0.001;
|
var time = Date.now() * 0.001;
|
||||||
|
|
||||||
// 绘制树枝
|
// === 1. 背景天空渐变 ===
|
||||||
|
var skyGrad = ctx.createLinearGradient(0, 0, 0, H);
|
||||||
|
if (isDark) {
|
||||||
|
skyGrad.addColorStop(0, '#040806');
|
||||||
|
skyGrad.addColorStop(0.3, '#080f0a');
|
||||||
|
skyGrad.addColorStop(0.6, '#0a120a');
|
||||||
|
skyGrad.addColorStop(1, '#060a06');
|
||||||
|
} else {
|
||||||
|
skyGrad.addColorStop(0, '#e8e4d8');
|
||||||
|
skyGrad.addColorStop(0.3, '#f0ede2');
|
||||||
|
skyGrad.addColorStop(0.6, '#f5f3ec');
|
||||||
|
skyGrad.addColorStop(1, '#efece2');
|
||||||
|
}
|
||||||
|
ctx.fillStyle = skyGrad;
|
||||||
|
ctx.fillRect(0, 0, W, H);
|
||||||
|
|
||||||
|
// === 2. 体积光(God Rays)===
|
||||||
|
if (isDark) {
|
||||||
|
godRays.forEach(function(gr) {
|
||||||
|
var len = H * 0.9;
|
||||||
|
var ex = gr.x + Math.cos(gr.angle) * len;
|
||||||
|
var ey = Math.sin(gr.angle) * len;
|
||||||
|
var rg = ctx.createLinearGradient(gr.x, 0, ex, ey);
|
||||||
|
rg.addColorStop(0, 'rgba(180,220,140,' + gr.opacity + ')');
|
||||||
|
rg.addColorStop(0.5, 'rgba(150,200,120,' + (gr.opacity * 0.5) + ')');
|
||||||
|
rg.addColorStop(1, 'transparent');
|
||||||
|
ctx.fillStyle = rg;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(gr.x - gr.width / 2, 0);
|
||||||
|
ctx.lineTo(gr.x + gr.width / 2, 0);
|
||||||
|
ctx.lineTo(ex + gr.width, ey);
|
||||||
|
ctx.lineTo(ex - gr.width, ey);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 3. 地面雾气 ===
|
||||||
|
var fogGrad = ctx.createLinearGradient(0, H * 0.6, 0, H);
|
||||||
|
if (isDark) {
|
||||||
|
fogGrad.addColorStop(0, 'transparent');
|
||||||
|
fogGrad.addColorStop(1, 'rgba(20,35,18,0.5)');
|
||||||
|
} else {
|
||||||
|
fogGrad.addColorStop(0, 'transparent');
|
||||||
|
fogGrad.addColorStop(1, 'rgba(180,175,155,0.3)');
|
||||||
|
}
|
||||||
|
ctx.fillStyle = fogGrad;
|
||||||
|
ctx.fillRect(0, H * 0.6, W, H * 0.4);
|
||||||
|
|
||||||
|
// === 4. 绘制树枝(带摇摆和深度感)===
|
||||||
branches.forEach(function(b) {
|
branches.forEach(function(b) {
|
||||||
|
var swayX = 0, swayY = 0;
|
||||||
|
if (!b.isRoot && b.swayAmount > 0) {
|
||||||
|
swayX = Math.sin(time * 0.8 + b.sway) * b.swayAmount;
|
||||||
|
swayY = Math.cos(time * 0.6 + b.sway) * b.swayAmount * 0.3;
|
||||||
|
}
|
||||||
|
b._sx2 = b.x2 + swayX;
|
||||||
|
b._sy2 = b.y2 + swayY;
|
||||||
|
|
||||||
|
// 鼠标光照增强
|
||||||
|
var midX = (b.x1 + b._sx2) / 2;
|
||||||
|
var midY = (b.y1 + b._sy2) / 2;
|
||||||
|
var dx = mx - midX, dy = my - midY;
|
||||||
|
var dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
var lightBoost = hasMouse && dist < 180 ? (1 - dist / 180) : 0;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(b.x1, b.y1);
|
ctx.moveTo(b.x1, b.y1);
|
||||||
ctx.lineTo(b.x2, b.y2);
|
ctx.lineTo(b._sx2, b._sy2);
|
||||||
var isDark = document.body.getAttribute('data-theme') === 'dark';
|
|
||||||
ctx.strokeStyle = b.isRoot ? (isDark ? 'rgba(80,60,40,0.6)' : 'rgba(100,80,50,0.4)') : (isDark ? 'rgba(100,80,60,0.7)' : 'rgba(120,100,70,0.5)');
|
var baseColor, r, g, bl;
|
||||||
|
if (b.isRoot) {
|
||||||
|
r = isDark ? 60 + lightBoost * 40 : 90;
|
||||||
|
g = isDark ? 45 + lightBoost * 30 : 70;
|
||||||
|
bl = isDark ? 30 + lightBoost * 20 : 45;
|
||||||
|
} else {
|
||||||
|
var depthFactor = b.depth / 8;
|
||||||
|
r = (isDark ? 80 : 110) + lightBoost * 50 + depthFactor * 20;
|
||||||
|
g = (isDark ? 65 : 90) + lightBoost * 35 + depthFactor * 15;
|
||||||
|
bl = (isDark ? 45 : 60) + lightBoost * 20;
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = 'rgba(' + Math.round(r) + ',' + Math.round(g) + ',' + Math.round(bl) + ',' + (0.65 + lightBoost * 0.25) + ')';
|
||||||
ctx.lineWidth = b.width;
|
ctx.lineWidth = b.width;
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 树干高光
|
||||||
|
if (b.depth >= 6 && !b.isRoot) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(b.x1 - b.width * 0.2, b.y1);
|
||||||
|
ctx.lineTo(b._sx2 - b.width * 0.2, b._sy2);
|
||||||
|
ctx.strokeStyle = 'rgba(' + Math.round(r + 30) + ',' + Math.round(g + 25) + ',' + Math.round(bl + 15) + ',0.3)';
|
||||||
|
ctx.lineWidth = b.width * 0.3;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 绘制叶子
|
// === 5. 绘制叶簇(枝繁叶茂)===
|
||||||
leaves.forEach(function(l) {
|
leafClusters.forEach(function(l) {
|
||||||
var sway = Math.sin(time + l.sway) * 3;
|
var sway = Math.sin(time * l.swaySpeed + l.sway) * 3;
|
||||||
var x = l.x + sway;
|
var x = l.x + sway;
|
||||||
var y = l.y + Math.cos(time * 0.7 + l.sway) * 2;
|
var y = l.y + Math.cos(time * 0.7 + l.sway) * 2;
|
||||||
|
|
||||||
// 鼠标附近叶子发光
|
// 鼠标附近叶子发光
|
||||||
var dx = mx - x, dy = my - y;
|
var dx = mx - x, dy = my - y;
|
||||||
var dist = Math.sqrt(dx * dx + dy * dy);
|
var dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
var glow = hasMouse && dist < 150 ? (1 - dist / 150) : 0;
|
var glow = hasMouse && dist < 160 ? (1 - dist / 160) : 0;
|
||||||
|
|
||||||
|
var lit = l.lit + glow * 25;
|
||||||
|
var alpha = l.alpha + glow * 0.4;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, l.size, 0, Math.PI * 2);
|
ctx.arc(x, y, l.size, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = 'hsla(' + l.hue + ',60%,' + (40 + glow * 30) + '%,' + (l.alpha + glow * 0.4) + ')';
|
ctx.fillStyle = 'hsla(' + l.hue + ',' + l.sat + '%,' + lit + '%,' + alpha + ')';
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
if (glow > 0.3) {
|
// 发光叶片
|
||||||
|
if (glow > 0.2) {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, l.size * 3, 0, Math.PI * 2);
|
ctx.arc(x, y, l.size * 2.5, 0, Math.PI * 2);
|
||||||
var lg = ctx.createRadialGradient(x, y, 0, x, y, l.size * 3);
|
var lg = ctx.createRadialGradient(x, y, 0, x, y, l.size * 2.5);
|
||||||
lg.addColorStop(0, 'hsla(' + l.hue + ',80%,60%,' + (glow * 0.3) + ')');
|
lg.addColorStop(0, 'hsla(' + l.hue + ',70%,55%,' + (glow * 0.35) + ')');
|
||||||
lg.addColorStop(1, 'transparent');
|
lg.addColorStop(1, 'transparent');
|
||||||
ctx.fillStyle = lg;
|
ctx.fillStyle = lg;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 绘制萤火虫(生命力流动)
|
// === 6. 能量脉冲沿树干流动(生命力)===
|
||||||
|
energyPulses.forEach(function(ep) {
|
||||||
|
var b = branches[ep.branchIdx];
|
||||||
|
if (!b || !b._sx2) return;
|
||||||
|
ep.progress += ep.speed;
|
||||||
|
if (ep.progress > 1) { ep.progress = 0; ep.life = 1; }
|
||||||
|
|
||||||
|
var px = b.x1 + (b._sx2 - b.x1) * ep.progress;
|
||||||
|
var py = b.y1 + (b._sy2 - b.y1) * ep.progress;
|
||||||
|
|
||||||
|
// 鼠标附近的脉冲更亮
|
||||||
|
var dx = mx - px, dy = my - py;
|
||||||
|
var dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
var boost = hasMouse && dist < 250 ? (1 - dist / 250) : 0;
|
||||||
|
var brightness = 0.5 + boost * 0.5;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(px, py, ep.size * (1 + boost), 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = 'rgba(150,255,120,' + brightness + ')';
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(px, py, ep.size * 4, 0, Math.PI * 2);
|
||||||
|
var pg = ctx.createRadialGradient(px, py, 0, px, py, ep.size * 4);
|
||||||
|
pg.addColorStop(0, 'rgba(120,255,100,' + (brightness * 0.4) + ')');
|
||||||
|
pg.addColorStop(1, 'transparent');
|
||||||
|
ctx.fillStyle = pg;
|
||||||
|
ctx.fill();
|
||||||
|
});
|
||||||
|
|
||||||
|
// === 7. 萤火虫粒子(生命力流动)===
|
||||||
fireflies.forEach(function(f) {
|
fireflies.forEach(function(f) {
|
||||||
f.progress += f.speed;
|
f.progress += f.speed;
|
||||||
if (f.progress > 1) f.progress = 0;
|
if (f.progress > 1) f.progress = 0;
|
||||||
|
|
||||||
// 沿枝干流动
|
var b = branches[f.branchIdx];
|
||||||
var px = f.baseX + (f.tx - f.baseX) * f.progress;
|
if (!b || !b._sx2) return;
|
||||||
var py = f.baseY + (f.ty - f.baseY) * f.progress;
|
|
||||||
// 添加浮动
|
|
||||||
px += Math.sin(time * 2 + f.offset) * 4;
|
|
||||||
py += Math.cos(time * 1.5 + f.offset) * 3;
|
|
||||||
|
|
||||||
// 鼠标光追 — 鼠标附近的萤火虫被激活,变亮变大
|
var px = b.x1 + (b._sx2 - b.x1) * f.progress;
|
||||||
|
var py = b.y1 + (b._sy2 - b.y1) * f.progress;
|
||||||
|
px += Math.sin(time * 2 + f.offset) * 3;
|
||||||
|
py += Math.cos(time * 1.5 + f.offset) * 2;
|
||||||
|
|
||||||
|
// 鼠标光追激活
|
||||||
var dx = mx - px, dy = my - py;
|
var dx = mx - px, dy = my - py;
|
||||||
var dist = Math.sqrt(dx * dx + dy * dy);
|
var dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
var activate = hasMouse && dist < 200 ? (1 - dist / 200) : 0;
|
var activate = hasMouse && dist < 220 ? (1 - dist / 220) : 0;
|
||||||
|
|
||||||
var brightness = 0.4 + activate * 0.6;
|
var brightness = 0.35 + activate * 0.65;
|
||||||
var size = f.size * (1 + activate * 1.5);
|
var size = f.size * (1 + activate * 1.8);
|
||||||
var hue = activate > 0.3 ? 60 + activate * 30 : f.hue; // 激活时变金黄色
|
var hue = activate > 0.3 ? 50 + activate * 30 : f.hue;
|
||||||
|
|
||||||
// 萤火虫核心
|
// 萤火虫核心
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(px, py, size, 0, Math.PI * 2);
|
ctx.arc(px, py, size, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = 'hsla(' + hue + ',90%,' + (55 + activate * 25) + '%,' + brightness + ')';
|
ctx.fillStyle = 'hsla(' + hue + ',95%,' + (55 + activate * 20) + '%,' + brightness + ')';
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// 光晕
|
// 光晕
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(px, py, size * 4, 0, Math.PI * 2);
|
ctx.arc(px, py, size * 5, 0, Math.PI * 2);
|
||||||
var gl = ctx.createRadialGradient(px, py, 0, px, py, size * 4);
|
var gl = ctx.createRadialGradient(px, py, 0, px, py, size * 5);
|
||||||
gl.addColorStop(0, 'hsla(' + hue + ',90%,60%,' + (brightness * 0.3) + ')');
|
gl.addColorStop(0, 'hsla(' + hue + ',90%,60%,' + (brightness * 0.35) + ')');
|
||||||
gl.addColorStop(1, 'transparent');
|
gl.addColorStop(1, 'transparent');
|
||||||
ctx.fillStyle = gl;
|
ctx.fillStyle = gl;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// 鼠标光追连线
|
// 鼠标光追连线 — 生命力从鼠标流向树
|
||||||
if (activate > 0.2) {
|
if (activate > 0.15) {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(px, py);
|
ctx.moveTo(px, py);
|
||||||
ctx.lineTo(mx, my);
|
ctx.lineTo(mx, my);
|
||||||
ctx.strokeStyle = 'hsla(' + hue + ',80%,60%,' + (activate * 0.25) + ')';
|
var lg = ctx.createLinearGradient(px, py, mx, my);
|
||||||
ctx.lineWidth = 0.8;
|
lg.addColorStop(0, 'hsla(' + hue + ',80%,60%,' + (activate * 0.3) + ')');
|
||||||
|
lg.addColorStop(1, 'hsla(' + hue + ',80%,70%,' + (activate * 0.05) + ')');
|
||||||
|
ctx.strokeStyle = lg;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 鼠标位置光追效果 — 生命力的源头
|
// === 8. 大气孢子粒子 ===
|
||||||
|
spores.forEach(function(s) {
|
||||||
|
s.x += s.vx;
|
||||||
|
s.y += s.vy;
|
||||||
|
if (s.y < -10) { s.y = H + 10; s.x = Math.random() * W; }
|
||||||
|
if (s.x < -10) s.x = W + 10;
|
||||||
|
if (s.x > W + 10) s.x = -10;
|
||||||
|
|
||||||
|
var dx = mx - s.x, dy = my - s.y;
|
||||||
|
var dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
var boost = hasMouse && dist < 150 ? (1 - dist / 150) : 0;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(s.x, s.y, s.size * (1 + boost), 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = 'hsla(' + s.hue + ',60%,70%,' + (s.alpha + boost * 0.3) + ')';
|
||||||
|
ctx.fill();
|
||||||
|
});
|
||||||
|
|
||||||
|
// === 9. 鼠标光源 — 生命力的源头 ===
|
||||||
if (hasMouse) {
|
if (hasMouse) {
|
||||||
// 鼠标处光点
|
// 外层大光晕
|
||||||
|
var outerGlow = ctx.createRadialGradient(mx, my, 0, mx, my, 180);
|
||||||
|
outerGlow.addColorStop(0, 'rgba(140,255,110,0.12)');
|
||||||
|
outerGlow.addColorStop(0.4, 'rgba(120,220,90,0.05)');
|
||||||
|
outerGlow.addColorStop(1, 'transparent');
|
||||||
|
ctx.fillStyle = outerGlow;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(mx, my, 3, 0, Math.PI * 2);
|
ctx.arc(mx, my, 180, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = 'rgba(180,255,150,0.9)';
|
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// 光晕
|
// 中层光晕
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(mx, my, 40, 0, Math.PI * 2);
|
ctx.arc(mx, my, 60, 0, Math.PI * 2);
|
||||||
var mg = ctx.createRadialGradient(mx, my, 0, mx, my, 40);
|
var midGlow = ctx.createRadialGradient(mx, my, 0, mx, my, 60);
|
||||||
mg.addColorStop(0, 'rgba(120,255,100,0.15)');
|
midGlow.addColorStop(0, 'rgba(160,255,120,0.25)');
|
||||||
mg.addColorStop(1, 'transparent');
|
midGlow.addColorStop(1, 'transparent');
|
||||||
ctx.fillStyle = mg;
|
ctx.fillStyle = midGlow;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// 放射光线
|
// 旋转放射光线
|
||||||
for (var k = 0; k < 6; k++) {
|
for (var k = 0; k < 8; k++) {
|
||||||
var angle = (k / 6) * Math.PI * 2 + time * 0.3;
|
var angle = (k / 8) * Math.PI * 2 + time * 0.25;
|
||||||
var len = 60 + Math.sin(time * 2 + k) * 15;
|
var len = 80 + Math.sin(time * 2 + k) * 20;
|
||||||
var ex = mx + Math.cos(angle) * len;
|
var ex = mx + Math.cos(angle) * len;
|
||||||
var ey = my + Math.sin(angle) * len;
|
var ey = my + Math.sin(angle) * len;
|
||||||
var rg = ctx.createLinearGradient(mx, my, ex, ey);
|
var rg = ctx.createLinearGradient(mx, my, ex, ey);
|
||||||
rg.addColorStop(0, 'rgba(120,255,100,0.12)');
|
rg.addColorStop(0, 'rgba(140,255,100,0.15)');
|
||||||
rg.addColorStop(1, 'transparent');
|
rg.addColorStop(1, 'transparent');
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(mx, my);
|
ctx.moveTo(mx, my);
|
||||||
ctx.lineTo(ex, ey);
|
ctx.lineTo(ex, ey);
|
||||||
ctx.strokeStyle = rg;
|
ctx.strokeStyle = rg;
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 2;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 核心光点
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(mx, my, 4, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = 'rgba(200,255,170,0.95)';
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(mx, my, 8, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = 'rgba(160,255,130,0.4)';
|
||||||
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(drawTree);
|
requestAnimationFrame(drawTree);
|
||||||
@@ -818,28 +1035,43 @@ function toggleCart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
|
var isDark = document.body.getAttribute('data-theme') === 'dark';
|
||||||
ctx.clearRect(0, 0, W, H);
|
ctx.clearRect(0, 0, W, H);
|
||||||
var trunkX = W * 0.5, trunkY = H * 0.98;
|
var trunkX = W * 0.5, trunkY = H * 0.98;
|
||||||
var trunkH = H * 0.4;
|
var trunkH = H * 0.45;
|
||||||
|
|
||||||
|
// 背景渐变
|
||||||
|
var bgGrad = ctx.createRadialGradient(W * 0.5, H * 0.6, 0, W * 0.5, H * 0.6, W * 0.7);
|
||||||
|
if (isDark) {
|
||||||
|
bgGrad.addColorStop(0, 'rgba(15,25,15,0.15)');
|
||||||
|
bgGrad.addColorStop(1, 'rgba(8,12,8,0.1)');
|
||||||
|
} else {
|
||||||
|
bgGrad.addColorStop(0, 'rgba(200,195,175,0.12)');
|
||||||
|
bgGrad.addColorStop(1, 'rgba(180,175,155,0.08)');
|
||||||
|
}
|
||||||
|
ctx.fillStyle = bgGrad;
|
||||||
|
ctx.fillRect(0, 0, W, H);
|
||||||
|
|
||||||
function branch(x, y, angle, length, depth, width) {
|
function branch(x, y, angle, length, depth, width) {
|
||||||
if (depth <= 0 || length < 6) return;
|
if (depth <= 0 || length < 5) {
|
||||||
|
// 叶簇
|
||||||
|
for (var i = 0; i < 5; i++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x + (Math.random() - 0.5) * 30, y + (Math.random() - 0.5) * 25, 3 + Math.random() * 4, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = isDark ? 'rgba(80,130,60,0.12)' : 'rgba(100,140,70,0.08)';
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
var ex = x + Math.cos(angle) * length;
|
var ex = x + Math.cos(angle) * length;
|
||||||
var ey = y + Math.sin(angle) * length;
|
var ey = y + Math.sin(angle) * length;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(x, y);
|
ctx.moveTo(x, y);
|
||||||
ctx.lineTo(ex, ey);
|
ctx.lineTo(ex, ey);
|
||||||
var isDark = document.body.getAttribute('data-theme') === 'dark';
|
ctx.strokeStyle = isDark ? 'rgba(90,70,50,0.2)' : 'rgba(100,85,60,0.12)';
|
||||||
ctx.strokeStyle = isDark ? 'rgba(90,70,50,0.3)' : 'rgba(100,85,60,0.2)';
|
|
||||||
ctx.lineWidth = width;
|
ctx.lineWidth = width;
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
if (depth <= 2) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(ex, ey, 5, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = isDark ? 'rgba(100,180,80,0.15)' : 'rgba(80,150,60,0.1)';
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
var n = 2;
|
var n = 2;
|
||||||
for (var i = 0; i < n; i++) {
|
for (var i = 0; i < n; i++) {
|
||||||
var a2 = angle + (i - 0.5) * 0.4 + (Math.random() - 0.5) * 0.1;
|
var a2 = angle + (i - 0.5) * 0.4 + (Math.random() - 0.5) * 0.1;
|
||||||
@@ -847,17 +1079,24 @@ function toggleCart() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
branch(trunkX, trunkY, -Math.PI / 2, trunkH, 6, 12);
|
branch(trunkX, trunkY, -Math.PI / 2, trunkH, 7, 14);
|
||||||
|
|
||||||
|
// 模糊层
|
||||||
|
if (canvas.width > 0 && canvas.height > 0) {
|
||||||
|
ctx.filter = 'blur(4px)';
|
||||||
|
ctx.globalAlpha = 0.6;
|
||||||
|
ctx.drawImage(canvas, 0, 0);
|
||||||
|
ctx.filter = 'none';
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resize();
|
resize();
|
||||||
window.addEventListener('resize', resize);
|
window.addEventListener('resize', resize);
|
||||||
// 主题切换时重绘
|
|
||||||
document.body.addEventListener('click', function() { setTimeout(resize, 100); }, true);
|
document.body.addEventListener('click', function() { setTimeout(resize, 100); }, true);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ========== 初始化 ==========
|
// ========== 初始化 ==========
|
||||||
loadCategories();
|
|
||||||
loadProducts();
|
loadProducts();
|
||||||
updateCartUI();
|
updateCartUI();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user