Compare commits

..

No commits in common. "170db69eb436e8de1df86a782dc50c303e2361c9" and "d1b5e8c5cf64e706a5f004b29553b6eb1987e464" have entirely different histories.

17 changed files with 85 additions and 1004 deletions

View File

@ -1,6 +1,5 @@
from flask import Flask from flask import Flask
from config.database import init_db from config.database import init_db
from app.utils.email_service import mail
from config.config import Config from config.config import Config
def create_app(config_name=None): def create_app(config_name=None):
@ -10,9 +9,6 @@ def create_app(config_name=None):
# 初始化数据库 # 初始化数据库
init_db(app) init_db(app)
# 初始化邮件服务
mail.init_app(app)
# 注册蓝图 # 注册蓝图
from app.views.auth import auth_bp from app.views.auth import auth_bp
from app.views.main import main_bp from app.views.main import main_bp

View File

@ -7,57 +7,7 @@
} }
.quantity-input { .quantity-input {
width: 50px !important; width: 60px;
min-width: 50px !important;
padding: 4px 8px !important;
text-align: center !important;
border-radius: 0 !important;
-moz-appearance: textfield !important;
background-color: #fff !important;
border: 1px solid #ced4da !important;
}
.quantity-input:focus {
border-color: #86b7fe !important;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25) !important;
outline: 0 !important;
}
.quantity-input::-webkit-outer-spin-button,
.quantity-input::-webkit-inner-spin-button {
-webkit-appearance: none !important;
margin: 0 !important;
}
.quantity-input::-webkit-outer-spin-button,
.quantity-input::-webkit-inner-spin-button {
-webkit-appearance: none !important;
margin: 0 !important;
}
.quantity-control {
width: 120px;
margin: 0 auto;
}
.quantity-btn {
width: 30px;
height: 30px;
padding: 0;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.quantity-btn:hover:not(:disabled) {
background-color: #e9ecef;
border-color: #adb5bd;
}
.quantity-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
} }
.item-checkbox { .item-checkbox {

View File

@ -3,239 +3,22 @@
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
/* 地址选择区域美化 */
.address-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.address-card { .address-card {
position: relative;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
border: 2px solid #e9ecef; border: 2px solid #e9ecef;
border-radius: 12px;
overflow: hidden;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
} }
.address-card:hover { .address-card:hover {
border-color: #007bff; border-color: #007bff;
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.15); box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
transform: translateY(-2px);
} }
.address-card.selected { .address-card.selected {
border-color: #007bff; border-color: #007bff;
background: linear-gradient(135deg, #e7f3ff 0%, #cce7ff 100%); background-color: #e7f3ff;
box-shadow: 0 4px 16px rgba(0, 123, 255, 0.2);
transform: translateY(-2px);
} }
.address-card.selected::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #007bff, #0056b3);
}
.address-card-body {
padding: 1.25rem;
position: relative;
}
.address-header {
display: flex;
justify-content: between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.address-receiver {
flex: 1;
}
.receiver-name {
font-size: 1.1rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 0.25rem;
display: flex;
align-items: center;
}
.receiver-name i {
margin-right: 0.5rem;
color: #007bff;
}
.receiver-phone {
color: #6c757d;
font-size: 0.9rem;
margin-bottom: 0;
display: flex;
align-items: center;
}
.receiver-phone i {
margin-right: 0.5rem;
color: #28a745;
}
.address-content {
background: rgba(255,255,255,0.7);
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 0.75rem;
border-left: 3px solid #e9ecef;
transition: all 0.3s ease;
}
.address-card.selected .address-content {
border-left-color: #007bff;
background: rgba(255,255,255,0.9);
}
.address-text {
color: #495057;
margin-bottom: 0;
line-height: 1.5;
display: flex;
align-items: flex-start;
}
.address-text i {
margin-right: 0.5rem;
margin-top: 0.2rem;
color: #6c757d;
flex-shrink: 0;
}
.address-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.default-badge {
background: linear-gradient(45deg, #ff6b6b, #ee5a52);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
display: inline-flex;
align-items: center;
box-shadow: 0 2px 4px rgba(255,107,107,0.3);
}
.default-badge i {
margin-right: 0.25rem;
}
/* 自定义单选按钮样式 */
.custom-radio {
position: relative;
width: 24px;
height: 24px;
}
.custom-radio input[type="radio"] {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
margin: 0;
cursor: pointer;
}
.custom-radio .radio-mark {
position: absolute;
top: 0;
left: 0;
width: 24px;
height: 24px;
background: #fff;
border: 2px solid #ddd;
border-radius: 50%;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.custom-radio input[type="radio"]:checked + .radio-mark {
border-color: #007bff;
background: #007bff;
box-shadow: 0 0 0 3px rgba(0,123,255,0.2);
}
.custom-radio .radio-mark::after {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
background: white;
opacity: 0;
transform: scale(0);
transition: all 0.2s ease;
}
.custom-radio input[type="radio"]:checked + .radio-mark::after {
opacity: 1;
transform: scale(1);
}
/* 新增地址按钮美化 */
.add-address-btn {
background: linear-gradient(45deg, #28a745, #20c997);
border: none;
color: white;
padding: 0.5rem 1rem;
border-radius: 25px;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(40,167,69,0.3);
}
.add-address-btn:hover {
background: linear-gradient(45deg, #218838, #1e7e34);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(40,167,69,0.4);
color: white;
}
.add-address-btn i {
margin-right: 0.5rem;
}
/* 卡片头部美化 */
.checkout-section .card-header {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-bottom: 1px solid #dee2e6;
padding: 1rem 1.25rem;
}
.checkout-section .card-header h5 {
margin-bottom: 0;
color: #495057;
font-weight: 600;
}
.checkout-section .card-header i {
margin-right: 0.5rem;
color: #007bff;
font-size: 1.1rem;
}
/* 其他原有样式保持不变 */
.product-item { .product-item {
padding: 1rem 0; padding: 1rem 0;
border-bottom: 1px solid #e9ecef; border-bottom: 1px solid #e9ecef;
@ -296,28 +79,19 @@
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 768px) { @media (max-width: 768px) {
.address-container {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.checkout-section .col-md-4, .checkout-section .col-md-4,
.checkout-section .col-md-3 { .checkout-section .col-md-3 {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.address-card {
margin-bottom: 1rem;
}
.product-item .col-md-2, .product-item .col-md-2,
.product-item .col-md-6 { .product-item .col-md-6 {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.address-card-body {
padding: 1rem;
}
.receiver-name {
font-size: 1rem;
}
} }
/* 动画效果 */ /* 动画效果 */
@ -332,39 +106,27 @@
} }
} }
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.alert { .alert {
animation: fadeIn 0.3s ease; animation: fadeIn 0.3s ease;
} }
.address-card {
animation: slideIn 0.4s ease;
}
.address-card:nth-child(2) {
animation-delay: 0.1s;
}
.address-card:nth-child(3) {
animation-delay: 0.2s;
}
/* 按钮样式 */ /* 按钮样式 */
.btn-lg { .btn-lg {
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
font-size: 1.1rem; font-size: 1.1rem;
} }
/* 卡片头部样式 */
.card-header h5 {
margin-bottom: 0;
color: #495057;
}
.card-header i {
margin-right: 0.5rem;
color: #007bff;
}
/* 表单标签样式 */ /* 表单标签样式 */
.form-check-label { .form-check-label {
cursor: pointer; cursor: pointer;
@ -400,20 +162,3 @@
.breadcrumb-item + .breadcrumb-item::before { .breadcrumb-item + .breadcrumb-item::before {
color: #6c757d; color: #6c757d;
} }
/* 加载状态动画 */
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(0, 123, 255, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(0, 123, 255, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(0, 123, 255, 0);
}
}
.address-card.loading {
animation: pulse 1.5s infinite;
}

View File

@ -75,13 +75,11 @@ function updateTotalPrice() {
// 修改数量 // 修改数量
function changeQuantity(cartId, delta) { function changeQuantity(cartId, delta) {
const input = document.querySelector(`[data-cart-id="${cartId}"].quantity-input`); const input = document.querySelector(`[data-cart-id="${cartId}"]`);
if (!input) return;
const currentValue = parseInt(input.value); const currentValue = parseInt(input.value);
const newValue = currentValue + delta; const newValue = currentValue + delta;
if (newValue >= 1) { if (newValue >= 1 && newValue <= parseInt(input.max)) {
updateQuantity(cartId, newValue); updateQuantity(cartId, newValue);
} }
} }
@ -105,39 +103,25 @@ function updateQuantity(cartId, quantity) {
if (data.success) { if (data.success) {
// 更新页面显示 // 更新页面显示
const cartItem = document.querySelector(`[data-cart-id="${cartId}"]`); const cartItem = document.querySelector(`[data-cart-id="${cartId}"]`);
const quantityInput = cartItem.querySelector('.quantity-input'); cartItem.querySelector('.quantity-input').value = quantity;
quantityInput.value = quantity;
cartItem.querySelector('.item-total').textContent = data.item_total.toFixed(2); cartItem.querySelector('.item-total').textContent = data.item_total.toFixed(2);
// 简单的按钮状态更新
const minusBtn = cartItem.querySelector('button[onclick*="-1"]');
const plusBtn = cartItem.querySelector('button[onclick*="1"]');
if (minusBtn) {
minusBtn.disabled = quantity <= 1;
}
// 暂时不限制最大值,避免选择器错误
if (plusBtn) {
plusBtn.disabled = false;
}
// 更新总价 // 更新总价
updateTotalPrice(); updateTotalPrice();
// 更新全局购物车数量 // 更新全局购物车数量
updateCartBadge(data.cart_count); updateCartBadge(data.cart_count);
// 显示成功消息
showSuccessMessage('数量更新成功'); showSuccessMessage('数量更新成功');
} else { } else {
alert(data.message || '更新失败'); alert(data.message);
// 恢复原始值
location.reload(); location.reload();
} }
}) })
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
alert('网络错误,请重试'); alert('更新失败');
location.reload(); location.reload();
}); });
} }
@ -233,82 +217,3 @@ function checkout() {
window.location.href = `/cart/checkout?${params.toString()}`; window.location.href = `/cart/checkout?${params.toString()}`;
} }
// 显示成功消息
function showSuccessMessage(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.remove();
}
}, 3000);
}
// 更新购物车徽章
// 验证数量输入
function validateQuantityInput(input) {
// 只允许数字
input.value = input.value.replace(/[^0-9]/g, "");
// 如果为空或0显示1
if (input.value === "" || parseInt(input.value) < 1) {
input.value = "1";
}
// 检查最大值
const maxStock = parseInt(input.getAttribute("max")) || 999;
if (parseInt(input.value) > maxStock) {
input.value = maxStock.toString();
}
}
// 从输入框更新数量
function updateQuantityFromInput(cartId, input) {
let quantity = parseInt(input.value);
// 验证输入
if (isNaN(quantity) || quantity < 1) {
quantity = 1;
input.value = "1";
}
const maxStock = parseInt(input.getAttribute("max")) || 999;
if (quantity > maxStock) {
quantity = maxStock;
input.value = maxStock.toString();
}
// 更新数量
updateQuantity(cartId, quantity);
}
// 处理键盘事件
function handleQuantityKeyPress(event, cartId, input) {
// 回车键确认
if (event.key === "Enter") {
input.blur();
return;
}
// 只允许数字键
if (!/[0-9]/.test(event.key) && !["Backspace", "Delete", "Tab", "Escape", "Enter", "Home", "End", "ArrowLeft", "ArrowRight"].includes(event.key)) {
event.preventDefault();
}
}
function updateCartBadge(count) {
const badge = document.querySelector('.cart-badge');
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? 'inline' : 'none';
}
}

View File

@ -15,145 +15,65 @@ document.addEventListener('DOMContentLoaded', function() {
if (subtotalElement) { if (subtotalElement) {
subtotal = parseFloat(subtotalElement.textContent.replace('¥', '')); subtotal = parseFloat(subtotalElement.textContent.replace('¥', ''));
} }
// 初始化地址卡片动画
initAddressAnimations();
}); });
// 初始化地址卡片动画 // 选择地址
function initAddressAnimations() {
const addressCards = document.querySelectorAll('.address-card');
addressCards.forEach((card, index) => {
card.style.animationDelay = `${index * 0.1}s`;
});
}
// 选择地址(优化版本)
function selectAddress(addressId) { function selectAddress(addressId) {
selectedAddressId = addressId; selectedAddressId = addressId;
// 添加加载状态 // 更新UI
const clickedCard = document.querySelector(`[data-address-id="${addressId}"]`); document.querySelectorAll('.address-card').forEach(card => {
if (clickedCard) { card.classList.remove('selected');
clickedCard.classList.add('loading'); });
const selectedCard = document.querySelector(`[data-address-id="${addressId}"]`);
if (selectedCard) {
selectedCard.classList.add('selected');
} }
// 延迟执行UI更新给用户视觉反馈 // 更新单选按钮
setTimeout(() => { const radioButton = document.querySelector(`input[value="${addressId}"]`);
// 更新UI if (radioButton) {
document.querySelectorAll('.address-card').forEach(card => { radioButton.checked = true;
card.classList.remove('selected', 'loading'); }
});
const selectedCard = document.querySelector(`[data-address-id="${addressId}"]`);
if (selectedCard) {
selectedCard.classList.add('selected');
// 添加选中动画效果
selectedCard.style.animation = 'none';
selectedCard.offsetHeight; // 触发重排
selectedCard.style.animation = 'slideIn 0.4s ease';
}
// 更新单选按钮
const radioButton = document.querySelector(`input[value="${addressId}"]`);
if (radioButton) {
radioButton.checked = true;
// 触发单选按钮动画
const radioMark = radioButton.nextElementSibling;
if (radioMark) {
radioMark.style.transform = 'scale(1.1)';
setTimeout(() => {
radioMark.style.transform = 'scale(1)';
}, 200);
}
}
// 显示成功提示
showSelectionFeedback('地址选择成功');
}, 150);
} }
// 显示选择反馈 // 更新运费
function showSelectionFeedback(message) {
// 创建临时提示元素
const feedback = document.createElement('div');
feedback.className = 'position-fixed top-0 start-50 translate-middle-x mt-3';
feedback.style.zIndex = '9999';
feedback.innerHTML = `
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle me-2"></i>
${message}
</div>
`;
document.body.appendChild(feedback);
// 3秒后自动移除
setTimeout(() => {
if (feedback.parentNode) {
feedback.remove();
}
}, 3000);
}
// 更新运费(优化版本)
function updateShippingFee() { function updateShippingFee() {
const shippingMethodElement = document.querySelector('input[name="shipping_method"]:checked'); const shippingMethodElement = document.querySelector('input[name="shipping_method"]:checked');
if (!shippingMethodElement) return; if (!shippingMethodElement) return;
const shippingMethod = shippingMethodElement.value; const shippingMethod = shippingMethodElement.value;
let fee = 0; let fee = 0;
let description = '';
switch(shippingMethod) { switch(shippingMethod) {
case 'express': case 'express':
fee = 10; fee = 10;
description = '次日达服务';
break; break;
case 'same_day': case 'same_day':
fee = 20; fee = 20;
description = '当日达服务';
break; break;
default: default:
fee = 0; fee = 0;
description = '标准配送';
} }
// 添加动画效果更新价格
const shippingFeeElement = document.getElementById('shippingFee'); const shippingFeeElement = document.getElementById('shippingFee');
const totalAmountElement = document.getElementById('totalAmount'); const totalAmountElement = document.getElementById('totalAmount');
if (shippingFeeElement && totalAmountElement) { if (shippingFeeElement) {
// 淡出效果 shippingFeeElement.textContent = `¥${fee.toFixed(2)}`;
shippingFeeElement.style.opacity = '0.5'; }
totalAmountElement.style.opacity = '0.5';
if (totalAmountElement) {
setTimeout(() => { totalAmountElement.textContent = `¥${(subtotal + fee).toFixed(2)}`;
shippingFeeElement.textContent = `¥${fee.toFixed(2)}`;
totalAmountElement.textContent = `¥${(subtotal + fee).toFixed(2)}`;
// 淡入效果
shippingFeeElement.style.opacity = '1';
totalAmountElement.style.opacity = '1';
// 显示配送方式反馈
if (fee > 0) {
showSelectionFeedback(`已选择${description} (+¥${fee})`);
} else {
showSelectionFeedback(`已选择${description} (免费)`);
}
}, 200);
} }
} }
// 提交订单(优化版本) // 提交订单
function submitOrder() { function submitOrder() {
// 验证地址选择 // 验证地址选择
if (!selectedAddressId) { if (!selectedAddressId) {
showAlert('请选择收货地址', 'warning'); showAlert('请选择收货地址', 'warning');
highlightAddressSection();
return; return;
} }
@ -164,13 +84,11 @@ function submitOrder() {
if (!shippingMethodElement) { if (!shippingMethodElement) {
showAlert('请选择配送方式', 'warning'); showAlert('请选择配送方式', 'warning');
highlightSection('shipping');
return; return;
} }
if (!paymentMethodElement) { if (!paymentMethodElement) {
showAlert('请选择支付方式', 'warning'); showAlert('请选择支付方式', 'warning');
highlightSection('payment');
return; return;
} }
@ -205,10 +123,6 @@ function submitOrder() {
const originalText = submitBtn.innerHTML; const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 提交中...'; submitBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> 提交中...';
submitBtn.disabled = true; submitBtn.disabled = true;
submitBtn.classList.add('loading');
// 添加页面加载遮罩
showLoadingOverlay();
// 提交订单 // 提交订单
fetch('/order/create', { fetch('/order/create', {
@ -225,141 +139,23 @@ function submitOrder() {
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
hideLoadingOverlay();
if (data.success) { if (data.success) {
showAlert('订单创建成功!正在跳转到支付页面...', 'success'); showAlert('订单创建成功!正在跳转到支付页面...', 'success');
// 添加成功动画
submitBtn.innerHTML = '<i class="bi bi-check-circle"></i> 订单创建成功';
submitBtn.classList.add('btn-success');
submitBtn.classList.remove('btn-danger');
setTimeout(() => { setTimeout(() => {
window.location.href = `/order/pay/${data.payment_sn}`; window.location.href = `/order/pay/${data.payment_sn}`;
}, 1500); }, 1500);
} else { } else {
showAlert(data.message || '订单创建失败', 'error'); showAlert(data.message || '订单创建失败', 'error');
restoreSubmitButton(submitBtn, originalText); // 恢复按钮状态
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
} }
}) })
.catch(error => { .catch(error => {
console.error('提交订单错误:', error); console.error('提交订单错误:', error);
hideLoadingOverlay();
showAlert('提交订单失败,请重试', 'error'); showAlert('提交订单失败,请重试', 'error');
restoreSubmitButton(submitBtn, originalText); // 恢复按钮状态
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}); });
} }
// 恢复提交按钮状态
function restoreSubmitButton(submitBtn, originalText) {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
submitBtn.classList.remove('loading', 'btn-success');
submitBtn.classList.add('btn-danger');
}
// 高亮地址选择区域
function highlightAddressSection() {
const addressSection = document.querySelector('#addressList');
if (addressSection) {
addressSection.style.border = '2px solid #dc3545';
addressSection.style.borderRadius = '8px';
addressSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => {
addressSection.style.border = '';
}, 3000);
}
}
// 高亮指定区域
function highlightSection(sectionType) {
let selector = '';
switch(sectionType) {
case 'shipping':
selector = 'input[name="shipping_method"]';
break;
case 'payment':
selector = 'input[name="payment_method"]';
break;
}
if (selector) {
const element = document.querySelector(selector);
if (element) {
const section = element.closest('.card');
if (section) {
section.style.border = '2px solid #dc3545';
section.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => {
section.style.border = '';
}, 3000);
}
}
}
}
// 显示加载遮罩
function showLoadingOverlay() {
const overlay = document.createElement('div');
overlay.id = 'loadingOverlay';
overlay.className = 'position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center';
overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
overlay.style.zIndex = '9999';
overlay.innerHTML = `
<div class="text-center text-white">
<div class="spinner-border mb-3" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<h5>正在创建订单...</h5>
<p>请稍候不要关闭页面</p>
</div>
`;
document.body.appendChild(overlay);
}
// 隐藏加载遮罩
function hideLoadingOverlay() {
const overlay = document.getElementById('loadingOverlay');
if (overlay) {
overlay.remove();
}
}
// 增强的提示函数
function showAlert(message, type = 'info') {
const alertClass = {
'success': 'alert-success',
'error': 'alert-danger',
'warning': 'alert-warning',
'info': 'alert-info'
}[type] || 'alert-info';
const icon = {
'success': 'bi-check-circle',
'error': 'bi-exclamation-triangle',
'warning': 'bi-exclamation-triangle',
'info': 'bi-info-circle'
}[type] || 'bi-info-circle';
const alertDiv = document.createElement('div');
alertDiv.className = `alert ${alertClass} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3`;
alertDiv.style.zIndex = '10000';
alertDiv.innerHTML = `
<i class="bi ${icon} me-2"></i>
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// 5秒后自动移除
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}

View File

@ -261,14 +261,14 @@
<div class="mb-3"> <div class="mb-3">
<label for="single_stock" class="form-label">库存数量</label> <label for="single_stock" class="form-label">库存数量</label>
<input type="number" class="form-control" id="single_stock" <input type="number" class="form-control" id="single_stock"
name="single_stock" min="0" value="{{ product.inventory[0].stock if product and product.inventory else 0 }}"> name="single_stock" min="0" value="0">
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-3"> <div class="mb-3">
<label for="warning_stock" class="form-label">预警库存</label> <label for="warning_stock" class="form-label">预警库存</label>
<input type="number" class="form-control" id="warning_stock" <input type="number" class="form-control" id="warning_stock"
name="warning_stock" min="0" value="{{ product.inventory[0].warning_stock if product and product.inventory else 10 }}"> name="warning_stock" min="0" value="10">
</div> </div>
</div> </div>
</div> </div>

View File

@ -106,8 +106,8 @@
<div class="col-md-6"> <div class="col-md-6">
<h6>联系我们</h6> <h6>联系我们</h6>
<p class="text-muted"> <p class="text-muted">
<i class="bi bi-envelope"></i> 392437483@qq.com<br> <i class="bi bi-envelope"></i> service@taibai-mall.com<br>
<i class="bi bi-telephone"></i> 17753825492 <i class="bi bi-telephone"></i> 400-888-8888
</p> </p>
</div> </div>
</div> </div>

View File

@ -99,25 +99,17 @@
<!-- 数量 --> <!-- 数量 -->
<div class="col-2 text-center"> <div class="col-2 text-center">
<div class="quantity-control d-flex align-items-center justify-content-center"> <div class="input-group" style="width: 100px; margin: 0 auto;">
<button class="btn btn-outline-secondary btn-sm quantity-btn" type="button" <button class="btn btn-outline-secondary btn-sm" type="button"
onclick="changeQuantity({{ item.id }}, -1)" onclick="changeQuantity({{ item.id }}, -1)"
{% if not item.is_available() or item.quantity <= 1 %}disabled{% endif %}> {% if not item.is_available() or item.quantity <= 1 %}disabled{% endif %}>-</button>
<i class="bi bi-dash"></i> <input type="number" class="form-control form-control-sm text-center quantity-input"
</button> value="{{ item.quantity }}" min="1" max="{{ item.get_stock() }}"
<input type="text" class="form-control form-control-sm text-center quantity-input mx-1" data-cart-id="{{ item.id }}" onchange="updateQuantity({{ item.id }}, this.value)"
value="{{ item.quantity }}"
data-cart-id="{{ item.id }}"
data-max-stock="{{ item.get_stock() }}"
oninput="validateQuantityInput(this)"
onblur="updateQuantityFromInput({{ item.id }}, this)"
onkeydown="handleQuantityKeyPress(event, {{ item.id }}, this)"
{% if not item.is_available() %}disabled{% endif %}> {% if not item.is_available() %}disabled{% endif %}>
<button class="btn btn-outline-secondary btn-sm quantity-btn" type="button" <button class="btn btn-outline-secondary btn-sm" type="button"
onclick="changeQuantity({{ item.id }}, 1)" onclick="changeQuantity({{ item.id }}, 1)"
{% if not item.is_available() or item.quantity >= item.get_stock() %}disabled{% endif %}> {% if not item.is_available() or item.quantity >= item.get_stock() %}disabled{% endif %}>+</button>
<i class="bi bi-plus"></i>
</button>
</div> </div>
<div class="mt-1"> <div class="mt-1">
<small class="text-muted">小计:¥<span class="item-total">{{ "%.2f"|format(item.get_total_price()) }}</span></small> <small class="text-muted">小计:¥<span class="item-total">{{ "%.2f"|format(item.get_total_price()) }}</span></small>

View File

@ -3,7 +3,6 @@
{% block head %} {% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/checkout.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/checkout.css') }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -21,68 +20,33 @@
<!-- 收货地址 --> <!-- 收货地址 -->
<div class="card checkout-section"> <div class="card checkout-section">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="bi bi-geo-alt-fill"></i> 收货地址</h5> <h5><i class="bi bi-geo-alt"></i> 收货地址</h5>
<a href="{{ url_for('address.add') }}" class="btn add-address-btn"> <a href="{{ url_for('address.add') }}" class="btn btn-outline-primary btn-sm">
<i class="bi bi-plus-circle"></i> 新增地址 <i class="bi bi-plus"></i> 新增地址
</a> </a>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="address-container" id="addressList"> <div class="row" id="addressList">
{% for address in addresses %} {% for address in addresses %}
<div class="address-card {% if address.is_default %}selected{% endif %}" <div class="col-md-6 mb-3">
data-address-id="{{ address.id }}" onclick="selectAddress({{ address.id }})"> <div class="card address-card {% if address.is_default %}selected{% endif %}"
<div class="address-card-body"> data-address-id="{{ address.id }}" onclick="selectAddress({{ address.id }})">
<div class="address-header"> <div class="card-body">
<div class="address-receiver"> <div class="d-flex justify-content-between align-items-start">
<h6 class="receiver-name"> <div>
<i class="bi bi-person-fill"></i> <h6 class="mb-1">{{ address.receiver_name }}</h6>
{{ address.receiver_name }} <p class="text-muted mb-1">{{ address.receiver_phone }}</p>
</h6> <p class="mb-0">{{ address.get_full_address() }}</p>
<p class="receiver-phone"> </div>
<i class="bi bi-telephone-fill"></i> <div class="form-check">
{{ address.receiver_phone }} <input class="form-check-input" type="radio" name="address_id"
</p> value="{{ address.id }}" {% if address.is_default %}checked{% endif %}>
</div>
</div> </div>
<div class="custom-radio">
<input type="radio" name="address_id" value="{{ address.id }}"
{% if address.is_default %}checked{% endif %}>
<span class="radio-mark"></span>
</div>
</div>
<div class="address-content">
<p class="address-text">
<i class="bi bi-geo-alt"></i>
{{ address.get_full_address() }}
</p>
</div>
<div class="address-footer">
{% if address.is_default %}
<span class="default-badge">
<i class="bi bi-star-fill"></i>
默认地址
</span>
{% else %}
<span></span>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{% if not addresses %}
<div class="col-12">
<div class="text-center py-4">
<i class="bi bi-geo-alt text-muted" style="font-size: 3rem;"></i>
<h5 class="text-muted mt-3">暂无收货地址</h5>
<p class="text-muted">请先添加收货地址,然后继续下单</p>
<a href="{{ url_for('address.add') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> 立即添加
</a>
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -90,7 +54,7 @@
<!-- 商品信息 --> <!-- 商品信息 -->
<div class="card checkout-section"> <div class="card checkout-section">
<div class="card-header"> <div class="card-header">
<h5><i class="bi bi-box-seam"></i> 商品信息</h5> <h5><i class="bi bi-box"></i> 商品信息</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
{% for item in cart_items %} {% for item in cart_items %}

View File

@ -65,7 +65,7 @@ def send_verification_email(email, code, code_type):
<div style="text-align: center; color: #666; font-size: 12px;"> <div style="text-align: center; color: #666; font-size: 12px;">
<p>此邮件由系统自动发送请勿回复</p> <p>此邮件由系统自动发送请勿回复</p>
<p>© 2025 太白购物平台 版权所有</p> <p>© 2024 太白购物平台 版权所有</p>
</div> </div>
</div> </div>
</body> </body>

View File

@ -1,49 +0,0 @@
# 使用Python 3.9作为基础镜像以匹配开发环境
FROM python:3.9-slim
# 设置工作目录
WORKDIR /app
# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
FLASK_APP=run.py \
FLASK_ENV=production
# 创建并配置国内镜像源
RUN echo "deb https://mirrors.ustc.edu.cn/debian/ bookworm main" > /etc/apt/sources.list && \
echo "deb https://mirrors.ustc.edu.cn/debian/ bookworm-updates main" >> /etc/apt/sources.list && \
echo "deb https://mirrors.ustc.edu.cn/debian-security/ bookworm-security main" >> /etc/apt/sources.list
# 安装系统依赖包括libmagic
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
default-libmysqlclient-dev \
libmagic1 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# 使用阿里云pip镜像源更稳定
RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \
pip config set install.trusted-host mirrors.aliyun.com
# 安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制项目文件
COPY . .
# 运行配置更新脚本
RUN python update_config.py
# 创建非root用户运行应用
RUN useradd -m appuser
RUN chown -R appuser:appuser /app
USER appuser
# 暴露自定义端口50400
EXPOSE 50400
# 启动命令
CMD ["gunicorn", "--bind", "0.0.0.0:50400", "run:app"]

View File

@ -1,72 +0,0 @@
# 太白购物商城 Docker 部署指南
## 前置条件
确保您的服务器上已安装:
- Docker (20.10.0+)
- Docker Compose (2.0.0+)
## 部署步骤
### 1. 克隆项目到服务器
git clone <项目仓库URL> shopping-platform
cd shopping-platform
### 2. 配置数据库连接
您有两个选择:
#### 选项1使用Docker内置MySQL数据库
编辑 docker/docker-compose.yml取消以下环境变量的注释
- MYSQL_HOST=db
- MYSQL_USER=shopping_user
- MYSQL_PASSWORD=shopping_password
- MYSQL_DB=shopping_db
- MYSQL_PORT=3306
#### 选项2使用现有外部数据库
默认配置使用外部数据库(27.124.22.104)。保持docker-compose.yml中的数据库环境变量为注释状态。
### 3. 修改密钥和敏感信息
编辑 docker/docker-compose.yml更改SECRET_KEY为安全的密钥。
### 4. 构建并启动应用
cd docker
docker-compose up -d --build
### 5. 验证部署
访问 http://服务器IP:50400 确认应用是否正常运行。
## 维护命令
### 查看日志
docker-compose logs -f app
### 重启应用
docker-compose restart app
### 完全重新部署
docker-compose down
docker-compose up -d --build
### 停止所有服务
docker-compose down
## 端口说明
- 应用端口50400
- MySQL端口3366 (避免与主机MySQL冲突)
## 疑难解答
### 应用无法连接到数据库
1. 检查数据库连接配置
2. 查看应用日志docker-compose logs app
### 端口冲突
修改 docker-compose.yml 中的端口映射
### 权限问题
确保当前用户有权限访问Docker

View File

@ -1,41 +0,0 @@
#!/bin/bash
echo "🚀 开始部署太白购物商城..."
# 检查Docker是否已安装
if ! command -v docker &> /dev/null; then
echo "❌ Docker未安装。请先安装Docker。"
exit 1
fi
if ! command -v docker-compose &> /dev/null; then
echo "❌ Docker Compose未安装。请先安装Docker Compose。"
exit 1
fi
# 确保gunicorn在依赖中
grep -q "gunicorn" ../requirements.txt || echo "gunicorn==20.1.0" >> ../requirements.txt
# 构建并启动应用
echo "🔨 构建Docker镜像..."
docker-compose build
echo "🚀 启动服务..."
docker-compose up -d
# 等待几秒钟让服务启动
sleep 5
# 检查服务状态
echo "📊 检查服务状态..."
docker-compose ps
echo ""
echo "✅ 部署完成!"
echo "🌐 应用地址: http://localhost:50400"
echo ""
echo "📋 常用命令:"
echo " 查看日志: docker-compose logs -f app"
echo " 重启应用: docker-compose restart app"
echo " 停止应用: docker-compose down"
echo " 查看状态: docker-compose ps"

View File

@ -1,56 +0,0 @@
version: '3.8'
services:
app:
build:
context: ..
dockerfile: docker/Dockerfile
restart: always
ports:
- "50400:50400"
depends_on:
- db
environment:
# 应用配置
- FLASK_CONFIG=production
- SECRET_KEY=change-me-in-production
# 数据库配置 - 两种选择之一:
# 选项1使用Docker内置MySQL取消下面的注释启用
# - MYSQL_HOST=db
# - MYSQL_USER=taibai
# - MYSQL_PASSWORD=taibaishopping
# - MYSQL_DB=shopping_db
# - MYSQL_PORT=3306
# 选项2使用现有外部数据库默认
# 保持上面的选项1注释即可使用配置文件中的默认设置
volumes:
- ../logs:/app/logs
- ../app/static/uploads:/app/app/static/uploads
networks:
- app-network
db:
image: mysql:8.0
restart: always
environment:
- MYSQL_DATABASE=online_shopping
- MYSQL_USER=taibai
- MYSQL_PASSWORD=taibaishopping
- MYSQL_ROOT_PASSWORD=root_password_here
volumes:
- mysql-data:/var/lib/mysql
ports:
- "3366:3306" # 使用3366避免与主机MySQL端口冲突
networks:
- app-network
command: --default-authentication-plugin=mysql_native_password
networks:
app-network:
driver: bridge
volumes:
mysql-data:

View File

@ -1,21 +0,0 @@
#!/bin/bash
echo "🧪 测试Docker部署..."
# 检查应用是否响应
echo "⏳ 等待应用启动..."
sleep 10
# 测试应用是否可访问
if curl -f http://localhost:50400 > /dev/null 2>&1; then
echo "✅ 应用运行正常!"
echo "🌐 访问地址: http://localhost:50400"
else
echo "❌ 应用无法访问,请检查日志:"
echo "docker-compose logs app"
fi
# 显示当前运行的容器
echo ""
echo "📋 当前运行的容器:"
docker-compose ps

View File

@ -11,4 +11,3 @@ Flask-Mail==0.9.1
cos-python-sdk-v5==1.9.24 cos-python-sdk-v5==1.9.24
Pillow==10.0.1 Pillow==10.0.1
python-magic==0.4.27 python-magic==0.4.27
gunicorn==20.1.0

View File

@ -1,27 +0,0 @@
import os
config_file = 'config/config.py'
with open(config_file, 'r') as f:
content = f.read()
# 修改数据库配置部分,使其支持环境变量
modified_content = content.replace(
''' # 数据库配置
MYSQL_HOST = '27.124.22.104'
MYSQL_USER = 'taibai'
MYSQL_PASSWORD = 'taibaishopping'
MYSQL_DB = 'online_shopping'
MYSQL_PORT = 3306''',
''' # 数据库配置
MYSQL_HOST = os.environ.get('MYSQL_HOST', '27.124.22.104')
MYSQL_USER = os.environ.get('MYSQL_USER', 'taibai')
MYSQL_PASSWORD = os.environ.get('MYSQL_PASSWORD', 'taibaishopping')
MYSQL_DB = os.environ.get('MYSQL_DB', 'online_shopping')
MYSQL_PORT = int(os.environ.get('MYSQL_PORT', '3306'))'''
)
with open(config_file, 'w') as f:
f.write(modified_content)
print(f"✅ 成功更新 {config_file} 以支持环境变量")