652 lines
21 KiB
HTML
652 lines
21 KiB
HTML
<!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> |