Files

652 lines
21 KiB
HTML
Raw Permalink Normal View History

2026-01-11 18:14:42 +08:00
<!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>