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

441 lines
18 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 { --bg-body: #f8fafc; --primary: #0f172a; --accent: #3b82f6; }
body { background: var(--bg-body); padding-bottom: 80px; font-family: -apple-system, sans-serif; }
/* 筛选条 */
.filter-bar {
position: sticky; top: 0; background: var(--bg-body); z-index: 10;
padding: 16px 16px 8px; overflow-x: auto; white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.filter-chip {
display: inline-block; padding: 6px 16px; margin-right: 8px;
background: #fff; border: 1px solid #e2e8f0; border-radius: 20px;
font-size: 13px; color: #64748b; transition: all 0.2s; cursor: pointer;
}
.filter-chip.active {
background: var(--primary); color: #fff; border-color: var(--primary);
}
/* 订单卡片 */
.order-card {
background: #fff; margin: 0 16px 16px; padding: 16px;
border-radius: 16px; border: none; box-shadow: 0 1px 2px rgba(0,0,0,0.02);
}
.order-top {
display: flex; justify-content: space-between; margin-bottom: 12px; font-size: 12px;
}
.order-no { color: #94a3b8; }
.order-status { font-weight: 600; }
.st-PENDING_PAY { color: #f59e0b; }
.st-MAKING { color: var(--accent); }
.st-COMPLETED { color: #10b981; }
.order-info { margin-bottom: 12px; }
.order-price { font-size: 16px; font-weight: 700; color: var(--primary); }
.order-date { font-size: 12px; color: #94a3b8; margin-top: 4px; }
.order-actions {
border-top: 1px solid #f8fafc; padding-top: 12px; display: flex; justify-content: flex-end; gap: 8px;
}
.btn-act {
padding: 6px 14px; border-radius: 18px; font-size: 12px; font-weight: 500;
background: #fff; border: 1px solid #cbd5e1; color: #475569;
}
.btn-act.primary { background: var(--primary); border-color: var(--primary); color: #fff; }
/* 评价展示样式 */
.review-display-stars { color: #f59e0b; letter-spacing: 2px; }
.review-reply-box { background: #f8fafc; padding: 12px; border-radius: 8px; margin-top: 12px; font-size: 13px; color: #475569; }
/* 评价星星 */
.rating-star { font-size: 24px; color: #e2e8f0; cursor: pointer; margin-right: 4px; }
.rating-star.active { color: #f59e0b; }
/* 商品推荐模块 */
.recommend-section {
margin: 24px 16px;
padding: 20px 16px;
background: #fff;
border-radius: 16px;
}
.recommend-title {
font-size: 16px; font-weight: 700; color: var(--primary);
margin-bottom: 16px; display: flex; align-items: center;
}
.recommend-title i { margin-right: 8px; color: var(--accent); }
.recommend-grid {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;
}
.recommend-item {
background: #f8fafc; border-radius: 12px; overflow: hidden;
cursor: pointer; transition: transform 0.2s;
}
.recommend-item:active { transform: scale(0.98); }
.recommend-img {
width: 100%; height: 120px; object-fit: cover; background: #f1f5f9;
}
.recommend-info {
padding: 10px;
}
.recommend-name {
font-size: 13px; font-weight: 600; color: var(--primary);
margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.recommend-price {
font-size: 14px; font-weight: 700; color: var(--primary);
}
/* 底部导航 */
.mobile-tabbar {
position: fixed; bottom: 0; left: 0; right: 0; background: #fff;
padding: 8px 0 20px; display: flex; border-top: 1px solid #f1f5f9; z-index: 99;
}
.tab-item {
flex: 1; text-align: center; color: #94a3b8; text-decoration: none;
font-size: 10px; display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.tab-item.active { color: var(--accent); }
.tab-item i { font-size: 22px; margin-bottom: 4px; line-height: 1; }
</style>
</head>
<body>
<div class="filter-bar">
<div class="filter-chip active" onclick="filter('')">全部</div>
<div class="filter-chip" onclick="filter('PENDING_PAY')">待支付</div>
<div class="filter-chip" onclick="filter('MAKING')">制作中</div>
<div class="filter-chip" onclick="filter('READY')">待取餐</div>
<div class="filter-chip" onclick="filter('COMPLETED')">已完成</div>
</div>
<div id="orderList" class="pt-2"></div>
<!-- 商品推荐模块 -->
<div class="recommend-section" id="recommendSection" style="display: none;">
<div class="recommend-title">
<i class="bi bi-fire"></i>
<span>为您推荐</span>
</div>
<div class="recommend-grid" id="recommendGrid"></div>
</div>
<!-- 评价输入模态框 -->
<div class="modal fade" id="reviewModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="border-radius: 16px; border: none;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">评价订单</h5>
</div>
<div class="modal-body">
<input type="hidden" id="reviewOrderId">
<input type="hidden" id="reviewRating" value="0">
<div class="mb-3">
<label class="small text-muted mb-2 d-block">选择商品</label>
<select class="form-select border-0 bg-light" id="reviewProductSelect"></select>
</div>
<div class="mb-3 text-center">
<div id="starContainer">
<i class="bi bi-star-fill rating-star" data-val="1"></i>
<i class="bi bi-star-fill rating-star" data-val="2"></i>
<i class="bi bi-star-fill rating-star" data-val="3"></i>
<i class="bi bi-star-fill rating-star" data-val="4"></i>
<i class="bi bi-star-fill rating-star" data-val="5"></i>
</div>
<div class="small text-muted mt-1" id="ratingText">点击星星打分</div>
</div>
<textarea class="form-control bg-light border-0" id="reviewContent" rows="3" placeholder="口味如何?服务周到吗?"></textarea>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-pill" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-dark rounded-pill px-4" onclick="submitReview()">提交评价</button>
</div>
</div>
</div>
</div>
<!-- 查看评价模态框 -->
<div class="modal fade" id="viewReviewModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="border-radius: 16px; border: none;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">我的评价</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="viewReviewContent">
<div class="text-center py-3">加载中...</div>
</div>
</div>
</div>
</div>
<!-- 底部导航 -->
<div class="mobile-tabbar">
<a href="/index.html" class="tab-item">
<i class="bi bi-cup-hot"></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 active">
<i class="bi bi-receipt-cutoff"></i>
<span>订单</span>
</a>
<a href="/profile.html" class="tab-item">
<i class="bi bi-person"></i>
<span>我的</span>
</a>
</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 src="/static/js/custom-modal.js"></script>
<script>
let currentStatus = '';
// 初始化
filter('');
loadRecommendations();
async function filter(status) {
currentStatus = status;
document.querySelectorAll('.filter-chip').forEach(el => {
const isActive = (status === '' && el.innerText === '全部') ||
(status !== '' && el.onclick.toString().includes(status));
el.classList.toggle('active', isActive);
});
await loadOrders();
}
async function loadOrders() {
const box = document.getElementById('orderList');
box.innerHTML = '<div class="text-center py-5 text-muted small">加载中...</div>';
try {
const url = currentStatus ? `/order/orders?status=${currentStatus}` : '/order/orders';
const res = await fetch(url, { credentials: 'include' });
const data = await res.json();
if (data.code === 200) {
const orders = data.data || [];
if (!orders.length) {
box.innerHTML = '<div class="text-center py-5 text-muted small">暂无订单</div>';
return;
}
// 批量查询评论状态
const orderIds = orders.map(o => o.id);
const reviewsMap = await loadReviewsForOrders(orderIds);
box.innerHTML = orders.map(o => {
const statusMap = { 'PENDING_PAY': '待支付', 'MAKING': '制作中', 'READY': '待取餐', 'COMPLETED': '已完成', 'CANCELLED': '已取消' };
const statusTxt = statusMap[o.orderStatus] || o.orderStatus;
let btns = `<button class="btn-act" onclick="location.href='/order-detail.html?id=${o.id}'">详情</button>`;
if (o.orderStatus === 'PENDING_PAY') {
btns += `<button class="btn-act primary" onclick="goToPayment(${o.id})">去支付</button>`;
} else if (o.orderStatus === 'COMPLETED') {
// 根据评论状态显示不同按钮
if (reviewsMap.has(o.id)) {
btns += `<button class="btn-act" onclick="showViewReviewModal(${o.id})">查看评价</button>`;
} else {
btns += `<button class="btn-act" onclick="openReviewModal(${o.id})">评价</button>`;
}
}
return `
<div class="order-card">
<div class="order-top">
<span class="order-no">单号 ${o.orderNo.slice(-8)}</span>
<span class="order-status st-${o.orderStatus}">${statusTxt}</span>
</div>
<div class="order-info">
<div class="order-price">¥${o.totalAmount}</div>
<div class="order-date">${new Date(o.createTime).toLocaleString()}</div>
</div>
<div class="order-actions">
${btns}
</div>
</div>
`;
}).join('');
} else if (data.message?.includes('登录')) {
window.location.href = '/login.html';
}
} catch (e) { console.error(e); }
}
// 批量获取评论状态
async function loadReviewsForOrders(orderIds) {
if (!orderIds || orderIds.length === 0) return new Map();
try {
const res = await fetch(`/review/by-orders?orderIds=${orderIds.join(',')}`, { credentials: 'include' });
const data = await res.json();
if (data.code === 200 && data.data) {
const map = new Map();
data.data.forEach(r => map.set(r.orderId, r));
return map;
}
} catch (e) { console.error(e); }
return new Map();
}
// 跳转到支付页面
function goToPayment(orderId) {
window.location.href = `/payment.html?orderId=${orderId}`;
}
// 评价输入逻辑
let reviewModal;
async function openReviewModal(orderId) {
try {
const res = await fetch(`/order/orders/${orderId}`, { credentials: 'include' });
const d = await res.json();
if (d.code === 200) {
const items = d.data.items || [];
const select = document.getElementById('reviewProductSelect');
select.innerHTML = items.map(i =>
`<option value="${i.productId}">${i.productName}</option>`
).join('');
document.getElementById('reviewOrderId').value = orderId;
document.getElementById('reviewRating').value = 0;
document.getElementById('reviewContent').value = '';
resetStars();
reviewModal = new bootstrap.Modal(document.getElementById('reviewModal'));
reviewModal.show();
}
} catch(e) { alert('无法加载订单信息'); }
}
// 查看评价逻辑
async function showViewReviewModal(orderId) {
const modal = new bootstrap.Modal(document.getElementById('viewReviewModal'));
document.getElementById('viewReviewContent').innerHTML = '<div class="text-center py-3">加载中...</div>';
modal.show();
try {
const res = await fetch(`/review/by-order?orderId=${orderId}`, { credentials: 'include' });
const d = await res.json();
if (d.code === 200 && d.data) {
const r = d.data;
const stars = '★'.repeat(r.rating) + '☆'.repeat(5 - r.rating);
let html = `
<div class="mb-3">
<div class="review-display-stars fs-4 mb-2">${stars}</div>
<div class="text-dark">${r.content || '未填写评价内容'}</div>
<div class="text-muted small mt-2">${new Date(r.createTime).toLocaleString()}</div>
</div>
`;
if (r.reply) {
html += `
<div class="review-reply-box">
<div class="fw-bold mb-1">商家回复:</div>
<div>${r.reply}</div>
</div>
`;
}
document.getElementById('viewReviewContent').innerHTML = html;
} else {
document.getElementById('viewReviewContent').innerHTML = '<div class="text-center">未找到评价信息</div>';
}
} catch (e) {
document.getElementById('viewReviewContent').innerHTML = '<div class="text-center">加载失败</div>';
}
}
// 星星交互
document.querySelectorAll('#starContainer .rating-star').forEach(star => {
star.onclick = function() {
const val = this.dataset.val;
document.getElementById('reviewRating').value = val;
document.getElementById('ratingText').innerText = val + ' 分';
updateStars(val);
}
});
function updateStars(val) {
document.querySelectorAll('#starContainer .rating-star').forEach(s => {
if (s.dataset.val <= val) s.classList.add('active');
else s.classList.remove('active');
});
}
function resetStars() {
updateStars(0);
document.getElementById('ratingText').innerText = '点击星星打分';
}
async function submitReview() {
const oid = document.getElementById('reviewOrderId').value;
const pid = document.getElementById('reviewProductSelect').value;
const rating = document.getElementById('reviewRating').value;
const content = document.getElementById('reviewContent').value;
if (rating == 0) return alert('请打分');
try {
const params = new URLSearchParams();
params.append('orderId', oid);
params.append('productId', pid);
params.append('rating', rating);
params.append('content', content);
const res = await fetch('/review', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
credentials: 'include',
body: params
});
const d = await res.json();
if (d.code === 200) {
alert('评价成功');
reviewModal.hide();
loadOrders(); // 刷新列表状态
} else { alert(d.message); }
} catch(e) { alert('提交失败'); }
}
// 加载推荐商品限制3-5个
async function loadRecommendations() {
try {
const limit = Math.floor(Math.random() * 3) + 3; // 3-5个随机
const res = await fetch(`/recommend?limit=${limit}`, { credentials: 'include' });
const data = await res.json();
if (data.code === 200 && data.data && data.data.length > 0) {
renderRecommendations(data.data);
document.getElementById('recommendSection').style.display = 'block';
}
} catch(e) { console.error(e); }
}
function renderRecommendations(products) {
const container = document.getElementById('recommendGrid');
container.innerHTML = products.map(p => `
<div class="recommend-item" onclick="location.href='/product-detail.html?id=${p.id}'">
<img src="${p.image || '/static/images/default.jpg'}" class="recommend-img" alt="${p.name}">
<div class="recommend-info">
<div class="recommend-name">${p.name}</div>
<div class="recommend-price">¥${p.basePrice}</div>
</div>
</div>
`).join('');
}
</script>
</body>
</html>