Files
2026-01-11 18:14:42 +08:00

652 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首页 - 荣光奶茶店</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root {
--primary-color: #3b82f6;
--bg-body: #f8fafc;
--text-main: #1e293b;
--text-sub: #64748b;
/* 修改 1: 加大圆角至 24px营造超级圆润的视觉感 */
--radius-box: 24px;
--card-shadow: 0 8px 24px rgba(149, 157, 165, 0.08);
}
body {
background-color: var(--bg-body);
color: var(--text-main);
font-family: -apple-system, "SF Pro Text", Roboto, "Helvetica Neue", sans-serif;
padding-bottom: 80px;
-webkit-font-smoothing: antialiased;
}
/* --- 店铺头部卡片样式 --- */
.store-header-card {
background: #ffffff;
border-radius: var(--radius-box);
padding: 20px;
margin-bottom: 24px;
display: flex;
align-items: center;
box-shadow: var(--card-shadow);
position: relative;
overflow: hidden;
}
.store-header-card::after {
content: '';
position: absolute;
top: -20px;
right: -20px;
width: 100px;
height: 100px;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0) 70%);
border-radius: 50%;
z-index: 0;
}
.store-logo-wrapper {
width: 64px;
height: 64px;
flex-shrink: 0;
margin-right: 16px;
position: relative;
z-index: 1;
}
.store-logo {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
border: 2px solid #f1f5f9;
}
.store-info {
flex: 1;
z-index: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.store-name {
font-size: 20px;
font-weight: 800;
color: var(--text-main);
margin: 0 0 4px 0;
letter-spacing: -0.5px;
}
.store-meta {
font-size: 12px;
color: var(--text-sub);
display: flex;
align-items: center;
gap: 12px;
}
.store-meta span {
display: flex;
align-items: center;
gap: 4px;
}
.store-actions {
margin-left: 8px;
z-index: 1;
}
.btn-icon-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background: #f8fafc;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-main);
border: none;
transition: all 0.2s;
}
.btn-icon-circle:active {
transform: scale(0.9);
background: #e2e8f0;
}
/* --- 基础布局样式 --- */
.section-header {
margin: 24px 0 16px;
padding: 0 4px;
display: flex;
align-items: center;
}
.section-header h2 {
font-size: 18px;
font-weight: 700;
margin: 0;
color: var(--text-main);
}
.section-header i {
margin-right: 8px;
color: var(--primary-color);
}
.banner-carousel {
border-radius: var(--radius-box);
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
margin-bottom: 24px;
background: #fff;
}
.carousel-item {
padding: 20px;
min-height: 120px;
background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%);
cursor: pointer;
}
.carousel-title {
font-size: 18px;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 8px;
}
.carousel-content {
font-size: 14px;
color: #475569;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.carousel-indicators [data-bs-target] {
background-color: var(--primary-color);
width: 6px; height: 6px; border-radius: 50%;
}
/* --- 商品卡片样式 --- */
.product-card {
border: none;
border-radius: var(--radius-box);
background: #fff;
height: 100%;
overflow: hidden;
cursor: pointer;
box-shadow: 0 4px 20px rgba(0,0,0,0.03);
transition: transform 0.2s ease, box-shadow 0.2s ease;
display: flex;
flex-direction: column;
position: relative;
}
.product-card:active {
transform: scale(0.97);
}
/* 修改 2: 图片容器不需要动,但父级的圆角变大会自动裁剪 */
.img-wrapper {
position: relative;
width: 100%;
padding-top: 100%;
background: #f8fafc;
}
.img-wrapper img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 0.3s;
}
/* 修改 3: 稍微增加内边距,配合大圆角,避免文字贴边 */
.card-body {
padding: 14px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.card-main-info {
margin-bottom: 8px;
}
.card-title {
font-size: 15px;
font-weight: 700;
color: var(--text-main);
margin-bottom: 4px;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-desc {
font-size: 11px;
color: #94a3b8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
}
.card-footer-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
}
.price-text {
font-size: 17px;
font-weight: 800;
color: var(--text-main);
letter-spacing: -0.5px;
display: flex;
align-items: baseline;
}
.price-text::before {
content: '¥';
font-size: 11px;
margin-right: 1px;
font-weight: 600;
color: var(--text-main);
}
.btn-add-icon {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--primary-color);
color: white;
border: none;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 10px rgba(59, 130, 246, 0.3);
transition: background 0.2s, transform 0.2s;
}
.btn-add-icon:active {
background: #2563eb;
transform: scale(0.9);
}
.btn-add-icon i {
font-size: 18px;
margin-top: 1px;
}
.mobile-tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 8px 0 20px;
display: flex;
justify-content: space-around;
box-shadow: 0 -1px 0 rgba(0,0,0,0.03);
z-index: 1000;
}
.tab-item {
text-decoration: none;
color: #94a3b8;
display: flex;
flex-direction: column;
align-items: center;
font-size: 10px;
flex: 1;
}
.tab-item i {
font-size: 24px;
margin-bottom: 2px;
}
.tab-item.active {
color: var(--primary-color);
}
/* 模态框也同步变圆润 */
.modal-content {
border: none;
border-radius: 24px;
}
.modal-header {
border-bottom: 1px solid #f1f5f9;
}
/* 加载提示样式 */
.loading-tip, .no-more-tip {
margin-top: 16px;
}
.spin {
display: inline-block;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container mt-3">
<!-- 店铺信息卡片 -->
<div class="store-header-card">
<div class="store-logo-wrapper">
<img src="https://picsum.photos/200/200?random=1" alt="Logo" class="store-logo">
</div>
<div class="store-info">
<h1 class="store-name">荣光奶茶店</h1>
<div class="store-meta">
<span>
<i class="bi bi-geo-alt-fill text-danger"></i> 科技园南区C栋
</span>
<span>
<i class="bi bi-star-fill text-warning"></i> 4.9
</span>
</div>
</div>
<div class="store-actions">
<button class="btn-icon-circle">
<i class="bi bi-telephone-fill" style="font-size: 16px;"></i>
</button>
</div>
</div>
<!-- 公告轮播区 -->
<div id="announcementCarousel" class="carousel slide banner-carousel" data-bs-ride="carousel" style="display: none;">
<div class="carousel-indicators" id="carouselIndicators"></div>
<div class="carousel-inner" id="carouselInner"></div>
</div>
<!-- 推荐区 -->
<div class="section-header">
<i class="bi bi-fire text-danger"></i>
<h2>店长推荐</h2>
</div>
<div id="recommendProducts" class="row gx-3 gy-3"></div>
<!-- 全部商品 -->
<div class="section-header mt-4">
<i class="bi bi-grid-fill"></i>
<h2>全部饮品</h2>
</div>
<div id="allProducts" class="row gx-3 gy-3"></div>
</div>
<!-- 公告模态框 -->
<div class="modal fade" id="announcementModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="announcementModalTitle">公告详情</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="announcementModalBody"></div>
</div>
</div>
</div>
<!-- 逻辑脚本 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// --- 核心逻辑 ---
// 分页状态管理(仅用于全部商品列表)
let currentPage = 1;
let hasMore = true;
let isLoading = false;
const pageSize = 12;
async function loadRecommendations() {
try {
const { data } = await UIEnhancements.enhancedFetch('/recommend?limit=6', { credentials: 'include' });
if (data?.code === 200 && data.data) {
renderProducts(data.data, 'recommendProducts', false);
} else {
showEmpty('recommendProducts', '暂无推荐');
}
} catch (e) { console.error(e); }
}
async function loadProducts(pageNum = 1, append = false) {
if (isLoading) return false;
if (!hasMore && append) return false;
isLoading = true;
try {
const { data } = await UIEnhancements.enhancedFetch(`/products?pageNum=${pageNum}&pageSize=${pageSize}`, { credentials: 'include' });
if (data?.code === 200 && data.data) {
const products = data.data.list || [];
const total = data.data.total || 0;
const totalPages = Math.ceil(total / pageSize);
if (products.length > 0) {
renderProducts(products, 'allProducts', append);
currentPage = pageNum;
hasMore = pageNum < totalPages;
// 移除加载提示
removeLoadingTip();
return true;
} else {
if (!append) {
showEmpty('allProducts', '暂无商品');
} else {
showNoMoreTip();
}
hasMore = false;
return false;
}
} else {
if (!append) {
showEmpty('allProducts', '暂无商品');
}
hasMore = false;
return false;
}
} catch (e) {
console.error(e);
if (!append) {
showEmpty('allProducts', '加载失败');
}
return false;
} finally {
isLoading = false;
}
}
// 更新后的渲染函数
function renderProducts(products, containerId, append = false) {
const container = document.getElementById(containerId);
if (!products?.length) {
if (!append) {
return showEmpty(containerId);
}
return;
}
const html = products.map(p => `
<div class="col-6 col-md-3">
<div class="product-card" onclick="location.href='/product-detail.html?id=${p.id}'">
<div class="img-wrapper">
<img src="${p.image || '/static/images/default.jpg'}" loading="lazy">
</div>
<div class="card-body">
<div class="card-main-info">
<div class="card-title">${p.name}</div>
<div class="card-desc">${p.description || p.category || '暂无描述'}</div>
</div>
<div class="card-footer-row">
<span class="price-text">${p.basePrice}</span>
<button class="btn-add-icon" aria-label="添加">
<i class="bi bi-plus"></i>
</button>
</div>
</div>
</div>
</div>
`).join('');
if (append) {
// 追加模式:在容器末尾追加
container.insertAdjacentHTML('beforeend', html);
} else {
// 替换模式:直接替换内容
container.innerHTML = html;
}
}
// 显示加载提示
function showLoadingTip() {
const container = document.getElementById('allProducts');
const existingTip = container.querySelector('.loading-tip');
if (existingTip) return;
const tip = document.createElement('div');
tip.className = 'loading-tip col-12 text-center text-muted py-3 small';
tip.innerHTML = '<i class="bi bi-arrow-repeat spin"></i> 加载中...';
container.appendChild(tip);
}
// 移除加载提示
function removeLoadingTip() {
const container = document.getElementById('allProducts');
const tip = container.querySelector('.loading-tip');
if (tip) tip.remove();
}
// 显示没有更多数据提示
function showNoMoreTip() {
const container = document.getElementById('allProducts');
const existingTip = container.querySelector('.no-more-tip');
if (existingTip) return;
const tip = document.createElement('div');
tip.className = 'no-more-tip col-12 text-center text-muted py-3 small';
tip.textContent = '没有更多商品了';
container.appendChild(tip);
}
function showEmpty(id, text='暂无数据') {
document.getElementById(id).innerHTML = `<div class="col-12 text-center text-muted py-4 small">${text}</div>`;
}
async function loadAnnouncements() {
try {
const { data } = await UIEnhancements.enhancedFetch('/announcements', { credentials: 'include' });
if (data?.code === 200 && data.data?.length) {
renderAnnouncementsCarousel(data.data);
}
} catch (e) { console.error(e); }
}
function renderAnnouncementsCarousel(list) {
const inner = document.getElementById('carouselInner');
const indicators = document.getElementById('carouselIndicators');
const container = document.getElementById('announcementCarousel');
if (!list.length) return;
container.style.display = 'block';
inner.innerHTML = list.map((a, i) => `
<div class="carousel-item ${i === 0 ? 'active' : ''}" onclick="showAnnouncement(${a.id})">
<div class="d-flex align-items-center mb-2">
<span class="badge ${a.type === 'ACTIVITY' ? 'bg-warning text-dark' : 'bg-primary'} me-2">
${a.type === 'ACTIVITY' ? '活动' : '通知'}
</span>
<div class="carousel-title mb-0">${a.title}</div>
</div>
<div class="carousel-content">${a.content}</div>
</div>
`).join('');
indicators.innerHTML = list.map((_, i) => `
<button type="button" data-bs-target="#announcementCarousel" data-bs-slide-to="${i}"
class="${i === 0 ? 'active' : ''}"></button>
`).join('');
}
async function showAnnouncement(id) {
const modal = new bootstrap.Modal(document.getElementById('announcementModal'));
// 显示加载状态
document.getElementById('announcementModalBody').innerHTML = '<div class="text-center py-3">加载中...</div>';
modal.show();
try {
const { data } = await UIEnhancements.enhancedFetch('/announcements', { credentials: 'include' });
const item = data?.data?.find(i => i.id === id);
if (item) {
document.getElementById('announcementModalTitle').innerText = item.title;
document.getElementById('announcementModalBody').innerHTML = `
<div class="mb-3 text-muted small">
${item.type === 'ACTIVITY' ? '<span class="badge bg-warning text-dark me-2">活动</span>' : '<span class="badge bg-primary me-2">通知</span>'}
${new Date(item.createTime).toLocaleString()}
</div>
<div style="line-height:1.6;white-space:pre-wrap;font-size:15px;color:#334155;">${item.content}</div>
`;
} else {
document.getElementById('announcementModalBody').innerHTML = '公告未找到';
}
} catch(e) {
document.getElementById('announcementModalBody').innerHTML = '加载失败';
}
}
// 触底加载
function handleScroll() {
if (isLoading || !hasMore) return;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// 距离底部100px时触发加载
if (scrollTop + windowHeight >= documentHeight - 100) {
showLoadingTip();
loadProducts(currentPage + 1, true);
}
}
// 添加滚动监听(使用防抖)
let scrollTimer = null;
window.addEventListener('scroll', () => {
if (scrollTimer) clearTimeout(scrollTimer);
scrollTimer = setTimeout(handleScroll, 100);
});
// Init
loadAnnouncements();
loadRecommendations();
loadProducts(1, false);
</script>
<!-- 底部导航 -->
<div class="mobile-tabbar">
<a href="/index.html" class="tab-item active">
<i class="bi bi-cup-hot-fill"></i>
<span>点餐</span>
</a>
<a href="/cart.html" class="tab-item">
<i class="bi bi-bag"></i>
<span>购物袋</span>
</a>
<a href="/orders.html" class="tab-item">
<i class="bi bi-receipt"></i>
<span>订单</span>
</a>
<a href="/profile.html" class="tab-item">
<i class="bi bi-person"></i>
<span>我的</span>
</a>
</div>
</body>
</html>